Old CVEs Leading to New Vulns - Reverse Engineering TrendNet-731BRv1

About The Project

I recently bought an discontinued TrendNet Router to become more proficient at reverse engineering embedded systems. Each year at DEF CON, the IoT Village,and Embedded Village have CTFs/hands-on workshops, and I’m hoping to get my skills up to par to go and take a crack at one of them next year.

TrendNet home router model “TEW-731BRv2” has a known vulnerability identified by CVE-2015-1187) that leads to remote code execution. This CVE effects numerous other routers as well due to code reuse across routers. This router has been discontinued since 2015, and patches for said vulnerability are available. This target system gives the perfect opportunity to perform root cause analysis on said firmware, diff patched and un-patched versions, and ultimately dig deeper into scaling this type of analysis.

This blog post is written from my perspective of success, failures, and blunders for this side project. I will walk through firmware extraction, root cause analysis, hunting for vulnerable code patterns in a variety of other firmware samples and ultimately discovering a new command injection vulnerability.

Background Research & Obtaining The Firmware

CVE-2015-1187 impacted numerous D-Link routers and specifically TrendNet router TEW-731BR v2. I purchased TrendNew TEW-311BRv1 to see if the vulnerability was present in this router and because it was less than twenty dollars on e-bay. While its possible to leverage qemu to emulate the firmware, having a physical device avoids the headaches that come with setting up an emulated environment. The product page contains firmware going back to 2011. This presents the opportunity to potentially flash back to older firmware to poke at older vulnerabilities and perform binary diffing between firmware releases with Ghidra to see what was patched. With this in hands, $20 usd seems like a reasonable investment for said side project.


Googling “CVE TrendNet TEW-731BR” revealed a CVE page with a CSS score of 9.8. The advisory goes onto say

The ping tool in multiple D-Link and TRENDnet devices allow remote attackers to execute arbitrary code via the ping_addr parameter to ping.ccp.

Breaking down the advisory, the ping web interface appears to be parsing user input insecurely leading to command injection and code execution. Neat, but what stood out to me piece is “D-Link and TRENDNet devices”. Code reuse across platforms brings up an interesting question; how do we know there aren’t more devices vulnerable to the same code injection? I’ll table that question for now, and explore it in a later section. Next, lets start looking for PoC code.

Returning to google, there’s also an exploit-db listing for a Metasploit module that exploits CVE-2015-1187 for D-Link devices. Analyzing the Ruby script, we see that the targeted URI is /ping.ccp, and some arguments provided for the POST request are “ccp_act” and “ping_v6”. Using this information, we’ll have some hardcoded string values to hunt for in the firmware.

Returning to google one-more time, and navigating to Rapid 7’s write-up on this vulnerability, we find the vulnerability exists within the ncc binary.

Now that we have plenty of information about the vulnerability and where it lives, we can dive into reversing the actual firmware.

Analyzing The Firmware - TrendNet TEW-731BRv1

Both the D-Link and TrendNet firmware are available as zip files on their respective websites. After unzipping the files, a binary file is presented in both the D-Link and TrendNet firmware. The TrendNet firmware also has release notes per-firmware zip file. This is yet another source of information to leverage in order to identify what (if anything) has changed that is of interest in between firmware releases. Interesting, there’s no notes about an ncc service or “ping” in the release notes below.

Model: TEW-731BR
Hardware Version: v1.0R/v1.1R

Firmware Version: 1.03b01
Release Date: 6/2015
-Fixed UPnP miniigd security vulnerability issue.
Firmware Version: 1.02b05
Release Date: 6/2014
-Fixed TFTP security vulnerability issues.
Firmware Version: 1.02b03
Release Date: 6/2014
-Allow port 65535 to be filtered in IP/Protocol Filters
Firmware Version: 1.01b09
Release Date: 12/2012
-Added IPv6 Support
-Fixed IEEE 802.3az power saving issue
-Fixed fastpath issue that prevents some websites from displaying completely
-Added Auto Lock Down State to prevent against WPS PIN attacks
-Updated graphical user interface (GUI) items
Firmware Version: 1.00b29
Release Date: 3/2012
-Fixed wireless authentication failure when "$" character is used in WPA2-PSK AES passphrase
-Fixed Russia L2TP connection issue
Firmware Version: 1.00b27
Release Date: 12/2011

All of these bin files are squashfs file systems and easily extracted via executing binwalk -e $FIRMWARE_VERSION_GOES_HERE. With the firmware extracted, the find command can be executed to easily identify the ncc binary via find /path/to/firmware -iname ncc -type f.


Running file on this binary reveals it’s a 32-bit MIPS binary that’s dynamically linked.

ncc: ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, no section header

