~8 min read

Your router might be a security nightmare: Tales from Pwn2Own Toronto 2022

Three years ago, Neodyme took aim the "SOHO Smashup" category at Pwn2Own Toronto 2022, targeting a Netgear RAX30 router and an HP M479fdw printer. We successfully gained remote code execution on both devices, pivoting from the router to the printer. In this post, we dive into the technical details of our router exploitation journey, resulting in reliable code execution via a MAC address lookup service.
Authored by:

This blogpost is the second part of our two-part series looking back at our accomplishments and journey to Pwn2Own 2022 in Toronto. The previous instalment detailed our journey in exploiting the HP M479fdw printer. We also cover our journey through the 2024 edition of Pwn2Own in Cork, Ireland, starting with the first post on exploiting the Canon imageCLASS MF656Cdw printer!

TL;DR

Back in 2022, Neodyme competed at Pwn2Own Toronto, uncovering several vulnerabilities in the Netgear RAX30 router. By chaining these bugs into a full “SOHO Smashup” (Small Office/Home Office) exploit, we earned a $50,000 prize. This blog post walks through the technical details of that journey — from initial discovery to a successful WAN-side exploit.

Intro

Participating in Pwn2Own had been on my to-do list for a long time and three years ago, I finally gave it a shot. Together with my colleagues at Neodyme, we decided to target the “SOHO Smashup” category at Pwn2Own Toronto 2022. The goal: exploit a router via its WAN interface, then pivot to another device on the LAN. Our target chain involved a Netgear RAX30 router and an HP M479fdw printer.

Netgear devices had already appeared in two previous Pwn2Own competitions, so we had a few solid write-ups from other researchers to learn from. That helped us gain a head start in understanding the target. After all, a router is really just a Linux box with an antenna, right?

Attack Surface Discovery

We started by just downloading the official firmware and extracting it with binwalk. The firmware was unencrypted and the rootfs already showed many interesting binaries. But with such a large attack surface, it is important to rule out the mundane parts as much and soon as possible.

This can easily be done dynamically by looking a which services are running and which services are exposed, and all we need for this is a shell. After spending some time exploring potential entry points, we found our first bug that gave us a shell — but unfortunately, it didn’t meet the criteria for Pwn2Own :( However, during the exploitation process, we stumbled upon an even easier way to get a shell: The router automatically executes PHP files placed on a USB thumb drive. ¯\_(ツ)_/¯

By uploading some generic shell.php (taken from the internet), we quickly gained a reverse shell, giving us a foothold on the device and allowing us to move forward.

With access to the running system, tools like ps and netstat helped us narrow down the attack surface, letting us focus on the services that were left.

To speed things up, we used Ghidra and its scripting capabilities to batch decompile all binaries, then used string search to look for common bug patterns.

Eventually, one of my colleagues discovered a bug in the JSON parser of the fing_dil binary. The fing service is used to map MAC addresses to device names and is enabled by default. It queries all MAC addresses from a database and sends them to https://netgear-devrecog.fing.io/2/devrecog, which in return responds with JSON data like this:

{
  "devices": [
    {
      "recognition": {
        "type-group-name": "Network",
        "mac-vendor": "LG Electronics",
        "rank": 75,
        "model": "Galaxy S6",
        "os-name": "Android",
        "type": "ROUTER",
        "brand": "Apple",
        "os-ver": "5.0.0",
        "type-name": "Router"
      },
      "mac": "001122334455"
    }
  ],
  "networks": [
    {
      "recognition": {
        "InternetInfo": {
          "hostname": "leet.host.name",
          "regioncode": "RM",
          "city": "Rome",
          "timezone": "America/Los_Angeles",
          "countrycode": "IT",
          "isp": "Verizon",
          "organization": "Verizon",
          "continentcode": "EU"
        }
      }
    }
  ]
}

Below, we provide the code that interprets the response. Maybe you can spot the bug? However, this snippet is just an example — there are more fields that are vulnerable to the same bug:

Response parsing code

Response parsing code

The code is retrieving the length of the JSON property value via strlen and is not restricting the size to the length of the destination buffer used in the following memcpy after that.

Yes, this is just a basic stack buffer overflow ;), but the binary will surely have stack canaries, right?

