Scapy
Z WiKi AIRdump.CZ
Scapy je interaktivní nástroj pro manipulaci paketů, generátor paketůr, síťový scanner, analytický nástroj, paketový sniffer. Tímto nástrojem je možné nahradit řadu nástrojů jako hping, nmap, arpspoof, arp-sk, arping, tcpdump, tethereal, p0f a další. Scapy používá python interpreter jako základ pro příkazy. To znamená, že lze přímo používat jazyk python (assign variables, loops, definované funkce atd). If you give a file as parameter when you run scapy, your session (variables, functions, intances, ...) will be saved when you leave the interpretor, and restored the next time you launch scapy.
Scapy is not user proof yet. But it is almost reliable. Some more things need to be done to support more platforms.
The idea is simple. Those kind of tools do two things : sending packets and receiving answers. That's what scapy does : you define a set of packets, it sends them, receives answers, matches requests with answers and returns a list of packet couples (request, answer) and a list of unmatched packets. This has the big advantage over tools like nmap or hping that an answer is not reduced to (open/closed/filtered), but is the whole packet.
On top of this can be build more high level functions, for example one that does traceroutes and give as a result only the start TTL of the request and the source IP of the answer. One that pings a whole network and gives the list of machines answering. One that does a portscan and returns a LaTeX report.
Demo Interaktivní session
If you are new to python and you really don't understand a word because of that, or if you want to learn this language, take an hour to read the very good tutorial from Guido Van Rossum zde. After that, you'll know python :) (really!)
First, we introduce the Net class, which implicitely defines a set of IP addresses. Note that this class does not need to be used to give set of addresses as parameters, it will be automatically used. We also see that sessions work :)
# ./scapy.py -s mysession
New session [mysession]
Welcome to Scapy (0.9beta)
>>> Net("192.168.1.0/24")
<Net 192.168.1.0/24>
>>> target=Net("www.target.com")
>>> targetnet=Net("www.target.com/30")
>>> [ip for ip in targetnet]
['173.29.39.100', '173.29.39.101', '173.29.39.102', '173.29.39.103']
>>> ^D
# ./scapy.py -s mysession
Using session [mysession]
Welcome to Scapy (0.9beta)
>>> target
<Net www.target.com>
The configuration is hold into a variable named conf, that is saved with the session.
>>> conf L2listen = <class scapy.L2ListenSocket at 0x83a77dc> L2socket = <class scapy.L2Socket at 0x83aabbc> L3socket = <class scapy.L3PacketSocket at 0x83aa8cc> checkIPID = 1 checkIPsrc = 1 debug_match = 0 except_filter = '' histfile = '/home/pbi/.scapy_history' iface = 'eth0' nmap_base = '/usr/share/nmap/nmap-os-fingerprints' p0f_base = '/etc/p0f.fp' padding = 1 promisc = 'not implemented' queso_base = '/etc/queso.conf' route = Network Netmask Gateway Iface 127.0.0.0 255.0.0.0 0.0.0.0 lo 192.168.8.0 255.255.255.0 0.0.0.0 eth0 0.0.0.0 0.0.0.0 192.168.8.1 eth0 session = '' sniff_promisc = 0 stealth = 'not implemented' verb = 2 >>> conf.verb=1
Now, let's manipulate some packets. Here you can see layers that are supported for the moment. It's really easy to add one.
>>> ls() Dot11Elt : 802.11 Information Element Dot11 : 802.11 SNAP : SNAP IPerror : IP in ICMP BOOTP : BOOTP PrismHeader : abstract packet Ether : Ethernet TCP : TCP Dot11ProbeResp : 802.11 Probe Response TCPerror : TCP in ICMP Dot11AssoResp : 802.11 Association Response Packet : abstract packet UDPerror : UDP in ICMP ISAKMP : ISAKMP Dot11ProbeReq : 802.11 Probe Request NTP : NTP Dot11Beacon : 802.11 Beacon DNSRR : DNS Resource Record STP : Spanning Tree Protocol ARP : ARP UDP : UDP Dot11ReassoResp : 802.11 Reassociation Response Dot11ReassoReq : 802.11 Reassociation Request Dot1Q : 802.1Q ICMPerror : ICMP in ICMP Raw : Raw IKETransform : IKE Transform IKE_SA : IKE SA ISAKMP_payload : ISAKMP payload LLPPP : PPP Link Layer IP : IP LLC : LLC Dot11Deauth : 802.11 Deauthentication Dot11AssoReq : 802.11 Association Request ICMP : ICMP Dot3 : 802.3 EAPOL : EAPOL Dot11Disas : 802.11 Disassociation Padding : Padding DNS : DNS Dot11Auth : 802.11 Authentication Dot11ATIM : 802.11 ATIM DNSQR : DNS Question Record EAP : EAP IKE_proposal : IKE proposal >>> IP() <IP |''> >>> a=IP(dst="172.16.1.40") >>> a <IP dst=172.16.1.40 |''> >>> a.dst '172.16.1.40' >>> a.ttl 64
A layer has default values for every field, so that you don't have to fill them all. If you give a value to the field, it will overload the default value. If you delete the field, the default value will be back. Moreover, fields with default values are not displayed.
>>> a.ttl=32 >>> a <IP dst=172.16.1.40 ttl=32 |''> >>> del(a.ttl) >>> a <IP dst=172.16.1.40 |''> >>> a.ttl 64
Fields can be made human readable. For example IP and TCP flags : (note the rfc3514 compliance for IP).
>>> t=TCP() >>> t.flags="SA" >>> t.flags 18 >>> t <TCP flags=SA |> >>> t.flags=23 >>> t <TCP flags=FSRA |> >>> >>> i=IP(flags="DF+MF") >>> i.flags 3 >>> i <IP flags=MF+DF |> >>> i.flags=6 >>> i <IP flags=DF+evil |>
Some default values are not constant values. For example, the source IP of a packet will default to the IP of the interface that should be used to send a packet to the given destination, according to the local routing tables.
>>> a.dst '172.16.1.40' >>> a.src '172.16.1.24' >>> del(a.dst) >>> a.dst '127.0.0.1' >>> a.src '127.0.0.1' >>> a.dst="192.168.11.10" >>> a.src '192.168.11.1' >>> a.dst=target >>> a.src '172.16.1.24' >>> a.src="1.2.3.4" >>> a <IP src=1.2.3.4 dst=<Net www.target.com> |''>
Here, you can guess that my routing table looks like :
$ route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 172.16.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0 192.168.11.0 0.0.0.0 255.255.255.0 U 0 0 0 eth1 0.0.0.0 172.16.1.1 0.0.0.0 UG 0 0 0 eth0
We will see later that scapy has its own routing table.
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 |''> >>> IP()/TCP() <IP proto=6 |<TCP |''>> >>> Ether()/IP()/TCP() <Ether type=0x800 |<IP proto=6 |<TCP |''>>> >>> IP()/TCP()/"GET /index.html HTTP/1.0\n\n" <IP proto=6 |<TCP |<Raw load='GET /index.html HTTP/1.0\n\n' |''>>> >>> Ether()/IP()/IP()/IP()/UDP() <Ether type=0x800 |<IP proto=0 |<IP proto=0 |<IP proto=17 |<UDP |''>>>>> >>> IP(proto=55)/TCP() <IP proto=55 |<TCP |''>>
Each packet can be build or dissected (note: in python _ (underscode) is the latest result) :
>>> str(IP()) 'E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01' >>> IP(_) <IP frag=0 src=127.0.0.1 proto=0 tos=0x0 dst=127.0.0.1 chksum=0x7ce7 len=20 version=4 flags=0 ihl=5 ttl=64 id=1 |''> >>> a=Ether()/IP(dst=target)/TCP()/"GET /index.html HTTP/1.0 \n\n" >>> b=str(a) >>> b '\x00\x90\x7f\x1e \xc8\x00\x03G\x88\x1d/\x08\x00E\x00\x00C\x00\x01\x00\x00@\x06 \xcd7\xac\x10\x01\x18\xad\x1d\'e\x00P\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x0 2\x00\x00/\xf9\x00\x00GET /index.html HTTP/1.0 \n\n' >>> c=Ether(b) >>> c <Ether src=00:03:47:88:1d:2f dst=00:90:7f:1e:26:c8 type=0x800 |<IP frag=0 src=172.16.1.24 proto=6 tos=0x0 dst=173.29.39.101 chksum=0xcd37 len=67 options='' version=4 flags=0 ihl=5 ttl=64 id=1 |<TCP reserved=0 seq=0L ack=0L dataofs=5 dport=80 window=0 flags=0x2 chksum=0x2ff9 urgptr=0 sport=80 options='' | <Raw load='GET /index.html HTTP/1.0 \n\n' |''>>>>
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 <Ether src=00:03:47:88:1d:2f dst=00:90:7f:1e:26:c8 type=0x800 |<IP src=172.16.1.24 proto=6 dst=173.29.39.101 chksum=0xcd37 len=67 ihl=5 |<TCP dataofs=5 chksum=0x2ff9 | <Raw load='GET /index.html HTTP/1.0 \n\n' |''>>>>
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 implicidely define a set of packets, generated using a kind of cartesian product between all the fields.
>>> a=IP(dst=targetnet) >>> a <IP dst=<Net www.target.com/30> |''> >>> [p for p in a] [<IP dst=173.29.39.100 |''>, <IP dst=173.29.39.101 |''>, <IP dst=173.29.39.102 |''>, <IP dst=173.29.39.103 |''>] >>> b=IP(ttl=[1,2,(5,9)]) >>> b <IP ttl=[1, 2, (5, 9)] |''> >>> [p for p in b] [<IP ttl=1 |''>, <IP ttl=2 |''>, <IP ttl=5 |''>, <IP ttl=6 |''>, <IP ttl=7 |''>, <IP ttl=8 |''>, <IP ttl=9 |''>] >>> c=TCP(dport=[80,443]) >>> [p for p in a/c] [<IP dst=173.29.39.100 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.100 proto=6 |<TCP dport=443 |''>>, <IP dst=173.29.39.101 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.101 proto=6 |<TCP dport=443 |''>>, <IP dst=173.29.39.102 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.102 proto=6 |<TCP dport=443 |''>>, <IP dst=173.29.39.103 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.103 proto=6 |<TCP dport=443 |''>>]
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.
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 return 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.).
>>> p=sr1(IP(dst="172.16.1.40")/ICMP()/"XXXXXXXXXXX")
Finished to send 1 packets.
*
Received 1 packets, got 1 answers, remaining 0 packets
>>> p
<IP frag=0 src=172.16.1.40 proto=1 tos=0x0 dst=172.16.1.24 chksum=0xd56c
len=39 options='' version=4 flags= ihl=5 ttl=255 id=35848 |<ICMP code=0
type=echo-reply id=0x0 seq=0x0 chksum=0xee45 |<Raw load='XXXXXXXXXXX' |
<Padding load='\x00\x00\x00\x00\x00\x00\x00' |>>>>
>>> p.display()
---[ IP ]---
version = 4
ihl = 5
tos = 0x0
len = 39
id = 35848
flags =
frag = 0
ttl = 255
proto = ICMP
chksum = 0xd56c
src = 172.16.1.40
dst = 172.16.1.24
options = ''
---[ ICMP ]---
type = echo-reply
code = 0
chksum = 0xee45
id = 0x0
seq = 0x0
---[ Raw ]---
load = 'XXXXXXXXXXX'
---[ Padding ]---
load = '\x00\x00\x00\x00\x00\x00\x00'
A DNS query (rd = recursion desired).
>>> sr1(IP(dst="172.16.1.40")/UDP()/DNS(rd=1,qd=DNSQR(qname="www.target.com"))) Finished to send 1 packets. .* Received 2 packets, got 1 answers, remaining 0 packets <IP frag=0 src=172.16.1.40 proto=UDP tos=0x0 dst=172.16.1.24 chksum=0xdfb8 len=212 options='' version=4 flags=DF ihl=5 ttl=64 id=0 |<UDP dport=80 sport=53 len=192 chksum=0x188 |<DNS aa=0 qr=1 an=<DNSRR rdata='173.29.33.99' ttl=300L rrname='www.target.com.' type=A class=IN |> ns=<DNSRR rdata='ns4.target.com.' ttl=345600L rrname='target.com.' type=NS class=IN |<DNSRR rdata='ns1.target.com.' ttl=345600L rrname='target.com.' type=NS class=IN |<DNSRR rdata='ns2.target.com.' ttl=345600L rrname='target.com.' type=NS class=IN |<DNSRR rdata='ns3.target.com.' ttl=345600L rrname='target.com.' type=NS class=IN |>>>> nscount=4 qdcount=1 tc=0 ancount=1 rd=1 arcount=4 ar=<DNSRR rdata='173.29.32.10' ttl=326818L rrname='ns1.target.com.' type=A class=IN |<DNSRR rdata='173.29.34.10' ttl=326818L rrname='ns2.target.com.' type=A class=IN |<DNSRR rdata='173.29.36.10' ttl=326818L rrname='ns3.target.com.' type=A class=IN |<DNSRR rdata='173.29.38.10' ttl=326818L rrname='ns4.target.com.' type=A class=IN |>>>> opcode=0 ra=1 z=0 rcode=0 id=0 qd=<DNSQR qclass=IN qtype=A qname='www.target.com.' |> |>>> >>> _.an <DNSRR rdata='173.29.33.99' ttl=300L rrname='www.target.com.' type=A class=IN |>
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 (<Results: UDP:0 ICMP:0 TCP:3 Other:0>, <Unanswered: UDP:0 ICMP:0 TCP:0 Other:0>) >>> 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 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 (<Results: UDP:0 ICMP:0 TCP:3 Other:0>, <Unanswered: UDP:0 ICMP:0 TCP:9 Other:0>)
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"
The process of sending packets and receiving is quite complicated. As I wanted to use the PF_PACKET interface to go through netfilter, I also needed to implement an ARP stack and ARP cache, and a LL stack. Well it seems to work, on ethernet and PPP interfaces, but I don't guarantee anything. Anyway, the fact I used a kind of super-socket for that mean that you can switch your IO layer very easily, and use PF_INET/SOCK_RAW, or use PF_PACKET at level 2 (giving the LL header (ethernet,...) and giving yourself mac addresses, ...). I've just added a super socket which use libdnet and libpcap, so that it should be portable :
>>> conf.L3socket=L3dnetSocket >>> conf.L3listen=L3pcapListenSocket
We can easily capture some packets or even clone tcpdump or tethereal. If no interface is given, sniffing will happen on every interfaces.
>>> sniff(count=2)
<Sniffed: UDP:0 ICMP:2 TCP:0 Other:0>
>>> _.summary()
Ether / IP / ICMP 192.168.8.14 echo-request 0 / Raw
Ether / IP / ICMP 80.67.173.5 echo-reply 0 / Raw
>>> _[:]
[<Ether src=00:d0:b7:88:50:f2 dst=00:03:47:88:1d:2f type=0x800 |<IP frag=0
src=172.16.1.40 proto=1 tos=0x0 dst=172.16.1.24 chksum=0x3974 len=84 options=''
version=4 flags=0 ihl=5 ttl=255 id=10196 |<ICMP code=0 type=0 id=0xdc0f
seq=0x7138 chksum=0x25e5 |<Raw load='>r\x15f\x00\x07M\xf0\x08\t\n\x0b
\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e
\x1f !"#$%&\'()*+,-./01234567' |''>>>>,
<Ether src=00:d0:b7:88:50:f2 dst=00:03:47:88:1d:2f type=0x800 |<IP frag=0
src=172.16.1.40 proto=1 tos=0x0 dst=172.16.1.24 chksum=0x3973 len=84 options=''i
version=4 flags=0 ihl=5 ttl=255 id=10197 |<ICMP code=0 type=0 id=0xdc0f
seq=0x7238 chksum=0x1792 |<Raw load='>r\x15f\x00\x07[C\x08\t\n\x0b\x0c\r\x0e
\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'
()*+,-./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
ARP who has 172.20.70.172 says 172.20.70.171 / Padding
ARP is at 00:0a:b7:4b:9c:dd says 172.20.70.172 / Padding
ICMP echo-request 0 / Raw
ICMP echo-reply 0 / Raw
>>> sniff(iface="lo", prn=lambda x: x.display())
---[ Ethernet ]---
dst = 00:00:00:00:00:00
src = 00:00:00:00:00:00
type = 0x800
---[ IP ]---
version = 4
ihl = 5
tos = 0x0
len = 84
id = 0
flags = 2
frag = 0
ttl = 64
proto = 1
chksum = 0x3ca7
src = 127.0.0.1
dst = 127.0.0.1
options = ''
---[ ICMP ]---
type = echo-request
code = 0
chksum = 0x4f7c
id = 0xe10f
seq = 0x0
---[ Raw ]---
load = '>r\x15\xe0\x00\n\x88\x14\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11
\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'
---[ Ethernet ]---
dst = 00:00:00:00:00:00
src = 00:00:00:00:00:00
type = 0x800
---[ IP ]---
version = 4
ihl = 5
tos = 0x0
len = 84
id = 35452
flags = 0
frag = 0
ttl = 64
proto = 1
chksum = 0xf22a
src = 127.0.0.1
dst = 127.0.0.1
options = ''
---[ ICMP ]---
type = echo-reply
code = 0
chksum = 0x577c
id = 0xe10f
seq = 0x0
---[ Raw ]---
load = '>r\x15\xe0\x00\n\x88\x14\x08\t\n\x0b\x0c\r\x0e\x0f\x10
\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'
We can sniff and do passive OS fingerprinting.
>>> p
<Ether src=00:40:33:96:7b:60 dst=00:10:4b:b3:7d:4e type=0x800 |<IP frag=0
src=192.168.8.10 proto=6 tos=0x10 dst=192.168.8.1 chksum=0xb85e len=60
options='' version=4 flags=2 ihl=5 ttl=64 id=61681 |<TCP reserved=0
seq=2023566040L ack=0L dataofs=10 dport=80 window=5840 flags=SEC
chksum=0x570c urgptr=0 sport=46511 options={'Timestamp': (342940201L, 0L),
'MSS': 1460, 'NOP': (), 'SAckOK': '', 'WScale': 0} |''>>>
>>> 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 accurracy of the guess.
Demo of both bpf filter and sprintf() method :
>>> a=sniff(filter="tcp and ( port 25 or port 110 )",
prn=lambda x: x.sprintf("%IP.src%:%TCP.sport% -> %IP.dst%:%TCP.dport% %2s,TCP.flags% : %TCP.payload%"))
192.168.8.10:47226 -> 213.228.0.14:110 S :
213.228.0.14:110 -> 192.168.8.10:47226 SA :
192.168.8.10:47226 -> 213.228.0.14:110 A :
213.228.0.14:110 -> 192.168.8.10:47226 PA : +OK <13103.1048117923@pop2-1.free.fr>
192.168.8.10:47226 -> 213.228.0.14:110 A :
192.168.8.10:47226 -> 213.228.0.14:110 PA : USER toto
213.228.0.14:110 -> 192.168.8.10:47226 A :
213.228.0.14:110 -> 192.168.8.10:47226 PA : +OK
192.168.8.10:47226 -> 213.228.0.14:110 A :
192.168.8.10:47226 -> 213.228.0.14:110 PA : PASS tata
213.228.0.14:110 -> 192.168.8.10:47226 PA : -ERR authorization failed
192.168.8.10:47226 -> 213.228.0.14:110 A :
213.228.0.14:110 -> 192.168.8.10:47226 FA :
192.168.8.10:47226 -> 213.228.0.14:110 FA :
213.228.0.14:110 -> 192.168.8.10:47226 A :
Here is an example of a (h)ping-like functionnality : you always send the same set of packets to see if something change :
>>> srloop(IP(dst="www.target.com/30")/TCP())
RECV 1: Ether / IP / TCP 192.168.11.99:80 > 192.168.8.14:20 SA / Padding
fail 3: IP / TCP 192.168.8.14:20 > 192.168.11.96:80 S
IP / TCP 192.168.8.14:20 > 192.168.11.98:80 S
IP / TCP 192.168.8.14:20 > 192.168.11.97:80 S
RECV 1: Ether / IP / TCP 192.168.11.99:80 > 192.168.8.14:20 SA / Padding
fail 3: IP / TCP 192.168.8.14:20 > 192.168.11.96:80 S
IP / TCP 192.168.8.14:20 > 192.168.11.98:80 S
IP / TCP 192.168.8.14:20 > 192.168.11.97:80 S
Now we have a demonstration of the make_table() presentation function. It takes a list as parameter, and 3 functions. The first gives the value on the x axis from an element of the list, the second is about the y value and the third is the value that we want to see at coordinates (x,y). The result is a table. This function has 2 variants, make_lined_table() and
make_tex_table() to copy/paste into your LaTeX pentest report. Those functions are available as methods of a result object :
Here we can see a multi-parallel traceroute (scapy already has a multi TCP traceroute function. See later).
>>> ans,unans=sr(IP(dst="www.test.fr/30", ttl=(1,6))/TCP()) Received 49 packets, got 24 answers, remaining 0 packets >>> ans.make_table( lambda x: x[0].dst, lambda x:x[0].ttl, lambda x:x[1].src) 216.15.189.192 216.15.189.193 216.15.189.194 216.15.189.195 1 192.168.8.1 192.168.8.1 192.168.8.1 192.168.8.1 2 81.57.239.254 81.57.239.254 81.57.239.254 81.57.239.254 3 213.228.4.254 213.228.4.254 213.228.4.254 213.228.4.254 4 213.228.3.3 213.228.3.3 213.228.3.3 213.228.3.3 5 193.251.254.1 193.251.251.69 193.251.254.1 193.251.251.69 6 193.251.241.174 193.251.241.178 193.251.241.174 193.251.241.178
Here is a more complex example to identify machines from their IPID field. We can see that 172.20.80.200:22 is answered by the same IP stack than 172.20.80.201 and that 172.20.80.197:25 is not answered by the sape IP stack than other ports on the same IP.
>>> ans,unans=sr(IP(dst="172.20.80.192/28")/TCP(dport=[20,21,22,25,53,80]))
Received 142 packets, got 25 answers, remaining 71 packets
>>> ans.make_table(lambda x: x[0].dst, lambda x:x[0].dport, lambda x:x[1].sprintf("%IP.id%"))
172.20.80.196 172.20.80.197 172.20.80.198 172.20.80.200 172.20.80.201
20 0 4203 7021 - 11562
21 0 4204 7022 - 11563
22 0 4205 7023 11561 11564
25 0 0 7024 - 11565
53 0 4207 7025 - 11566
80 0 4028 7026 - 11567
It can help identify network topologies very easily when playing with TTL, displaying received TTL, etc.
Now scapy has its own routing table, so that you can have your packets routed diffrently than the system.
>>> conf.route Network Netmask Gateway Iface 127.0.0.0 255.0.0.0 0.0.0.0 lo 192.168.8.0 255.255.255.0 0.0.0.0 eth0 0.0.0.0 0.0.0.0 192.168.8.1 eth0 >>> conf.route.delt(net="0.0.0.0/0",gw="192.168.8.1") >>> conf.route.add(net="0.0.0.0/0",gw="192.168.8.254") >>> conf.route.add(host="192.168.1.1",gw="192.168.8.1") >>> conf.route Network Netmask Gateway Iface 127.0.0.0 255.0.0.0 0.0.0.0 lo 192.168.8.0 255.255.255.0 0.0.0.0 eth0 0.0.0.0 0.0.0.0 192.168.8.254 eth0 192.168.1.1 255.255.255.255 192.168.8.1 eth0 >>> conf.route.resync() >>> conf.route Network Netmask Gateway Iface 127.0.0.0 255.0.0.0 0.0.0.0 lo 192.168.8.0 255.255.255.0 0.0.0.0 eth0 0.0.0.0 0.0.0.0 192.168.8.1 eth0
We can easily plot some harvested values using Gnuplot. For example, we can observe the IP ID patterns to know how many distinct IP stacks are used behind a load balancer :
>>> a,b=sr(IP(dst="www.target.com")/TCP(sport=[RandShort()]*1000)) >>> a.plot(lambda x:x[1].id) <Gnuplot._Gnuplot.Gnuplot instance at 0xb7d6a74c>
ipid.png
Scapy also has a powerful TCP traceroute function. Unlike other traceroute programs that wait for each node to reply before going to the next, scapy sends all the packets at the same time. This has the disadvantage that it can't know when to stop (thus the maxttl parameter) but the great advantage that it took less than 3 seconds to get this multi-target traceroute result :
>>> traceroute(["yahoo.com","altavista.com","wisenut.com","copernic.com"],maxttl=20) Received 80 packets, got 80 answers, remaining 0 packets 193.45.10.88:80 216.109.118.79:80 64.241.242.243:80 66.94.229.254:80 1 192.168.8.1 192.168.8.1 192.168.8.1 192.168.8.1 2 82.243.5.254 82.243.5.254 82.243.5.254 82.243.5.254 3 213.228.4.254 213.228.4.254 213.228.4.254 213.228.4.254 4 212.27.50.46 212.27.50.46 212.27.50.46 212.27.50.46 5 212.27.50.37 212.27.50.41 212.27.50.37 212.27.50.41 6 212.27.50.34 212.27.50.34 213.228.3.234 193.251.251.69 7 213.248.71.141 217.118.239.149 208.184.231.214 193.251.241.178 8 213.248.65.81 217.118.224.44 64.125.31.129 193.251.242.98 9 213.248.70.14 213.206.129.85 64.125.31.186 193.251.243.89 10 193.45.10.88 SA 213.206.128.160 64.125.29.122 193.251.254.126 11 193.45.10.88 SA 206.24.169.41 64.125.28.70 216.115.97.178 12 193.45.10.88 SA 206.24.226.99 64.125.28.209 66.218.64.146 13 193.45.10.88 SA 206.24.227.106 64.125.29.45 66.218.82.230 14 193.45.10.88 SA 216.109.74.30 64.125.31.214 66.94.229.254 SA 15 193.45.10.88 SA 216.109.120.149 64.124.229.109 66.94.229.254 SA 16 193.45.10.88 SA 216.109.118.79 SA 64.241.242.243 SA 66.94.229.254 SA 17 193.45.10.88 SA 216.109.118.79 SA 64.241.242.243 SA 66.94.229.254 SA 18 193.45.10.88 SA 216.109.118.79 SA 64.241.242.243 SA 66.94.229.254 SA 19 193.45.10.88 SA 216.109.118.79 SA 64.241.242.243 SA 66.94.229.254 SA 20 193.45.10.88 SA 216.109.118.79 SA 64.241.242.243 SA 66.94.229.254 SA <Traceroute: UDP:0 TCP:28 ICMP:52 Other:0>
The last line is in fact a traceroute result object. It is a more specialised version (a subclass, in fact) of a classic result object. We can save it to consult the traceroute result again a bit later, or to deeply inspect one of the answers, for example to check padding.
>>> result=_ >>> result.display() 193.45.10.88:80 216.109.118.79:80 64.241.242.243:80 66.94.229.254:80 1 192.168.8.1 192.168.8.1 192.168.8.1 192.168.8.1 2 82.251.4.254 82.251.4.254 82.251.4.254 82.251.4.254 3 213.228.4.254 213.228.4.254 213.228.4.254 213.228.4.254 [...] >>> result.filter(lambda x:x[1].haslayer(Padding)) <filtered Traceroute: UDP:0 TCP:24 ICMP:8 Other:0>
Like any result object, traceroute objects can be added :
>>> r2=traceroute(["www.voila.com"],maxttl=20) Received 19 packets, got 19 answers, remaining 1 packets 195.101.94.25:80 1 192.168.8.1 2 82.251.4.254 3 213.228.4.254 4 212.27.50.169 5 212.27.50.162 6 193.252.161.97 7 193.252.103.86 8 193.252.103.77 9 193.252.101.1 10 193.252.227.245 12 195.101.94.25 SA 13 195.101.94.25 SA 14 195.101.94.25 SA 15 195.101.94.25 SA 16 195.101.94.25 SA 17 195.101.94.25 SA 18 195.101.94.25 SA 19 195.101.94.25 SA 20 195.101.94.25 SA >>> >>> r3=result+r2 >>> r3.display() 195.101.94.25:80 212.23.37.13:80 216.109.118.72:80 64.241.242.243:80 66.94.229.254:80 1 192.168.8.1 192.168.8.1 192.168.8.1 192.168.8.1 192.168.8.1 2 82.251.4.254 82.251.4.254 82.251.4.254 82.251.4.254 82.251.4.254 3 213.228.4.254 213.228.4.254 213.228.4.254 213.228.4.254 213.228.4.254 4 212.27.50.169 212.27.50.169 212.27.50.46 - 212.27.50.46 5 212.27.50.162 212.27.50.162 212.27.50.37 212.27.50.41 212.27.50.37 6 193.252.161.97 194.68.129.168 212.27.50.34 213.228.3.234 193.251.251.69 7 193.252.103.86 212.23.42.33 217.118.239.185 208.184.231.214 193.251.241.178 8 193.252.103.77 212.23.42.6 217.118.224.44 64.125.31.129 193.251.242.98 9 193.252.101.1 212.23.37.13 SA 213.206.129.85 64.125.31.186 193.251.243.89 10 193.252.227.245 212.23.37.13 SA 213.206.128.160 64.125.29.122 193.251.254.126 11 - 212.23.37.13 SA 206.24.169.41 64.125.28.70 216.115.97.178 12 195.101.94.25 SA 212.23.37.13 SA 206.24.226.100 64.125.28.209 216.115.101.46 13 195.101.94.25 SA 212.23.37.13 SA 206.24.238.166 64.125.29.45 66.218.82.234 14 195.101.94.25 SA 212.23.37.13 SA 216.109.74.30 64.125.31.214 66.94.229.254 SA 15 195.101.94.25 SA 212.23.37.13 SA 216.109.120.151 64.124.229.109 66.94.229.254 SA 16 195.101.94.25 SA 212.23.37.13 SA 216.109.118.72 SA 64.241.242.243 SA 66.94.229.254 SA 17 195.101.94.25 SA 212.23.37.13 SA 216.109.118.72 SA 64.241.242.243 SA 66.94.229.254 SA 18 195.101.94.25 SA 212.23.37.13 SA 216.109.118.72 SA 64.241.242.243 SA 66.94.229.254 SA 19 195.101.94.25 SA 212.23.37.13 SA 216.109.118.72 SA 64.241.242.243 SA 66.94.229.254 SA 20 195.101.94.25 SA 212.23.37.13 SA 216.109.118.72 SA 64.241.242.243 SA 66.94.229.254 SA
Traceroute result object also have a very neat feature : they can make a directed graph from all the routes they got, and cluster them by AS. You will need graphviz. By default, ImageMagick is used to display the graph.
>>> res = traceroute(["microsoft.com","cisco.com","yahoo.com","wanadoo.fr","pacsec.com"],dport=[80,443],maxttl=20,retry=-2) Received 190 packets, got 190 answers, remaining 10 packets 193.252.122.103:443 193.252.122.103:80 198.133.219.25:443 198.133.219.25:80 207.46... 1 192.168.8.1 192.168.8.1 192.168.8.1 192.168.8.1 192.16... 2 82.251.4.254 82.251.4.254 82.251.4.254 82.251.4.254 82.251... 3 213.228.4.254 213.228.4.254 213.228.4.254 213.228.4.254 213.22... [...] >>> res.graph() # piped to ImageMagick's display program. Image below. >>> res.graph(type="ps",target="| lp") # piped to postscript printer >>> res.graph(target="> /tmp/graph.svg") # saved to file
graph_traceroute.gif graph_traceroute.svg The same in SVG
Soon to come : examples for arping, scanning, arp cache poisoning, dns spoofing, etc. (they are present in the scapy_lsm2003.pdf)
Bugy
- Link layer not well managed yet
- Does not give the right source IP for routes that use interface aliases (/proc/net/route reports only master interface)
- May miss packets under heavy load