Scapy is an interesting tool. It can be used to generate arbitrary network traffic, and to view traffic on your local network interfaces. Whilst I don’t believe it can be used to completely man in the middle (MitM) traffic (i.e. it can sit between the application and TCP/IP stack on computer 1, but not between the TCP/IP stacks of computer 1 and computer 2), the ability to manualy generate traffic can be very useful. FYI if you want to MitM traffic between systems, I believe Ettercap/Bettercap can be used for this if you are suitably positioned (e.g. grab traffic from eth0, tamper and forward on to eth1).
To have something to play around with, I’ve written a basic Python Guest Book. It makes use of Python3 sockets, so it goes over TCP/IP. If you capture the traffic of someone reading the guest book, it’s super basic.
At the start, you’ll see the standard SYN/ACK TCP handshake. In this example I’m reading the guest book, meaning that the client is sending the server the message code “1” and then my nickname “Vic”.
All fairly standard so far. So, what can we do here with Scapy? Let’s go in to some Scapy basics.
First up, let’s run Scapy. You can install it using apt, or whatever tool you use.
sudo apt install scapy sudo scapy
From here, we can sniff some packets. If you’ve used Wireshark before, you’ll be familiar with the need to select the network interface that you wish to sniff. In Scapy, you can start a capture and display the summaries of sniffed packets with the following command:
We’re using “lo” or the loopback interface here as the guest book app is hosted on 127.0.0.1. I’d recommend running apps on this when messing with network traffic, as there’s way less noise. You can of course change the interface to “eth0” or whatever else you’re using if the system you’re interacting with is remote.
Scapy makes use of network layer encapsulation. I won’t go in to the OSI model here, but in Scapy you can specify the protocol to be used at each layer. Our end goal is to have our application data encapsulated by the TCP layer, encapsulated by the IP layer. This would be described in Scapy as:
Our basic packet can be seen here:
It might get a bit messy if we specified the entire format of the packet here though. Scapy lets us create variables which inherit the various attributes of the layer they’re based on. So instead of the above, we could use:
The guest book app runs on 127.0.0.1:65432. So what happens if construct a packet and send it there? Packets are sent by passing them to the send() method:
However, this packet wasn’t “seen” by the guest book server. Maybe it’s because we’re just sending a packet without running through the usual TCP handshake process? How do we do that in Scapy? Thankfully, I found a Stack Overflow discussion on the matter: https://stackoverflow.com/questions/26480854/3-way-handshake-in-scapy
So let’s recreate what the recommended solution in that thread was:
The “sport” value seen here is the “source port” number. When the initial assignment is run, a random value from the unprivileged port range is chosen. FYI you’ll want to rerun this if you want a different port number. From here we’ve conducted our packet, with the flag “S”. This is how to set the “SYN” flag in Scapy. Basically, we should be able to send this packet, and expect to receive back a SYN-ACK packet. But, in this case I did not.
We can see the packet being sent, but the guest book server didn’t respond. Let’s take a look at Wireshark, and compare the SYN being generated by us here to the SYN being generated by the guest book client during a legitimate connection.
We’re seeing some different flags being set here. Maybe that’s the issue? How do we set these flags in Scapy?
IMO, this is an annoying point with Scapy. It uses different names for options on packets to other tools such as Wireshark. They also do not appear to be readily documented. After trying to guess at the names, I managed to find some of them sort of documented in an issue raised on the Scapy project on Github: https://github.com/secdev/scapy/issues/1459
The options that we could see in Wireshark were:
You will notice, a lot of the names are different from those seen in our list. We’ve set:
- MSS: Maximum segment size, specifies how big of a TCP data segment the system can handle
- SAckOK: Selective acknowledgment, a TCP mechanism to reduce the amounts of packets needing to be reset during data loss. I don’t know why Scapy called the field that, or why it’s set with an empty byte.
- Timestamp: TSval is the timestamp of the system clock. If it’s sent, Tsecr should be set in the responding ACK packet. If we set the Timestamp value, Scapy auto sets the TSval and Tsecr values appropriately.
- WScale: Controls the TCP sliding window mechanism
So what does the server make of us setting these values? In my case, nothing. It still didn’t accept them. I’m guessing because the timestamp was being manually set by me, it make something appear wrong? God knows. At this point I started looking in to if Scapy has anything to handle sockets, since the guest book is written to use Python sockets. Turns out, it does! Information is available here: https://scapy.readthedocs.io/en/latest/layers/tcp.html
Let’s try that out:
Aaaand it worked fine. It did all the negotiations for us. I was able to send raw data in the expected format (i.e. “1 vic” to read the guestbook with nickname vic), and receive back data in the expected format. Success! We could now perform testing of how the server handles unexpected or malformed packets. In this case, not very well because I am a lazy dev.
If anyone can figure out why I couldn’t manually initiate the handshaking process, please do let me know. It’d be cool to be able to do it manually!
Edit: I’ve tried adding the ethernet frame manually, but still no joy. It was pointed out that the MAC address was being set incorrectly in my Scapy SYN packets, so this was an attempt to rectify that.
Note: The previously used send() command was appropriate for sending IP packets; if you want to send ethernet frames, you will need to use the sendp() method.