Usage

Scapy’s interactive shell is run in a terminal session. Root privileges are needed to send the packets, so we’re using sudo here:

$ sudo scapy -H Welcome to Scapy (2.4.0) >>>

On Windows, please open a command prompt ( cmd.exe ) and make sure that you have administrator privileges:

C:\>scapy Welcome to Scapy (2.4.0) >>> 

If you do not have all optional packages installed, Scapy will inform you that some features will not be available:

INFO: Can't import python matplotlib wrapper. Won't be able to plot. INFO: Can't import PyX. Won't be able to use psdump() or pdfdump(). 

The basic features of sending and receiving packets should still work, though.

Interactive tutorial

This section will show you several of Scapy’s features with Python 2. Just open a Scapy session as shown above and try the examples yourself.

You can configure the Scapy terminal by modifying the ~/.config/scapy/prestart.py file.

First steps

Let’s build a packet and play with it:

>>> a=IP(ttl=10) >>> a >>> a.src ’127.0.0.1’ >>> a.dst="192.168.1.1" >>> a >>> a.src ’192.168.8.14’ >>> del(a.ttl) >>> a >>> a.ttl 64 

Stacking layers

The / operator has been used as a composition operator between two layers. When doing so, the lower layer can have one or more of its defaults fields overloaded according to the upper layer. (You still can give the value you want). A string can be used as a raw layer.

>>> IP() >>> IP()/TCP() > >>> Ether()/IP()/TCP() > >>> IP()/TCP()/"GET / HTTP/1.0\r\n\r\n" >> >>> Ether()/IP()/IP()/UDP() >>> >>> IP(proto=55)/TCP() > 

_images/fieldsmanagement.png

Each packet can be built or dissected (note: in Python _ (underscore) is the latest result):

>>> raw(IP()) 'E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01' >>> IP(_)   chksum=0x7ce7 src=127.0.0.1 dst=127.0.0.1 |> >>> a=Ether()/IP(dst="www.slashdot.org")/TCP()/"GET /index.html HTTP/1.0 \n\n" >>> hexdump(a) 00 02 15 37 A2 44 00 AE F3 52 AA D1 08 00 45 00 . 7.D. R. E. 00 43 00 01 00 00 40 06 78 3C C0 A8 05 15 42 23 .C. @.x FA 97 00 14 00 50 00 00 00 00 00 00 00 00 50 02 . P. P. 20 00 BB 39 00 00 47 45 54 20 2F 69 6E 64 65 78 ..9..GET /index 2E 68 74 6D 6C 20 48 54 54 50 2F 31 2E 30 20 0A .html HTTP/1.0 . 0A . >>> b=raw(a) >>> b '\x00\x02\x157\xa2D\x00\xae\xf3R\xaa\xd1\x08\x00E\x00\x00C\x00\x01\x00\x00@\x06x  \xa8\x05\x15B#\xfa\x97\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00 \xbb9\x00\x00GET /index.html HTTP/1.0 \n\n' >>> c=Ether(b) >>> c   ihl=5L tos=0x0 len=67 flags= frag=0L ttl=64 proto=TCP chksum=0x783c src=192.168.5.21 dst=66.35.250.151 options='' |  ack=0L dataofs=5L reserved=0L flags=S window=8192 chksum=0xbb39 urgptr=0 options=[] |>>> 

We see that a dissected packet has all its fields filled. That’s because I consider that each field has its value imposed by the original string. If this is too verbose, the method hide_defaults() will delete every field that has the same value as the default:

>>> c.hide_defaults() >>> c   frag=0 proto=TCP chksum=0x783c src=192.168.5.21 dst=66.35.250.151 |  chksum=0xbb39 options=[] |>>> 

Reading PCAP files

You can read packets from a pcap file and write them to a pcap file.

>>> a=rdpcap("/spare/captures/isakmp.cap") >>> a

Graphical dumps (PDF, PS)

If you have PyX installed, you can make a graphical PostScript/PDF dump of a packet or a list of packets (see the ugly PNG image below. PostScript/PDF are far better quality…):

>>> a[423].pdfdump(layer_shift=1) >>> a[423].psdump("/tmp/isakmp_pkt.eps",layer_shift=1) 

_images/isakmp_dump.png

assemble the packet

have a hexadecimal dump

have the list of fields values

for a one-line summary

for a developed view of the packet

same as show but on the assembled packet (checksum is calculated, for instance)

fills a format string with fields values of the packet

changes the way the payload is decoded

draws a PostScript diagram with explained dissection

draws a PDF with explained dissection

return a Scapy command that can generate the packet

return a JSON string representing the packet

Generating sets of packets

For the moment, we have only generated one packet. Let see how to specify sets of packets as easily. Each field of the whole packet (ever layers) can be a set. This implicitly defines a set of packets, generated using a kind of cartesian product between all the fields.

>>> a=IP(dst="www.slashdot.org/30") >>> a  >>> [p for p in a] [, , , ] >>> b=IP(ttl=[1,2,(5,9)]) >>> b >>> [p for p in b] [, , , , , , ] >>> c=TCP(dport=[80,443]) >>> [p for p in a/c] [>, >, >, >, >, >, >, >] 

Some operations (like building the string from a packet) can’t work on a set of packets. In these cases, if you forgot to unroll your set of packets, only the first element of the list you forgot to generate will be used to assemble the packet.

On the other hand, it is possible to move sets of packets into a PacketList object, which provides some operations on lists of packets.

>>> p = PacketList(a) >>> p >>> p = PacketList([p for p in a/c]) >>> p

displays a list of summaries of each packet

same as previous, with the packet number

displays a graph of conversations

displays the preferred representation (usually nsummary())

returns a packet list filtered with a lambda function

returns a hexdump of all packets

returns a hexdump of the Raw layer of all packets

returns a hexdump of packets with padding

returns a hexdump of packets with non-zero padding

plots a lambda function applied to the packet list

displays a table according to a lambda function

Sending packets

Now that we know how to manipulate packets. Let’s see how to send them. The send() function will send packets at layer 3. That is to say, it will handle routing and layer 2 for you. The sendp() function will work at layer 2. It’s up to you to choose the right interface and the right link layer protocol. send() and sendp() will also return sent packet list if return_packets=True is passed as parameter.

>>> send(IP(dst="1.2.3.4")/ICMP()) . Sent 1 packets. >>> sendp(Ether()/IP(dst="1.2.3.4",ttl=(1,4)), iface="eth1") . Sent 4 packets. >>> sendp("I'm travelling on Ethernet", iface="eth1", loop=1, inter=0.2) . ^C Sent 16 packets. >>> sendp(rdpcap("/tmp/pcapfile")) # tcpreplay . Sent 11 packets. Returns packets sent by send() >>> send(IP(dst='127.0.0.1'), return_packets=True) . Sent 1 packets.

Multicast on layer 3: Scope Identifiers

This feature is only available since Scapy 2.6.0.

If you try to use multicast addresses (IPv4) or link-local addresses (IPv6), you’ll notice that Scapy follows the routing table and takes the first entry. In order to specify which interface to use when looking through the routing table, Scapy supports scope identifiers (similar to RFC6874 but for both IPv6 and IPv4).

>>> conf.checkIPaddr = False # answer IP will be != from the one we requested # send on interface 'eth0' >>> sr(IP(dst="224.0.0.1%eth0")/ICMP(), multi=True) >>> sr(IPv6(dst="ff02::1%eth0")/ICMPv6EchoRequest(), multi=True) 

You can use both %eth0 format or %15 (the interface id) format. You can query those using conf.ifaces .

Behind the scene, calling IP(dst="224.0.0.1%eth0") creates a ScopedIP object that contains 224.0.0.1 on the scope of the interface eth0 . If you are using an interface object (for instance conf.iface ), you can also craft that object. For instance::

>>> pkt = IP(dst=ScopedIP("224.0.0.1", scope=conf.iface))/ICMP() 

Fuzzing

