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 firewalk.* optional arguments. From an original idea of M. Schiffman and D. Goldsmith, authors of the firewalk tool. ]] --- -- @usage -- nmap --script=firewalk --traceroute -- -- @usage -- nmap --script=firewalk --traceroute --script-args=firewalk.max-retries=1 -- -- @usage -- nmap --script=firewalk --traceroute --script-args=firewalk.probe-timeout=400ms -- -- @usage -- nmap --script=firewalk --traceroute --script-args=firewalk.max-probed-ports=7 -- -- -- @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 ports -- -- -- @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 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 gateway(s)/TTL(s) to probe -- o remove traceroute dependency require('bin') require('stdnse') require('packet') require('tab') -----= 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 -- 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 = { --- 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].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.print_debug("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 = 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) 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 local udp_funcs = { --- 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].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.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 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) 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, } -- list of supported protocols local supported_protocols = { tcp = tcp_funcs, udp = udp_funcs, } --- 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 --- 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] portlist[proto] = {} 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][port.number] = pentry i = i + 1 end until not port or i == MaxProbedPorts end return portlist end --- 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 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.print_debug("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.print_debug("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.print_verbose("%s not running for lack of privileges.", SCRIPT_NAME) end nmap.registry[SCRIPT_NAME].rootfail = true return nil; end if nmap.address_family() ~= 'inet' then stdnse.print_debug("%s is IPv4 compatible only.", SCRIPT_NAME) 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 local portlist = build_portlist(host) local nb_ports = 0 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, portlist) return true 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.print_debug("%s requires unavailable traceroute informations.", SCRIPT_NAME) end return nil end 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 = {} 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 stdnse.strjoin(",", strrange) 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 local function check(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_EXCEEDED 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 = 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 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 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].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) 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_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 scanner.pcap:pcap_open(host.interface, 104, false, "icmp and dst host " .. saddr) local try = nmap.new_try() try(scanner.sock:ip_open()) generate_initial_probes(scanner) while not finished(scanner) do send_next_probes(scanner) read_replies(scanner) update_probe_queues(scanner) end scanner.sock:ip_close() scanner.pcap:pcap_close() return report(scanner) end