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