As I’ve mentioned in previous posts, I’ve been hunting for bugs on the Netgear Orbi for about a year and half. A few weeks ago, I came across an advisory for an unauthenticated command injection vulnerability that was reported to Netgear back in December 2020 and realized that the vulnerable firmware version was the same one that was installed on my device back when I first started looking for bugs on it. In fact, the issue had only been fixed about a month before then. If only I’d started looking just a little bit sooner! Since it was way too late for that, I thought it’d be fun to see if I could find where the vulnerability was located and write a functional exploit to gain full control of the device.

initial analysis

There were no known exploits for this vulnerability at the time I started looking and the only useful information came from the CVE entry on Mitre:

This vulnerability allows network-adjacent attackers to execute arbitrary code on affected installations of NETGEAR Orbi routers. Authentication is not required to exploit this vulnerability. The specific flaw exists within the UA_Parser utility. A crafted Host Name option in a DHCP request can trigger execution of a system call composed from a user-supplied string. An attacker can leverage this vulnerability to execute code in the context of root. Was ZDI-CAN-11076.

Even though it’s not much, this provided enough information to narrow things down to a specific binary and a specific input path. It would mostly come down to finding the bug and working up from there to trace the input back to the source.

finding the vulnerability

With the info taken from the advisory, I moved on to analyzing the vulnerable version of UA_Parser to try to find where the vulnerability occurred. Since I knew this was command injection, the most likely suspect was insecure use of system() to execute commands. I loaded the binary up in Ghidra and used the symbol table to select system() and then used the Function Call Tree to check the functions that had incoming references to it. While doing this, I came across the following code snippet in one of these functions (annotated for clarity):


This code stood out as a good candidate for command injection given that the argument passed to system() is a string constructed with what looked like user-controlled data. Additionally, the values are placed inside double quotes, which would allow for expansion.

I then took a look at the function I labeled get_host_from_file() at line 78 in the image above and learned that this function eventually reads from a file at /tmp/netscan/attach_device , which contains entries for each client connected to the router, including MAC, IP, and hostname. It parses out the hostname it finds and fills the static buffer hostname which is passed as an argument to get_host_from_file(). This value is eventually passed as the 4th format string arg to snprintf() on line 84, which constructs the string that is passed to system().

At this point I felt pretty confident this was where the vulnerability occurred, but to get additional confirmation I took a look at the version of UA_Parser included with a firmware that was released after this bug was fixed (v2.7.3.22) to compare. The only thing that changed between the two snippets is the double-quotes that surround the user-controlled values were replaced with single-quotes (also known as ‘strong-quotes’), where no expansion/meta-character interpretation occurs:


tracing the data from sink → source

The next thing I needed to figure out was how the hostname value eventually reached this code. As mentioned above, UA_Parser reads the hostname value from the file /tmp/netscan/attach_device. I didn’t find any other references to this file in UA_Parser that indicated it writing to the file, so I used grep to recursively search the root filesystem I had extracted from the firmware image to find other files that referenced it. This is when I came across the binary /usr/sbin/net-scan which seemed like a good lead given the name.

After spending some time going through the code in Ghidra, I eventually found the function used to write /tmp/netscan/attach_device which I labeled update_attach_devices(). There was only a single reference to this function, which occured within another function that appeared to be the main entrypoint to trigger a ‘device scan’; this function is also only referenced once (from main()):


Naturally, my next question was about where/how net-scan was getting the values it used to populate the attached devices file. I spent some more time looking through the decompiled code and eventually came across a function that opened a file at /tmp/dhcpd_hostlist for reading. This caught my attention because the advisory for the vulnerability mentioned that a “crafted Host Name option in a DHCP request can trigger execution of a system call”, so it made sense that net-scan would get it’s values from whatever the DHCP server had received.

Another recursive grep later, I had confirmed that the DHCP server binary (/sbin/udhcpd and /sbin/udhcpd-ext) contained references to /tmp/dhcpd_hostlist. Since the source code for udhcp is included in the GPL sources for the device provided by the vendor, I took a look there and found the string in leases.h as a constant called HOSTNAME_SHOWFILE. This value is used in some custom code in a function called show_clients_hostname() in dhcpd.c (shown below at line 111) which writes the MAC/IP and hostname for each lease in the global leases structure to this file.