Because it’s dynamically linked, additional shared objects will be loaded at runtime to provided necessary functionality to the ncc binary. The readelf utility reveals the following shared objects required by the ncc binary.

Dynamic section at offset 0x140 contains 31 entries:

  Tag        Type                 Name/Value
0x00000001 (NEEDED)     Shared library: [libleopard.so]
0x00000001 (NEEDED)     Shared library: [libncc_comm.so]
0x00000001 (NEEDED)     Shared library: [libpthread.so.0]
0x00000001 (NEEDED)     Shared library: [libresolv.so.0]
0x00000001 (NEEDED)     Shared library: [libc.so.0]
0x00000001 (NEEDED)     Shared library: [libgcc_s_4181.so.1]
0x0000000f (RPATH)      Library rpath: [/lib]

The shared objects libleopard.so, libncc_comm.so, libpthread.so.0, libresolv.so.0, libc.so.0, libgcc_s_4181.so.1 all live within the /lib directory of the squash filesystem. When loading a binary in Ghidra, it’s possible to specify what additional paths to search for in order to load and help reverse engineering efforts. The image below shows how to configure this at binary load time to ensure these Shared Objects also get loaded to aid in the reverse engineering process.


Now that the binary is loaded, analysis can begin! But wouldn’t it be nice if we had debugging symbols?

No Symbols? No problems - Hunting for Artifacts in GPL Code Releases

Referencing the earlier file command output, we see that the binary is dynamically linked and does not have debug symbols.

ncc: ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, no section header

Here’s where I got a lucky break thanks to the GPL license! The GPL license has a requirement for users that leverage GPL code to release the code they modified. Given that the router’s using the Linux kernel, I figured maybe I could find a local privesc or some old vulnerability to exploit locally, thus chaining the ipv6 ping RCE with a local privesc to obtain root.

The GPL tar files contained object files (compiled, but not linked to libc) of ELF binaries with debug symbols. This was a big find, and leveraging the nm utility, function names of interest like doPingv4 and doPingv6 are discovered.

00412934 T callback_ccp_ping
004127bc t cancelPing
00411e20 t doPingV4
00412218 t doPingV6 <---- WHOA what's this?!
00464e24 t fakePing
004584fc T ping_test
0045fe64 T probePortMapping
004124d0 t queryPingV4Ret

Analyzing the Vulnerable Function - doPingv6

I started with the function doPingV6 because of the Metasploit module referencing ping_v6. Browsing to this function stub and leveraging Ghidra’s decompiled output, a few interesting function calls can be seen. Some value is obtained via a form (assuming the web form), and then passed to a function called system_read. The arguments of system_read suggest a concatenation with a known command string followed by user input obtained from a web form and potentially passed to “system”, if we believe the function name system_read.

If this assumption is true, then the command injection vulnerability is plain as day. User input gets substituted into a string and executed, easy-peasy code execution. However, we must find this “system_read” function to truly understand what’s going on.


Hunting for the exported symbol of system_read, the function is found in libleopard.so.

[0x00002d70]> iE | grep -i 'system'
115 0x00007850 0x00007850 GLOBAL FUNC   108      _system
156 0x000078bc 0x000078bc GLOBAL FUNC   104      _system_read
200 0x00007924 0x00007924 GLOBAL FUNC   28       _system_close

Popping this Shared Object into Ghidra and navigating to system_read shows a simple, but dangerous function. User input appears to be concatenated into a buffer of 1024 bytes, and then executed via popen. Popen’s man page states that The popen function opens a process by creating a pipe, forking, and invoking the shell.... What we’ve got here folks is some good old fashion arbitrary code execution from taking user input from a webform, performing unsanitized string substitution and then passing the data to popen for execution.


There’s also a CTF-esque problem here as well. The parameters from user arguments are below the fixed length buffer of 1024 bytes. If user input from the webform were to exceed 1024 bytes, it would begin overwriting parameter values. Better yet, a quick checksec shows that modern memory protections are not enabled!

$> pwn checksec ./_TEW731BRV1_FW100B27.bin.extracted/squashfs-root/sbin/ncc

    Arch:     mips-32-big
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      No PIE (0x400000)
    Stack:    Executable
    RWX:      Has RWX segments

This is very much a CTF problem come to life! (example: tamu19_pwn1_ctf_problem) However, since RCE is already obtained via command injection, it doesn’t make sense to do this.

With the root cause of the vulnerability understood, lets revisit the question, how would one hunt for this code snippet in N-number of binaries to understand what’s affected? To hunt for this vulnerable code across a binary code base, I’ll be using BigGrep to index and perform N-gram byte searching for specific values of interest.

BigGrep - Searching for Binary Values N-bytes at a Time

