1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-09 14:11:29 +00:00
Files
nmap/scripts/firewalk.nse
david 80605e3e09 In firewalk.nse, bail out if we have neither of the scripts args
firewalk.ttl and firewalk.gateway. Otherwise we would get a nil
dereference when running with
	--script=firewalk --traceroute
Ron reported this.
2010-11-02 19:03:35 +00:00

455 lines
12 KiB
Lua

description = [[
Try to discover firewall rules with 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 <code>firewalk.ttl</code> script argument, or indirectly with
the <code>firewalk.gateway</code> script argument. For
<code>firewalk.gateway</code>, Nmap must be run with the
<code>--traceroute</code> option and the gateway must appear as one of the
traceroute hops.
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 <code>firewalk.ttl</code> and
-- <code>firewalk.gateway</code> IP address are
-- supplied, <code>firewalk.gateway</code> is ignored.
--
-- @output
-- | firewalk:
-- | PROTOCOL FORWARDED PORTS
-- | udp 123,137,161
-- |_tcp 21-80,443
--
-- 08/28/2010
author = "Henri Doreau <henri.doreau[at]gmail.com>"
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
local IPPROTO_TCP = packet.IPPROTO_TCP
local IPPROTO_UDP = packet.IPPROTO_UDP
-- number of retries for unanswered probes
local MAX_RETRIES = 2
--- 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
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
-- TCP ports
if orig.ip_p == IPPROTO_TCP then
return ip2.tcp_sport == orig.tcp_sport and
ip2.tcp_dport == orig.tcp_dport
-- UDP ports
elseif orig.ip_p == IPPROTO_UDP then
return ip2.udp_sport == orig.udp_sport and
ip2.udp_dport == orig.udp_dport
end
end
return false
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)
if ip.ip_p == IPPROTO_TCP then
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()
elseif ip.ip_p == IPPROTO_UDP then
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()
end
ip:ip_count_checksum()
end
--- build a generic probe packet
-- @param proto the desired layer 4 protocol
-- @return the desired packet as a raw buffer
local function basepkt(proto)
local ibin = bin.pack("H",
"4500 0014 0000 4000 8000 0000 0000 0000 0000 0000"
)
local tbin = bin.pack("H",
"0000 0000 0000 0000 0000 0000 6002 0c00 0000 0000 0204 05b4"
)
local ubin = bin.pack("H",
"0000 0000 0800 0000"
)
if proto == IPPROTO_TCP then
return ibin .. tbin
elseif proto == IPPROTO_UDP then
return ibin .. ubin
end
end
--- create a generic probe packet, with ip ttl and destination port set to zero
-- @param host Host object that represents the destination
-- @param protostr the layer 4 protocol of the desired packet ("tcp" or "udp")
-- @return the ip packet object or nil on error
local function genericpkt(host, protostr)
local proto
if protostr == "tcp" then
proto = IPPROTO_TCP
elseif protostr == "udp" then
proto = IPPROTO_UDP
else
return nil
end
local pkt = basepkt(proto)
local ip = packet.Packet:new(pkt, pkt:len())
if proto == IPPROTO_TCP then
ip:tcp_parse(false)
elseif proto == IPPROTO_UDP then
ip:udp_parse(false)
end
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())
return ip
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 = {}
local protocols = {
{"tcp", "filtered"},
{"udp", "open|filtered"}
}
for _, combo in ipairs(protocols) do
local port = nil
local proto = combo[1]
local state = combo[2]
ports[proto] = {}
repeat
port = nmap.get_ports(host, port, proto, state)
if port then
table.insert(ports[proto], port.number)
end
until not port
end
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 function setregs(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
-- get the list of ports to probe
local ports = getports(host)
local nb_ports = 0
for proto in pairs(ports) do
nb_ports = nb_ports + #ports[proto]
end
-- nothing to probe: cancel the execution
if nb_ports < 1 then
return false
end
setregs(host, ports)
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
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 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
--- pcap check function
-- @return destination ip address, the ip protocol and icmp type
local function check (layer3)
local ip = packet.Packet:new(layer3, layer3:len())
return bin.pack('ACC', ip.ip_bin_dst, ip.ip_p, ip.icmp_type)
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(2)
tab.add(output, 1, "PROTOCOL")
tab.add(output, 2, "FORWARDED PORTS")
-- script output: one line per protocol
for proto in pairs(tested) do
if #tested[proto] ~= 0 then
tab.nextrow(output)
tab.add(output, 1, proto)
tab.add(output, 2, portrange(forwarded[proto]))
end
end
return tab.dump(output)
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, false, "icmp and dst host " .. saddr)
try(sock:ip_open())
try = nmap.new_try(function() sock:ip_close() end)
pcap:set_timeout(3000)
-- ports are sorted by protocol
for proto in pairs(ports) do
fwdports[proto] = {}
local pkt = genericpkt(host, proto)
-- 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)
end