Skip to content

Commit 0a98c19

Browse files
authored
Stun improvements (#4712)
* STUN: Fix building binding response Building a binding success response with an XOR-MAPPED-ADDRESS attribute filed with: File "/home/simon/src/scapy/scapy/contrib/stun.py", line 152, in i2m return struct.pack(">i", struct.unpack(">i", inet_aton(x)) ^ MAGIC_COOKIE) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~ TypeError: While dissecting field 'attributes': While dissecting field 'xip': unsupported operand type(s) for ^: 'tuple' and 'int' Fixed by properly handling the return value of struct.unpack (a tuple with one int). * STUN: Support IPv6 in binding response The XOR-MAPPED-ADDRESS attribute can contain either IPv4 or IPv6 addresses in the xip field. The address_family field selects the type of the xip field. * STUN: Support MAPPED-ADDRESS attribute Even RFC 8489 compliant STUN servers respond with the non-XORed MAPPED-ADDRESS attribute, if the magic cookie in the request doesn't have the expected value. * STUN: Fix UDP/TCP layer binding STUN responses weren't properly dissected, because the protocol was only bound to the destination port. Fixed by binding both source and destination port number 3478 to the STUN protocol. Also set both port numbers when building messages, otherwise the UDP/TCP default source port (DNS / FTP) would be used.
1 parent 8f9c7a6 commit 0a98c19

2 files changed

Lines changed: 158 additions & 11 deletions

File tree

scapy/contrib/stun.py

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from scapy.layers.inet import UDP, TCP
2222
from scapy.config import conf
23-
from scapy.packet import Packet, bind_layers
23+
from scapy.packet import Packet, bind_bottom_up, bind_top_down
2424
from scapy.utils import inet_ntoa, inet_aton
2525
from scapy.fields import (
2626
BitField,
@@ -39,7 +39,9 @@
3939
XLongField,
4040
XIntField,
4141
XBitField,
42-
IPField
42+
IPField,
43+
IP6Field,
44+
MultipleTypeField,
4345
)
4446

4547
MAGIC_COOKIE = 0x2112A442
@@ -149,19 +151,64 @@ def m2i(self, pkt, x):
149151
def i2m(self, pkt, x):
150152
if x is None:
151153
return b"\x00\x00\x00\x00"
152-
return struct.pack(">i", struct.unpack(">i", inet_aton(x)) ^ MAGIC_COOKIE)
154+
return struct.pack(">i", struct.unpack(">i", inet_aton(x))[0] ^ MAGIC_COOKIE)
155+
156+
157+
class XorIp6(IP6Field):
158+
159+
def m2i(self, pkt, x):
160+
addr = self._xor_address(pkt, x)
161+
return super().m2i(pkt, addr)
162+
163+
def i2m(self, pkt, x):
164+
addr = super().i2m(pkt, x)
165+
return self._xor_address(pkt, addr)
166+
167+
def _xor_address(self, pkt, addr):
168+
xor_words = [pkt.parent.magic_cookie]
169+
xor_words += struct.unpack(
170+
">III", pkt.parent.transaction_id.to_bytes(12, "big")
171+
)
172+
addr_words = struct.unpack(">IIII", addr)
173+
xor_addr = [a ^ b for a, b in zip(addr_words, xor_words)]
174+
return struct.pack(">IIII", *xor_addr)
153175

154176

155177
class STUNXorMappedAddress(STUNGenericTlv):
156178
name = "STUN XOR Mapped Address"
157179

158180
fields_desc = [
159181
XShortField("type", 0x0020),
160-
ShortField("length", 8),
182+
FieldLenField("length", None, length_of="xip", adjust=lambda pkt, x: x + 4),
161183
ByteField("RESERVED", 0),
162184
ByteEnumField("address_family", 1, _xor_mapped_address_family),
163185
XorPort("xport", 0),
164-
XorIp("xip", 0) # FIXME <- only IPv4 addresses will work
186+
MultipleTypeField(
187+
[
188+
(XorIp("xip", "127.0.0.1"), lambda pkt: pkt.address_family == 1),
189+
(XorIp6("xip", "::1"), lambda pkt: pkt.address_family == 2),
190+
],
191+
XorIp("xip", "127.0.0.1"),
192+
),
193+
]
194+
195+
196+
class STUNMappedAddress(STUNGenericTlv):
197+
name = "STUN Mapped Address"
198+
199+
fields_desc = [
200+
XShortField("type", 0x0001),
201+
FieldLenField("length", None, length_of="ip", adjust=lambda pkt, x: x + 4),
202+
ByteField("RESERVED", 0),
203+
ByteEnumField("address_family", 1, _xor_mapped_address_family),
204+
ShortField("port", 0),
205+
MultipleTypeField(
206+
[
207+
(IPField("ip", "127.0.0.1"), lambda pkt: pkt.address_family == 1),
208+
(IP6Field("ip", "::1"), lambda pkt: pkt.address_family == 2),
209+
],
210+
IPField("ip", "127.0.0.1"),
211+
),
165212
]
166213

167214

@@ -207,6 +254,7 @@ class STUNGoogNetworkInfo(STUNGenericTlv):
207254

208255

209256
_stun_tlv_class = {
257+
0x0001: STUNMappedAddress,
210258
0x0006: STUNUsername,
211259
0x0008: STUNMessageIntegrity,
212260
0x0020: STUNXorMappedAddress,
@@ -259,5 +307,10 @@ def post_build(self, pkt, pay):
259307
return pkt
260308

261309

262-
bind_layers(UDP, STUN, dport=3478)
263-
bind_layers(TCP, STUN, dport=3478)
310+
bind_bottom_up(UDP, STUN, sport=3478)
311+
bind_bottom_up(UDP, STUN, dport=3478)
312+
bind_top_down(UDP, STUN, sport=3478, dport=3478)
313+
314+
bind_bottom_up(TCP, STUN, sport=3478)
315+
bind_bottom_up(TCP, STUN, dport=3478)
316+
bind_top_down(TCP, STUN, sport=3478, dport=3478)

test/contrib/stun.uts

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ assert parsed.length == 44
7878
assert parsed.magic_cookie == 0x2112A442
7979
assert parsed.transaction_id == 0xcfacb2a43aa2de5a9d56d85a, parsed.transaction_id
8080
assert parsed.attributes == [
81-
STUNXorMappedAddress(xport=40480, xip="172.20.0.42"),
81+
STUNXorMappedAddress(length=8, xport=40480, xip="172.20.0.42"),
8282
STUNMessageIntegrity(hmac_sha1=0xb71fc9235897c802e3fff8e3d889fa41428d967d),
8383
STUNFingerprint(crc_32=0xea9b6559)
8484
]
@@ -99,14 +99,51 @@ assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding
9999
assert parsed.length == 88
100100
assert parsed.magic_cookie == 0x2112A442
101101
assert parsed.transaction_id == 0x3479476534635936316a796a, parsed.transaction_id
102-
assert parsed.attributes[0] == STUNXorMappedAddress(xport=25000, xip="172.20.0.200"), parsed.attributes
102+
assert parsed.attributes[0] == STUNXorMappedAddress(length=8, xport=25000, xip="172.20.0.200"), parsed.attributes
103103
assert parsed.attributes == [
104-
STUNXorMappedAddress(xport=25000, xip="172.20.0.200"),
104+
STUNXorMappedAddress(length=8, xport=25000, xip="172.20.0.200"),
105105
STUNUsername(length=37, username="Ht11MaRZHc4GOLJUsbu1R3YCs72HYN25:oNph"),
106106
STUNMessageIntegrity(hmac_sha1=0x4b67036dfb65ca84d63bcac86c8d5981df657031),
107107
STUNFingerprint(crc_32=0x4041e9c3)
108108
]
109109

110+
= test STUN binding success response IPv6
111+
112+
raw = b"\x01\x01\x00\x18\x21\x12\xa4\x42\x91\x1b\x25\x32\x99\x8d\xa0\x1c" \
113+
b"\xf9\xd0\x53\xd9\x00\x20\x00\x14\x00\x02\x3c\xd7\x21\x12\xa4\x42" \
114+
b"\x91\x1b\x25\x32\x99\x8d\xa0\x1c\xf9\xd0\x53\xd8"
115+
116+
parsed = STUN(raw)
117+
assert parsed.RESERVED == 0x00, parsed.RESERVED
118+
assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding success response"
119+
assert parsed.length == 24
120+
assert parsed.magic_cookie == 0x2112A442
121+
assert parsed.transaction_id == 0x911b2532998da01cf9d053d9, parsed.transaction_id
122+
assert len(parsed.attributes) == 1, len(parsed.attributes)
123+
assert parsed.attributes[0].type == 0x0020, parsed.attributes[0].type
124+
assert parsed.attributes[0].length == 20, parsed.attributes[0].length
125+
assert parsed.attributes[0].address_family == 0x02, parsed.attributes[0].address_family
126+
assert parsed.attributes[0].xport == 7621, parsed.attributes[0].xport
127+
assert parsed.attributes[0].xip == "::1", parsed.attributes[0].xip
128+
129+
= test STUN classic binding success response
130+
131+
raw = b"\x01\x01\x00\x0c\x37\x06\xd1\x4d\x38\x3a\xd6\xc8\x40\x5e\x17\x9a" \
132+
b"\x93\x92\xea\xa8\x00\x01\x00\x08\x00\x01\x0d\x14\xc0\xa8\x00\x05"
133+
134+
parsed = STUN(raw)
135+
assert parsed.RESERVED == 0x00, parsed.RESERVED
136+
assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding success response"
137+
assert parsed.length == 12
138+
assert parsed.magic_cookie == 0x3706d14d
139+
assert parsed.transaction_id == 0x383ad6c8405e179a9392eaa8, parsed.transaction_id
140+
assert len(parsed.attributes) == 1, len(parsed.attributes)
141+
assert parsed.attributes[0].type == 0x0001, parsed.attributes[0].type
142+
assert parsed.attributes[0].length == 8, parsed.attributes[0].length
143+
assert parsed.attributes[0].address_family == 0x01, parsed.attributes[0].address_family
144+
assert parsed.attributes[0].port == 3348, parsed.attributes[0].port
145+
assert parsed.attributes[0].ip == "192.168.0.5", parsed.attributes[0].ip
146+
110147
= test STUN binding indication 1
111148

112149
raw = b"\x00\x11\x00\x08\x21\x12\xa4\x42\x29\x3d\x68\x7b\x0f\xbc\x44\x7c" \
@@ -145,4 +182,61 @@ stun = STUN(
145182
built = stun.build()
146183
parsed = STUN(built)
147184

148-
assert parsed.build() == built
185+
assert parsed.build() == built
186+
187+
= test STUN packet build with attributes
188+
stun = STUN(
189+
stun_message_type="Binding success response",
190+
transaction_id=0x3479476534635936316a796a,
191+
attributes=[
192+
STUNXorMappedAddress(xport=25000, xip="172.20.0.200"),
193+
STUNUsername(length=37, username="Ht11MaRZHc4GOLJUsbu1R3YCs72HYN25:oNph"),
194+
STUNMessageIntegrity(hmac_sha1=0x4b67036dfb65ca84d63bcac86c8d5981df657031),
195+
STUNFingerprint(crc_32=0x4041e9c3)
196+
]
197+
)
198+
199+
built = stun.build()
200+
parsed = STUN(built)
201+
202+
assert parsed.build() == built
203+
204+
= test STUN packet build IPv6
205+
206+
stun = STUN(
207+
stun_message_type="Binding success response",
208+
transaction_id=0x911b2532998da01cf9d053d9,
209+
attributes=[
210+
STUNXorMappedAddress(xport=7621, address_family="IPv6", xip="::1")
211+
]
212+
)
213+
built = stun.build()
214+
parsed = STUN(built)
215+
216+
assert parsed.build() == built
217+
assert parsed.attributes[0].length == 20
218+
219+
= test STUN bottom up binding 1
220+
221+
udp = UDP(sport=62049, dport=3478) / STUN()
222+
built = udp.build()
223+
parsed = UDP(built)
224+
225+
assert type(parsed.payload) == STUN, parsed.show(dump=True)
226+
227+
= test STUN bottom up binding 2
228+
229+
udp = UDP(sport=3478, dport=62049) / STUN(stun_message_type="Binding error response")
230+
built = udp.build()
231+
parsed = UDP(built)
232+
233+
assert type(parsed.payload) == STUN, parsed.show(dump=True)
234+
235+
= test STUN top down binding
236+
237+
udp = UDP() / STUN()
238+
built = udp.build()
239+
parsed = UDP(built)
240+
241+
assert parsed.sport == 3478, parsed.sport
242+
assert parsed.dport == 3478, parsed.dport

0 commit comments

Comments
 (0)