BigGrep is a utility released by Carnegie Mellon University Software Engineering Institute (CMU-SEI) to perform byte searching across a large index of binary files. The original paper on this software was published in 2012 and can be found here. BigGrep leverages N-gram indexing to store binaries in a given index to make searching fast. The academic paper states SEI uses this for millions of help with searching millions of malware samples. For this project, I’m using BigGrep for about 5 firmware samples. Is it overkill? Sure, but if you consider the value of having already indexed binary files to perform retro-hunts (much like a malware analysis would in VirusTotal) of vulnerable code as new vulnerabilities are discovered in popular libraries; it starts to become an interesting idea. While this same approach could be achieved with yara rules, an indexed database is easier to archive and move around than a bunch of binaries. Finally, identifying vulnerable code reuse across N-binaries allows researchers to identify what other products may be effected by a given dependency such as a Shared Object.

Within a corpus of TrendNet ncc binaries, bgindex will create a local index of all ncc binaries. The md5sum shows each file contains a different hash , thus code changes are present. Finally, the actual indexing of the ncc files via bgindex is shown.

$> ls -l
total 4956
-rwxr-xr-x 1 dllcoolj dllcoolj 575960 Oct 24 18:27 2011_dec_22_ncc
-rwxr-xr-x 1 dllcoolj dllcoolj 612576 Oct 24 18:28 2012_april_6_ncc
-rwxr-xr-x 1 dllcoolj dllcoolj 769360 Oct 24 18:28 2013_jan_1_ncc
-rwxr-xr-x 1 dllcoolj dllcoolj 776476 Oct 24 18:29 2014_Dec_ncc
-rwxr-xr-x 1 dllcoolj dllcoolj 776476 Oct 24 18:30 2014_June_26_ncc
-rwxr-xr-x 1 dllcoolj dllcoolj 776364 Oct 24 18:31 2014_June_3_ncc
-rwxr-xr-x 1 dllcoolj dllcoolj 776476 Oct 24 18:34 2015_June_8_ncc

$> md5sum *
444fa772c664252576fb74a271407e07  2011_dec_22_ncc
6f7e9d85a3b5aa76c71bc02bf872843e  2012_april_6_ncc
e425b8a25474986bebe7bdac8b89f29d  2013_jan_1_ncc
330d0f102236c5907be8efe47e0a2802  2014_Dec_ncc
330d0f102236c5907be8efe47e0a2802  2014_June_26_ncc
80d3ea8984d7b28f80662e3829765774  2014_June_3_ncc
3d1f2dcf08b8cf41da3f5987bd91c055  2015_June_8_ncc

$> ls -1 | bgindex -p /tmp/bgi/firmwareidx -v

The image below shows the output of bgindex indexing each NCC file to be searched. This occurred relatively quickly due to the few samples in the corpus.


Next, the bgsearch Python utility searches for the desired hex values (but could also be a string) against a BigGrep index. Highlighted in green below in Ghidra’s disassembly listing bytes containing the vulnerable system call to _system_read, the pingv6 string and 8 additional bytes are used to hunt for .


The values shown in the terminal output show these bytes are identified in every firmware version except for two, 2012_april_6_ncc and 2013_jan_1_ncc.


Upon loading these firmware’s ncc binaries, there were single byte differences, thus the search command came up blank. That’s kind of interesting, and really shows the value in crafting well formed byte searches. With that tangent out of the way, back to the router!

The Fun Realization of TrendNet-731BRv1 vs TrendNet-731BRv2

After reversing the vulnerable doPingv6 function, I was ready to throw an exploit against the real router. The only issue? TrendNet-731BRv1 does not support IPv6! When I tried to ping the IPv6 localhost address (::1) on the router, a result of “fail” was returned.


Upon referencing user manuals of both TrendNet-731BRv1 and TrendNet-731BRv2 it was clear that v2 supported IPv6, while there was no mention of IPv6 in v1’s manual. The vulnerable code in the ncc binary exists on v1, but there is no way to invoke it. Womp womp.


With IPv6 out of the picture, I went onto analyze doPingv4. Perhaps there’s still a chance for command injection here? Sure enough there was a command string being built based on user data, but there were no calls to the system_read function previously reversed. In doPingv4, a check for specific characters (\ ; | `` ) is performed to determine how to construct a command string that is ultimately passed to system. The command itself is:

"(ping -c 1 -W 2 -w 3 %s | grep \"received\" || echo \"0 0 0 0\") | awk \'{print $4}\' > %s &"

The %s above is where user input commands is inserted from the web form. The result of the ping, is parsed by awk and then written to /var/tmp/pingtest indicating success or failure. If code execution was achieved, it’s what’s known as blind execution as there’s no output of the commands being executed coming returning to the end user. In situations like this, one element you might be able to control is time.