Below is a snippet of code from sendACK() in serverpacket.c:, one of two locations where show_clients_hostname() is called. One detail that became really important later is the call to toupper() on line 535 — it’s called for each character in the hostname string before it is saved to the lease object where it’s saved. This causes all alphabetic letters to be uppercased in the value that’s written out to /tmp/dhcpd_hostlist and eventually ends up in the vulnerable call to system().


summary of the analysis

  • the vulnerability occurs in UA_Parser due to a call to system() using a string containing a attacker-controlled hostname value
  • UA_Parser reads the hostname value from /etc/netscan/attach_device
  • UA_Parser is executed by a binary called net-scan used to detect attached devices
  • net-scan creates the file /etc/netscan/attach_device
  • net-scan reads the hostname values from the file /tmp/dhcpd_hostlist
  • /tmp/dhcpd_hostlist is created by udhcpd using the hostnames saved in it’s global leases array
  • udhcpd populates the hostname field for each lease struct in the global array using values received in DHCP REQUEST packets

testing setup


I created the following GDB script to set breakpoints on main() and system() when I attached to UA_Parser , as well as set the fork-mode settings to be sure the debugger follows the processes as they spawn.

set breakpoint pending on
set follow-fork-mode child
break main
commands 1
set follow-fork-mode parent

break system
commands 2
info args
backtrace full
info frame
info registers
x/s $r0
x/s $r1
x/s $r2

net-scan runs periodically in the background on the device but a re-scan can also be manually triggered by loading the “Attached Devices” page in the web admin UI. This is what I used during testing to force it to run the vulnerable code. Interestingly, I later discovered it would run even with unauthenticated requests to the homepage, so an attacker would actually be able to trigger the vulnerable code on-demand.

Below is a screenshot of the GDB script breaking on system() while attached to the UA_Parser process and triggering the vulnerable code. The argument that was passed to system() can be seen near the bottom of the register listing (the call to ‘devices_info update …’).


payload delivery

In order to send custom DHCP hostname values easily and establish DHCP connections, I used udcpc, which allows for passing in a custom hostname at the command line. I used this command after connecting to the Orbi using a static IP and confirmed the payload appeared in the relevant files.

sudo ./udhcpc -H "\$PATH" -f -i <interface> -n

With everything set up to be able to deliver payloads, it was time to start building one.

crafting payloads

using parameter+substring expansion to build a payload

As mentioned earlier in this post, each character in the hostname value that udhcpd receives from clients is uppercased before it’s saved to the global leases array and eventually written to /tmp/dhcpd_hostlist. This means that by the time UA_Parser reads these value from /tmp/netscan/attach_device, a payload like $(reboot) would be transformed to $(REBOOT). Linux and native Linux file systems are case-sensitive, which means any such payload would fail to execute the desired program. So, I needed to find a way to call binaries using only uppercase letters, alphanumeric symbols, and numbers.

My first thought was I would likely need to use shell expansion and environment variables since they typically use uppercase names. I did a bit of Googling and came across this page about bash parameter expansion, which seemed like a viable way to construct a working payload. Specifically, I thought substring expansion could be used slice up pieces of environment variables to grab the characters needed to build a payload. With this in mind, I checked what environment variables were available in the shell for root (the UA_Parser process runs as root) and quickly realized I would have to get pretty creative given what was was there.


I played around with different patterns in the shell while connected via serial and eventually found one that made use of both parameter and filepath expansion which would expand out to /sbin/reboot built up from characters sliced from the $PATH env variable


In this context, the ? symbol is used to match up to a single character — the expansion works because reboot is the only binary in /sbin that starts with r followed by 5 characters. The screenshot below shows the final payload constructed piece by piece.


The final step was to place this payload within a shell context using command substitution so that once the string expanded out it would be interpreted as a command. To do this I wrapped the payload inside $() syntax and set everything up to do a test run. After getting a DHCP lease with the crafted request and attaching to the running UA_Parser process, I loaded the Attached Devices page in the web UI and caused net-scan to run. Easy peasy, right?

Yeah, right. It’s never that easy.

more constraints: html encoding

The screenshow below shows the debugger output when catching the vulnerable call to system() with the payload above.


