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 >>> 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 >>> 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.
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 + "[-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 == "\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 in essids: print t + '\t' + t + '\t' + t + (('\t' + f) if verbose else '') except Exception, e: print e print "Send error to: tim[at]securitywhole.com" pass