Yes, this is state of security in a major brands router in 2022, yikes. They could have at least also made the stack executable :(

Exploitation

To exploit the bug we would need to MITM the request but the traffic is SSL encrypted. Unfortunately to them, they explicitly disabled certificate validation in curl, so that didn’t pose a problem.

This exploit only works since the code explicitly disables both the host and the general certificate verification in curl. Please do not attempt that configuration at home ;)

Since by then, we already had code execution on the router, we immediately attached gdb to the process to help us with harnessing the overflow.

For testing, we redirected the request for the fing-api to our own server, responding with a cyclic pattern in the device type. With this, we successfully crashed the application.

State of the executable after our payload

State of the executable after our payload

…and gained control over the registers r4 to r11 and pc, awesome!

Since the checksec output earlier showed us that the binary has neither stack canaries nor PIE, this exploit should be ROP101, ezpz.

Well, the fing_dil binary isn’t compiled as PIE, but it’s loaded at a fixed base address of 0x00010000. Unfortunately, that address begins with a null byte as its most significant byte. Since we are using strlen, our input gets cut off early, as we stop at a nullbyte :( . Additionally, system-wide ASLR is enabled, so external library addresses are randomized.

These circumstances limit our control over the stack. We can only use a one-gadget, and it has to be located within the binary itself; plus that gadget would need to create a reverse shell. Of course, there’s always a chance that the perfect gadget for these harsh conditions exists, but this was not the case for us this time.

But since we are CTFers we got creative and had a shell the next day anyway ;)

As mentioned earlier, our primitive gave us control over registers starting from r4. This also means that r0 to r3 remained untouched, with r2 and r3 containing valid data. It should be noted that an invalid address read crashes the program and the service wouldn’t restart automatically.

Leveraging our arbitrary read primitive, we tried to leak a library address by jumping back into the HTTP request handling code. The idea was to have the router send us a pointer address as the value of the Content-Length header, and then respond with our overflow payload again to retain control over the execution flow.

If successful, the next step would be to leak a got table address and do a textbook ROP from this point. However, this approach often failed to return a libcurl address; instead, we would receive a pointer to the heap. It remains a mystery why this happened, but the behaviour definitely helped us in the following steps.

If the leaked heap address points to somewhere > 0x10000000 we are good to go and may create a nullbyte-free ROP chain. Otherwise, we need to rely on luck. We noticed that most heap allocations with a size of 16MB end up around 0xB00..., so after spraying af few crafted objects onto the heap, we could somewhat assume that 0xAF010101 had been allocated with our data and hope for the best with a one-shot exploit path.

Regardless of which path succeeded, we eventually achieved remote code execution. To persist access, we created a cronjob that spawned a minimal reverse SSH server, providing us with an interactive session. This is accomplished by jumping to a function in the binary that allowed us to create a file in /var/spool/cron/crontabs, the directory used by the Cron service.

On the Pwn2Own Main Stage

Like advertisements of toothpaste, we only rated the effectiveness or success rate of our exploit around 99% and at the competition we were reminded of that fact, as our exploit failed in our first try :(

But as with everything that’s performed live, these things happen and our second attempt worked without issues and let us continue our SOHO smashup chain with the HP M479fdw printer, which you can read more about in our previous blogpost. Overall we learned quite a lot about fault-proofing our exploit for the competition and were eager to try again at more Pwn2Own competitions. Don’t forget to check out the first blogpost in the next series about our exploits and setup for 2024 in Ireland!

We at Neodyme thrive in complex challenges and are always eager to dive deep into custom protocols or architectures, providing expert level insights to our clients. Just get in touch if you need research driven assessments tailored to your needs, we are happy to face those challenges!

Share: