Blog post

Patches, Collisions, and Root Shells: A Pwn2Own Adventure

Paul Gerste, Thomas Chauchefoin, Stefan Schiller

Vulnerability Researchers


  • Security
Patches, Collisions, and Root Shells: A Pwn2Own Adventure

At the end of last year, members of our Vulnerability Research team participated in Pwn2Own Toronto 2022. In the months following our article relating our experience during this event, vendors have released security updates to address the security issues we reported during the competition. This article describes the technical details of these vulnerabilities and outlines one could exploit these.

Pwn2Own - Discovered Vulnerabilities

Router vulnerabilities submitted as an entry for the Pwn2Own competition are divided into the attack vector categories LAN-side (exploitable from within the local network) or WAN-side (exploitable via the upstream ethernet port).

We discovered the following vulnerabilities when preparing for the competition:

  • 2 x LAN-side on the NETGEAR RAX30
  • 1 x WAN-side on the NETGEAR RAX30
  • 1 x WAN-side on the Synology RT6600ax

A last-minute patch published by NETGEAR right before the competition fixed the two LAN-side vulnerabilities and made our NETGEAR WAN-side vulnerability ineligible for Pwn2Own. Since the underlying vulnerability was still present, we reported it to ZDI shortly after the Pwn2Own competition (ZDI-23-839).

Our only valid entry for the competition was the WAN-side vulnerability on the Synology RT6600ax. Although we succeeded in demonstrating our exploit, teams used the same vulnerability before, making ours a duplicate.

We presented all the nitty-gritty details of these vulnerabilities at TyphoonCon 2023, and if you missed it we brought them here too! Let's dive into it. 

The LAN-side Vulnerabilities

Both LAN-side findings on the NETGEAR router were also identified and documented in great lengths by other researchers. As we shared in the introduction, they were also addressed by a last-minute patch from NETGEAR that made them invalid for the contest. 

Because both vulnerabilities are very similar and considered easy to spot and exploit, we won't cover them in this publication; please refer to the external publications will link if you want to know more. On the day on which we got our hands on the NETGEAR RAX30, we identified a LAN-side command injection on a service named puhttpsniff. This service is not directly listening on the network, but rather using netfilter to get packets – you could find it by looking at NFLOG entries in the firewall. The vulnerability was also identified at least by SEFCOM T0, Synacktiv, and NCC Group.

Shortly after, we found another command injection in the DHCP server of the NETGEAR router. The open-source daemon was customized to also call an external command to log information about the new DHCP leases. This, again, introduced a command injection vulnerability. This vulnerability was also identified at least by Starlabs.

We can now get into the more interesting findings!

NETGEAR RAX30 - cmsCli_authenticated Buffer Overflow RCE

Vulnerability Discovery

The telnet service is implemented in /bin/telnetd. The binary accepts connections on port tcp/23, forks a new process, and binds stdin/stdout to the socket connection. In order to authenticate connecting users, the function cmsCli_authenticate, implemented in the library, reads the username and password in an infinite loop. The password is read via getpass, which dynamically allocates a buffer and is not limited by size on glibc. The password is later copied to a 256-byte stack buffer using strcpy. This results in a classical stack-overflow (code snippets shortened for better readability):

