diff --git a/CHANGELOG b/CHANGELOG index 0ee3aa0a4..d84c54718 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the firewalk script, which tries to find whether a + firewall blocks or forwards ports like the firewall tool does. [Henri + Doreau] + o [NSE] Host tables now have a host.traceroute member when --traceroute is used. This array contains the IP address, reverse DNS name, and RTT for each traceroute hop. [Henri Doreau] diff --git a/scripts/firewalk.nse b/scripts/firewalk.nse new file mode 100644 index 000000000..341df093a --- /dev/null +++ b/scripts/firewalk.nse @@ -0,0 +1,336 @@ +description = [[ +Try to discover firewall rules by using IP TTL expiration technique (method +also 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 supplied directly or +retrieved by the script from traceroute results. In this second case, the +script requires both the gateway IP address and the Nmap --traceroute flag. + +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. + +From an original idea of M. Schiffman and D. Goldsmith, authors of the +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 +-- +-- @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: forwarded ports (tcp): 21-80,443 +-- + + +-- 08/28/2010 +author = "Henri Doreau " + +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +categories = {"safe", "discovery"} + + +require('bin') +require('packet') +require('tab') + + +local ICMP_TIME_EXCEEDED = 11 + + +-- number of retries for unanswered probes +local MAX_RETRIES = 2 + + +--- ensure that the catched reply is a valid icmp time exceeded and return +-- wether the reply appears to be valid or not +local checkpkt = function(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 + 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 ~= packet.IPPROTO_TCP or + ip2.ip_bin_src ~= orig.ip_bin_src or + ip2.ip_bin_dst ~= orig.ip_bin_dst or + ip2.tcp_sport ~= orig.tcp_sport or + ip2.tcp_dport ~= orig.tcp_dport then + + return false + end + + return true +end + +--- pcap callback +-- @return destination ip address, the ip protocol and icmp type +local callback = function(size, layer2, layer3) + local ip = packet.Packet:new(layer3, layer3:len()) + return bin.pack('ACC', ip.ip_bin_dst, ip.ip_p, ip.icmp_type) +end + +--- set destination port and ip ttl to a generic tcp packet +-- @param ip the ip object +-- @param dport the layer 4 destination port +-- @param ttl the ip ttl to set +local updatepkt = function(ip, dport, ttl) + ip:ip_set_ttl(ttl) + 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_len) + ip:ip_count_checksum() +end + +--- create a generic tcp packet, with ip ttl and destination port set to zero +-- @param host Host object that represents the destination +-- @return the ip packet object +local genericpkt = function(host) + local pkt = bin.pack("H", + "4500 002c 55d1 0000 8006 0000 0000 0000" .. + "0000 0000 0000 0000 0000 0000 0000 0000" .. + "6002 0c00 0000 0000 0204 05b4" + ) + + local tcp = packet.Packet:new(pkt, pkt:len()) + + tcp:ip_set_bin_src(host.bin_ip_src) + tcp:ip_set_bin_dst(host.bin_ip) + + updatepkt(tcp, 0, 0) + + return tcp +end + +--- get the list of ports to probe +-- @param host Host object that represents the targetted host +-- @return list of ports to probe +local getports = function(host) + local ports = {} + local port = nil + + repeat + port = nmap.get_ports(host, port, "tcp", "filtered") + if port then + table.insert(ports, port.number) + end + until not port + + return ports +end + +--- store the firewalk ports into the registry +-- @param host Host object that represents the targetted host +-- @param ports list of ports to firewalk +local setregs = function(host, ports) + if not nmap.registry[host.ip] then + nmap.registry[host.ip] = {} + end + nmap.registry[host.ip]['firewalk_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'] = {} + end + if nmap.registry['firewalk']['rootfail'] then + return false + end + nmap.registry['firewalk']['rootfail'] = true + if nmap.verbosity() > 0 then + nmap.log_write("stdout", "FIREWALK: not running for lack of privileges") + end + return false + end + if not host.interface then + return false + end + local ports = getports(host) + if #ports < 1 then + return false + end + setregs(host, ports) + return true +end + +--- bind the scan 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 ttlmetric = function(host) + local ttl = stdnse.get_script_args("firewalk.ttl") + if ttl ~= nil 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 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 + -- XXX maybe talk about the ttl option? + nmap.log_write("stdout", "FIREWALK: using the argument `firewalk.gateway' 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 + 2 + end + end + + if nmap.verbosity() > 0 then + nmap.log_write("stdout", "FIREWALK: metric " .. gateway .. " doesn't appear in traceroute") + end + + return nil +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 portrange = function(ports) + table.sort(ports) + local numranges = {} + 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 + +-- 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 = {} + + -- abort if unable to bind the scan + if not ttl then + return nil + end + + -- filter for incoming icmp time exceeded replies + pcap:pcap_open(host.interface, 104, 0, callback, "icmp and dst host " .. saddr) + + try(sock:ip_open()) + + try = nmap.new_try(function() sock:ip_close() end) + + pcap:set_timeout(3000) + + local pkt = genericpkt(host) + + -- iterate over the list of ports + for _, port in ipairs(ports) 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)) + + pcap:pcap_register(bin.pack('ACC', pkt.ip_bin_src, packet.IPPROTO_ICMP, ICMP_TIME_EXCEEDED)) + local status, _, _, rep = pcap:pcap_receive() + + if status then + if checkpkt(rep, pkt) then + stdnse.print_debug(1, "Firewalk: discovered fwd port " .. port) + table.insert(fwdports, port) + break + end + else + retry = retry + 1 + end + end + end + + sock:ip_close() + pcap:pcap_close() + + if #fwdports < 1 then + return "\n no forwarded ports found" + else + return "\n forwarded ports (tcp): " .. portrange(fwdports) + end +end + diff --git a/scripts/script.db b/scripts/script.db index 7e9487dbb..f4acb7674 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -31,6 +31,7 @@ Entry { filename = "domino-enum-users.nse", categories = { "auth", "intrusive", Entry { filename = "drda-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "drda-info.nse", categories = { "discovery", "safe", "version", } } Entry { filename = "finger.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "firewalk.nse", categories = { "discovery", "safe", } } Entry { filename = "ftp-anon.nse", categories = { "auth", "default", "safe", } } Entry { filename = "ftp-bounce.nse", categories = { "default", "intrusive", } } Entry { filename = "ftp-brute.nse", categories = { "auth", "intrusive", } }