Skip to main content

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 = p.payload

found it:

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 =[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 =[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.

$ ./

usage: /pentest/wireless/ [-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:

$ ./ -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.

$ ./ -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!


This is written for Python 2.6 and may require modifications for other versions of 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")

def GetSsidAndApName(packet):

p = packet #.getlayer(Dot11Elt)

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

return (bssid, essid, ap)

files = []
essids = []

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"

verbose = False
for opt, arg in opts:
if opt == '-h':
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):
elif opt == '-f':
elif opt == '-e':
elif opt == '-v':
verbose = True

if len(files) == 0:

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]"


Popular posts from this blog

Extracting Users from LinkedIn via Burp

We do a lot of pen tests and red teaming at Red Siege. Part of reconnaissance includes gathering a list of employees from a target organization. Typically, those usernames will be used in either phishing or password spray attacks (trying a few passwords across a long list of users). LinkedIn is a treasure trove of information! I'm going to use my good friends at Black Hills Information Security as my guinea pigs (sorry, and thanks!). The tool is here. First, let's look at what the data from LinkedIn looks like a response.

After performing a search for "Black Hills Information Security" we can look at the requests and responses. LinkedIn includes all the user information in responses to "/voyager/api/mux".

We can click the "Next" button a few times in our search to load multiple pages of info. Now, for the extraction. First, select everything in the "HTTP history" with Ctrl+A or Command+A on macOS. Second, right click in the top portion. …

Beyond Net User - Part 1: Limitations of the "Net" commands

I've had a number of cases where the Windows "net user", "net group", and "net localgroup" have failed me. I've had SQLMap fail to give the last line of "net user" output, I've had "net group /domain" not give me the full names (I still don't get how that failed!). On top of that, the commands don't support wildcards. Also, the output of those commands is a pain to parse due to the columns. I'd much prefer to use the AD PowerShell cmdlets, but those aren't always available. I set to find other ways to get the same data. First, let's look at the limitations of the "net" commands.
Net command limitations Hiding Groups in Groups Often when pen testing and red teaming, we would like to figure out information about the domain, most notably the members of the Domain Admins group. Output of the net group "domain admins" command as shown below.

It shows three members: Administrator, sqlagent,…

Beyond Net User - Part 2: DS Commands

In the previous post we discussed some of the limitations of Net commands. Most notably, the output limitation (doesn't show all groups) and it doesn't allow for flexible searching. In this post we'll discuss the DS commands to get around these limitations.
DSGet, DSQuery, DS* While these tools are useful, they aren't always available. As a pen tester and red teamer, I have to live with what I can find on the systems I come across. I find that these tools are still more widespread than the latest PowerShell Active Directory cmdlets, at least on non-system administrator systems. Here is a useful Stack Overflow post on the subject. Recursive Searches In the last post, we discussed a limitation in net group in that it doesn't show groups in other groups. The DS commands do! As a reminder, let's take a look at what we saw with net group when looking at the list of domain administrators.

Now let's do the same search, but use the dsquery and dsget.
dsquery group -…