By injecting a sleep command its possible to identify whether or not you delayed execution of ping, thus achieving code execution on the target device. The very blurry gif below demonstrates this execution with a simple ping to show the immediate response followed by executing a command with sleep that extends the time it takes for the command to be processed. This demonstrates command execution on the router via the doPingV4function has been achieved.


While sleep is great, what else can we do? Taking a look at what functionality the Busybox binary has compiled in and leverage this for followed on code execution. Busybox is the “swiss army knife of embedded Linux”. Numerous unix utilities are bundled into a single executable and then on an embedded filesystem, symlinks provide usage of these bundled utilities. For example, the bin directory of the squashfs file system has utilities such as “awk”, “ash” and “adduser”, but in reality they’re all just symlinks to busybox.

total 14M
drwxr-xr-x 1 dllcoolj dllcoolj 1.2K Oct 29 13:48 .
drwxr-xr-x 1 dllcoolj dllcoolj  158 Oct 29 14:02 ..
-rwxr-xr-x 1 dllcoolj dllcoolj 9.8K May 25  2015 acltd
lrwxrwxrwx 1 dllcoolj dllcoolj    7 May 25  2015 adduser -> busybox
lrwxrwxrwx 1 dllcoolj dllcoolj    7 May 25  2015 ash -> busybox
-rwxr-xr-x 1 dllcoolj dllcoolj 174K May 25  2015 auth
-rwxr-xr-x 1 dllcoolj dllcoolj   87 May 25  2015 auth-fail
lrwxrwxrwx 1 dllcoolj dllcoolj    7 May 25  2015 awk -> busybox
-rwxr-xr-x 1 dllcoolj dllcoolj  23K May 25  2015 brctl
lrwxrwxrwx 1 dllcoolj dllcoolj    7 May 25  2015 bunzip2 -> busybox

Its also possible to leverage qemu to emulate the mips binary on your current machine to see if there are any additional features bundled into the version of Busybox you have that are not symlinked to files on the filesystem. Executing squashfs-root sudo chroot . ./qemu-mips-static -E LD_LIBRARY_PATH="/lib" ./bin/busybox produced the following output:

BusyBox v1.13.4 (2015-05-25 17:45:21 CST) multi-call binary
Copyright (C) 1998-2008 Erik Andersen, Rob Landley, Denys Vlasenko
and others. Licensed under GPLv2.
See source distribution for full notice.

Usage: busybox [function] [arguments]...
   or: function [arguments]...

	BusyBox is a multi-call binary that combines many common Unix
	utilities into a single executable.  Most people will create a
	link to busybox for each function they wish to use and BusyBox
	will act like whatever it was invoked as!

Currently defined functions:
	adduser, ash, awk, brctl, bunzip2, bzcat, cat, chmod, cp, crond,
	cut, date, dmesg, dos2unix, echo, expr, false, free, getty, grep,
	halt, head, hostname, ifconfig, init, ip, kill, killall, klogd, ln,
	login, ls, mkdir, mount, netstat, passwd, ping, ping6, poweroff,
	ps, reboot, renice, rm, route, sh, sleep, syslogd, tail, top, true,
	umount, unix2dos, uptime, wc

Poweroff stands out as a visual demo gif to end this blog with. Taking the previous payload of IPADDRESS $(cmd_here), I’ll substitute $(cmd_here) for poweroff and disable the target device. More creative post-execution commands are left as an exercise to the reader :)


From what I can tell, there is no publicly documented acknowledgement of a command injection vulnerability in the IPv4 ping functionality of this router. A new vulnerability in a 5+ year old product was discovered by playing around with old CVEs. That’s pretty neat. While its not going to land any crazy bug bounty, it gives me the ability to discuss my process of “RE to PoC” process in my next interview. After all, isn’t it the journey we’re all after anyway?


The process of recreating an exploit from a known vulnerability allows you to understand what went wrong where, how you could fix it, and find similar vulnerabilities in newer products. Additionally, you may get lucky and find new vulnerabilities along the way! Leveraging all publicly available information can greatly guide the reverse engineering process and avoid rabbit holes and unnecessary purchases of older hardware :).

If this type of project excites you, CVE advisories are an excellent place to dive into, but pivoting on proof-of-concepts help you further uncover what another researcher already has allowing you to stand on the shoulders of giants and fill in your knowledge gaps. Treat CVEs like CTF challenges and known exploit code like CTF write-ups. Even if you have the “way” of solving the challenge (i.e exploiting the vulnerability with a PoC script), its on you to fully understand the why.

Thank you for reading, if you found this interesting/useful, please share!