It wouldn’t be the first time on a penetration test (and certainly won’t be the last) that I’ve come across a target vulnerable to an exploit for which a proof-of-concept (PoC) for only one firmware version or architecture. In this case, the router of concern is the Tenda Networks AC15 WiFi router, which is no longer supported by the manufacturer but can helpfully be emulated using the emux framework championed by embedded device expert Saumil Shah .
The vulnerability under consideration here is CVE-2018-5767, a buffer overflow in the "password" field of a Cookie header in a web request, because it's not like that's the oldest trick in the book. The discoverer of the vulnerability redacted critical offsets from the released PoC because the vendor had not patched the bug at publication (several months after disclosure to the vendor). However, since then, patched firmware has been released, so what’s the harm in fixing this PoC?
Why Is It Always The “Password” Field?
The exploit under examination in this section (as well as the story behind its discovery) is the original work of Fidus Information Security. It is entirely described in their blog  and is codified as 44253 in the Exploit Database  and exploits an unguarded sscanf onto the stack in the parsing of the “password” Cookie in a GET request.
To explain briefly how the exploit works, it uses a return-oriented attack to bypass data execution prevention in a classic return to the system call in libc. The device utilizes weakly enforcing address space layout randomization (ASLR), so the location of libc cannot be accurately guessed (the overflow is not paired with a memory leak). Still, the device has a 'watchdog' process that monitors the health of the web management console and respawns it if it crashes. Thus, the exploit can be repeated ad infimum, if necessary, until the linker launches httpd with the library in the presumed location. (Since ASLR is weakly enforced on this device, this will occur with probability 1-in-256.)
The firmware emulated in the emux framework is not vulnerable to this exploit. Still, there are many tricks hardware hackers can use to obtain current or older firmware versions, such as downloading it from the manufacturers' website. Other options include downloading the firmware from a crowdsourced firmware location such as drivers.softpedia.com, which hosts a vulnerable version .
The firmware found at the referenced link and the firmware used in emux both function with the same set of libraries, so the exploit-generation steps here can be repeated by simply following the emux instructions, then unpacking the firmware to a vulnerable version (as in the link) and copying the /bin/httpd binary from the vulnerable firmware over the same binary in the emulated system.
Finally, it should be mentioned that experienced firmware hackers may own specialty hardware that can be used to scrape the data from the memory of a running exemplar. This is an admirable level of dedication to exploit development, but it is beyond the scope of this post.
Trust Your Tools!
The following treatment uses three classic tools to follow along. The first is the binary extraction tool binwalk by ReFirmLabs , which is indispensable in unpacking firmware. The second is ropper, a gadget-finding tool developed and maintained by security professional Sascha Schirra . Lastly, readelf from GNU's binutils will complete the puzzle, and comes pre-installed on most distributions of Linux. On a standard distribution of Kali Linux, both can be installed from standard repositories following a synchronization update by simply entering the following:
It should also be noted that these are not the only tools that can accomplish the task, and any hacker interested in memory corruption should endeavor to try them all. (For instance, ROPgadget can do anything that ropper can, and even objdump from GNU's binutils can accomplish the task of finding gadgets.) To throw a weaponized version of the exploit for an actual attempt, you will need either an exemplar of the hardware or run the web server under emulation using QEMU or a framework like emux, mentioned above.
So, with a vulnerable copy of the firmware downloaded from one of the referenced resources (or scraped from the device itself), use binwalk to extract the complete file system with the command like the following:
binwalk -e US_AC15V1.0BR_V15.03.1.16_multi.rar
Be Dynamic at Runtime
The first puzzle piece is the offset at which point the overflow takes over the program counter (pc, the equivalent of eip/rip on ARM.) The classic way to do this on x86/x64 would be to run the binary under a debugger on a system controlled by the exploit developer and or include enough 0x41 characters (or an appropriately random pattern) and diagnose the offset by reading off eip or rip after the segmentation fault once the buffer gets large enough.
On an embedded device that doesn’t give the user console access, this can be a little bit trickier but is not impossible with a little effort.
The dynamic route is more successful but more challenging to get to work. With the unpacked firmware in hand, it might be possible to emulate the program using qemu while chrooted in the base of the file system of the unpacked firmware. This can also cause all sorts of problems, especially if the firmware unpacks with links such as to the /proc/ file system or /dev/null, which are created by the operating system and won’t be available in chroot.
If this is the case and there are no easy workarounds, an exploit developer can use qemu for a complete system emulation, and hope that the embedded device’s file system is compatible enough with the emulated system for libraries and executables to be copied back-and-forth. Luckily, IoT exploit champion Saumil Shah has already done this for the Tenda AC15 in the emux framework. (Links follow in the References.) The httpd binary in that emulation has been patched for this CVE, but you can overwrite it with a version from the vulnerable firmware, and the web server will still work.
Figure 1: The output of curl –cookie “password=`python3 –c \”print(‘A’*512 + ‘.gif’)\”`” http://127.1 shows that the vulnerability can still be replicated under emulation.
This won’t be the case for every executable. It only works here because the executable does not use position-independent code, and the required libraries do not carry any significantchanges between the patched and unpatched versions.
Two Heads Are Better Than One (Or Did Ghidra Have Three?)
Alternatively, the offset can be read quite easily using an open-source tool released by an organization that the InfoSec community has never ever been given a reason to distrust. Ghidra’s parameters are named by their type (which is often guessed) and their distance from the top of the function’s stack frame.
Figure 2: The suffix of acStack_1c4 is 128 higher than the suffix of local_144, reflecting the size of the array. The reverse ordering reflects the downward growth of the stack.
Knowing that in ARM, the four bytes at the top of the stack reflect the value of the saved link register, to which the function returns after completion, the value of the necessary offset can be determined from the suffix of the variable that is written to cause the stack overflow, minus four to account for the fact that that is the address execution will be redirected to upon a return.
Figure 3: The unguarded sscanf commands on line 152 and 155, copying from a local pointer directly to the stack, indicate what variable contains the overflow and how large of a buffer is needed.
It’s Nothing to Get Offset About
The next set of offsets needed to repair this exploit are the addresses of the system call in libc and the two gadgets used to redirect program execution there. For the former, GNU binutils' readelf utility gives a ready answer using the following command from the root directory of the extracted squashfs filesystem:
readelf -a lib/libc.so.0 | grep ' system$'
The two gadgets from libc are similarly discovered in single commands using ropper. From the root directory of the extracted squashfs filesystem:
will find them both. The second command will generate several results. Technically, any of them can be used, since the blx instruction (Branch with Link+Exchange) will redirect the program counter to r3.
For those unfamiliar with exploitation on ARM systems: the first gadget will pop values from the stack into the registers r3, r4, r7, and pc -- three general purpose registers then the equivalent of eip/rip on ARM). The exploit leaves the address of system at the top of the stack, causing it to be popped into r3. After padding to be popped into r4 and r7, the exploit pops the address of the next gadget into the program counter, redirecting execution there. At this point, the mov instruction puts the address of the stack pointer (sp) – and due to the nature of the overflow, the command to be run -- into r0, which this architecture uses to carry the first argument to a function. Finally, the blx instruction redirects execution to the value in r3 where the address of the system call will be waiting. Because of the efficiency gadgets like this will add to exploits, compilers not running strict optimizations will typically avoid creating sequences that can be interpreted as mov r0, sp.
Figure 4: ropper delivering a gadget that gives an attacker control of far more registers than they need for this exploit. The address on the left is the offset into libc.so.0 where this gadget can be found.
All Your Library Base Address Are Belong to Us
The final piece of the puzzle is the most difficult: discovering the address that httpd will have loaded libc into. It will also be the hardest, as this cannot be determined definitively due to the presence of Address Space Layout Randomization (ASLR), which will randomize the memory locations of loaded libraries. Unlike figuring out the offsets to gadgets from the base of the library, these locations will change on every program execution. Unlike figuring out the length of the buffer, this cannot be done under emulation: qemu will load emulated process libraries in different locations than when run natively, and GDB doesn’t randomly assign the address space.
Some things work in our favor. Since this device is 32-bit, ASLR only offers 8 bits of randomization, so there are only 256 locations in which this library can be loaded. The author of the original PoC for CVE-2018-5767 takes advantage of this and the watchdog process that respawns httpd after crashes to throw the exploit repeatedly until the library is loaded into an address predicted in advance. The question is: how is such a prediction to be made?
The easiest way is to get console access to an exemplar device, either by connecting to the serial port or chaining another set of exploits known to work on the device, such as CVE-2018-5770. Then, the file /proc/`pidof httpd | cut -d ' ' -f 1`/exe can be parsed for the base address of the read-execute (r-xp page). Any address will work as a candidate for a candidate base for libc. This will work fine … if you already have a shell on the device. (If you are, why are you developing an exploit?) It is more challenging if you do not.
There are several steps to this, but going through them makes a good guess as to where libc might be linked at runtime. Follow along:
Install qemu_system and create or download a system image as close to your exemplar device as possible. (The Debian project maintains several generations of Debian across many different architectures.) Install build-essential (for gcc) and your favorite editor, vim.
Deactivate ASLR by entering the following as root: echo 0 > /proc/sys/kernel/randomize_va_space.
Write a program that needs no more than libc and compile it with gcc. Run ldd on the compiled binary and identify the load address of /lib/ld-linux.so.3. This is a “start address” for the library loading space.
Identify the libraries loaded by your target location by running the following: readelf –a /path/to/target/binary | grep NEEDED
Add together the sizes of each library from the device’s firmware (except for libc) loaded by the target binary (round each up to the nearest 0x1000 as that is the smallest page size Linux will allocate).
The sum of the observations from Steps 3 and 5 will give a decent guess as to where libc might get loaded. The closer the system installed in Step 1 is to the target system, the more accurate it will be. Without an exemplar, the closest that can be obtained would be to find a statically compiled ldd for your target firmware, unpack the target system’s firmware on the system spun up in Step 1, and run ldd against the target binary. At the same time, chroot‘d inside the root of the file system of the unpacked firmware.
Figure 5: A simple program demonstrating the base address of loaded libraries and mapped memory for Debian on 32-bit little-endian ARM. The closer that the system, processor, CPU, and loaded libraries match those of the target, the closer the predicted library locations will be.
This is more than just an academic exercise in resurrecting an old vulnerability. The vendor patched the AC15’s firmware in a 2019 release sometime after the article's publication. However, the same vulnerability reappears in CVE-2022-44172 in the model AC18, CVE-2023-25211 in the model AC5, and CVE-2023-27016 in the AC10 , all by the same manufacturer. It is entirely likely that this vulnerability may have existed across this entire product line.
If that were the case, the same general methods used above can also port the exploit to those platforms. The same gadgets might not be available, and the overflow length may have changed due to code refactoring across versions, but control of the same registers would point attackers in the right direction.
Want proof? Here’s the exploit working on the AC9, which isn’t even covered by the existing CVEs.
Figure 6: The presence of /tmp/pwned and the corefile show that the exploit can be successfully ported across platforms to the AC9, for which this bug hasn’t even been previously described. The one-shot success was achieved by knowing the base address of libc in advance, and the hard-coded root password is well-documented.
NB: This device relies pretty heavily on its tmpfs and restarts if it fills up, so if you’re using it as a router because your new one hasn’t gotten here from Amazon yet and you’re grinding memory addresses while Law and Order is on in the background and suddenly your Smart TV reports a loss of connection then it’s probably because you filled up /tmp with corefiles and caused your router to reboot.