diff --git a/CHANGELOG b/CHANGELOG index 2b60f27d3..0201daa52 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,16 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the path-mtu script to perform Path MTU Discovery to the + target host using TCP or UDP. The script tries to conserve bandwidth and + time by starting with the outgoing interface's MTU and properly handling + the Next-Hop MTU field in ICMP responses generated by RFC-compliant + intermediate routers. [Kris] + +o [NSE] Scripts can now access the MTU of the host.interface device using + host.interface_mtu. [Kris] + +o Nmap now prints the MTU for interfaces when using --iflist. [Kris] + o [NSE] Removed references to MD2, as OpenSSL 1.x.x doesn't support it anymore [alexandru] diff --git a/Target.cc b/Target.cc index be4f0ac52..99679c162 100644 --- a/Target.cc +++ b/Target.cc @@ -133,8 +133,9 @@ void Target::Initialize() { htn.toclock_running = false; htn.host_start = htn.host_end = 0; interface_type = devt_other; - devname[0] = '\0'; - devfullname[0] = '\0'; + devname[0] = '\0'; + devfullname[0] = '\0'; + mtu = 0; state_reason_init(&reason); memset(&pingprobe, 0, sizeof(pingprobe)); pingprobe_state = PORT_UNKNOWN; @@ -378,6 +379,15 @@ void Target::setNextHop(struct sockaddr_storage *next_hop, size_t next_hop_len) nexthopsocklen = next_hop_len; } +/* Set MTU (to correspond with devname) */ +void Target::setMTU(int devmtu) { + mtu = devmtu; +} + +/* Get MTU (to correspond with devname) */ +int Target::MTU(void) { + return mtu; +} /* Starts the timeout clock for the host running (e.g. you are beginning a scan). If you do not have the current time handy, diff --git a/Target.h b/Target.h index 89758949d..6ad22c566 100644 --- a/Target.h +++ b/Target.h @@ -225,6 +225,9 @@ class Target { next_hop has never been set */ bool nextHop(struct sockaddr_storage *next_hop, size_t *next_hop_len); + void setMTU(int devmtu); + int MTU(void); + /* Sets the interface type to one of: devt_ethernet, devt_loopback, devt_p2p, devt_other */ @@ -318,7 +321,8 @@ class Target { struct host_timeout_nfo htn; devtype interface_type; char devname[32]; - char devfullname[32]; + char devfullname[32]; + int mtu; /* 0 (OS_NOTPERF) if os detection not performed * 1 (OS_PERF) if os detection performed * 2 (OS_PERF_UNREL) if an unreliable os detection has been performed */ diff --git a/docs/scripting.xml b/docs/scripting.xml index ae7daced2..8201afc26 100644 --- a/docs/scripting.xml +++ b/docs/scripting.xml @@ -1513,6 +1513,15 @@ LUALIB_API int luaopen_openssl(lua_State *L) { + + + + + The MTU (maximum transmission unit) for host.interface, + or 0 if not known. + + + diff --git a/libnetutil/netutil.cc b/libnetutil/netutil.cc index 910de92ed..e375060a9 100644 --- a/libnetutil/netutil.cc +++ b/libnetutil/netutil.cc @@ -995,6 +995,8 @@ static int collect_dnet_interfaces(const struct intf_entry *entry, void *arg) { dcrn->ifaces[dcrn->numifaces].device_type = devt_other; } + dcrn->ifaces[dcrn->numifaces].mtu = entry->intf_mtu; + /* Is the interface up and running? */ dcrn->ifaces[dcrn->numifaces].device_up = (entry->intf_flags & INTF_FLAG_UP) ? true : false; @@ -1197,6 +1199,27 @@ static struct interface_info *getinterfaces_siocgifconf(int *howmany, char *errs else devs[count].device_up = 0; +#ifdef SIOCGIFMTU + memcpy(&tmpifr.ifr_addr, sin, MIN(sizeof(tmpifr.ifr_addr), sizeof(*sin))); + rc = ioctl(sd, SIOCGIFMTU, &tmpifr); + if (rc < 0) { + if(errstr) Snprintf(errstr, errstrlen, "Failed to determine the mtu of %s!", tmpifr.ifr_name); + *howmany=-1; + return NULL; + } else { +#ifdef ifr_mtu + devs[count].mtu = tmpifr.ifr_mtu; +#else + /* Some systems lack ifr_mtu and a common solution (see pcap, dnet and + * others) is using ifr_metric instead + */ + devs[count].mtu = tmpifr.ifr_metric; +#endif + } +#else + devs[count].mtu = 0; +#endif + /* All done with this interface. Increase the count. */ count++; } diff --git a/libnetutil/netutil.h b/libnetutil/netutil.h index 637598410..3e5837982 100644 --- a/libnetutil/netutil.h +++ b/libnetutil/netutil.h @@ -228,6 +228,7 @@ struct interface_info { u16 netmask_bits; /* CIDR-style. So 24 means class C (255.255.255.0)*/ devtype device_type; /* devt_ethernet, devt_loopback, devt_p2p, devt_other */ int device_up; /* True if the device is up (enabled) */ + int mtu; /* Interface's MTU size */ u8 mac[6]; /* Interface MAC address if device_type is devt_ethernet */ }; diff --git a/nse_nmaplib.cc b/nse_nmaplib.cc index 185875ec4..edba40cab 100644 --- a/nse_nmaplib.cc +++ b/nse_nmaplib.cc @@ -132,6 +132,7 @@ void set_hostinfo(lua_State *L, Target *currenths) { lua_setfield(L, -2, "mac_addr_src"); } setsfield(L, -1, "interface", currenths->deviceName()); + setnfield(L, -1, "interface_mtu", currenths->MTU()); if ((u32)(currenths->v4host().s_addr)) { struct in_addr adr = currenths->v4host(); diff --git a/output.cc b/output.cc index 10c2e91b6..eac12791b 100644 --- a/output.cc +++ b/output.cc @@ -336,13 +336,14 @@ int print_iflist(void) { if (o.debugging) log_write(LOG_STDOUT, "Reason: %s\n", errstr); } else { - int devcol = 0, shortdevcol = 1, ipcol = 2, typecol = 3, upcol = 4, maccol = 5; - Tbl = new NmapOutputTable(numifs + 1, 6); + int devcol = 0, shortdevcol = 1, ipcol = 2, typecol = 3, upcol = 4, mtucol = 5, maccol = 6; + Tbl = new NmapOutputTable(numifs + 1, 7); Tbl->addItem(0, devcol, false, "DEV", 3); Tbl->addItem(0, shortdevcol, false, "(SHORT)", 7); Tbl->addItem(0, ipcol, false, "IP/MASK", 7); Tbl->addItem(0, typecol, false, "TYPE", 4); Tbl->addItem(0, upcol, false, "UP", 2); + Tbl->addItem(0, mtucol, false, "MTU", 3); Tbl->addItem(0, maccol, false, "MAC", 3); for (i = 0; i < numifs; i++) { Tbl->addItem(i + 1, devcol, false, iflist[i].devfullname); @@ -366,6 +367,7 @@ int print_iflist(void) { Tbl->addItem(i + 1, typecol, false, "other"); Tbl->addItem(i + 1, upcol, false, (iflist[i].device_up ? "up" : "down")); + Tbl->addItemFormatted(i + 1, mtucol, false, "%d", iflist[i].mtu); } log_write(LOG_PLAIN, "************************INTERFACES************************\n"); log_write(LOG_PLAIN, "%s\n", Tbl->printableTable(NULL)); diff --git a/scripts/path-mtu.nse b/scripts/path-mtu.nse new file mode 100644 index 000000000..2f875314d --- /dev/null +++ b/scripts/path-mtu.nse @@ -0,0 +1,391 @@ +description = [[ +Performs simple Path MTU Discovery to the target host. + +TCP or UDP packets are sent to the host with the DF (don't fragment) bit +set and with varying amounts of data. If an ICMP Fragmentation Needed +is received, or no reply is received after retransmissions, the amount +of data is lowered and another packet is sent. This continues until +(assuming no errors occur) a reply from the final host is received, +indicating the packet reached the host without being fragmented. + +Not all MTUs are attempted so as to not expend too much time or network +resources. Currently the relatively short list of MTUs to try contains +the plateau values from Table 7-1 in RFC 1191, "Path MTU Discovery". +Using these values significantly cuts down the MTU search space. On top +of that, this list is rarely traversed in whole because: + * the MTU of the outgoing interface is used as a starting point, and + * we can jump down the list when an intermediate router sending a + "can't fragment" message includes its next hop MTU (as described + in RFC 1191 and required by RFC 1812) +]] + +--- +-- @usage +-- nmap --script path-mtu target +-- +-- @output +-- Host script results: +-- |_path-mtu: 1492 <= PMTU < 1500 +-- +-- Host script results: +-- |_path-mtu: PMTU == 1006 + +author = "Kris Katterjohn" + +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +categories = {"safe", "discovery"} + +require 'bin' +require 'packet' + +local IPPROTO_ICMP = packet.IPPROTO_ICMP +local IPPROTO_TCP = packet.IPPROTO_TCP +local IPPROTO_UDP = packet.IPPROTO_UDP + +-- Number of times to retransmit for no reply before dropping to +-- another MTU value +local RETRIES = 1 + +-- RFC 1191, Table 7-1: Plateaus. Even the massive MTU values are +-- here since we skip down the list based on the outgoing interface +-- so its no harm. +local MTUS = { + 65535, + 32000, + 17914, + 8166, + 4352, + 2002, + 1492, + 1006, + 508, + 296, + 68 +} + +-- Find the index in MTUS{} to use based on the MTU +new+. If +new+ is in +-- between values in MTUS, then insert it into the table appropriately. +local searchmtu = function(cidx, new) + if new == 0 then + return cidx + end + + while cidx <= #MTUS do + if new >= MTUS[cidx] then + if new ~= MTUS[cidx] then + table.insert(MTUS, cidx, new) + end + return cidx + end + cidx = cidx + 1 + end + return cidx +end + +local dport = function(ip) + if ip.ip_p == IPPROTO_TCP then + return ip.tcp_dport + elseif ip.ip_p == IPPROTO_UDP then + return ip.udp_dport + end +end + +local sport = function(ip) + if ip.ip_p == IPPROTO_TCP then + return ip.tcp_sport + elseif ip.ip_p == IPPROTO_UDP then + return ip.udp_sport + end +end + +-- Checks how we should react to this packet +local checkpkt = function(reply, orig) + local ip = packet.Packet:new(reply, reply:len()) + + if ip.ip_p == IPPROTO_ICMP then + if ip.icmp_type ~= 3 then + return "recap" + end + -- Port Unreachable + if ip.icmp_code == 3 then + local is = ip.buf:sub(ip.icmp_offset + 9) + local ip2 = packet.Packet:new(is, is:len()) + + -- Check sent packet against ICMP payload + if ip2.ip_p ~= IPPROTO_UDP or + ip2.ip_p ~= orig.ip_p or + ip2.ip_bin_src ~= orig.ip_bin_src or + ip2.ip_bin_dst ~= orig.ip_bin_dst or + sport(ip2) ~= sport(orig) or + dport(ip2) ~= dport(orig) then + return "recap" + end + + return "gotreply" + end + -- Frag needed, DF set + if ip.icmp_code == 4 then + local val = ip:u16(ip.icmp_offset + 6) + return "nextmtu", val + end + return "recap" + end + + if ip.ip_p ~= orig.ip_p or + ip.ip_bin_src ~= orig.ip_bin_dst or + ip.ip_bin_dst ~= orig.ip_bin_src or + dport(ip) ~= sport(orig) or + sport(ip) ~= dport(orig) then + return "recap" + end + + return "gotreply" +end + +-- This is all we can use since we can get various protocols back from +-- different hosts +local callback = function(size, layer2, layer3) + local ip = packet.Packet:new(layer3, layer3:len()) + return bin.pack('A', ip.ip_bin_dst) +end + +-- Updates a packet's info and calculates checksum +local updatepkt = function(ip) + if ip.ip_p == IPPROTO_TCP then + ip:tcp_set_sport(math.random(0x401, 0xffff)) + 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_length(ip.ip_len - ip.ip_hl * 4) + ip:udp_count_checksum() + end + ip:ip_count_checksum() +end + +-- Set up packet header and data to satisfy a certain MTU +local setmtu = function(pkt, mtu) + if pkt.ip_len < mtu then + pkt.buf = pkt.buf .. string.rep("\0", mtu - pkt.ip_len) + else + pkt.buf = pkt.buf:sub(1, mtu) + end + + pkt:ip_set_len(mtu) + pkt.packet_length = mtu + updatepkt(pkt) +end + +local basepkt = function(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 + +-- Creates a Packet object for the given proto and port +local genericpkt = function(host, proto, port) + local pkt = basepkt(proto) + local ip = packet.Packet:new(pkt, pkt:len()) + + 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()) + + if proto == IPPROTO_TCP then + ip:tcp_parse(false) + ip:tcp_set_dport(port) + elseif proto == IPPROTO_UDP then + ip:udp_parse(false) + ip:udp_set_dport(port) + end + + updatepkt(ip) + + return ip +end + +local ipproto = function(p) + if p == "tcp" then + return IPPROTO_TCP + elseif p == "udp" then + return IPPROTO_UDP + end + return -1 +end + +-- Determines how to probe +local getprobe = function(host) + local combos = { + { "tcp", "open" }, + { "tcp", "closed" }, + -- udp/open probably only happens when Nmap sends proper + -- payloads, which doesn't happen in here + { "udp", "closed" } + } + local proto = nil + local port = nil + + for _, c in ipairs(combos) do + port = nmap.get_ports(host, nil, c[1], c[2]) + if port then + proto = c[1] + break + end + end + + return proto, port +end + +-- Sets necessary probe data in registry +local setreg = function(host, proto, port) + if not nmap.registry[host.ip] then + nmap.registry[host.ip] = {} + end + nmap.registry[host.ip]['pathmtuprobe'] = { + ['proto'] = proto, + ['port'] = port + } +end + +hostrule = function(host) + if not nmap.is_privileged() then + if not nmap.registry['pathmtu'] then + nmap.registry['pathmtu'] = {} + end + if nmap.registry['pathmtu']['rootfail'] then + return false + end + nmap.registry['pathmtu']['rootfail'] = true + if nmap.verbosity() > 0 then + nmap.log_write("stdout", "PATH-MTU: not running for lack of privileges") + end + return false + end + if not (host.interface and host.interface_mtu) then + return false + end + local proto, port = getprobe(host) + if not (proto and port) then + return false + end + setreg(host, proto, port.number) + return true +end + +action = function(host) + local m, r + local gotit = false + local mtuset + local sock = nmap.new_dnet() + local pcap = nmap.new_socket() + local proto = nmap.registry[host.ip]['pathmtuprobe']['proto'] + local port = nmap.registry[host.ip]['pathmtuprobe']['port'] + local saddr = packet.toip(host.bin_ip_src) + local daddr = packet.toip(host.bin_ip) + local try = nmap.new_try() + local status, pkt, ip + + try(sock:ip_open()) + + try = nmap.new_try(function() sock:ip_close() end) + + pcap:pcap_open(host.interface, 104, 0, callback, "dst host " .. saddr .. " and (icmp or (" .. proto .. " and src host " .. daddr .. " and src port " .. port .. "))") + + pcap:set_timeout(3000) + + m = searchmtu(1, host.interface_mtu) + + mtuset = MTUS[m] + + local pkt = genericpkt(host, ipproto(proto), port) + + while m <= #MTUS do + setmtu(pkt, MTUS[m]) + + r = 0 + status = false + while true do + if not status then + if not sock:ip_send(pkt.buf) then + -- Got a send error, perhaps EMSGSIZE + -- when we don't know our interface's + -- MTU. Drop an MTU and keep trying. + break + end + end + + pcap:pcap_register(bin.pack('A', pkt.ip_bin_src)) + + status, _, _, ip = pcap:pcap_receive() + + if status then + local t, v = checkpkt(ip, pkt) + if t == "gotreply" then + gotit = true + break + elseif t == "recap" then + elseif t == "nextmtu" then + if v == 0 then + -- Router didn't send its + -- next-hop MTU. Just drop + -- a level. + break + end + -- Lua's lack of a continue statement + -- for loop control sucks, so dec m + -- here as it's inc'd below. Ugh. + m = searchmtu(m, v) - 1 + mtuset = v + break + end + else + if r >= RETRIES then + break + end + r = r + 1 + end + end + + if gotit then + break + end + + m = m + 1 + end + + pcap:close() + sock:ip_close() + + if not gotit then + if nmap.debugging() > 0 then + return "Error: Unable to determine PMTU (no replies)" + end + return + end + + if MTUS[m] == mtuset then + return "PMTU == " .. MTUS[m] + elseif m == 1 then + return "PMTU >= " .. MTUS[m] + else + return "" .. MTUS[m] .. " <= PMTU < " .. MTUS[m - 1] + end +end + diff --git a/scripts/script.db b/scripts/script.db index ce6f72500..7e9487dbb 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -92,6 +92,7 @@ Entry { filename = "oracle-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "oracle-enum-users.nse", categories = { "auth", "intrusive", } } Entry { filename = "oracle-sid-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "p2p-conficker.nse", categories = { "default", "safe", } } +Entry { filename = "path-mtu.nse", categories = { "discovery", "safe", } } Entry { filename = "pgsql-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "pjl-ready-message.nse", categories = { "intrusive", } } Entry { filename = "pop3-brute.nse", categories = { "auth", "intrusive", } } diff --git a/targets.cc b/targets.cc index 5321f3463..496a0802c 100644 --- a/targets.cc +++ b/targets.cc @@ -480,6 +480,7 @@ Target *nexthost(HostGroupState *hs, TargetGroup *exclude_group, if (hs->current_batch_sz == 0) /* Because later ones can have different src addy and be cut off group */ o.decoys[o.decoyturn] = t->v4source(); t->setDeviceNames(rnfo.ii.devname, rnfo.ii.devfullname); + t->setMTU(rnfo.ii.mtu); // printf("Target %s %s directly connected, goes through local iface %s, which %s ethernet\n", t->NameIP(), t->directlyConnected()? "IS" : "IS NOT", t->deviceName(), (t->ifType() == devt_ethernet)? "IS" : "IS NOT"); }