Skip to content

Commit 9e4dc1d

Browse files
authored
Select interface in TFTP automatons (#4684)
* Select interface in TFTP automatons * Add TFTP docstrings * Merge the 2 tftp.uts
1 parent 4d499c2 commit 9e4dc1d

3 files changed

Lines changed: 224 additions & 181 deletions

File tree

scapy/layers/tftp.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,30 @@
55

66
"""
77
TFTP (Trivial File Transfer Protocol).
8+
9+
This provides TFTP implementation and 4 small automata:
10+
- TFTP_read: read a remote file
11+
- TFTP_RRQ_server: server that answers to read requests
12+
- TFTP_write: write a remote file
13+
- TFTP_WRQ_server: server than accepts write requests
814
"""
915

1016
import os
1117
import random
1218

1319
from scapy.packet import Packet, bind_layers, split_bottom_up, bind_bottom_up
14-
from scapy.fields import PacketListField, ShortEnumField, ShortField, \
15-
StrNullField
20+
from scapy.fields import (
21+
PacketListField,
22+
ShortEnumField,
23+
ShortField,
24+
StrNullField,
25+
)
1626
from scapy.automaton import ATMT, Automaton
17-
from scapy.layers.inet import UDP, IP
27+
from scapy.base_classes import Net
1828
from scapy.config import conf
1929
from scapy.volatile import RandShort
2030

31+
from scapy.layers.inet import UDP, IP
2132

2233
TFTP_operations = {1: "RRQ", 2: "WRQ", 3: "DATA", 4: "ACK", 5: "ERROR", 6: "OACK"} # noqa: E501
2334

@@ -138,9 +149,17 @@ def answers(self, other):
138149
class TFTP_read(Automaton):
139150
"""
140151
TFTP automaton to read a remote file on a TFTP server.
152+
153+
:param filename: the name of the remote file to read.
154+
:param server: the host on which to read (IP or name).
155+
:param sport: (optional) the source port to use. (default: random)
156+
:param port: (optional) the TFTP port (default: 69)
141157
"""
142158

143159
def parse_args(self, filename, server, sport=None, port=69, **kargs):
160+
if "iface" not in kargs:
161+
server = str(Net(server))
162+
kargs["iface"] = conf.route.route(server)[0]
144163
Automaton.parse_args(self, **kargs)
145164
self.filename = filename
146165
self.server = server
@@ -229,9 +248,18 @@ def END(self):
229248
class TFTP_write(Automaton):
230249
"""
231250
TFTP automaton to write a local file onto a TFTP server.
251+
252+
:param filename: the name of the remote file to write.
253+
:param data: the bytes data to write.
254+
:param server: the host on which to read (IP or name).
255+
:param sport: (optional) the source port to use. (default: random)
256+
:param port: (optional) the TFTP port (default: 69)
232257
"""
233258

234259
def parse_args(self, filename, data, server, sport=None, port=69, **kargs):
260+
if "iface" not in kargs:
261+
server = str(Net(server))
262+
kargs["iface"] = conf.route.route(server)[0]
235263
Automaton.parse_args(self, **kargs)
236264
self.filename = filename
237265
self.server = server
@@ -313,9 +341,15 @@ def END(self):
313341
class TFTP_WRQ_server(Automaton):
314342
"""
315343
TFTP automaton to wait for incoming files
344+
345+
:param ip: (optional) the local IP to listen on.
346+
:param sport: (optional) the local port (by default: random)
316347
"""
317348

318349
def parse_args(self, ip=None, sport=None, *args, **kargs):
350+
if "iface" not in kargs:
351+
ip = str(Net(ip))
352+
kargs["iface"] = conf.route.route(ip)[0]
319353
Automaton.parse_args(self, *args, **kargs)
320354
self.ip = ip
321355
self.sport = sport
@@ -393,9 +427,22 @@ def END(self):
393427
class TFTP_RRQ_server(Automaton):
394428
"""
395429
TFTP automaton to serve local files
430+
431+
You can't use 'store' and 'dir' at the same time.
432+
433+
:param store: (optional) a dictionary that contains the file data, like
434+
{"thefile": b"data"}.
435+
:param dir: (optional) a folder that contains the data file data.
436+
:param joker: (optional) data to return when no file/data is found.
437+
:param ip: (optional) the local IP to listen on.
438+
:param sport: (optional) the local port (by default: random)
439+
:param serve_one: (optional) close after serving one client (default: False)
396440
"""
397441

398442
def parse_args(self, store=None, joker=None, dir=None, ip=None, sport=None, serve_one=False, **kargs): # noqa: E501
443+
if "iface" not in kargs:
444+
ip = str(Net(ip))
445+
kargs["iface"] = conf.route.route(ip)[0]
399446
Automaton.parse_args(self, **kargs)
400447
if store is None:
401448
store = {}

test/scapy/layers/tftp.uts

Lines changed: 174 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,186 @@
1-
% TFTP regression tests for Scapy
1+
% Regression tests for TFTP
22

33
# More information at http://www.secdev.org/projects/UTscapy/
44

5+
+ TFTP coverage tests
56

6-
############
7-
############
8-
+ TFTP tests
7+
= Test answers
8+
9+
assert TFTP_DATA(block=1).answers(TFTP_RRQ())
10+
assert not TFTP_WRQ().answers(TFTP_RRQ())
11+
assert not TFTP_RRQ().answers(TFTP_WRQ())
12+
assert TFTP_ACK(block=1).answers(TFTP_DATA(block=1))
13+
assert not TFTP_ACK(block=0).answers(TFTP_DATA(block=1))
14+
assert TFTP_ACK(block=0).answers(TFTP_RRQ())
15+
assert not TFTP_ACK().answers(TFTP_ACK())
16+
assert TFTP_ERROR().answers(TFTP_DATA()) and TFTP_ERROR().answers(TFTP_ACK())
17+
assert TFTP_OACK().answers(TFTP_WRQ())
918

1019
= TFTP Options
20+
1121
x=IP()/UDP(sport=12345)/TFTP()/TFTP_RRQ(filename="fname")/TFTP_Options(options=[TFTP_Option(oname="blksize", value="8192"),TFTP_Option(oname="other", value="othervalue")])
1222
assert raw(x) == b'E\x00\x00H\x00\x01\x00\x00@\x11|\xa2\x7f\x00\x00\x01\x7f\x00\x00\x0109\x00E\x004B6\x00\x01fname\x00octet\x00blksize\x008192\x00other\x00othervalue\x00'
1323
y=IP(raw(x))
1424
y[TFTP_Option].oname
1525
y[TFTP_Option:2].oname
1626
assert len(y[TFTP_Options].options) == 2 and y[TFTP_Option].oname == b"blksize"
27+
28+
29+
+ TFTP Automatons
30+
~ linux
31+
32+
= Utilities
33+
~ linux
34+
35+
from scapy.automaton import select_objects
36+
37+
class MockTFTPSocket(object):
38+
packets = []
39+
def __init__(self, iface):
40+
self.iface = iface
41+
def recv(self, n=None):
42+
pkt = self.packets.pop(0)
43+
return pkt
44+
def send(self, *args, **kargs):
45+
pass
46+
def close(self):
47+
pass
48+
@classmethod
49+
def select(classname, inputs, remain):
50+
test = [s for s in inputs if isinstance(s, classname)]
51+
if test:
52+
if len(test[0].packets):
53+
return test
54+
else:
55+
inputs = [s for s in inputs if not isinstance(s, classname)]
56+
return select_objects(inputs, remain)
57+
58+
59+
= TFTP_read() automaton
60+
~ linux
61+
62+
class MockReadSocket(MockTFTPSocket):
63+
packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_DATA(block=1) / ("P" * 512),
64+
IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_DATA(block=2) / "<3"]
65+
66+
tftp_read = TFTP_read("file.txt", "1.2.3.4", sport=0x2807,
67+
ll=MockReadSocket,
68+
recvsock=MockReadSocket, debug=5)
69+
70+
res = tftp_read.run()
71+
assert res == (b"P" * 512 + b"<3")
72+
73+
= TFTP_read() automaton error
74+
~ linux
75+
76+
class MockReadSocket(MockTFTPSocket):
77+
packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ERROR(errorcode=2, errormsg="Fatal error")]
78+
79+
tftp_read = TFTP_read("file.txt", "1.2.3.4", sport=0x2807,
80+
ll=MockReadSocket,
81+
recvsock=MockReadSocket)
82+
83+
try:
84+
tftp_read.run()
85+
assert False
86+
except Automaton.ErrorState as e:
87+
assert "Reached ERROR" in str(e)
88+
assert "ERROR Access violation" in str(e)
89+
90+
91+
= TFTP_write() automaton
92+
~ linux
93+
94+
data_received = b""
95+
96+
class MockWriteSocket(MockTFTPSocket):
97+
packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ACK(block=0),
98+
IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ACK(block=1) ]
99+
def send(self, *args, **kargs):
100+
if len(args) and Raw in args[0]:
101+
global data_received
102+
data_received += args[0][Raw].load
103+
104+
tftp_write = TFTP_write("file.txt", "P" * 767 + "Scapy <3", "1.2.3.4", sport=0x2807,
105+
ll=MockWriteSocket,
106+
recvsock=MockWriteSocket)
107+
108+
tftp_write.run()
109+
assert data_received == (b"P" * 767 + b"Scapy <3")
110+
111+
= TFTP_write() automaton error
112+
~ linux
113+
114+
class MockWriteSocket(MockTFTPSocket):
115+
packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ERROR(errorcode=2, errormsg="Fatal error")]
116+
117+
tftp_write = TFTP_write("file.txt", "P" * 767 + "Scapy <3", "1.2.3.4", sport=0x2807,
118+
ll=MockWriteSocket,
119+
recvsock=MockWriteSocket)
120+
121+
try:
122+
tftp_write.run()
123+
assert False
124+
except Automaton.ErrorState as e:
125+
assert "Reached ERROR" in str(e)
126+
assert "ERROR Access violation" in str(e)
127+
128+
129+
= TFTP_WRQ_server() automaton
130+
~ linux
131+
132+
class MockWRQSocket(MockTFTPSocket):
133+
packets = [IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_WRQ(filename="scapy.txt"),
134+
IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=1) / ("P" * 512),
135+
IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=2) / "<3"]
136+
137+
tftp_wrq = TFTP_WRQ_server(ip="1.2.3.4", sport=0x2807,
138+
ll=MockWRQSocket,
139+
recvsock=MockWRQSocket)
140+
assert tftp_wrq.run() == (b"scapy.txt", (b"P" * 512 + b"<3"))
141+
142+
= TFTP_WRQ_server() automaton with options
143+
~ linux
144+
145+
class MockWRQSocket(MockTFTPSocket):
146+
packets = [IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_WRQ(filename="scapy.txt") / TFTP_Options(options=[TFTP_Option(oname="blksize", value="100")]),
147+
IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=1) / ("P" * 100),
148+
IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=2) / "<3"]
149+
150+
tftp_wrq = TFTP_WRQ_server(ip="1.2.3.4", sport=0x2807,
151+
ll=MockWRQSocket,
152+
recvsock=MockWRQSocket)
153+
assert tftp_wrq.run() == (b"scapy.txt", (b"P" * 100 + b"<3"))
154+
155+
= TFTP_RRQ_server() automaton
156+
~ linux
157+
158+
sent_data = "P" * 512 + "<3"
159+
import tempfile
160+
filename = tempfile.mktemp(suffix=".txt")
161+
fdesc = open(filename, "w")
162+
fdesc.write(sent_data)
163+
fdesc.close()
164+
165+
received_data = ""
166+
167+
class MockRRQSocket(MockTFTPSocket):
168+
packets = [IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_RRQ(filename="scapy.txt") / TFTP_Options(options=[TFTP_Option(oname="blksize", value="100")]),
169+
IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_RRQ(filename=filename[5:]) / TFTP_Options(),
170+
IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_ACK(block=1),
171+
IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_ACK(block=2) ]
172+
def send(self, *args, **kargs):
173+
if len(args):
174+
pkt = args[0]
175+
if TFTP_DATA in pkt:
176+
global received_data
177+
received_data += pkt[Raw].load.decode("utf-8")
178+
179+
tftp_rrq = TFTP_RRQ_server(ip="1.2.3.4", sport=0x2807, dir="/tmp/", serve_one=True,
180+
ll=MockRRQSocket,
181+
recvsock=MockRRQSocket, debug=4)
182+
tftp_rrq.run()
183+
assert received_data == sent_data
184+
185+
import os
186+
os.unlink(filename)

0 commit comments

Comments
 (0)