diff --git a/CHANGELOG b/CHANGELOG index 5639878e2..8c3324e9e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,13 @@ [NOT YET RELEASED] +o [NSE] Added the qscan script to repeatedly probe ports on a host to + gather round-trip times for each port. The script then uses these + times to group together ports with statistically equivalent RTTs. + Ports in different groups could be the result of things such as port + forwarding to hosts behind a NAT. This is based on work by Doug + Hoyte. [Kris] + o [NSE] Added RPC library and three new NFS scripts. Modified the rpcinfo and nfs-showmount scripts to use the new library. The new scripts are: - nfs-acls shows the owner and directory mode of NFS exports diff --git a/scripts/qscan.nse b/scripts/qscan.nse new file mode 100644 index 000000000..32bc0532e --- /dev/null +++ b/scripts/qscan.nse @@ -0,0 +1,455 @@ +description = [[ + Repeatedly probe open and/or closed ports on a host to obtain a series + of round-trip time values for each port. These values are used to + group collections of ports which are statistically different from other + groups. Ports being in different groups (or "families") may be due to + network mechanisms such as port forwarding to machines behind a NAT. + + In order to group these ports into different families, some statistical + values must be computed. Among these values are the mean and standard + deviation of the round-trip times for each port. Once all of the times + have been recorded and these values have been computed, the Student's + t-test is used to test the statistical significance of the differences + between each port's data. Ports which have round-trip times that are + statistically the same are grouped together in the same family. + + This script is based on Doug Hoyte's Qscan documentation and patches + for Nmap. +]] + +-- See http://hcsw.org/nmap/QSCAN for more on Doug's research + +--- +-- @usage +-- nmap --script qscan --script-args qscan.confidence=,qscan.delay=,qscan.numtrips= target +-- +-- @args confidence Confidence level: 0.75, 0.9, 0.95, 0.975, 0.99, 0.995, 0.9995 +-- @args delay Average delay between packet sends (milliseconds): between 0.5d and 1.5d +-- @args numtrips Number of round-trip times to try to get +-- +-- @output +-- | qscan: +-- | PORT FAMILY MEAN (ms) STDDEV LOSS (%) +-- | 21 0 2.70 1.25 0.0% +-- | 22 0 2.50 0.53 0.0% +-- | 23 1 4.80 0.63 0.0% +-- | 25 0 2.40 0.52 0.0% +-- | 80 0 2.40 0.70 0.0% +-- |_443 0 2.40 0.52 0.0% +-- + +-- 03/17/2010 + +author = "Kris Katterjohn " + +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +categories = {"safe", "discovery"} + +require 'bin' +require 'packet' +require 'tab' + +-- defaults +local DELAY = 200 +local NUMTRIPS = 10 +local CONF = 0.95 + +-- The following tdist{} and tinv() are based off of +-- http://www.owlnet.rice.edu/~elec428/projects/tinv.c +local tdist = { + -- 75% 90% 95% 97.5% 99% 99.5% 99.95% + { 1.0000, 3.0777, 6.3138, 12.7062, 31.8207, 63.6574, 636.6192 }, -- 1 + { 0.8165, 1.8856, 2.9200, 4.3027, 6.9646, 9.9248, 31.5991 }, -- 2 + { 0.7649, 1.6377, 2.3534, 3.1824, 4.5407, 5.8409, 12.9240 }, -- 3 + { 0.7407, 1.5332, 2.1318, 2.7764, 3.7649, 4.6041, 8.6103 }, -- 4 + { 0.7267, 1.4759, 2.0150, 2.5706, 3.3649, 4.0322, 6.8688 }, -- 5 + { 0.7176, 1.4398, 1.9432, 2.4469, 3.1427, 3.7074, 5.9588 }, -- 6 + { 0.7111, 1.4149, 1.8946, 2.3646, 2.9980, 3.4995, 5.4079 }, -- 7 + { 0.7064, 1.3968, 1.8595, 3.3060, 2.8965, 3.3554, 5.0413 }, -- 8 + { 0.7027, 1.3830, 1.8331, 2.2622, 2.8214, 3.2498, 4.7809 }, -- 9 + { 0.6998, 1.3722, 1.8125, 2.2281, 2.7638, 1.1693, 4.5869 }, -- 10 + { 0.6974, 1.3634, 1.7959, 2.2010, 2.7181, 3.1058, 4.4370 }, -- 11 + { 0.6955, 1.3562, 1.7823, 2.1788, 2.6810, 3.0545, 4.3178 }, -- 12 + { 0.6938, 1.3502, 1.7709, 2.1604, 2.6403, 3.0123, 4.2208 }, -- 13 + { 0.6924, 1.3450, 1.7613, 2.1448, 2.6245, 2.9768, 4.1405 }, -- 14 + { 0.6912, 1.3406, 1.7531, 2.1315, 2.6025, 2.9467, 4.0728 }, -- 15 + { 0.6901, 1.3368, 1.7459, 2.1199, 2.5835, 2.9208, 4.0150 }, -- 16 + { 0.6892, 1.3334, 1.7396, 2.1098, 2.5669, 2.8982, 3.9651 }, -- 17 + { 0.6884, 1.3304, 1.7341, 2.1009, 2.5524, 2.8784, 3.9216 }, -- 18 + { 0.6876, 1.3277, 1.7291, 2.0930, 2.5395, 2.8609, 3.8834 }, -- 19 + { 0.6870, 1.3253, 1.7247, 2.0860, 2.5280, 2.8453, 3.8495 }, -- 20 + { 0.6844, 1.3163, 1.7081, 2.0595, 2.4851, 2.7874, 3.7251 }, -- 25 + { 0.6828, 1.3104, 1.6973, 2.0423, 2.4573, 2.7500, 3.6460 }, -- 30 + { 0.6816, 1.3062, 1.6896, 2.0301, 2.4377, 2.7238, 3.5911 }, -- 35 + { 0.6807, 1.3031, 1.6839, 2.0211, 2.4233, 2.7045, 3.5510 }, -- 40 + { 0.6800, 1.3006, 1.6794, 2.0141, 2.4121, 2.6896, 3.5203 }, -- 45 + { 0.6794, 1.2987, 1.6759, 2.0086, 2.4033, 2.6778, 3.4960 }, -- 50 + { 0.6786, 1.2958, 1.6706, 2.0003, 2.3901, 2.6603, 3.4602 }, -- 60 + { 0.6780, 1.2938, 1.6669, 1.9944, 2.3808, 2.6479, 3.4350 }, -- 70 + { 0.6776, 1.2922, 1.6641, 1.9901, 2.3739, 2.6387, 3.4163 }, -- 80 + { 0.6772, 1.2910, 1.6620, 1.9867, 2.3685, 2.6316, 3.4019 }, -- 90 + { 0.6770, 1.2901, 1.6602, 1.9840, 2.3642, 2.6259, 3.3905 } -- 100 +} + +local tinv = function(p, dof) + local din, pin + + if dof >= 1 and dof <= 20 then + din = dof + elseif dof < 25 then + din = 20 + elseif dof < 30 then + din = 21 + elseif dof < 35 then + din = 22 + elseif dof < 40 then + din = 23 + elseif dof < 45 then + din = 24 + elseif dof < 50 then + din = 25 + elseif dof < 60 then + din = 26 + elseif dof < 70 then + din = 27 + elseif dof < 80 then + din = 28 + elseif dof < 90 then + din = 29 + elseif dof < 100 then + din = 30 + elseif dof >= 100 then + din = 31 + end + + if p == 0.75 then + pin = 1 + elseif p == 0.9 then + pin = 2 + elseif p == 0.95 then + pin = 3 + elseif p == 0.975 then + pin = 4 + elseif p == 0.99 then + pin = 5 + elseif p == 0.995 then + pin = 6 + elseif p == 0.9995 then + pin = 7 + end + + return tdist[din][pin] +end + +--- Calculates intermediate t statistic +local tstat = function(n1, n2, u1, u2, v1, v2) + local dof = n1 + n2 - 2 + local a = (n1 + n2) / (n1 * n2) + --local b = ((n1 - 1) * (s1 * s1) + (n2 - 1) * (s2 * s2)) + local b = ((n1 - 1) * v1) + ((n2 - 1) * v2) + return math.abs(u1 - u2) / math.sqrt(a * (b / dof)) +end + +--- Pcap callback +-- @return Destination and source IP addresses and TCP ports +local callback = function(size, layer2, layer3) + local ip = packet.Packet:new(layer3, layer3:len()) + return bin.pack('AA=S=S', ip.ip_bin_dst, ip.ip_bin_src, ip.tcp_dport, ip.tcp_sport) +end + +--- Updates a TCP Packet object +-- @param tcp The TCP object +local updatepkt = function(tcp, dport) + 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_len) + tcp:ip_count_checksum() +end + +--- Create a TCP Packet object +-- @param host Host object +-- @param port Port number +-- @return TCP 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) + + return tcp +end + +--- Calculates "family" values for grouping +-- @param stats Statistics table +-- @param conf Confidence level +local calcfamilies = function(stats, conf) + local i, j + local famidx = 0 + local stat + local crit + + for _, i in pairs(stats) do repeat + if i.fam ~= -1 then + break + end + + i.fam = famidx + famidx = famidx + 1 + + for _, j in pairs(stats) do repeat + if j.port == i.port or j.fam ~= -1 then + break + end + + stat = tstat(i.num, j.num, i.mean, j.mean, i.K / (i.num - 1), j.K / (j.num - 1)) + crit = tinv(conf, i.num + j.num - 2) + + if stat < crit then + j.fam = i.fam + end + until true end + until true end +end + +--- Builds report for output +-- @param stats Array of port statistics +-- @return Output report +local report = function(stats) + local j + local outtab = tab.new(5) + + tab.add(outtab, 1, "PORT") + tab.add(outtab, 2, "FAMILY") + tab.add(outtab, 3, "MEAN (ms)") + tab.add(outtab, 4, "STDDEV") + tab.add(outtab, 5, "LOSS (%)") + + for _, j in pairs(stats) do + port = tostring(j.port) + fam = tostring(j.fam) + mean = string.format("%.2f", j.mean) + stddev = string.format("%.2f", math.sqrt(j.K / (j.num - 1))) + loss = string.format("%.1f%%", 100 * (1 - j.num / j.sent)) + + tab.nextrow(outtab) + tab.add(outtab, 1, port) + tab.add(outtab, 2, fam) + tab.add(outtab, 3, mean) + tab.add(outtab, 4, stddev) + tab.add(outtab, 5, loss) + end + + return tab.dump(outtab) +end + +--- Returns option values based on script arguments and defaults +-- @return Confidence level, delay and number of trips +local getopts = function() + local conf, delay, numtrips = CONF, DELAY, NUMTRIPS + local bool, err + local k + + for _, k in ipairs({"qscan.confidence", "confidence"}) do + if nmap.registry.args[k] then + conf = tonumber(nmap.registry.args[k]) + break + end + end + + for _, k in ipairs({"qscan.delay", "delay"}) do + if nmap.registry.args[k] then + delay = tonumber(nmap.registry.args[k]) + break + end + end + + for _, k in ipairs({"qscan.numtrips", "numtrips"}) do + if nmap.registry.args[k] then + numtrips = tonumber(nmap.registry.args[k]) + break + end + end + + bool = true + + if conf ~= 0.75 and conf ~= 0.9 and + conf ~= 0.95 and conf ~= 0.975 and + conf ~= 0.99 and conf ~= 0.995 and conf ~= 0.995 then + bool = false + err = "Invalid confidence level" + end + + if delay < 0 then + bool = false + err = "Invalid (negative) delay" + end + + if numtrips < 3 then + bool = false + err = "Invalid number of trips (should be >= 3)" + end + + if bool then + return bool, conf, delay, numtrips + else + return bool, err + end +end + +--- Get ports to probe +-- @param host Host object +local getports = function(host) + local states = { "closed", "open" } + local ports = {} + local port = nil + + for _, s in ipairs(states) do + repeat + port = nmap.get_ports(host, port, "tcp", s) + if port then + table.insert(ports, port.number) + end + until not port + end + + return ports +end + +--- Sets probe port list in registry +-- @param host Host object +-- @param ports Port list +local setreg = function(host, ports) + if not nmap.registry[host.ip] then + nmap.registry[host.ip] = {} + end + nmap.registry[host.ip]['qscanports'] = ports +end + +hostrule = function(host) + if not nmap.is_privileged() then + if not nmap.registry['qscan'] then + nmap.registry['qscan'] = {} + end + if nmap.registry['qscan']['rootfail'] then + return false + end + nmap.registry['qscan']['rootfail'] = true + if nmap.verbosity() > 0 then + nmap.log_write("stdout", "QSCAN: 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 + setreg(host, ports) + return true +end + +action = function(host) + local i, j, k + local sock = nmap.new_dnet() + local pcap = nmap.new_socket() + local ports = nmap.registry[host.ip]['qscanports'] + local saddr = packet.toip(host.bin_ip_src) + local daddr = packet.toip(host.bin_ip) + local port + local start, stop + local rtt + local stats = {} + local try = nmap.new_try() + + local conf, delay, numtrips = try(getopts()) + + pcap:pcap_open(host.interface, 104, 0, callback, "tcp and dst host " .. saddr .. " and src host " .. daddr) + + try(sock:ip_open()) + + try = nmap.new_try(function() sock:ip_close() end) + + pcap:set_timeout(3000) + + local tcp = genericpkt(host) + + i = 1 + + while i <= numtrips do + for j, _ in ipairs(ports) do + port = ports[j] + + updatepkt(tcp, port) + + if not stats[j] then + stats[j] = {} + stats[j].port = port + stats[j].num = 0 + stats[j].sent = 0 + stats[j].mean = 0 + stats[j].K = 0 + stats[j].fam = -1 + end + + pcap:pcap_register(bin.pack('AA=S=S', tcp.ip_bin_src, tcp.ip_bin_dst, tcp.tcp_sport, tcp.tcp_dport)) + + start = nmap.clock_ms() + + try(sock:ip_send(tcp.buf)) + + stats[j].sent = stats[j].sent + 1 + + local status, len, _, pkt = pcap:pcap_receive() + + stop = nmap.clock_ms() + + rtt = stop - start + + if status then + -- update more stats on the port, Knuth-style + local delta + stats[j].num = stats[j].num + 1 + delta = rtt - stats[j].mean + stats[j].mean = stats[j].mean + delta / stats[j].num + stats[j].K = stats[j].K + delta * (rtt - stats[j].mean) + end + + -- Unlike qscan.cc which loops around while waiting for + -- the delay, I just sleep here (depending on rtt) + if rtt < (3 * delay) / 2 then + if rtt < (delay / 2) then + k = ((delay / 2) + math.random(0, delay) - rtt) + else + k = math.random((3 * delay) / 2 - rtt) + end + + stdnse.sleep(k / 1000) + end + end + + i = i + 1 + end + + sock:ip_close() + pcap:pcap_close() + + -- sort by port number + table.sort(stats, function(t1, t2) return t1.port < t2.port end) + + calcfamilies(stats, conf) + + return " \n" .. report(stats) +end + diff --git a/scripts/script.db b/scripts/script.db index 7bd4ddb53..0dc6b19a2 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -1,5 +1,3 @@ -Entry { filename = "afp-path-exploit.nse", categories = { "safe", "vuln", } } -Entry { filename = "afp-path-vuln.nse", categories = { "safe", "vuln", } } Entry { filename = "afp-showmount.nse", categories = { "discovery", "safe", } } Entry { filename = "asn-query.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "auth-owners.nse", categories = { "default", "safe", } } @@ -71,6 +69,7 @@ Entry { filename = "pjl-ready-message.nse", categories = { "intrusive", } } Entry { filename = "pop3-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "pop3-capabilities.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "pptp-version.nse", categories = { "version", } } +Entry { filename = "qscan.nse", categories = { "discovery", "safe", } } Entry { filename = "realvnc-auth-bypass.nse", categories = { "default", "safe", "vuln", } } Entry { filename = "robots.txt.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "rpcinfo.nse", categories = { "discovery", "safe", } }