The function fuzz() is able to change any default value that is not to be calculated (like checksums) by an object whose value is random and whose type is adapted to the field. This enables quickly building fuzzing templates and sending them in a loop. In the following example, the IP layer is normal, and the UDP and NTP layers are fuzzed. The UDP checksum will be correct, the UDP destination port will be overloaded by NTP to be 123 and the NTP version will be forced to be 4. All the other ports will be randomized. Note: If you use fuzz() in IP layer, src and dst parameter won’t be random so in order to do that use RandIP().:

>>> send(IP(dst="target")/fuzz(UDP()/NTP(version=4)),loop=1) . ^C Sent 16 packets. 

Injecting bytes

In a packet, each field has a specific type. For instance, the length field of the IP packet len expects an integer. More on that later. If you’re developing a PoC, there are times where you’ll want to inject some value that doesn’t fit that type. This is possible using RawVal

>>> pkt = IP(len=RawVal(b"NotAnInteger"), src="127.0.0.1") >>> bytes(pkt) b'H\x00NotAnInt\x0f\xb3er\x00\x01\x00\x00@\x00\x00\x00\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x00' 

Send and receive packets (sr)

Now, let’s try to do some fun things. The sr() function is for sending packets and receiving answers. The function returns a couple of packet and answers, and the unanswered packets. The function sr1() is a variant that only returns one packet that answered the packet (or the packet set) sent. The packets must be layer 3 packets (IP, ARP, etc.). The function srp() do the same for layer 2 packets (Ethernet, 802.3, etc.). If there is no response, a None value will be assigned instead when the timeout is reached.

>>> p = sr1(IP(dst="www.slashdot.org")/ICMP()/"XXXXXXXXXXX") Begin emission: . Finished to send 1 packets. .* Received 5 packets, got 1 answers, remaining 0 packets >>> p   chksum=0x51dd src=66.35.250.151 dst=192.168.5.21 options='' |  code=0 chksum=0xee45 seq=0x0 |  |>>> >>> p.show() ---[ IP ]--- version = 4L ihl = 5L tos = 0x0 len = 39 id = 15489 flags = frag = 0L ttl = 42 proto = ICMP chksum = 0x51dd src = 66.35.250.151 dst = 192.168.5.21 options = '' ---[ ICMP ]--- type = echo-reply code = 0 chksum = 0xee45 > seq = 0x0 ---[ Raw ]--- load = 'XXXXXXXXXXX' ---[ Padding ]--- load = '\x00\x00\x00\x00' 

A DNS query ( rd = recursion desired). The host 192.168.5.1 is my DNS server. Note the non-null padding coming from my Linksys having the Etherleak flaw:

>>> sr1(IP(dst="192.168.5.1")/UDP()/DNS(rd=1,qd=DNSQR(qname="www.slashdot.org"))) Begin emission: Finished to send 1 packets. ..* Received 3 packets, got 1 answers, remaining 0 packets   src=192.168.5.1 dst=192.168.5.21 options='' |  |  nscount=0 arcount=0 qd= an= ns=0 ar=0 |>>> 

The “send’n’receive” functions family is the heart of Scapy. They return a couple of two lists. The first element is a list of couples (packet sent, answer), and the second element is the list of unanswered packets. These two elements are lists, but they are wrapped by an object to present them better, and to provide them with some methods that do most frequently needed actions:

>>> sr(IP(dst="192.168.8.1")/TCP(dport=[21,22,23])) Received 6 packets, got 3 answers, remaining 0 packets (, ) >>> ans, unans = _ >>> ans.summary() IP / TCP 192.168.8.14:20 > 192.168.8.1:21 S ==> Ether / IP / TCP 192.168.8.1:21 > 192.168.8.14:20 RA / Padding IP / TCP 192.168.8.14:20 > 192.168.8.1:22 S ==> Ether / IP / TCP 192.168.8.1:22 > 192.168.8.14:20 RA / Padding IP / TCP 192.168.8.14:20 > 192.168.8.1:23 S ==> Ether / IP / TCP 192.168.8.1:23 > 192.168.8.14:20 RA / Padding 

