diff options
Diffstat (limited to 'scripts/firewalk.nse')
-rw-r--r-- | scripts/firewalk.nse | 1062 |
1 files changed, 1062 insertions, 0 deletions
diff --git a/scripts/firewalk.nse b/scripts/firewalk.nse new file mode 100644 index 0000000..31b4d31 --- /dev/null +++ b/scripts/firewalk.nse @@ -0,0 +1,1062 @@ +local ipOps = require "ipOps" +local math = require "math" +local nmap = require "nmap" +local packet = require "packet" +local stdnse = require "stdnse" +local tab = require "tab" +local table = require "table" + +description = [[ +Tries to discover firewall rules using an IP TTL expiration technique known +as firewalking. + +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. + +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 <code>firewalk.*</code> +optional arguments. + +From an original idea of M. Schiffman and D. Goldsmith, authors of the +firewalk tool. +]] + + +--- +-- @usage +-- nmap --script=firewalk --traceroute <host> +-- +-- @usage +-- nmap --script=firewalk --traceroute --script-args=firewalk.max-retries=1 <host> +-- +-- @usage +-- nmap --script=firewalk --traceroute --script-args=firewalk.probe-timeout=400ms <host> +-- +-- @usage +-- nmap --script=firewalk --traceroute --script-args=firewalk.max-probed-ports=7 <host> +-- +-- +-- @args firewalk.max-retries the maximum number of allowed retransmissions. +-- @args firewalk.recv-timeout the duration of the packets capture loop (in milliseconds). +-- @args firewalk.probe-timeout validity period of a probe (in milliseconds). +-- @args firewalk.max-active-probes maximum number of parallel active probes. +-- @args firewalk.max-probed-ports maximum number of ports to probe per protocol. Set to -1 to scan every filtered port. +-- +-- +-- @output +-- | firewalk: +-- | 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 +-- +-- + + +-- 11/29/2010: initial version +-- 03/28/2011: added IPv4 check +-- 01/02/2012: added IPv6 support + +author = "Henri Doreau" + +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" + +categories = {"safe", "discovery"} + + +-- TODO +-- o add an option to select gateway(s)/TTL(s) to probe +-- o remove traceroute dependency + + + + +-----= scan parameters defaults =----- + +-- number of retries for unanswered probes +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 + +-- maximum number of probed ports per protocol +local DEFAULT_MAX_PROBED_PORTS = 10 + +---------------------------------------- + + + +-- global scan parameters +local MaxRetries +local RecvTimeout +local ProbeTimeout +local MaxActiveProbes +local MaxProbedPorts + +-- cache ports to probe between the hostrule and the action function +local FirewalkPorts + + +-- ICMP constants +local ICMP_TIME_EXCEEDEDv4 = 11 +local ICMP_TIME_EXCEEDEDv6 = 03 + + + +-- Layer 4 specific function tables +local proto_vtable = {} + +-- Layer 3 specific function tables for the scanner +local Firewalk = {} + + +--- 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 + -- check 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 + +--- 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) + + if proto == packet.IPPROTO_TCP then + return "tcp" + elseif proto == packet.IPPROTO_UDP then + return "udp" + end + + return nil +end + + +--= +-- Protocol specific functions are broken down per protocol, in separate tables. +-- This design eases the addition of new protocols. +-- +-- Layer 4 (TCP, UDP) tables are duplicated to distinguish IPv4 and IPv6 +-- versions. +--= + +--- TCP related functions (IPv4 versions) +local tcp_funcs_v4 = { + + --- 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.debug1("Marking port %d/tcp v4 as forwarded (reply from %s)", ip2.tcp_dport, ip.ip_src) + + -- mark the gateway as forwarding the packet + scanner.ports.tcp[port].final_ttl = gateway_ttl(scanner.target.traceroute, ip.ip_src) + scanner.ports.tcp[port].scanned = true + + -- 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.debug1("Invalid reply to port %d/tcp", ip2.tcp_dport) + end + end, + + --- 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 + getprobe = function(host, dport, ttl) + local pktbin = stdnse.fromhex( + "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) + + ip:set_u8(ip.ip_offset + 9, packet.IPPROTO_TCP) + 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, + +} + +-- UDP related functions (IPv4 versions) +local udp_funcs_v4 = { + + --- 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.debug1("Marking port %d/udp v4 as forwarded", ip2.udp_dport) + + -- mark the gateway as forwarding the packet + scanner.ports.udp[port].final_ttl = gateway_ttl(scanner.target.traceroute, ip.ip_src) + scanner.ports.udp[port].scanned = true + + 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.debug1("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 + getprobe = function(host, dport, ttl) + local pktbin = stdnse.fromhex( + "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) + ip:ip_set_bin_src(host.bin_ip_src) + ip:ip_set_bin_dst(host.bin_ip) + + ip:set_u8(ip.ip_offset + 9, packet.IPPROTO_UDP) + 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, +} + +--- TCP related functions (IPv6 versions) +local tcp_funcs_v6 = { + + --- 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.debug1("Marking port %d/tcp v6 as forwarded (reply from %s)", ip2.tcp_dport, ip.ip_src) + + -- mark the gateway as forwarding the packet + scanner.ports.tcp[port].final_ttl = gateway_ttl(scanner.target.traceroute, ip.ip_src) + scanner.ports.tcp[port].scanned = true + + -- 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.debug1("Invalid reply to port %d/tcp", ip2.tcp_dport) + end + end, + + --- 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 + getprobe = function(host, dport, ttl) + local pktbin = stdnse.fromhex( + "4500 0014 0000 4000 8000 0000 0000 0000 0000 0000" .. + "0000 0000 0000 0000 0000 0000 6002 0c00 0000 0000 0204 05b4" + ) + + local tcp = packet.Packet:new(pktbin, pktbin:len()) + local ip = packet.Packet:new() + + tcp:tcp_parse(false) + + tcp:tcp_set_sport(math.random(0x401, 0xffff)) + tcp:tcp_set_dport(dport) + tcp:tcp_set_seq(math.random(1, 0x7fffffff)) + tcp:tcp_count_checksum() + tcp:ip_count_checksum() + + -- Extract layer 4 part and add it as payload to the IP packet + local tcp_buf = tcp.buf:sub(tcp.tcp_offset + 1, tcp.buf:len()) + ip:build_ipv6_packet(host.bin_ip_src, host.bin_ip, packet.IPPROTO_TCP, tcp_buf, ttl) + + return ip + end, + +} + +-- UDP related functions (IPv6 versions) +local udp_funcs_v6 = { + + --- 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.debug1("Marking port %d/udp v6 as forwarded (reply from %s)", ip2.udp_dport, ip2.ip_src) + + -- mark the gateway as forwarding the packet + scanner.ports.udp[port].final_ttl = gateway_ttl(scanner.target.traceroute, ip.ip_src) + scanner.ports.udp[port].scanned = true + + 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.debug1("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 + getprobe = function(host, dport, ttl) + local pktbin = stdnse.fromhex( + "4500 0014 0000 4000 8000 0000 0000 0000 0000 0000" .. + "0000 0000 0800 0000" + ) + + local udp = packet.Packet:new(pktbin, pktbin:len()) + local ip = packet.Packet:new() + + udp:udp_parse(false) + + udp:udp_set_sport(math.random(0x401, 0xffff)) + udp:udp_set_dport(dport) + udp:udp_set_length(8) + udp:udp_count_checksum() + udp:ip_count_checksum() + + -- Extract layer 4 part and add it as payload to the IP packet + local udp_buf = udp.buf:sub(udp.udp_offset + 1, udp.buf:len()) + ip:build_ipv6_packet(host.bin_ip_src, host.bin_ip, packet.IPPROTO_UDP, udp_buf, ttl) + + return ip + end, +} + + + +--= +-- IP-specific functions. The following tables provides scanner functions that +-- depend on the IP version. +--= + + +-- IPv4 functions +local Firewalk_v4 = { + + --- IPv4 initialization function. Open injection and reception sockets. + -- @param scanner the scanner handle + init = function(scanner) + local saddr = ipOps.str_to_ip(scanner.target.bin_ip_src) + + scanner.sock = nmap.new_dnet() + scanner.pcap = nmap.new_socket() + + -- filter for incoming ICMP time exceeded replies + scanner.pcap:pcap_open(scanner.target.interface, 104, false, "icmp and dst host " .. saddr) + + local try = nmap.new_try() + try(scanner.sock:ip_open()) + end, + + --- IPv4 cleanup function. Close injection and reception sockets. + -- @param scanner the scanner handle + shutdown = function(scanner) + scanner.sock:ip_close() + scanner.pcap:pcap_close() + end, + + --- 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 + check = function(src, layer3) + local ip = packet.Packet:new(layer3, layer3:len()) + return ip.ip_bin_dst == src + and ip.ip_p == packet.IPPROTO_ICMP + and ip.icmp_type == ICMP_TIME_EXCEEDEDv4 + end, + + --- update global state with an incoming reply + -- @param scanner the scanner handle + -- @param pkt an incoming valid IP packet + parse_reply = function(scanner, pkt) + local ip = packet.Packet:new(pkt, pkt:len()) + + if ip.ip_p ~= packet.IPPROTO_ICMP or ip.icmp_type ~= ICMP_TIME_EXCEEDEDv4 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 = proto_vtable[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.debug1("Invalid protocol for reply (%d)", ip2.ip_p) + end + end + end, +} + + +-- IPv6 functions +local Firewalk_v6 = { + + --- IPv6 initialization function. Open injection and reception sockets. + -- @param scanner the scanner handle + init = function(scanner) + local saddr = ipOps.str_to_ip(scanner.target.bin_ip_src) + + scanner.sock = nmap.new_dnet() + scanner.pcap = nmap.new_socket() + + -- filter for incoming ICMP time exceeded replies + scanner.pcap:pcap_open(scanner.target.interface, 1500, false, "icmp6 and dst host " .. saddr) + + local try = nmap.new_try() + try(scanner.sock:ip_open()) + end, + + --- IPv6 cleanup function. Close injection and reception sockets. + -- @param scanner the scanner handle + shutdown = function(scanner) + scanner.sock:ip_close() + scanner.pcap:pcap_close() + end, + + --- 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 + check = function(src, layer3) + local ip = packet.Packet:new(layer3) + return ip.ip_bin_dst == src + and ip.ip_p == packet.IPPROTO_ICMPV6 + and ip.icmpv6_type == ICMP_TIME_EXCEEDEDv6 + end, + + --- update global state with an incoming reply + -- @param scanner the scanner handle + -- @param pkt an incoming valid IP packet + parse_reply = function(scanner, pkt) + local ip = packet.Packet:new(pkt) + + if ip.ip_p ~= packet.IPPROTO_ICMPV6 or ip.icmpv6_type ~= ICMP_TIME_EXCEEDEDv6 then + return + end + + local is = ip.buf:sub(ip.icmpv6_offset + 9, ip.buf:len()) + local ip2 = packet.Packet:new(is) + + -- 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 = proto_vtable[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.debug1("Invalid protocol for reply (%d)", ip2.ip_p) + end + end + end, +} + +--- Initialize global function tables according to the current address family +local function firewalk_init() + if nmap.address_family() == "inet" then + proto_vtable.tcp = tcp_funcs_v4 + proto_vtable.udp = udp_funcs_v4 + Firewalk = Firewalk_v4 + else + proto_vtable.tcp = tcp_funcs_v6 + proto_vtable.udp = udp_funcs_v6 + Firewalk = Firewalk_v6 + end +end + +--- 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"} + } + + for _, combo in ipairs(combos) do + local i = 0 + local port = nil + local proto = combo[1] + local state = combo[2] + + repeat + port = nmap.get_ports(host, port, proto, state) + + -- do not include administratively prohibited ports + if port and port.reason == "no-response" then + local pentry = { + final_ttl = 0, -- TTL of the blocking gateway + scanned = false, -- initial state: unprobed + } + + portlist[proto] = portlist[proto] or {} + + portlist[proto][port.number] = pentry + i = i + 1 + end + + until not port or i == MaxProbedPorts + end + + return portlist + +end + +--- wrapper for stdnse.parse_timespec() to get specified value in milliseconds +-- @param spec the time specification string (like "10s", "120ms"...) +-- @return the equivalent number of milliseconds or nil on failure +local function parse_timespec_ms(spec) + local t = stdnse.parse_timespec(spec) + if t then + return t * 1000 + else + return nil + end +end + +--- set scan parameters using user values if specified or defaults otherwise +local function getopts() + + -- assign parameters to scan constants or use defaults + + MaxRetries = tonumber(stdnse.get_script_args("firewalk.max-retries")) or DEFAULT_MAX_RETRIES + + MaxActiveProbes = tonumber(stdnse.get_script_args("firewalk.max-active-probes")) or DEFAULT_MAX_ACTIVE_PROBES + + MaxProbedPorts = tonumber(stdnse.get_script_args("firewalk.max-probed-ports")) or DEFAULT_MAX_PROBED_PORTS + + + -- use stdnse time specification parser for ProbeTimeout and RecvTimeout + + local timespec = stdnse.get_script_args("firewalk.recv-timeout") + + if timespec then + + RecvTimeout = parse_timespec_ms(timespec) + + if not RecvTimeout then + stdnse.debug1("Invalid time specification for option: firewalk.recv-timeout (%s)", timespec) + return false + end + + else + -- no value supplied: use default + RecvTimeout = DEFAULT_RECV_TIMEOUT + end + + + timespec = stdnse.get_script_args("firewalk.probe-timeout") + + if timespec then + + ProbeTimeout = parse_timespec_ms(timespec) + + if not ProbeTimeout then + stdnse.debug1("Invalid time specification for option: firewalk.probe-timeout (%s)", timespec) + return false + end + + else + -- no value supplied: use default + ProbeTimeout = DEFAULT_PROBE_TIMEOUT + end + + return true + +end + +--- host rule, check for requirements before to launch the script +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 false + end + + if not host.interface then + return false + end + + -- assign user's values to scan parameters or use defaults + if not getopts() then + return false + end + + -- get the list of ports to probe + FirewalkPorts = build_portlist(host) + + -- schedule the execution if there are filtered ports to probe + return (next(FirewalkPorts) ~= nil) + +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'] = {} + end + + if nmap.registry['firewalk']['traceroutefail'] then + return nil + end + + nmap.registry['firewalk']['traceroutefail'] = true + + if nmap.verbosity() > 0 then + stdnse.debug1("requires unavailable traceroute information.") + end + + return nil + end + + stdnse.debug1("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 = {} + + if #ports == 0 then + return "(none found)" + 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 + numranges[k]["start"] = p + stored = true + + -- increase an existing range by the right + 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 = {} + range["start"] = p + 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 + -- contiguous ranges are represented as x-z + table.insert(strrange, start .. "-" .. stop) + end + end + + -- ranges are delimited by `,' + return table.concat(strrange, ",") + +end + +--- 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, "HOP") + tab.add(output, 2, "HOST") + tab.add(output, 3, "PROTOCOL") + tab.add(output, 4, "BLOCKED PORTS") + tab.nextrow(output) + + -- duplicate traceroute results and add localhost at the beginning + local path = { + -- XXX 'localhost' might be a better choice? + {ip = ipOps.str_to_ip(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 + + if entries > 0 then + return "\n" .. tab.dump(output) + else + return "None found" + end +end + +--- 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) + + 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 not port.scanned 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 information +local function send_probe(scanner, probe) + + local try = nmap.new_try(function() scanner.sock:ip_close() end) + + stdnse.debug1("Sending new probe (%d/%s ttl=%d)", probe.portno, probe.proto, probe.ttl) + + -- craft the raw packet + local pkt = proto_vtable[probe.proto].getprobe(scanner.target, probe.portno, probe.ttl) + + try(scanner.sock:ip_send(pkt.buf, scanner.target)) + + -- update probe information + 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 + + local probe + -- 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 + +--- 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 Firewalk.check(scanner.target.bin_ip_src, l3) then + Firewalk.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].scanned = true + + 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 + +--- firewalk entry point +action = function(host) + + firewalk_init() -- global script initialization process + + -- scan handle, scanner state is saved in this table + local scanner = { + target = host, + ttl = initial_ttl(host), + + ports = FirewalkPorts, + + 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 + + Firewalk.init(scanner) + + generate_initial_probes(scanner) + + while not finished(scanner) do + send_next_probes(scanner) + read_replies(scanner) + update_probe_queues(scanner) + end + + Firewalk.shutdown(scanner) + + return report(scanner) +end |