1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
|
local ipOps = require "ipOps"
local math = require "math"
local nmap = require "nmap"
local packet = require "packet"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
description = [[
Performs simple Path MTU Discovery to target hosts.
TCP or UDP packets are sent to the host with the DF (don't fragment) bit set
and with varying amounts of data. If an ICMP Fragmentation Needed is received,
or no reply is received after retransmissions, the amount of data is lowered
and another packet is sent. This continues until (assuming no errors occur) a
reply from the final host is received, indicating the packet reached the host
without being fragmented.
Not all MTUs are attempted so as to not expend too much time or network
resources. Currently the relatively short list of MTUs to try contains
the plateau values from Table 7-1 in RFC 1191, "Path MTU Discovery".
Using these values significantly cuts down the MTU search space. On top
of that, this list is rarely traversed in whole because:
* the MTU of the outgoing interface is used as a starting point, and
* we can jump down the list when an intermediate router sending a "can't fragment" message includes its next hop MTU (as described in RFC 1191 and required by RFC 1812)
]]
---
-- @usage
-- nmap --script path-mtu target
--
-- @output
-- Host script results:
-- |_path-mtu: 1492 <= PMTU < 1500
--
-- Host script results:
-- |_path-mtu: PMTU == 1006
author = "Kris Katterjohn"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"safe", "discovery"}
local IPPROTO_ICMP = packet.IPPROTO_ICMP
local IPPROTO_TCP = packet.IPPROTO_TCP
local IPPROTO_UDP = packet.IPPROTO_UDP
-- Number of times to retransmit for no reply before dropping to
-- another MTU value
local RETRIES = 1
-- RFC 1191, Table 7-1: Plateaus. Even the massive MTU values are
-- here since we skip down the list based on the outgoing interface
-- so its no harm.
local MTUS = {
65535,
32000,
17914,
8166,
4352,
2002,
1492,
1006,
508,
296,
68
}
-- Find the index in MTUS{} to use based on the MTU +new+. If +new+ is in
-- between values in MTUS, then insert it into the table appropriately.
local searchmtu = function(cidx, new)
if new == 0 then
return cidx
end
while cidx <= #MTUS do
if new >= MTUS[cidx] then
if new ~= MTUS[cidx] then
table.insert(MTUS, cidx, new)
end
return cidx
end
cidx = cidx + 1
end
return cidx
end
local dport = function(ip)
if ip.ip_p == IPPROTO_TCP then
return ip.tcp_dport
elseif ip.ip_p == IPPROTO_UDP then
return ip.udp_dport
end
end
local sport = function(ip)
if ip.ip_p == IPPROTO_TCP then
return ip.tcp_sport
elseif ip.ip_p == IPPROTO_UDP then
return ip.udp_sport
end
end
-- Checks how we should react to this packet
local checkpkt = function(reply, orig)
local ip = packet.Packet:new(reply, reply:len())
if ip.ip_p == IPPROTO_ICMP then
if ip.icmp_type ~= 3 then
return "recap"
end
-- Port Unreachable
if ip.icmp_code == 3 then
local is = ip.buf:sub(ip.icmp_offset + 9)
local ip2 = packet.Packet:new(is, is:len())
-- Check sent packet against ICMP payload
if ip2.ip_p ~= IPPROTO_UDP or
ip2.ip_p ~= orig.ip_p or
ip2.ip_bin_src ~= orig.ip_bin_src or
ip2.ip_bin_dst ~= orig.ip_bin_dst or
sport(ip2) ~= sport(orig) or
dport(ip2) ~= dport(orig) then
return "recap"
end
return "gotreply"
end
-- Frag needed, DF set
if ip.icmp_code == 4 then
local val = ip:u16(ip.icmp_offset + 6)
return "nextmtu", val
end
return "recap"
end
if ip.ip_p ~= orig.ip_p or
ip.ip_bin_src ~= orig.ip_bin_dst or
ip.ip_bin_dst ~= orig.ip_bin_src or
dport(ip) ~= sport(orig) or
sport(ip) ~= dport(orig) then
return "recap"
end
return "gotreply"
end
-- This is all we can use since we can get various protocols back from
-- different hosts
local check = function(layer3)
local ip = packet.Packet:new(layer3, layer3:len())
return ip.ip_bin_dst
end
-- Updates a packet's info and calculates checksum
local updatepkt = function(ip)
if ip.ip_p == IPPROTO_TCP then
ip:tcp_set_sport(math.random(0x401, 0xffff))
ip:tcp_set_seq(math.random(1, 0x7fffffff))
ip:tcp_count_checksum()
elseif ip.ip_p == IPPROTO_UDP then
ip:udp_set_sport(math.random(0x401, 0xffff))
ip:udp_set_length(ip.ip_len - ip.ip_hl * 4)
ip:udp_count_checksum()
end
ip:ip_count_checksum()
end
-- Set up packet header and data to satisfy a certain MTU
local setmtu = function(pkt, mtu)
if pkt.ip_len < mtu then
pkt.buf = pkt.buf .. string.rep("\0", mtu - pkt.ip_len)
else
pkt.buf = pkt.buf:sub(1, mtu)
end
pkt:ip_set_len(mtu)
pkt.packet_length = mtu
updatepkt(pkt)
end
local basepkt = function(proto)
local ibin = stdnse.fromhex(
"4500 0014 0000 4000 8000 0000 0000 0000 0000 0000"
)
local tbin = stdnse.fromhex(
"0000 0000 0000 0000 0000 0000 6002 0c00 0000 0000 0204 05b4"
)
local ubin = stdnse.fromhex(
"0000 0000 0800 0000"
)
if proto == IPPROTO_TCP then
return ibin .. tbin
elseif proto == IPPROTO_UDP then
return ibin .. ubin
end
end
-- Creates a Packet object for the given proto and port
local genericpkt = function(host, proto, port)
local pkt = basepkt(proto)
local ip = packet.Packet:new(pkt, pkt:len())
ip:ip_set_bin_src(host.bin_ip_src)
ip:ip_set_bin_dst(host.bin_ip)
ip:set_u8(ip.ip_offset + 9, proto)
ip.ip_p = proto
ip:ip_set_len(pkt:len())
if proto == IPPROTO_TCP then
ip:tcp_parse(false)
ip:tcp_set_dport(port)
elseif proto == IPPROTO_UDP then
ip:udp_parse(false)
ip:udp_set_dport(port)
end
updatepkt(ip)
return ip
end
local ipproto = function(p)
if p == "tcp" then
return IPPROTO_TCP
elseif p == "udp" then
return IPPROTO_UDP
end
return -1
end
-- Determines how to probe
local getprobe = function(host)
local combos = {
{ "tcp", "open" },
{ "tcp", "closed" },
-- udp/open probably only happens when Nmap sends proper
-- payloads, which doesn't happen in here
{ "udp", "closed" }
}
local proto = nil
local port = nil
for _, c in ipairs(combos) do
port = nmap.get_ports(host, nil, c[1], c[2])
if port then
proto = c[1]
break
end
end
return proto, port
end
-- Sets necessary probe data in registry
local setreg = function(host, proto, port)
host.registry['pathmtuprobe'] = {
['proto'] = proto,
['port'] = port
}
end
hostrule = function(host)
if not nmap.is_privileged() then
nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
if not nmap.registry[SCRIPT_NAME].rootfail then
stdnse.verbose1("not running for lack of privileges.")
end
nmap.registry[SCRIPT_NAME].rootfail = true
return nil
end
if nmap.address_family() ~= 'inet' then
stdnse.debug1("is IPv4 compatible only.")
return false
end
if not (host.interface and host.interface_mtu) then
return false
end
local proto, port = getprobe(host)
if not (proto and port) then
return false
end
setreg(host, proto, port.number)
return true
end
action = function(host)
local m, r
local gotit = false
local mtuset
local sock = nmap.new_dnet()
local pcap = nmap.new_socket()
local proto = host.registry['pathmtuprobe']['proto']
local port = host.registry['pathmtuprobe']['port']
local saddr = ipOps.str_to_ip(host.bin_ip_src)
local daddr = ipOps.str_to_ip(host.bin_ip)
local try = nmap.new_try()
local status, pkt, ip
try(sock:ip_open())
try = nmap.new_try(function() sock:ip_close() end)
pcap:pcap_open(host.interface, 104, false, "dst host " .. saddr .. " and (icmp or (" .. proto .. " and src host " .. daddr .. " and src port " .. port .. "))")
-- Since we're sending potentially large amounts of data per packet,
-- simply bump up the host's calculated timeout value. Most replies
-- should come from routers along the path, fragmentation reassembly
-- times isn't an issue and the large amount of data is only traveling
-- in one direction; still, we want a response from the target so call
-- it 1.5*timeout to play it safer.
pcap:set_timeout(1.5 * host.times.timeout * 1000)
m = searchmtu(1, host.interface_mtu)
mtuset = MTUS[m]
local pkt = genericpkt(host, ipproto(proto), port)
while m <= #MTUS do
setmtu(pkt, MTUS[m])
r = 0
status = false
while true do
if not status then
if not sock:ip_send(pkt.buf, host) then
-- Got a send error, perhaps EMSGSIZE
-- when we don't know our interface's
-- MTU. Drop an MTU and keep trying.
break
end
end
local test = pkt.ip_bin_src
local status, length, _, layer3 = pcap:pcap_receive()
while status and test ~= check(layer3) do
status, length, _, layer3 = pcap:pcap_receive()
end
if status then
local t, v = checkpkt(layer3, pkt)
if t == "gotreply" then
gotit = true
break
elseif t == "recap" then
elseif t == "nextmtu" then
if v == 0 then
-- Router didn't send its
-- next-hop MTU. Just drop
-- a level.
break
end
-- Lua's lack of a continue statement
-- for loop control sucks, so dec m
-- here as it's inc'd below. Ugh.
m = searchmtu(m, v) - 1
mtuset = v
break
end
else
if r >= RETRIES then
break
end
r = r + 1
end
end
if gotit then
break
end
m = m + 1
end
pcap:close()
sock:ip_close()
if not gotit then
if nmap.debugging() > 0 then
return "Error: Unable to determine PMTU (no replies)"
end
return
end
if MTUS[m] == mtuset then
return "PMTU == " .. MTUS[m]
elseif m == 1 then
return "PMTU >= " .. MTUS[m]
else
return "" .. MTUS[m] .. " <= PMTU < " .. MTUS[m - 1]
end
end
|