If there is a limited rate of answers, you can specify a time interval (in seconds) to wait between two packets with the inter parameter. If some packets are lost or if specifying an interval is not enough, you can resend all the unanswered packets, either by calling the function again, directly with the unanswered list, or by specifying a retry parameter. If retry is 3, Scapy will try to resend unanswered packets 3 times. If retry is -3, Scapy will resend unanswered packets until no more answer is given for the same set of unanswered packets 3 times in a row. The timeout parameter specify the time to wait after the last packet has been sent:

>>> sr(IP(dst="172.20.29.5/30")/TCP(dport=[21,22,23]),inter=0.5,retry=-2,timeout=1) Begin emission: Finished to send 12 packets. Begin emission: Finished to send 9 packets. Begin emission: Finished to send 9 packets. Received 100 packets, got 3 answers, remaining 9 packets (, ) 

SYN Scans

Classic SYN Scan can be initialized by executing the following command from Scapy’s prompt:

>>> sr1(IP(dst="72.14.207.99")/TCP(dport=80,flags="S")) 

The above will send a single SYN packet to Google’s port 80 and will quit after receiving a single response:

Begin emission: .Finished to send 1 packets. * Received 2 packets, got 1 answers, remaining 0 packets IP version=4L ihl=5L tos=0x20 len=44 id=33529 flags= frag=0L ttl=244 proto=TCP chksum=0x6a34 src=72.14.207.99 dst=192.168.1.100 options=// | TCP sport=www dport=ftp-data seq=2487238601L ack=1 dataofs=6L reserved=0L flags=SA window=8190 chksum=0xcdc7 urgptr=0 options=[('MSS', 536)] | Padding load='V\xf7' |>>> 

From the above output, we can see Google returned “SA” or SYN-ACK flags indicating an open port.

Use either notations to scan ports 440 through 443 on the system:

>>> sr(IP(dst="192.168.1.1")/TCP(sport=666,dport=(440,443),flags="S")) 
>>> sr(IP(dst="192.168.1.1")/TCP(sport=RandShort(),dport=[440,441,442,443],flags="S")) 

In order to quickly review responses simply request a summary of collected packets:

>>> ans, unans = _ >>> ans.summary() IP / TCP 192.168.1.100:ftp-data > 192.168.1.1:440 S ======> IP / TCP 192.168.1.1:440 > 192.168.1.100:ftp-data RA / Padding IP / TCP 192.168.1.100:ftp-data > 192.168.1.1:441 S ======> IP / TCP 192.168.1.1:441 > 192.168.1.100:ftp-data RA / Padding IP / TCP 192.168.1.100:ftp-data > 192.168.1.1:442 S ======> IP / TCP 192.168.1.1:442 > 192.168.1.100:ftp-data RA / Padding IP / TCP 192.168.1.100:ftp-data > 192.168.1.1:https S ======> IP / TCP 192.168.1.1:https > 192.168.1.100:ftp-data SA / Padding 

The above will display stimulus/response pairs for answered probes. We can display only the information we are interested in by using a simple loop:

>>> ans.summary( lambda s,r: r.sprintf("%TCP.sport% \t %TCP.flags%") ) 440 RA 441 RA 442 RA https SA 

Even better, a table can be built using the make_table() function to display information about multiple targets:

>>> ans, unans = sr(IP(dst=["192.168.1.1","yahoo.com","slashdot.org"])/TCP(dport=[22,80,443],flags="S")) Begin emission: . *.**. Finished to send 9 packets. **.*.*..*. Received 362 packets, got 8 answers, remaining 1 packets >>> ans.make_table( . lambda s,r: (s.dst, s.dport, . r.sprintf("% - %ICMP.type%>"))) 66.35.250.150 192.168.1.1 216.109.112.135 22 66.35.250.150 - dest-unreach RA - 80 SA RA SA 443 SA SA SA 

The above example will even print the ICMP error type if the ICMP packet was received as a response instead of expected TCP.

For larger scans, we could be interested in displaying only certain responses. The example below will only display packets with the “SA” flag set:

>>> ans.nsummary(lfilter = lambda s,r: r.sprintf("%TCP.flags%") == "SA") 0003 IP / TCP 192.168.1.100:ftp_data > 192.168.1.1:https S ======> IP / TCP 192.168.1.1:https > 192.168.1.100:ftp_data SA 

