This is the third and final post of a short series that demonstrate the creation of a simple security/penetration-testing application. The end-result is a simple NetBIOS Name Service spoofer, written in Python.
If you’re enjoying the packet analysis aspects of this, you might be interested in the SANS IP Packet Analysis course I’ll be teaching soon
In the previous post, we got nbnspoof to the point that it could sniff NetBIOS Name Service (NBNS) queries and responses, as well as a basic framework for the rest of the application. Today, I’m going to cover the additional steps we can take to make nbnspoof actually recognize when it should spoof a response, and craft the necessary packet to make the victim associate a given name with an IP address of our choice. This should conclude this series for the most part (although I may revisit it later if something interesting comes up).
If you want to follow along with the code, or just want to go ahead and start using nbnspoof (it’s in pretty good shape right now), here’s the current code:
First, let’s discuss some of the changes that were made to the last entry’s code. Most notable is the fact that we’ve changed some of the variables to be global:
global verbose global regexp global ip global interface global mac_addr
The reason for this change is that our get_packet() function will need to use this information, however the sniff() function does not pass the above data to get_packet() as an argument. The easiest solution to this was to simply make these user-specified options global. You’ll notice the addition of the “mac_addr” variable. This is meant to specify what MAC address the spoofed responses should have their source set as. There is also a command-line option added for this, and the usage() text reflects this:
-m The source MAC address for spoofed responses
I have decided to make this a required option, rather than defaulting to the actual MAC address of the interface. I believe that if one wants to use the actual MAC address, that should be a conscious decision. Typically, you would want this to reflect the address of the host on the local network that you are directing the victim to with your spoofing (if it is on the local network) or perhaps the gateway (if it’s outside the local network).
You’ll remember from the previous post that Scapy was dissecting NBNS responses as queries with some raw data stuck on the end. Because of this, we’ll be doing the packing and unpacking of IP addresses for responses ourselves. Here’s the code for that:
def pack_ip(addr): temp = IP(src=addr) return str(temp)[0x0c:0x10] def unpack_ip(bin): temp = IP() temp = str(temp)[:0x0c] + bin + str(temp)[0x10:] temp = IP(temp) return temp.src
You’ll notice that these two functions leverage Scapy’s ability to pack and unpack IP addresses by crafting an IP packet in memory and setting or reading its source address. This wouldn’t have been hard to do ourselves (it’s just four bytes, each representing a number in the dotted-quad format), but it seems cleaner to let Scapy do it for us, even if it is a bit of a hack. It’s nice how Scapy can go back and forth between using attributes of packets and binary string representations so easily. One benefit of this is that if, for whatever reason, one wanted to supply a domain name instead of an IP address for the -h option, Scapy would do the lookup and conversion to IP address for us.
Another issue we glossed over yesterday was how we were going to match queries with the regular expression the user provides. We compile the regular expression in main() so we don’t have to do it for each query:
regexp = re.compile(name_regexp,re.IGNORECASE)
Note that, since NetBIOS names are always going to be uppercase, we have specified the IGNORECASE option, so that the user-supplied regular expressions are case-insensitive. In our get_packet() function, we kick off the code block to craft fake NBNS responses with this test:
if query and regexp.match(pkt.QUESTION_NAME.rstrip(),1):
The .rstrip() function of strings in Python removes the trailing whitespace (remember that the names in NBNS packets are padded out to 15 characters). So, if the current packet is a query, and matches the regexp the user provided, we can move on to crafting and sending a response:
response = Ether(dst=pkt.src,src=mac_addr) response /= IP(dst=pkt.getlayer(IP).src,src=ip) response /= UDP(sport=137,dport=137)
One neat thing about Scapy is that it overloads the division operator (‘/’) for packets to make it a sort of concatenation/layering operator. Here we craft our response packet by giving it an Ethernet header, then tacking IP and UDP headers onto the end of it. The destination MAC and IP addresses are set to the source of the sniffed packet, and the source MAC and IP are set to the information supplied by the user. Next, we get into the creation of the NBNS section of the packet, starting with the information that Scapy can deal with:
response /= NBNSQueryRequest(NAME_TRN_ID=pkt.getlayer(NBNSQueryRequest).NAME_TRN_ID,\ FLAGS=0x8500,\ QDCOUNT=0,\ ANCOUNT=1,\ NSCOUNT=0,\ ARCOUNT=0,\ QUESTION_NAME=pkt.getlayer(NBNSQueryRequest).QUESTION_NAME,\ SUFFIX=pkt.getlayer(NBNSQueryRequest).SUFFIX,\ NULL=0,\ QUESTION_TYPE=pkt.getlayer(NBNSQueryRequest).QUESTION_TYPE,\ QUESTION_CLASS=pkt.getlayer(NBNSQueryRequest).QUESTION_CLASS)
This monster function call adds in a “NBNSQueryRequest” layer, and the arguments specify all the information needed to make this into a response. You’ll notice that the options we’re setting to actual values, we’re using the values from the response packet we sniffed in the first part of this series. The other options are being set from the corresponding information from the sniffed request, such as the transaction ID and name. An NBNS response requires some more data than just this, and Scapy won’t handle the rest for us, so we’ll add it to the packet as a “Raw” payload and pack it in ourselves:
response /= Raw() # Time to live: 3 days, 11 hours, 20 minutes response.getlayer(Raw).load += '\\x00\\x04\\x93\\xe0' # Data length: 6 response.getlayer(Raw).load += '\\x00\\x06' # Flags: (B-node, unique) response.getlayer(Raw).load += '\\x00\\x00' # The IP we're giving them: response.getlayer(Raw).load += pack_ip(ip)
I’ve separated this part out by field, and commented them based on how they’re named in Wireshark’s dissection, so that it’s not just one incomprehensible string of data. Something you want to pay attention to is that “Time to Live” field. I’m not sure how long Windows will really let you cache one of these responses, but this would be something to modify if you’re into playing around with this script. The time that’s hard-coded seems to be what Windows XP likes to hand out with its responses, though.
Finally, after packing in the last field, which is the IP address we’re making the name resolve to, we send the packet on its way:
sendp(response,iface=interface,verbose=0) if verbose: print 'Sent spoofed reply to #' + str(response.getlayer(NBNSQueryRequest).NAME_TRN_ID)
We specify the interface, and tell it to silence sendp’s visual confirmation of packet sending. If the user wanted verbose output, we print out a message saying that we spoofed a reply to the request with a specific transaction ID. The response we sent will be picked up by sniff() as well, so it will be displayed with a dissection as well in verbose mode.
That’s it! It works! Let’s see how it looks when we run it:
$ sudo ./nbnspoof.py -v -i vmnet8 -n ".*\..*" -h 172.16.185.1 -m 00:0c:29:27:be:ef 32949: Q SRC:172.16.185.133 DST:172.16.185.255 NAME:"HELLO.WOR " Sent spoofed reply to #32949 32949: R SRC:172.16.185.1 DST:172.16.185.133 NAME:"HELLO.WOR " IP:172.16.185.1 32950: Q SRC:172.16.185.133 DST:172.16.185.255 NAME:"XPPROTEST2 " 32950: R SRC:172.16.185.132 DST:172.16.185.133 NAME:"XPPROTEST2 " IP:172.16.185.132
In this test run, on the VMWare network between the host Linux machine (172.16.185.1) and the two Windows VMs (.132 and .133), I ran nbnspoof in verbose mode, set with a regular expression to only spoof responses for names that contain a period (‘.’). This is a quick-hack way of catching mistyped domain names and such, while letting most legitimate request local network systems go. I specified the Linux host for the IP address, and a silly MAC address with a VMWare vendor prefix. From “xpprotest”, I first pinged “hello.wor” which nbnspoof matched and spoofed for me, and then I pinged “xpprotest2″ which is (correctly) ignored.
So that settles it. We now have a nice tool for demonstrating how Windows name resolution can be spoofed in certain situations. More importantly, I hope some people have learned a bit about Scapy and the sort of procedure one might follow in developing a simple penetration testing application like this. Hopefully, this will be something you can apply to many situations .