int cmsCli_authenticate() {
    char username[256];
    char pwd[256];

    // infinite loop
    while (true) {
        // read username
        printf("Login: ");
        cli_readString(username, 0x100);
        // read password
        char *p = getpass("Password: ");
        if (p != 0) {
            // copy password to 256 byte stack-buffer -> OVERFLOW!
            strcpy(pwd, p); 

This is one of the many vulnerability types SonarCloud detects automatically. With the new Automatic Analysis feature for C&C++, it is not even required to manually set up your project. With just one click, you can feed the engine with the decompiled C source code and it will be analyzed without any setup pain (see it for yourself):

If you would like to know more about SonarCloud's Automatic Analysis for C and C++, have a look at our related blog post: No, C++ static analysis does not have to be painful.

Back to our code snippet: After the username and password are read, the function cmsLck_acquireLockWithTimeoutTraced is used to acquire a global mutex. The second parameter of this function (6000) defines the timeout in milliseconds for acquiring the mutex. If the mutex cannot be acquired within this timeout, the function fails (return value != 0) and the infinite loop is left:

        // acquire mutex with 6000ms timeout
        ret = cmsLck_acquireLockWithTimeoutTraced("cmsCli_authenticate", 6000);
        if (ret != 0) {
            // failed to acquire mutex? leave loop!
            goto FAILED_MUTEX;
        // ... perform actual authentication ...
     // ...

    // failed to acquire mutex -> log failure and leave function
    log_log(3, "cmsCli_authenticate", 0x73, "failed to get lock, ret=%d", ret);
    return ret;

If the mutex was successfully acquired, the function cmsDal_authenticate is called to perform the actual authentication. If this function returns 1, the authentication was successful and the infinite loop is left:

        // perform actual authentication
        ret = cmsDal_authenticate(&local_240, param_1, username, pwd);
        if (ret == 1) {
            // successfully authenticated
            log_log(7, "cmsCli_authenticate", 0xf1, "current logged in user %s perm=0x%x", currUser, currPerm);
            // leave function
            return 0;

If the authentication fails three times, the next login attempt is delayed 3 seconds:

        if (login_attempts < 3) {
            puts("Login incorrect. Try again.");
        else {
            printf("Authorization failed after trying %d times!!!.\n", login_attempts);
            login_attempts = 0;

Controlling the Instruction Pointer

Exploiting the stack overflow itself is straightforward. The vulnerable function (cmsCli_authenticate) is implemented in, which does not have stack canaries:

$ checksec ./lib/ 
    Arch:     arm-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found  # <--
    NX:       NX enabled
    PIE:      PIE enabled

Thus the return address on the stack can be overwritten without triggering a stack smashing detection. The only challenge is to actually reach the ret instruction of the vulnerable function. 

As we have seen, there are only two conditions, on which the infinite loop is left:

  • the authentication was successful
  • the global mutex cannot be acquired

Without assuming that we have valid credentials, the only viable option is to make the acquisition of the global mutex fail.

In order to do this, we can put a heavy load on the router/mutex, so that the acquisition of the mutex times out after 6000ms. A suitable function for this can be reached via the web interface. The CGI script /webs/tm_block/tm_block.cgi can be accessed unauthenticated and uses the global mutex in a very unfortunate way: After acquiring the mutex, some user-provided JSON data is parsed. After the data is parsed, the mutex is released:

void main() {
    // ...
    obj = json_tokener_parse(env.pSetQueryString);
    // ...
    // acquire mutex with 6000ms timeout
    ret = cmsLck_acquireLockWithTimeoutTraced(&DAT_00015306, 6000);
    if (ret == 0) {
        // parse user-provided JSON data
        json_object_object_get_ex(obj, "data", &local_24);
        // ...
        // release mutex

By providing a huge amount of data in the request body, the parsing takes a lot of time. During this time the mutex is locked. By making multiple, simultaneous requests to this endpoint a heavy load is put on the router/mutex and the mutex acquisition of the telnet service eventually fails. This way the ret instruction of the vulnerable function can be reached and we can control the instruction pointer:

Overcoming Null-Byte Restrictions

After controlling the instruction pointer, we can create the actual exploit. The telnetd binary itself is compiled without PIE, which means we can use gadgets from it:

$ checksec ./bin/telnetd
    Arch:     arm-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x10000)  # <--

One challenge still remains: The base address of telnetd is 0x10000 and thus the upper byte of any gadget address is null. Also, the choice of gadgets is very limited without a suitable pivot gadget. Thus we need more than one gadget, which requires us to write null bytes.

This time, though, the infinite loop comes in very handy since we can exploit the stack-overflow multiple times. We can iteratively write all null-bytes by replacing these with a placeholder value and then reinsert the null-byte starting from the last one up to the first null-byte:

This technique enables us to write an arbitrary ROP chain onto the stack. The overall strategy for this ROP-chain looks like this:

  • leak libc base address
  • calculate the address of gets and system
  • call gets to read user input to a static, writable address
  • call system with this address

This way, an arbitrary system command can be executed. The following video demonstrates the exploit by establishing a reverse shell:

Last-minute patch

The vulnerability could initially be exploited via the WAN interface due to a misconfiguration of the IPv6 firewall, which made the telnet service accessible via the IPv6 link-local address of the router. Unfortunately, this misconfiguration was fixed right before the end of the registration period for Pwn2Own. Since the buffer-overflow vulnerability was still present, it could be exploited via the LAN interface.

Disclosure and patch

We reported the vulnerability to ZDI outside the Pwn2Own competition (ZDI-23-839, CVE-2023-34285) and NETGEAR released Hot Fix on 05/31/2023 to address it. The patch adds an additional memset call to initialize the password destination buffer, which prevents the before-mentioned null-byte technique. Also, the call to strcpy is replaced with strncpy limiting the amounts of bytes copied:

char pwd[256];
char *p = getpass("Password: ");
memset(pwd, 0, 256);
strncpy(pwd, p, 255);

Synology RT6600ax - dhcpcd WAN RCE

The Synology RT6600ax firmware SRM 1.3.1-9346 Update 2 uses dhcpcd 1.3.22-pl1 as a DHCP client to get an IP address from its WAN-side upstream router. When the device receives a configuration via DHCP, the newly assigned IP address and other values supplied by the DHCP server are written to the file /etc/dhcpc/

These values are stored in the same way you declare shell variables, with an uppercase name, an equal sign, and the value. These values are not encoded nor sanitized in the process–they can be arbitrary strings. Remote attackers have very limited control over values like IP addresses, but more control over other DHCP options like domain-name.

After receiving a configuration via DHCP, the client invokes the shell script at /etc/iproute2/script/dhcpcd-up that runs some commands to properly configure the device's networking. This script evaluates the previously written file (/etc/dhcpc/, which constitutes a Command Injection vulnerability via DHCP-supplied values:

Since the dhcpcd client runs the shell script as root, any sub-shell command will also be run as root. An attacker can use this to run arbitrary commands and compromise the device:


Synology addressed this vulnerability by using their utility synogetkeyvalue to parse and extract values from the file in a way they don't need to evaluate it as a shell script.


During our Pwn2Own live attempt for this entry took we were able to successfully exploit the vulnerability to retrieve a root shell on the router. 

Unfortunately, we were picked as the very last during the random drawing to determine the order of attempts and another team already leveraged the same vulnerability before us making this a duplicate. Nevertheless, we were still satisfied to at least get one successful entry through. 

The vulnerability is tracked as ZDI-23-662 / CVE-2023-32955.

Pwn2Own - Summary

The Pwn2Own competition is not only fun but also a great opportunity to contribute to the security of popular consumer devices. There are a lot of interesting targets and the discovered vulnerabilities have a real impact on the security of these devices. During the competition, an astonishing amount of 63 unique zero days were reported; congratulation to all the participants for these findings.

From a technical point of view, it is interesting to note that most of the system daemons deployed on the routers we look at are based on open-source implementation with some customizations. These changes are not done by the original developers of these daemons, and are very prone to vulnerabilities!

In addition to the great contribution to the security of these devices, there is a lot of educational content created around the Pwn2Own competition. The writeups and articles created by attendees are a great resource for all security researchers. We are keeping track of publications and adding these to the corresponding Wikipedia article; feel free to add any missing references!

At last, many thanks to the ZDI for this great event! We are looking forward to the next Pwn2Own competition.

Related Blog Posts

Get new blogs delivered directly to your inbox!

Stay up-to-date with the latest Sonar content. Subscribe now to receive the latest blog articles. 

  • Follow SonarSource on Twitter
  • Follow SonarSource on Linkedin

© 2008-2023, SonarSource S.A, Switzerland. All content is copyright protected. SONAR, SONARSOURCE, SONARLINT, SONARQUBE and SONARCLOUD are trademarks of SonarSource SA. All other trademarks and copyrights are the property of their respective owners. All rights are expressly reserved.