In case we want to do some expert analysis of responses, we can use the following command to indicate which ports are open:

>>> ans.summary(lfilter = lambda s,r: r.sprintf("%TCP.flags%") == "SA",prn=lambda s,r: r.sprintf("%TCP.sport% is open")) https is open 

Again, for larger scans we can build a table of open ports:

>>> ans.filter(lambda s,r: TCP in r and r[TCP].flags&2).make_table(lambda s,r: . (s.dst, s.dport, "X")) 66.35.250.150 192.168.1.1 216.109.112.135 80 X - X 443 X X X 

If all of the above methods were not enough, Scapy includes a report_ports() function which not only automates the SYN scan, but also produces a LaTeX output with collected results:

>>> report_ports("192.168.1.1",(440,443)) Begin emission: . *.**Finished to send 4 packets. * Received 8 packets, got 4 answers, remaining 0 packets '\\begin<|r|l|l|>\n\\hline\nhttps & open & SA \\\\\n\\hline\n440 & closed & TCP RA \\\\\n441 & closed & TCP RA \\\\\n442 & closed & TCP RA \\\\\n\\hline\n\\hline\n\\end\n' 

TCP traceroute

A TCP traceroute:

>>> ans, unans = sr(IP(dst=target, ttl=(4,25),id=RandShort())/TCP(flags=0x2)) *****.******.*.***..*.**Finished to send 22 packets. ***. Received 33 packets, got 21 answers, remaining 1 packets >>> for snd,rcv in ans: . print snd.ttl, rcv.src, isinstance(rcv.payload, TCP) . 5 194.51.159.65 0 6 194.51.159.49 0 4 194.250.107.181 0 7 193.251.126.34 0 8 193.251.126.154 0 9 193.251.241.89 0 10 193.251.241.110 0 11 193.251.241.173 0 13 208.172.251.165 0 12 193.251.241.173 0 14 208.172.251.165 0 15 206.24.226.99 0 16 206.24.238.34 0 17 173.109.66.90 0 18 173.109.88.218 0 19 173.29.39.101 1 20 173.29.39.101 1 21 173.29.39.101 1 22 173.29.39.101 1 23 173.29.39.101 1 24 173.29.39.101 1 

Note that the TCP traceroute and some other high-level functions are already coded:

>>> lsc() sr : Send and receive packets at layer 3 sr1 : Send packets at layer 3 and return only the first answer srp : Send and receive packets at layer 2 srp1 : Send and receive packets at layer 2 and return only the first answer srloop : Send a packet at layer 3 in loop and print the answer each time srploop : Send a packet at layer 2 in loop and print the answer each time sniff : Sniff packets p0f : Passive OS fingerprinting: which OS emitted this TCP SYN ? arpcachepoison : Poison target's cache with (your MAC,victim's IP) couple send : Send packets at layer 3 sendp : Send packets at layer 2 traceroute : Instant TCP traceroute arping : Send ARP who-has requests to determine which hosts are up ls : List available layers, or infos on a given layer lsc : List user commands queso : Queso OS fingerprinting nmap_fp : nmap fingerprinting report_ports : portscan a target and output a LaTeX table dyndns_add : Send a DNS add message to a nameserver for "name" to have a new "rdata" dyndns_del : Send a DNS delete message to a nameserver for "name" [. ] 

Scapy may also use the GeoIP2 module, in combination with matplotlib and cartopy to generate fancy graphics such as below:

_images/traceroute_worldplot.png

In this example, we used the traceroute_map() function to print the graphic. This method is a shortcut which uses the world_trace of the TracerouteResult objects. It could have been done differently:

>>> conf.geoip_city = "path/to/GeoLite2-City.mmdb" >>> a = traceroute(["www.google.co.uk", "www.secdev.org"], verbose=0) >>> a.world_trace() 

or such as above:

>>> conf.geoip_city = "path/to/GeoLite2-City.mmdb" >>> traceroute_map(["www.google.co.uk", "www.secdev.org"]) 

