Scapy is a powerful Python-based interactive packet manipulation program and library. It can be used to forge or decode packets for a wide number of protocols, send them on the wire, capture them, match requests and replies, and much more.
This iPython notebook provides a short tour of the main Scapy features. It assumes that you are familiar with networking terminology. All examples where built using the development version from https://github.com/secdev/scapy, and tested on Linux. They should work as well on OS X, and other BSD.
The current documentation is available on http://scapy.readthedocs.io/ !
Scapy eases network packets manipulation, and allows you to forge complicated packets to perform advanced tests. As a teaser, let's have a look a two examples that are difficult to express without Scapy:
1_ Sending a TCP segment with maximum segment size set to 0 to a specific port is an interesting test to perform against embedded TCP stacks. It can be achieved with the following one-liner:
In [30]:
send(IP(dst="1.2.3.4")/TCP(dport=502, options=[("MSS", 0)]))
2_ Adanced firewalking using IP options is sometimes useful to perform network enumeration. Here is more complicate one-liner:
In [31]:
ans = sr([IP(dst="8.8.8.8", ttl=(1, 8), options=IPOption_RR())/ICMP(seq=RandShort()), IP(dst="8.8.8.8", ttl=(1, 8), options=IPOption_Traceroute())/ICMP(seq=RandShort()), IP(dst="8.8.8.8", ttl=(1, 8))/ICMP(seq=RandShort())], verbose=False, timeout=3)[0]
ans.make_table(lambda x, y: (", ".join(z.summary() for z in x[IP].options) or '-', x[IP].ttl, y.sprintf("%IP.src% %ICMP.type%")))
The easiest way to try Scapy is to clone the github repository, then launch the run_scapy
script as root. The following examples can be pasted on the Scapy prompt. There is no need to install any external Python modules.
git clone https://github.com/secdev/scapy --depth=1
sudo ./run_scapy
Welcome to Scapy (2.4.0)
>>>
Note: iPython users must import scapy as follows
In [13]:
from scapy.all import *
With Scapy, each network layer is a Python class.
The '/'
operator is used to bind layers together. Let's put a TCP segment on top of IP and assign it to the packet
variable, then stack it on top of Ethernet.
In [2]:
packet = IP()/TCP()
Ether()/packet
Out[2]:
This last output displays the packet summary. Here, Scapy automatically filled the Ethernet type as well as the IP protocol field.
Protocol fields can be listed using the ls()
function:
In [ ]:
>>> ls(IP, verbose=True)
version : BitField (4 bits) = (4)
ihl : BitField (4 bits) = (None)
tos : XByteField = (0)
len : ShortField = (None)
id : ShortField = (1)
flags : FlagsField (3 bits) = (0)
MF, DF, evil
frag : BitField (13 bits) = (0)
ttl : ByteField = (64)
proto : ByteEnumField = (0)
chksum : XShortField = (None)
src : SourceIPField (Emph) = (None)
dst : DestIPField (Emph) = (None)
options : PacketListField = ([])
Let's create a new packet to a specific IP destination. With Scapy, each protocol field can be specified. As shown in the ls()
output, the interesting field is dst
.
Scapy packets are objects with some useful methods, such as summary()
.
In [3]:
p = Ether()/IP(dst="www.secdev.org")/TCP()
p.summary()
Out[3]:
There are not many differences with the previous example. However, Scapy used the specific destination to perform some magic tricks !
Using internal mechanisms (such as DNS resolution, routing table and ARP resolution), Scapy has automatically set fields necessary to send the packet. This fields can of course be accessed and displayed.
In [10]:
print(p.dst) # first layer that has an src field, here Ether
print(p[IP].src) # explicitly access the src field of the IP layer
# sprintf() is a useful method to display fields
print(p.sprintf("%Ether.src% > %Ether.dst%\n%IP.src% > %IP.dst%"))
Scapy uses default values that work most of the time. For example, TCP()
is a SYN segment to port 80.
In [9]:
print(p.sprintf("%TCP.flags% %TCP.dport%"))
Moreover, Scapy has implicit packets. For example, they are useful to make the TTL field value vary from 1 to 5 to mimic traceroute.
In [11]:
[p for p in IP(ttl=(1,5))/ICMP()]
Out[11]:
Currently, you know how to build packets with Scapy. The next step is to send them over the network !
The sr1()
function sends a packet and return the corresponding answer. srp1()
does the same for layer two packets, i.e. Ethernet. If you are only interested in sending packets send()
is your friend.
As an example, we can use the DNS protocol to get www.example.com IPv4 address.
In [23]:
p = sr1(IP(dst="8.8.8.8")/UDP()/DNS(qd=DNSQR()))
p[DNS].an
Out[23]:
Another alternative is the sr()
function. Like srp1()
, the sr1()
function can be used for layer 2 packets.
In [47]:
r, u = srp(Ether()/IP(dst="8.8.8.8", ttl=(5,10))/UDP()/DNS(rd=1, qd=DNSQR(qname="www.example.com")))
r, u
Out[47]:
sr()
sent a list of packets, and returns two variables, here r
and u
, where:
r
is a list of results (i.e tuples of the packet sent and its answer)u
is a list of unanswered packets
In [48]:
# Access the first tuple
print(r[0][0].summary()) # the packet sent
print(r[0][1].summary()) # the answer received
# Access the ICMP layer. Scapy received a time-exceeded error message
r[0][1][ICMP]
Out[48]:
With Scapy, list of packets, such as r
or u
, can be easily written to, or read from PCAP files.
In [50]:
wrpcap("scapy.pcap", r)
pcap_p = rdpcap("scapy.pcap")
pcap_p[0]
Out[50]:
Sniffing the network is a straightforward as sending and receiving packets. The sniff()
function returns a list of Scapy packets, that can be manipulated as previously described.
In [52]:
s = sniff(count=2)
s
Out[52]:
sniff()
has many arguments. The prn
one accepts a function name that will be called on received packets. Using the lambda
keyword, Scapy could be used to mimic the tshark
command behavior.
In [53]:
sniff(count=2, prn=lambda p: p.summary())
Out[53]:
Alternatively, Scapy can use OS sockets to send and receive packets. The following example assigns an UDP socket to a Scapy StreamSocket
, which is then used to query www.example.com IPv4 address.
Unlike other Scapy sockets, StreamSockets
do not require root privileges.
In [79]:
import socket
sck = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # create an UDP socket
sck.connect(("8.8.8.8", 53)) # connect to 8.8.8.8 on 53/UDP
# Create the StreamSocket and gives the class used to decode the answer
ssck = StreamSocket(sck)
ssck.basecls = DNS
# Send the DNS query
ssck.sr1(DNS(rd=1, qd=DNSQR(qname="www.example.com")))
Out[79]:
Parts of the following examples require the matplotlib module.
With srloop()
, we can send 100 ICMP packets to 8.8.8.8 and 8.8.4.4.
In [25]:
ans, unans = srloop(IP(dst=["8.8.8.8", "8.8.4.4"])/ICMP(), inter=.1, timeout=.1, count=100, verbose=False)
Then we can use the results to plot the IP id values.
In [26]:
%matplotlib inline
ans.multiplot(lambda x, y: (y[IP].src, (y.time, y[IP].id)), plot_xy=True)
Out[26]:
The raw()
constructor can be used to "build" the packet's bytes as they would be sent on the wire.
In [8]:
pkt = IP() / UDP() / DNS(qd=DNSQR())
print(repr(raw(pkt)))
Since some people cannot read this representation, Scapy can:
In [10]:
print(pkt.summary())
In [18]:
hexdump(pkt)
In [11]:
pkt.show()
In [15]:
pkt.canvas_dump()
Out[15]:
Scapy has a traceroute()
function, which basically runs a sr(IP(ttl=(1..30))
and creates a TracerouteResult
object, which is a specific subclass of SndRcvList()
.
In [22]:
ans, unans = traceroute('www.secdev.org', maxttl=15)
The result can be plotted with .world_trace()
(this requires GeoIP module and data, from MaxMind)
In [23]:
ans.world_trace()
Out[23]:
The PacketList.make_table()
function can be very helpful. Here is a simple "port scanner":
In [29]:
ans = sr(IP(dst=["scanme.nmap.org", "nmap.org"])/TCP(dport=[22, 80, 443, 31337]), timeout=3, verbose=False)[0]
ans.extend(sr(IP(dst=["scanme.nmap.org", "nmap.org"])/UDP(dport=53)/DNS(qd=DNSQR()), timeout=3, verbose=False)[0])
ans.make_table(lambda x, y: (x[IP].dst, x.sprintf('%IP.proto%/{TCP:%r,TCP.dport%}{UDP:%r,UDP.dport%}'), y.sprintf('{TCP:%TCP.flags%}{ICMP:%ICMP.type%}')))
Scapy can be easily extended to support new protocols.
The following example defines DNS over TCP. The DNSTCP
class inherits from Packet
and defines two field: the length, and the real DNS message. The length_of
and length_from
arguments link the len
and dns
fields together. Scapy will be able to automatically compute the len
value.
In [119]:
class DNSTCP(Packet):
name = "DNS over TCP"
fields_desc = [ FieldLenField("len", None, fmt="!H", length_of="dns"),
PacketLenField("dns", 0, DNS, length_from=lambda p: p.len)]
# This method tells Scapy that the next packet must be decoded with DNSTCP
def guess_payload_class(self, payload):
return DNSTCP
This new packet definition can be direcly used to build a DNS message over TCP.
In [120]:
# Build then decode a DNS message over TCP
DNSTCP(raw(DNSTCP(dns=DNS())))
Out[120]:
Modifying the previous StreamSocket
example to use TCP allows to use the new DNSCTP
layer easily.
In [122]:
import socket
sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # create an TCP socket
sck.connect(("8.8.8.8", 53)) # connect to 8.8.8.8 on 53/TCP
# Create the StreamSocket and gives the class used to decode the answer
ssck = StreamSocket(sck)
ssck.basecls = DNSTCP
# Send the DNS query
ssck.sr1(DNSTCP(dns=DNS(rd=1, qd=DNSQR(qname="www.example.com"))))
Out[122]:
So far, Scapy was only used from the command line. It is also a Python module than can be used to build specific network tools, such as ping6.py:
In [ ]:
from scapy.all import *
import argparse
parser = argparse.ArgumentParser(description="A simple ping6")
parser.add_argument("ipv6_address", help="An IPv6 address")
args = parser.parse_args()
print(sr1(IPv6(dst=args.ipv6_address)/ICMPv6EchoRequest(), verbose=0).summary())
A lot of attack scenarios look the same: you want to wait for a specific packet, then send an answer to trigger the attack.
To this extent, Scapy provides the AnsweringMachine
object. Two methods are especially useful:
is_request()
: return True if the pkt
is the expected requestmake_reply()
: return the packet that must be sentThe following example uses Scapy Wi-Fi capabilities to pretend that a "Scapy !" access point exists.
Note: your Wi-Fi interface must be set to monitor mode !
In [129]:
# Specify the Wi-Fi monitor interface
#conf.iface = "mon0" # uncomment to test
# Create an answering machine
class ProbeRequest_am(AnsweringMachine):
function_name = "pram"
# The fake mac of the fake access point
mac = "00:11:22:33:44:55"
def is_request(self, pkt):
return Dot11ProbeReq in pkt
def make_reply(self, req):
rep = RadioTap()
# Note: depending on your Wi-Fi card, you might need a different header than RadioTap()
rep /= Dot11(addr1=req.addr2, addr2=self.mac, addr3=self.mac, ID=RandShort(), SC=RandShort())
rep /= Dot11ProbeResp(cap="ESS", timestamp=time.time())
rep /= Dot11Elt(ID="SSID",info="Scapy !")
rep /= Dot11Elt(ID="Rates",info=b'\x82\x84\x0b\x16\x96')
rep /= Dot11Elt(ID="DSset",info=chr(10))
OK,return rep
# Start the answering machine
#ProbeRequest_am()() # uncomment to test
NFQUEUE is an iptables target than can be used to transfer packets to userland process. As a nfqueue module is available in Python, you can take advantage of this Linux feature to perform Scapy based MiTM.
This example intercepts ICMP Echo request messages sent to 8.8.8.8, sent with the ping command, and modify their sequence numbers. In order to pass packets to Scapy, the following iptable
command put packets into the NFQUEUE #2807:
$ sudo iptables -I OUTPUT --destination 8.8.8.8 -p icmp -o eth0 -j NFQUEUE --queue-num 2807
In [ ]:
from scapy.all import *
import nfqueue, socket
def scapy_cb(i, payload):
s = payload.get_data() # get and parse the packet
p = IP(s)
# Check if the packet is an ICMP Echo Request to 8.8.8.8
if p.dst == "8.8.8.8" and ICMP in p:
# Delete checksums to force Scapy to compute them
del(p[IP].chksum, p[ICMP].chksum)
# Set the ICMP sequence number to 0
p[ICMP].seq = 0
# Let the modified packet go through
ret = payload.set_verdict_modified(nfqueue.NF_ACCEPT, raw(p), len(p))
else:
# Accept all packets
payload.set_verdict(nfqueue.NF_ACCEPT)
# Get an NFQUEUE handler
q = nfqueue.queue()
# Set the function that will be call on each received packet
q.set_callback(scapy_cb)
# Open the queue & start parsing packes
q.fast_open(2807, socket.AF_INET)
q.try_run()
When more logic is needed, Scapy provides a clever way abstraction to define an automaton. In a nutshell, you need to define an object that inherits from Automaton
, and implement specific methods:
@ATMT.state
decorator. They usually do nothing@ATMT.condition
and @ATMT.receive_condition
decorators. They describe how to go from one state to anotherATMT.action
decorator. They describe what to do, like sending a back, when changing stateThe following example does nothing more than trying to mimic a TCP scanner:
In [6]:
class TCPScanner(Automaton):
@ATMT.state(initial=1)
def BEGIN(self):
pass
@ATMT.state()
def SYN(self):
print("-> SYN")
@ATMT.state()
def SYN_ACK(self):
print("<- SYN/ACK")
raise self.END()
@ATMT.state()
def RST(self):
print("<- RST")
raise self.END()
@ATMT.state()
def ERROR(self):
print("!! ERROR")
raise self.END()
@ATMT.state(final=1)
def END(self):
pass
@ATMT.condition(BEGIN)
def condition_BEGIN(self):
raise self.SYN()
@ATMT.condition(SYN)
def condition_SYN(self):
if random.randint(0, 1):
raise self.SYN_ACK()
else:
raise self.RST()
@ATMT.timeout(SYN, 1)
def timeout_SYN(self):
raise self.ERROR()
TCPScanner().run()
In [7]:
TCPScanner().run()
Pipes are an advanced Scapy feature that aims sniffing, modifying and printing packets. The API provides several buildings blocks. All of them, have high entries and exits (>>) as well as low (>) ones.
For example, the CliFeeder
is used to send message from the Python command line to a low exit. It can be combined to the InjectSink
that reads message on its low entry and inject them to the specified network interface. These blocks can be combined as follows:
In [ ]:
# Instantiate the blocks
clf = CLIFeeder()
ijs = InjectSink("enx3495db043a28")
# Plug blocks together
clf > ijs
# Create and start the engine
pe = PipeEngine(clf)
pe.start()
Packet can be sent using the following command on the prompt:
In [ ]:
clf.send("Hello Scapy !")