rax30 patch diff analysis & nday exploit for zdi-23-496
Overview
Target bugs:
- ZDI-23-499:
soap_serverd
buffer overflow - ZDI-23-496:
lighttpd
misconfiguration -> RCE
Target firmware versions:
- Prepatch: 1.0.9
- Patched: 1.0.10
Analysis
ZDI-23-499: soap_serverd stack-based buffer overflow
The description of this bug says the flaw occurs because “when parsing SOAP message headers, the process does not properly validate user supplied data before copying it to a fixed-length stack buffer.” Based on my previous experience with Netgear and specifically their SOAP server implementations, I had a pretty good idea where to look. In fact, I even a hunch that this was the same/similar to a bug I’d found on another Netgear device last year, and so before spending much time going through the diff’ed binaries, I did a quick search for references to sscanf
in the patched and nonpatched versions and pretty easily identified where the bug occured. Interestingly, this bug is very similar but also unique to the issue I’d previously found – leave it to Netgear to ship two unique vulnerabilities using the same vulnerable C functions and in code that processes the same data lol.
The bug is caused by use of sscanf()
without supplying length-limit values in the format string. The vulnerable version of the code attempts to parse out the HTTP method, path, and HTTP version string from the header using the following code:
iVar1 = __isoc99_sscanf(local_3024,"%[^ ] %[^ ] %[^ ]",&v1, &v2, &v3);
if (iVar1 == 3) {
iVar7 = strcasecmp((char *)&v1, "post");
This is the patched version of the same code, showing the addition of length limit specs in the format string:
iVar1 = __isoc99_sscanf(&local_1824,"%511[^ ] %511[^ ] %511[^ ]",&v1, &v2, &v3);
if (iVar1 == 3) {
iVar7 = strcasecmp((char *)&v1,"post");
On the surface this looks like a pretty straightforward stack-based buffer overflow, and there seem to be some variables within the function that may be interesting targets for overwrite. An interesting note is that the advisory does not indicate this bug results in code execution but instead specifically mentions that it can be used to bypass authentication. This may be due to the fact that the binary is built with stack canaries, which makes exploitation more difficult. This is the same reason why I’d been unable to exploit the variant of this bug I mentioned earlier.
NOTE: The folks who discovered and exploited this bug at Pwn2Own posted their write-up of this bug before I had finished this post so I’ve been able to confirm the above assumption was correct: the stack canaries resulted in the bug not being directly exploitable for code exec. Check out their write-up here to see how they chained this bug with a few others to get RCE.
ZDI-23-496: lighttpd Misconfiguration RCE
The information provided about this bug indicates it isn’t a memory corruption issue but a misconfiguration that results in arbitrary code execution. Specifically it says:
The specific flaw exists within the configuration of the lighttpd HTTP server. The issue results from allowing execution of files from untrusted sources. An attacker can leverage this vulnerability to execute code in the context of root.
Based on this description, I focused on comparing the lighttpd configuration files in etc/lighttpd
between the two versions.
Comparing Config Changes
etc/lighttpd/conf.d/lighttpd4.conf
The patches version of this file includes the follow additions:
- Addition of
alias.url = ("/shares" => "/var/samba/share/"
at the global level - Inside the
HTTP["url"] = "^/shares"
definition for “usb storage” in the IPv4 sectionserver.follow-symlink = "disable"
static-file.exclude-extensions = ()
fastcgi.server = ()
etc/lighttpd/conf.d/usb_lighttpd.conf
- Addition of
alias.url = ("/shares" => "/var/samba/share/"
at the global level
etc/lighttpd/conf.d/usb_allow.inc
- Inside the
HTTP["url"] = "^/shares"
definition for “usb storage” in the IPv4 sectionserver.follow-symlink = "disable"
static-file.exclude-extensions = ()
fastcgi.server = ()
etc/lighttpd/conf.d/usb_allow_auth.inc
- Inside the
HTTP["url"] = "^/shares"
definition for “usb storage” in the IPv4 sectionserver.follow-symlink = "disable"
static-file.exclude-extensions = ()
fastcgi.server = ()
Conclusions Based on Changes
The issue seems to be focused around the /shares
path, which is mapped to the samba share directory where mounted USB drives can be accessed on the network like a NAS. The addition of server.follow-symlink = "disabled"
suggests the issue may result in the ability to access files on the host filesystem using symlinks on the mounted drive.
Its possible that the addition of fastcgi.server = ()
was needed to avoid having the global settings for this handlers apply. The top level fastcgi.server assignment in conf.d/lighttpd4.conf
shows the following:
fastcgi.server += (
".php" => ((
"socket" => "/var/run/php-fpm.sock",
#"bin-path" => "/bin/php-fpm -n -R -y /etc/php-fpm.conf",
#"max-procs" => 1,
"broken-scriptfilename" => "enable"
))
)
This lead me to believe the exclusion of the explicit fastcgi.server = ()
in the pre-patched version resulted in the global setting being applied, which would allow PHP files to be executed.
Exploits: ZDI-23-496 Lighttpd Misconfiguration
Based on the conclusions drawn from the changes made, the most likely entry point was going to be files mounted via USB drive so I created a ext2-formatted drive to use for testing (ext2 since it was going to be necessary to create symlinks) and downgraded to the vulnerable firmware version.
Local File Inclusion via Symlink
My assumption was that the addition of the symlink config options in the latest patches meant that the vulnerable version would follow symlinks resulting in the ability to reference (and access) files outside of the USB filesystem. To test this, I created a symlink on the USB drive pointing to /var/passwd
as this is where the actual passwd file is stored on the device at runtime. If the assumption is correct, accessing this file from the router should return the actual password file from the device.
After connecting the USB drive to the router, the files become visible at http://<routerip>/shares/
. Accessing the symlink that was created pointing to /var/passwd
results in the actual password file on the device (found at that path) being returned and downloaded locally.
RCE via PHP Files
To test the assumption regarding PHP files mounted via USB being executable, I used the same USB stick, this time simply creating a PHP file to execute phpinfo();
as a simple way to confirm execution. This worked as expected and the PHP info output was shown on the page. I then created a simple PHP page that would allow me to pass shell commands in a URL parameter for easy shell access.
PHP shell command proxy:
<?php echo system($_GET['cmd']); ?>
Finally, I used this to have the device download a shell script over HTTP from my machine to open a reverse shell:
Conclusion
This turned out to be a fun exercise and it turned out some of my prior experience with Netgear devices proved to be useful.
The soap_serverd
issue was exploited during the latest Pwn2Own as part of a longer exploit chain that eventually resulted in RCE, though direct exploitation seems infeasible due to the presence of stack canaries.
For the lighttpd
issue, exploitation requires physical access to the device, at least long enough to plug in the USB drive and send the necessary requests. This isn’t infeasible, though considering these routers are primarily used in SOHO settings this may not be quite as critical as a fully remote RCE.