To use those functions, it is required to have installed the geoip2 module, its database (direct download) but also the cartopy module.

Configuring super sockets

Different super sockets are available in Scapy: the native ones, and the ones that use libpcap (to send/receive packets).

By default, Scapy will try to use the native ones (except on Windows, where the winpcap/npcap ones are preferred). To manually use the libpcap ones, you must:

>>> conf.use_pcap = True 

This will automatically update the sockets pointing to conf.L2socket and conf.L3socket .

If you want to manually set them, you have a bunch of sockets available, depending on your platform. For instance, you might want to use:

>>> conf.L3socket=L3pcapSocket # Receive/send L3 packets through libpcap >>> conf.L2listen=L2ListenTcpdump # Receive L2 packets through TCPDump 

Sniffing

We can easily capture some packets or even clone tcpdump or tshark. Either one interface or a list of interfaces to sniff on can be provided. If no interface is given, sniffing will happen on conf.iface :

>>> sniff(filter="icmp and host 66.35.250.151", count=2) >>> a=_ >>> a.nsummary() 0000 Ether / IP / ICMP 192.168.5.21 echo-request 0 / Raw 0001 Ether / IP / ICMP 192.168.5.21 echo-request 0 / Raw >>> a[1]   ihl=5L tos=0x0 len=84 flags=DF frag=0L ttl=64 proto=ICMP chksum=0x3831 src=192.168.5.21 dst=66.35.250.151 options='' |  chksum=0x6571 seq=0x0 |  \x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d \x1e\x1f !\x22#$%&\'()*+,-./01234567' |>>>> >>> sniff(iface="wifi0", prn=lambda x: x.summary()) 802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133 802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates 802.11 Management 5 00:0a:41:ee:a5:50 / 802.11 Probe Response / Info SSID / Info Rates / Info DSset / Info 133 802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates 802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates 802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133 802.11 Management 11 00:07:50:d6:44:3f / 802.11 Authentication 802.11 Management 11 00:0a:41:ee:a5:50 / 802.11 Authentication 802.11 Management 0 00:07:50:d6:44:3f / 802.11 Association Request / Info SSID / Info Rates / Info 133 / Info 149 802.11 Management 1 00:0a:41:ee:a5:50 / 802.11 Association Response / Info Rates / Info 133 / Info 149 802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133 802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133 802.11 / LLC / SNAP / ARP who has 172.20.70.172 says 172.20.70.171 / Padding 802.11 / LLC / SNAP / ARP is at 00:0a:b7:4b:9c:dd says 172.20.70.172 / Padding 802.11 / LLC / SNAP / IP / ICMP echo-request 0 / Raw 802.11 / LLC / SNAP / IP / ICMP echo-reply 0 / Raw >>> sniff(iface="eth1", prn=lambda x: x.show()) ---[ Ethernet ]--- dst = 00:ae:f3:52:aa:d1 src = 00:02:15:37:a2:44 type = 0x800 ---[ IP ]--- version = 4L ihl = 5L tos = 0x0 len = 84 > flags = DF frag = 0L ttl = 64 proto = ICMP chksum = 0x3831 src = 192.168.5.21 dst = 66.35.250.151 options = '' ---[ ICMP ]--- type = echo-request code = 0 chksum = 0x89d9 > seq = 0x0 ---[ Raw ]--- load = 'B\xf7i\xa9\x00\x04\x149\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\x22#$%&\'()*+,-./01234567' ---[ Ethernet ]--- dst = 00:02:15:37:a2:44 src = 00:ae:f3:52:aa:d1 type = 0x800 ---[ IP ]--- version = 4L ihl = 5L tos = 0x0 len = 84 > flags = frag = 0L ttl = 42 proto = ICMP chksum = 0x861b src = 66.35.250.151 dst = 192.168.5.21 options = '' ---[ ICMP ]--- type = echo-reply code = 0 chksum = 0x91d9 > seq = 0x0 ---[ Raw ]--- load = 'B\xf7i\xa9\x00\x04\x149\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\x22#$%&\'()*+,-./01234567' ---[ Padding ]--- load = '\n_\x00\x0b' >>> sniff(iface=["eth1","eth2"], prn=lambda x: x.sniffed_on+": "+x.summary()) eth3: Ether / IP / ICMP 192.168.5.21 > 66.35.250.151 echo-request 0 / Raw eth3: Ether / IP / ICMP 66.35.250.151 > 192.168.5.21 echo-reply 0 / Raw eth2: Ether / IP / ICMP 192.168.5.22 > 66.35.250.152 echo-request 0 / Raw eth2: Ether / IP / ICMP 66.35.250.152 > 192.168.5.22 echo-reply 0 / Raw 