This is when I discovered there was some filtering happening and certain characters are HTML encoded. Specifically, the parentheses characters are encoded in the payload above. I went back through code for net-scan and found the function responsible for the encoding — the full list of encoded characters are:

  • < > ( ) & ' " \

I also learned that net-scan truncates the hostname string read from the DHCP host list at 32 characters before writing it to /tmp/netscan/attach_device.

Pretty rough! Almost every useful character (in regards to shell manipulation) is encoded and there’s a pretty tight length limit, which only makes writing a functional exploit that much more difficult. But, first I had to get code execution, so I pushed on.

success: backquote command substitution

There was really only one other option left to get to get command execution considering parenthese were filtered and that was the use the older backquote form of substitution:


I did the usual dance to connect and send a DHCP request while attached to the UA_Parser process with GDB. After triggering net-scan again, I caught the call to system() and saw that a new chain of calls had occurred and reboot had been called. The device then began to reboot!


And there it is, a working proof-of-concept showing the bug could be exploited…

escaping constraints: arbitrary code execution

Okay, but rebooting the device isn’t particularly cool. Now it was time to think about what could actually be done with the bug.

To recap, the contraints for the payload are:

  • 32 character length limit
  • All letters get uppercased
  • Filters chars: < > ( ) & ' " \
  • must build payload from chars in env variables + file/parameter expansion:
    • PATH=/usr/sbin:/usr/bin:/sbin:/bin
    • HOME=/tmp

The length limit was probably the most frustrating part of this whole thing when combined with the uppercasing issue — it could easily take 7-9 characters to do the expansion needed to grab a single character for the final payload, so using up those 32 characters was very easy. The inability to use > or < also meant using redirection to overwrite files wasn’t an option.

With all of this in mind, I narrowed down the possible attack scenarios to the following:

  • Leak admin credentials back to the attacker somehow
  • Reset the admin password
  • Download and execute a script to run arbitrary commands without dealing with constraints

I spent a couple of days experimenting with what felt like hundreds of payloads and possible angles to achieve one of the results above. Again, that length limit SUCKED — on multiple occasions I had figured out working payloads that ended up exceeding the limit by 1-2 characters and immediately became useless. At the end of each session I would tell myself I would give up and that it just wasn’t possible to do anything useful given the restrictions. Then the next day when I signed back on I would get sucked back in, convinced there just had to be a way.

going after curl

After tons of failed attempts, I eventually came to a conclusion: in order to do anything useful, I would need to find a way to break out of / get around the length limit. Having determined this, I knew the only feasible way forward would be to find a way to download a script from a remote source and run it, which would avoid the uppercasing issues, length limits, etc. With this in mind, I shifted my focus to figuring out a way to use the curl or wget binaries on the router to achieve this.

This finally paid off after another couple of hacking sessions when I figured out the following payload:


The first part makes use of expansion to match /usr/bin/curl, which is used to make a request to a server ( in this example), and pipes the contents of the response to a shell (pointed to by $0).

In order to test this and show it working without having to actually go out and buy a two character domain (which apparently go for anywhere between $500 - $15k), I edited /etc/hosts on the device to add a record pointing to my ‘evil’ server where I was running a Python web server that would respond with the contents of a shell script to requests for the root path (to use as few characters as possible).

The screenshot below shows everything in action, going counter-clockwise starting at the top-left:

  • the udhcpc process that sent the payload
  • the code for the Python webapp that returns code to spawn a reverse shell
  • the running webapp showing requests were received from the Orbi
  • (open telnet session, unrelated)
  • the netcat listener receiving the connection from reverse shell



There it is: there’s now an exploit for CVE-2020-27861 and it can be used to completely take over a vulnerable Orbi device. In total, it took about a week to go from starting to initial analysis to finally getting arbitrary code execution, with most of the time being spent on figuring out how to create a payload to actually do something useful given the restrictions. It was pretty fun and I was able to pick up some new techniques for dealing with payload constraints for command execution.

The most important lesson learned? Persistence pays off (usually). I really didn’t think a full exploit would be possible and almost gave up before getting full system control, but I’m glad I kept pushing. I’ll probably start spending more time doing this kind of n-day research and monitoring advisories for specific devices/applications to hopefully be able to do this for a more recent vuln.