diff --git a/scripts/firewalk.nse b/scripts/firewalk.nse
index 0c09c0364..3f62dc69a 100644
--- a/scripts/firewalk.nse
+++ b/scripts/firewalk.nse
@@ -2,20 +2,22 @@ description = [[
Tries to discover firewall rules using an IP TTL expiration technique known
as firewalking.
-The scan requires a firewall (or "gateway") and a metric (or "target").
-For each filtered port on the target, send a probe with an IP TTL one greater
-than the number of hops to the gateway. The TTL can be given in two ways:
-directly with the firewalk.ttl script argument, or indirectly with
-the firewalk.gateway script argument. For
-firewalk.gateway, Nmap must be run with the
---traceroute option and the gateway must appear as one of the
-traceroute hops.
+To determine a rule on a given gateway, the scanner sends a probe to a metric
+located behind the gateway, with a TTL one higher than the gateway. If the probe
+is forwarded by the gateway, then we can expect to receive an ICMP_TIME_EXCEEDED
+reply from the gateway next hop router, or eventually the metric itself if it is
+directly connected to the gateway. Otherwise, the probe will timeout.
-If the probe is forwarded by the gateway, then we can expect to receive an
-ICMP_TIME_EXCEEDED reply from the gateway next hop router, or eventually the
-target if it is directly connected to the gateway. Otherwise, the probe will
-timeout. As for UDP scans, this process can be quite slow if lots of ports are
-blocked by the gateway.
+It starts with a TTL equals to the distance to the target. If the probe timeout,
+then it is resent with a TTL decreased by one. If we get an ICMP_TIME_EXCEEDED,
+then the scan is over for this probe.
+
+Every "no-reply" filtered TCP and UDP ports are probed. As for UDP scans, this
+process can be quite slow if lots of ports are blocked by a gateway close to the
+scanner.
+
+Scan parameters can be controlled using the
+optionnal arguments.
From an original idea of M. Schiffman and D. Goldsmith, authors of the
firewalk tool.
@@ -24,77 +26,156 @@ firewalk tool.
---
-- @usage
--- nmap --script firewalk --script-args firewalk.gateway=a.b.c.d --traceroute target
--- @usage
--- nmap --script firewalk --script-args firewalk.ttl=7 target
+-- nmap --script=firewalk-path --traceroute
+--
+-- @usage
+-- nmap --script=firewalk-path --traceroute --script-args=firewalk-path.max-retries=1
+--
+-- @usage
+-- nmap --script=firewalk-path --traceroute --script-args=firewalk-path.probe-timeout=1000
+--
+--
+-- @args firewalk-path.max-retries the maximum number of allowed retransmissions
+-- @args firewalk-path.recv-timeout the duration of the packets capture loop (in milliseconds)
+-- @args firewalk-path.probe-timeout validity period of a probe (in milliseconds)
+-- @args firewalk-path.max-active-probes maximum number of parallel active probes
--
--- @args firewalk.gateway IP address of the tested firewall. Must be present in the traceroute results.
--- @args firewalk.ttl value of the TTL to use. Should be one greater than the
--- number of hops to the gateway. In case both firewalk.ttl and
--- firewalk.gateway IP address are
--- supplied, firewalk.gateway is ignored.
--
-- @output
-- | firewalk:
--- | PROTOCOL FORWARDED PORTS
--- | udp 123,137,161
--- |_tcp 21-80,443
+-- | HOP HOST PROTOCOL BLOCKED PORTS
+-- | 2 192.168.1.1 tcp 21-23,80
+-- | udp 21-23,80
+-- | 6 10.0.1.1 tcp 67-68
+-- | 7 10.0.1.254 tcp 25
+-- |_ udp 25
+--
--
--- 08/28/2010
-author = "Henri Doreau"
+-- 12/29/2010
+author = "Henri Doreau "
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"safe", "discovery"}
+-- TODO
+-- o add an option to select ports to probe
+-- o add an option to select gateway(s)/TTL(s) to probe
+-- o remove traceroute dependency
+
+
require('bin')
+require('stdnse')
require('packet')
require('tab')
-local ICMP_TIME_EXCEEDED = 11
+
+-----= scan parameters defaults =-----
-- number of retries for unanswered probes
-local MAX_RETRIES = 2
+local DEFAULT_MAX_RETRIES = 2
---
+-- packets capture loop timeout in milliseconds
+local DEFAULT_RECV_TIMEOUT = 20
+
+-- probe life time in milliseconds
+local DEFAULT_PROBE_TIMEOUT = 2000
+
+-- max number of simultaneously neither replied nor timed out probes
+local DEFAULT_MAX_ACTIVE_PROBES = 20
+
+----------------------------------------
+
+
+
+-- global scan parameters
+local MaxRetries
+local RecvTimeout
+local ProbeTimeout
+local MaxActiveProbes
+
+
+
+-- probed port states
+local PSTATE_UNKNOWN = 0
+local PSTATE_SCANNED = 1
+
+
+-- ICMP constant
+local ICMP_TIME_EXCEEDED = 11
+
+
+
+--- lookup for TTL of a given gateway in a traceroute results table
+-- @param traceroute a host traceroute results table
+-- @param gw the IP address of the gateway (as a decimal-dotted string)
+-- @return the TTL of the gateway or -1 on error
+local function gateway_ttl(traceroute, gw)
+
+ for ttl, hop in ipairs(traceroute) do
+ -- chekc hop.ip ~= nil as timedout hops are represented by empty tables
+ if hop.ip and hop.ip == gw then
+ return ttl
+ end
+ end
+
+ return -1
+end
+
+
+--=
-- Protocol specific functions are broken down per protocol, in separate tables.
-- This design eases the addition of new protocols
---
+--=
--- TCP related functions
local tcp_funcs = {
- --- perform layer 4 validation: compare TCP ports between two packets
- -- @param ip a TCP packet
- -- @param orig the reference TCP packet
- -- @return wether source and destination ports are the same for the two
- -- packets or not.
- l4check = function(ip, orig)
- return ip.tcp_sport == orig.tcp_sport and ip.tcp_dport == orig.tcp_dport
+
+ --- update the global scan status with a reply
+ -- @param scanner the scanner handle
+ -- @param ip the ICMP time exceeded error packet
+ -- @param ip2 the ICMP payload (our original expired probe)
+ update_scan = function(scanner, ip, ip2)
+
+ local port = ip2.tcp_dport
+
+ if port and scanner.ports.tcp[port] then
+
+ stdnse.print_debug("Marking port %d/tcp as forwarded (reply from %s)", ip2.tcp_dport, packet.toip(ip.ip_bin_src))
+
+ -- mark the gateway as forwarding the packet
+ scanner.ports.tcp[port].final_ttl = gateway_ttl(scanner.target.traceroute, packet.toip(ip.ip_bin_src))
+ scanner.ports.tcp[port].state = PSTATE_SCANNED
+
+ -- remove the related probe
+ for i, probe in ipairs(scanner.active_probes) do
+ if probe.proto == "tcp" and probe.portno == ip2.tcp_dport then
+ table.remove(scanner.active_probes, i)
+ end
+ end
+
+ else
+ stdnse.print_debug("Invalid reply to port %d/tcp", ip2.tcp_dport)
+ end
end,
- --- update informations of a generic TCP packet
- -- @param ip the IP packet to update
- -- @param dport the new TCP destination port to set
- updatepkt = function(ip, dport)
- ip:tcp_set_sport(math.random(0x401, 0xffff))
- ip:tcp_set_dport(dport)
- ip:tcp_set_seq(math.random(1, 0x7fffffff))
- ip:tcp_count_checksum()
- end,
-
- --- create a generic TCP probe packet, with IP ttl and destination port set to zero
+ --- create a TCP probe packet
-- @param host Host object that represents the destination
+ -- @param dport the TCP destination port
+ -- @param ttl the IP time to live
-- @return the newly crafted IP packet
- genericpkt = function(host)
+ getprobe = function(host, dport, ttl)
local pktbin = bin.pack("H",
"4500 0014 0000 4000 8000 0000 0000 0000 0000 0000" ..
"0000 0000 0000 0000 0000 0000 6002 0c00 0000 0000 0204 05b4"
)
+
local ip = packet.Packet:new(pktbin, pktbin:len())
+
ip:tcp_parse(false)
ip:ip_set_bin_src(host.bin_ip_src)
ip:ip_set_bin_dst(host.bin_ip)
@@ -103,6 +184,13 @@ local tcp_funcs = {
ip.ip_p = packet.IPPROTO_TCP
ip:ip_set_len(pktbin:len())
+ ip:tcp_set_sport(math.random(0x401, 0xffff))
+ ip:tcp_set_dport(dport)
+ ip:tcp_set_seq(math.random(1, 0x7fffffff))
+ ip:tcp_count_checksum()
+ ip:ip_set_ttl(ttl)
+ ip:ip_count_checksum()
+
return ip
end,
@@ -110,33 +198,45 @@ local tcp_funcs = {
-- UDP related functions
local udp_funcs = {
- --- perform layer 4 validation: compare UDP ports between two packets
- -- @param ip a TCP packet
- -- @param orig the reference UDP packet
- -- @return wether source and destination ports are the same for the two
- -- packets or not.
- l4check = function(ip, orig)
- return ip.udp_sport == orig.udp_sport and ip.udp_dport == orig.udp_dport
- end,
- --- update informations of a generic UDP packet
- -- @param ip the IP packet to update
- -- @param dport the new UDP destination port to set
- updatepkt = function(ip, dport)
- ip:udp_set_sport(math.random(0x401, 0xffff))
- ip:udp_set_dport(dport)
- ip:udp_set_length(ip.ip_len - ip.ip_hl * 4)
- ip:udp_count_checksum()
+ --- update the global scan status with a reply
+ -- @param scanner the scanner handle
+ -- @param ip the ICMP time exceeded error packet
+ -- @param ip2 the ICMP payload (our original expired probe)
+ update_scan = function(scanner, ip, ip2)
+
+ local port = ip2.udp_dport
+
+ if port and scanner.ports.udp[port] then
+
+ stdnse.print_debug("Marking port %d/udp as forwarded", ip2.udp_dport)
+
+ -- mark the gateway as forwarding the packet
+ scanner.ports.udp[port].final_ttl = gateway_ttl(scanner.target.traceroute, packet.toip(ip.ip_bin_src))
+ scanner.ports.udp[port].state = PSTATE_SCANNED
+
+ for i, probe in ipairs(scanner.active_probes) do
+ if probe.proto == "udp" and probe.portno == ip2.udp_dport then
+ table.remove(scanner.active_probes, i)
+ end
+ end
+
+ else
+ stdnse.print_debug("Invalid reply to port %d/udp", ip2.udp_dport)
+ end
end,
--- create a generic UDP probe packet, with IP ttl and destination port set to zero
-- @param host Host object that represents the destination
+ -- @param dport the UDP destination port
+ -- @param ttl the IP time to live
-- @return the newly crafted IP packet
- genericpkt = function(host)
+ getprobe = function(host, dport, ttl)
local pktbin = bin.pack("H",
"4500 0014 0000 4000 8000 0000 0000 0000 0000 0000" ..
"0000 0000 0800 0000"
)
+
local ip = packet.Packet:new(pktbin, pktbin:len())
ip:udp_parse(false)
@@ -147,6 +247,13 @@ local udp_funcs = {
ip.ip_p = packet.IPPROTO_UDP
ip:ip_set_len(pktbin:len())
+ ip:udp_set_sport(math.random(0x401, 0xffff))
+ ip:udp_set_dport(dport)
+ ip:udp_set_length(ip.ip_len - ip.ip_hl * 4)
+ ip:udp_count_checksum()
+ ip:ip_set_ttl(ttl)
+ ip:ip_count_checksum()
+
return ip
end,
}
@@ -157,54 +264,25 @@ local supported_protocols = {
udp = udp_funcs,
}
--- cache the currently tested protocol for the whole script
-local Protocol
+--- get the protocol name given its "packet" value
+-- @param proto the protocol value (eg. packet.IPPROTO_*)
+-- @return the protocol name as a string
+local function proto2str(proto)
-
---- ensure that the catched reply is a valid icmp time exceeded
--- @param reply the packet from the probed target
--- @param orig the sent probe
--- @return wether the reply appears to be valid or not
-local function checkpkt(reply, orig)
- local ip = packet.Packet:new(reply, reply:len())
-
- if ip.ip_p ~= packet.IPPROTO_ICMP or ip.icmp_type ~= ICMP_TIME_EXCEEDED then
- return false
+ if proto == packet.IPPROTO_TCP then
+ return "tcp"
+ elseif proto == packet.IPPROTO_UDP then
+ return "udp"
end
- local is = ip.buf:sub(ip.icmp_offset + 9)
- local ip2 = packet.Packet:new(is, is:len(), true)
-
- -- Check sent packet against ICMP payload
- if ip2.ip_p == orig.ip_p and
- ip2.ip_bin_src == orig.ip_bin_src and
- ip2.ip_bin_dst == orig.ip_bin_dst then
-
- -- check layer 4 protocols ports
- return Protocol.l4check(ip2, orig)
- end
-
- return false
+ return nil
end
---- set destination port and ip ttl to a generic probe packet
--- @param ip the ip object
--- @param dport the layer 4 destination port
--- @param ttl the ip ttl to set
-local function updatepkt(ip, dport, ttl)
- ip:ip_set_ttl(ttl)
-
- -- call the protocol specific function to update the layer 4 destination port
- Protocol.updatepkt(ip, dport)
-
- ip:ip_count_checksum()
-end
-
---- get the list of ports to probe
--- @param host Host object that represents the targetted host
--- @return array of ports to probe, sorted per protocol
-local function getports(host)
- local ports = {}
+--- generate list of ports to probe
+-- @param host the destination host object
+-- @return an array of the ports to probe, sorted per protocol
+local function build_portlist(host)
+ local portlist = {}
local combos = {
{"tcp", "filtered"},
{"udp", "open|filtered"}
@@ -215,120 +293,118 @@ local function getports(host)
local proto = combo[1]
local state = combo[2]
- ports[proto] = {}
+ portlist[proto] = {}
repeat
port = nmap.get_ports(host, port, proto, state)
- if port then
- table.insert(ports[proto], port.number)
+
+ -- do not include administratively prohibited ports
+ if port and port.reason == "no-response" then
+ local pentry = {
+ final_ttl = 0, -- TTL of the blocking gateway
+ state = PSTATE_UNKNOWN, -- initial state: unprobed => unknown
+ }
+
+ portlist[proto][port.number] = pentry
end
until not port
end
- return ports
+ return portlist
+
end
---- store the firewalk ports into the registry
--- @param host Host object that represents the targetted host
--- @param ports list of ports to firewalk
+--- store the portlist in the register
+-- @param host the destination host object
+-- @param ports the table of ports to probe
local function setregs(host, ports)
+
if not nmap.registry[host.ip] then
nmap.registry[host.ip] = {}
end
- nmap.registry[host.ip]['firewalk_ports'] = ports
+
+ nmap.registry[host.ip]['firewalk-path_ports'] = ports
+
end
--- host rule, check for requirements before to launch the script
hostrule = function(host)
+
-- firewalk requires privileges to run
if not nmap.is_privileged() then
- if not nmap.registry['firewalk'] then
- nmap.registry['firewalk'] = {}
+ if not nmap.registry['firewalk-path'] then
+ nmap.registry['firewalk-path'] = {}
end
- if nmap.registry['firewalk']['rootfail'] then
+
+ if nmap.registry['firewalk-path']['rootfail'] then
return false
end
- nmap.registry['firewalk']['rootfail'] = true
+
+ nmap.registry['firewalk-path']['rootfail'] = true
+
if nmap.verbosity() > 0 then
- nmap.log_write("stdout", "FIREWALK: not running for lack of privileges")
+ nmap.log_write("stdout", SCRIPT_NAME .. ": not running for lack of privileges")
end
+
return false
end
+
if not host.interface then
return false
end
+
-- get the list of ports to probe
- local ports = getports(host)
+ local portlist = build_portlist(host)
local nb_ports = 0
- for proto in pairs(ports) do
- nb_ports = nb_ports + #ports[proto]
+
+ for _, proto in pairs(portlist) do
+ for _ in pairs(proto) do
+ nb_ports = nb_ports + 1
+ end
end
+
-- nothing to probe: cancel the execution
if nb_ports < 1 then
return false
end
- setregs(host, ports)
+
+ setregs(host, portlist)
+
return true
end
---- bind the scan to the supplied ttl if given or to gateway(ttl) + 1
--- @param host Host object that represents the targetted host
--- @return the value of the ttl to use in our probes (or nil on error)
-local function ttlmetric(host)
- local ttl = stdnse.get_script_args("firewalk.ttl")
- if ttl then
- return ttl
- end
-
- -- if no ttl is supplied, the script requires the gateway IP address and the
- -- nmap traceroute resutls to find out the tt value to use
- local gateway = stdnse.get_script_args("firewalk.gateway")
- if not gateway then
- if nmap.verbosity() > 0 then
- nmap.log_write("stdout", "FIREWALK: can't run without one of the script args firewalk.gateway or firewalk.ttl")
- return nil
- end
- end
+--- return the initial TTL to use (the one of the last gateway before the target)
+-- @param host the object representing the target with traceroute results available
+-- @return the IP TTL of the last gateway before the target
+local function initial_ttl(host)
if not host.traceroute then
- if not nmap.registry['firewalk'] then
- nmap.registry['firewalk'] = {}
+ if not nmap.registry['firewalk-path'] then
+ nmap.registry['firewalk-path'] = {}
end
- if nmap.registry['firewalk']['traceroutefail'] then
+
+ if nmap.registry['firewalk-path']['traceroutefail'] then
return nil
end
- nmap.registry['firewalk']['traceroutefail'] = true
+
+ nmap.registry['firewalk-path']['traceroutefail'] = true
+
if nmap.verbosity() > 0 then
- -- XXX maybe talk about the ttl option?
- nmap.log_write("stdout", "FIREWALK: using the argument `firewalk.gateway' requires unavailable traceroute informations")
+ nmap.log_write("stdout", SCRIPT_NAME .. ": requires unavailable traceroute informations")
end
+
return nil
end
- if gateway == host.ip then
- if nmap.verbosity() > 0 then
- nmap.log_write("stdout", "FIREWALK: metric and gateway cannot be the same host")
- return nil
- end
- end
- -- look for the ttl value to use according to traceroute results
- for i, hop in pairs(host.traceroute) do
- if hop.ip == gateway then
- return i + 1
- end
- end
-
- if nmap.verbosity() > 0 then
- nmap.log_write("stdout", "FIREWALK: metric " .. gateway .. " doesn't appear in traceroute")
- end
-
- return nil
+ stdnse.print_debug("Using ttl %d", #host.traceroute)
+ return #host.traceroute
end
--- convert an array of ports into a port ranges string like "x,y-z"
-- @param ports an array of numbers
-- @return a string representing the ports as folded ranges
local function portrange(ports)
+
table.sort(ports)
local numranges = {}
@@ -337,23 +413,29 @@ local function portrange(ports)
end
for _, p in ipairs(ports) do
+
local stored = false
-- iterate over the ports list
for k, range in ipairs(numranges) do
+
-- increase an existing range by the left
- if p == range["start"]-1 then
+ if p == range["start"] - 1 then
numranges[k]["start"] = p
stored = true
+
-- increase an existing range by the right
- elseif p == range["stop"]+1 then
+ elseif p == range["stop"] + 1 then
numranges[k]["stop"] = p
stored = true
+
-- port contained in an already existing range (catch doublons)
elseif p >= range["start"] and p <= range["stop"] then
stored = true
end
+
end
+
-- start a new range
if not stored then
local range = {}
@@ -361,13 +443,16 @@ local function portrange(ports)
range["stop"] = p
table.insert(numranges, range)
end
+
end
-- stringify the ranges
local strrange = {}
for i, val in ipairs(numranges) do
+
local start = tostring(val["start"])
local stop = tostring(val["stop"])
+
if start == stop then
table.insert(strrange, start)
else
@@ -375,114 +460,342 @@ local function portrange(ports)
table.insert(strrange, start .. "-" .. stop)
end
end
+
-- ranges are delimited by `,'
return stdnse.strjoin(",", strrange)
+
end
---- pcap check function
--- @return destination ip address, the ip protocol and icmp type
-local function check (layer3)
+--- check whether an incoming IP packet is an ICMP TIME_EXCEEDED packet or not
+-- @param src the source IP address
+-- @param layer3 the IP incoming datagram
+-- @return whether the packet seems to be a valid reply or not
+local function check(src, layer3)
+
local ip = packet.Packet:new(layer3, layer3:len())
- return bin.pack('ACC', ip.ip_bin_dst, ip.ip_p, ip.icmp_type)
+ return ip.ip_bin_dst == src and ip.ip_p == packet.IPPROTO_ICMP and ip.icmp_type == ICMP_TIME_EXCEEDED
+
end
---- fill a table with the results and dump it to generate the scan report
--- @param tested array of probed ports, one row per protocol
--- @param forwarded array of ports we discovered as forwarded, one row per protocol
--- @return the report string
-local function report(tested, forwarded)
- local output = tab.new()
+--- return a printable report of the scan
+-- @param scanner the scanner handle
+-- @return a printable table of scan results
+local function report(scanner)
+ local entries = 0
+ local output = tab.new(4)
- tab.add(output, 1, "PROTOCOL")
- tab.add(output, 2, "FORWARDED PORTS")
+ tab.add(output, 1, "HOP")
+ tab.add(output, 2, "HOST")
+ tab.add(output, 3, "PROTOCOL")
+ tab.add(output, 4, "BLOCKED PORTS")
tab.nextrow(output)
- -- script output: one line per protocol
- for proto in pairs(tested) do
- if #tested[proto] ~= 0 then
- tab.add(output, 1, proto)
- tab.add(output, 2, portrange(forwarded[proto]))
- tab.nextrow(output)
+ -- duplicate traceroute results and add localhost at the beginning
+ local path = {
+ -- XXX 'localhost' might be a better choice?
+ {ip = packet.toip(scanner.target.bin_ip_src)}
+ }
+
+ for _, v in pairs(scanner.target.traceroute) do
+ table.insert(path, v)
+ end
+
+
+ for ttl = 0, #path - 1 do
+ local fwdedports = {}
+
+ for proto, portlist in pairs(scanner.ports) do
+ fwdedports[proto] = {}
+
+ for portno, port in pairs(portlist) do
+
+ if port.final_ttl == ttl then
+ table.insert(fwdedports[proto], portno)
+ end
+ end
+ end
+
+
+ local nb_fports = 0
+
+ for _, proto in pairs(fwdedports) do
+ for _ in pairs(proto) do
+ nb_fports = nb_fports + 1
+ end
+ end
+
+ if nb_fports > 0 then
+
+ entries = entries + 1
+
+ -- the blocking gateway is just after the last forwarding one
+ tab.add(output, 1, tostring(ttl))
+
+ -- timedout traceroute hops are represented by empty tables
+ if path[ttl + 1].ip then
+ tab.add(output, 2, path[ttl + 1].ip)
+ else
+ tab.add(output, 2, "???")
+ end
+
+ for proto, ports in pairs(fwdedports) do
+ if #fwdedports[proto] > 0 then
+ tab.add(output, 3, proto)
+ tab.add(output, 4, portrange(ports))
+ tab.nextrow(output)
+ end
+ end
end
end
- return tab.dump(output)
+ if entries > 0 then
+ return "\n" .. tab.dump(output)
+ else
+ return "None found"
+ end
end
--- main firewalking logic
-action = function(host)
- local sock = nmap.new_dnet()
- local pcap = nmap.new_socket()
- local saddr = packet.toip(host.bin_ip_src)
- local ports = nmap.registry[host.ip]['firewalk_ports']
- local ttl = ttlmetric(host)
- local try = nmap.new_try()
- local fwdports = {} -- results table
+--- check whether the scan is finished or not
+-- @param scanner the scanner handle
+-- @return if some port is still in unknown state
+local function finished(scanner)
- -- abort if unable to bind the scan
- if not ttl then
+ for proto, ports in pairs(scanner.ports) do
+
+ -- ports are sorted per protocol
+ for _, port in pairs(ports) do
+
+ -- if a port is still unprobed => we're not done!
+ if port.state == PSTATE_UNKNOWN then
+ return false
+ end
+ end
+ end
+
+ -- every ports have been scanned
+ return true
+end
+
+--- send a probe and update it
+-- @param scanner the scanner handle
+-- @param probe the probe specifications and related informations
+local function send_probe(scanner, probe)
+
+ local try = nmap.new_try(function() scanner.sock:ip_close() end)
+
+ stdnse.print_debug("Sending new probe (%d/%s ttl=%d)", probe.portno, probe.proto, probe.ttl)
+
+ -- craft the raw packet
+ local pkt = supported_protocols[probe.proto].getprobe(scanner.target, probe.portno, probe.ttl)
+
+ try(scanner.sock:ip_send(pkt.buf))
+
+ -- update probe informations
+ probe.retry = probe.retry + 1
+ probe.sent_time = nmap.clock_ms()
+
+end
+
+--- send some new probes
+-- @param scanner the scanner handle
+local function send_next_probes(scanner)
+
+ -- this prevents sending too much probes at the same time
+ while #scanner.active_probes < MaxActiveProbes do
+
+ -- perform resends
+ if #scanner.pending_resends > 0 then
+
+ probe = scanner.pending_resends[1]
+ table.remove(scanner.pending_resends, 1)
+ table.insert(scanner.active_probes, probe)
+ send_probe(scanner, probe)
+
+ -- send new probes
+ elseif #scanner.sendqueue > 0 then
+
+ probe = scanner.sendqueue[1]
+ table.remove(scanner.sendqueue, 1)
+ table.insert(scanner.active_probes, probe)
+ send_probe(scanner, probe)
+
+ -- nothing else to send right now
+ else
+ return
+ end
+ end
+
+end
+
+--- update global state with an incoming reply
+-- @param scanner the scanner handle
+-- @param pkt an incoming valid IP packet
+local function parse_reply(scanner, pkt)
+
+ local ip = packet.Packet:new(pkt, pkt:len())
+
+ if ip.ip_p ~= packet.IPPROTO_ICMP or ip.icmp_type ~= ICMP_TIME_EXCEEDED then
+ return
+ end
+
+ local is = ip.buf:sub(ip.icmp_offset + 9)
+ local ip2 = packet.Packet:new(is, is:len(), true)
+
+ -- check ICMP payload
+ if ip2.ip_bin_src == scanner.target.bin_ip_src and
+ ip2.ip_bin_dst == scanner.target.bin_ip then
+
+ -- layer 4 checks
+ local proto_func = supported_protocols[proto2str(ip2.ip_p)]
+ if proto_func then
+ -- mark port as forwarded and discard any related pending probes
+ proto_func.update_scan(scanner, ip, ip2)
+ else
+ stdnse.print_debug("Invalid protocol for reply (%d)", ip2.ip_p)
+ end
+ end
+end
+
+--- wait for incoming replies
+-- @param scanner the scanner handle
+local function read_replies(scanner)
+
+ -- capture loop
+ local timeout = RecvTimeout
+ repeat
+
+ local start = nmap.clock_ms()
+
+ scanner.pcap:set_timeout(timeout)
+
+ local status, _, _, l3, _ = scanner.pcap:pcap_receive()
+
+ if status and check(scanner.target.bin_ip_src, l3) then
+ parse_reply(scanner, l3)
+ end
+
+ timeout = timeout - (nmap.clock_ms() - start)
+
+ until timeout <= 0 or #scanner.active_probes == 0
+end
+
+--- delete timedout probes, update pending probes
+-- @param scanner the scanner handle
+local function update_probe_queues(scanner)
+
+ local now = nmap.clock_ms()
+
+ -- remove timedout probes
+ for i, probe in ipairs(scanner.active_probes) do
+
+ if (now - probe.sent_time) >= ProbeTimeout then
+
+ table.remove(scanner.active_probes, i)
+
+ if probe.retry < MaxRetries then
+ table.insert(scanner.pending_resends, probe)
+ else
+
+ -- decrease ttl, reset retries counter and put probes in send queue
+ if probe.ttl > 1 then
+
+ probe.ttl = probe.ttl - 1
+ probe.retry = 0
+ table.insert(scanner.sendqueue, probe)
+
+ else
+
+ -- set final_ttl to zero (=> probe might be blocked by localhost)
+ scanner.ports[probe.proto][probe.portno].final_ttl = 0
+ scanner.ports[probe.proto][probe.portno].state = PSTATE_SCANNED
+
+ end
+ end
+ end
+ end
+end
+
+--- fills the send queue with initial probes
+-- @param scanner the scanner handle
+local function generate_initial_probes(scanner)
+
+ for proto, ports in pairs(scanner.ports) do
+
+ for portno in pairs(ports) do
+
+ -- simply store probe parameters and craft packet at send time
+ local probe = {
+ ttl = scanner.ttl, -- initial ttl value
+ proto = proto, -- layer 4 protocol (string)
+ portno = portno, -- layer 4 port number
+ retry = 0, -- retries counter
+ sent_time = 0 -- last sending time
+ }
+
+ table.insert(scanner.sendqueue, probe)
+
+ end
+ end
+end
+
+local function set_scan_parameters()
+
+ -- Assign parameters to scan constants or use defaults --
+
+ MaxRetries = tonumber(stdnse.get_script_args("firewalk-path.max-retries")) or DEFAULT_MAX_RETRIES
+
+ RecvTimeout = tonumber(stdnse.get_script_args("firewalk-path.recv-timeout")) or DEFAULT_RECV_TIMEOUT
+
+ ProbeTimeout = tonumber(stdnse.get_script_args("firewalk-path.probe-timeout")) or DEFAULT_PROBE_TIMEOUT
+
+ MaxActiveProbes = tonumber(stdnse.get_script_args("firewalk-path.max-active-probes")) or DEFAULT_MAX_ACTIVE_PROBES
+
+end
+
+--- firewalk entry point
+action = function(host)
+ local saddr = packet.toip(host.bin_ip_src)
+
+ -- scan handle, scanner state is saved in this table
+ local scanner = {
+ target = host,
+ ttl = initial_ttl(host),
+
+ sock = nmap.new_dnet(),
+ pcap = nmap.new_socket(),
+
+ ports = nmap.registry[host.ip]['firewalk-path_ports'],
+
+ sendqueue = {}, -- pending probes
+ pending_resends = {}, -- probes needing to be resent
+ active_probes = {}, -- probes currently neither replied nor timedout
+ }
+
+ if not scanner.ttl then
return nil
end
- -- filter for incoming icmp time exceeded replies
- pcap:pcap_open(host.interface, 104, false, "icmp and dst host " .. saddr)
+ -- assign user's values to scan parameters or use defaults
+ set_scan_parameters()
- try(sock:ip_open())
+ -- filter for incoming ICMP time exceeded replies
+ scanner.pcap:pcap_open(host.interface, 104, false, "icmp and dst host " .. saddr)
- try = nmap.new_try(function() sock:ip_close() end)
+ local try = nmap.new_try()
- pcap:set_timeout(3000)
+ try(scanner.sock:ip_open())
- -- ports are sorted by protocol
- for proto in pairs(ports) do
- -- cache the currently probed protocol for the whole script
- Protocol = supported_protocols[proto]
+ generate_initial_probes(scanner)
- if not Protocol then
- if nmap.verbosity() > 1 then
- nmap.log_write("stdout", "FIREWALK: Unsupported protocol: " .. proto)
- end
- return
- end
+ while not finished(scanner) do
+ send_next_probes(scanner)
+ read_replies(scanner)
+ update_probe_queues(scanner)
+ end
- fwdports[proto] = {}
+ scanner.sock:ip_close()
+ scanner.pcap:pcap_close()
- local pkt = Protocol.genericpkt(host)
-
- -- iterate over the list of ports for the current protocol
- for _, port in ipairs(ports[proto]) do
-
- updatepkt(pkt, port, ttl)
-
- local retry = 0
-
- -- resend on timeout to increase reliability
- while retry < MAX_RETRIES do
-
- try(sock:ip_send(pkt.buf))
- stdnse.print_debug(1, "Firewalk: trying port " .. port .. "/" .. proto)
-
- local status, _, _, rep = pcap:pcap_receive()
- local test = bin.pack('ACC', pkt.ip_bin_src, packet.IPPROTO_ICMP, ICMP_TIME_EXCEEDED);
- while status and test ~= check(rep) do
- status, length, _, layer3 = pcap:pcap_receive();
- end
-
- if status and checkpkt(rep, pkt) then
- stdnse.print_debug(1, "Firewalk: discovered forwarded port " .. port .. "/" .. proto)
- table.insert(fwdports[proto], port)
- break
- else
- retry = retry + 1
- end
-
- end -- retry
- end -- port
- end -- proto
-
- sock:ip_close()
- pcap:pcap_close()
-
- return "\n" .. report(ports, fwdports)
+ return report(scanner)
end