For even more control over displayed information we can use the sprintf() function:

>>> pkts = sniff(prn=lambda x:x.sprintf(" %IP.dst%\n>\n>")) 192.168.1.100 -> 64.233.167.99 64.233.167.99 -> 192.168.1.100 192.168.1.100 -> 64.233.167.99 192.168.1.100 -> 64.233.167.99 'GET / HTTP/1.1\r\nHost: 64.233.167.99\r\nUser-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.8) Gecko/20071022 Ubuntu/7.10 (gutsy) Firefox/2.0.0.8\r\nAccept: text/xml,application/xml,application/xhtml+xml, text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\nAccept-Language: en-us,en;q=0.5\r\nAccept-Encoding: gzip,deflate\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\nKeep-Alive: 300\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\n\r\n' 

We can sniff and do passive OS fingerprinting:

>>> p   ihl=5L tos=0x0 len=60 flags=DF frag=0L ttl=64 proto=TCP chksum=0xb85e src=192.168.8.10 dst=192.168.8.1 options='' |  seq=2023566040L ack=0L dataofs=10L reserved=0L flags=SEC window=5840 chksum=0x570c urgptr=0 options=[('Timestamp', (342940201L, 0L)), ('MSS', 1460), ('NOP', ()), ('SAckOK', ''), ('WScale', 0)] |>>> >>> load_module("p0f") >>> p0f(p) (1.0, ['Linux 2.4.2 - 2.4.14 (1)']) >>> a=sniff(prn=prnp0f) (1.0, ['Linux 2.4.2 - 2.4.14 (1)']) (1.0, ['Linux 2.4.2 - 2.4.14 (1)']) (0.875, ['Linux 2.4.2 - 2.4.14 (1)', 'Linux 2.4.10 (1)', 'Windows 98 (?)']) (1.0, ['Windows 2000 (9)']) 

The number before the OS guess is the accuracy of the guess.

When sniffing on several interfaces (e.g. iface=["eth0", . ] ), you can check what interface a packet was sniffed on by using the sniffed_on attribute, as shown in one of the examples above.

Asynchronous Sniffing

Asynchronous sniffing is only available since Scapy 2.4.3

Asynchronous sniffing does not necessarily improves performance (it’s rather the opposite). If you want to sniff on multiple interfaces / socket, remember you can pass them all to a single sniff() call

It is possible to sniff asynchronously. This allows to stop the sniffer programmatically, rather than with ctrl^C. It provides start() , stop() and join() utils.

The basic usage would be:

>>> t = AsyncSniffer() >>> t.start() >>> print("hey") hey [. ] >>> results = t.stop() 

The AsyncSniffer class has a few useful keys, such as results (the packets collected) or running , that can be used. It accepts the same arguments than sniff() (in fact, their implementations are merged). For instance:

>>> t = AsyncSniffer(iface="enp0s3", count=200) >>> t.start() >>> t.join() # this will hold until 200 packets are collected >>> results = t.results >>> print(len(results)) 200 

Another example: using prn and store=False

>>> t = AsyncSniffer(prn=lambda x: x.summary(), store=False, filter="tcp") >>> t.start() >>> time.sleep(20) >>> t.stop() 

Advanced Sniffing - Sniffing Sessions

Sessions are only available since Scapy 2.4.3

sniff() also provides Sessions, that allows to dissect a flow of packets seamlessly. For instance, you may want your sniff(prn=. ) function to automatically defragment IP packets, before executing the prn .

Scapy includes some basic Sessions, but it is possible to implement your own. Available by default: