diff --git a/CHANGELOG b/CHANGELOG index 7b0c8ef44..218283494 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added an EAP library and the script eap-info which discovers supported + EAP authentication methods. [Riccardo Cecolin] + o [NSE] Added a Versant object database library and the scripts broadcast-versant-locate and versant-info. The first discovers Versant databases on the LAN and the second queries them for information. [Patrik] diff --git a/nselib/eap.lua b/nselib/eap.lua new file mode 100644 index 000000000..7d89fbb0d --- /dev/null +++ b/nselib/eap.lua @@ -0,0 +1,287 @@ +--- +-- EAP library supporting a limited subset of features. +-- +-- The library was designed and tested against hostapd v0.6.10 +-- The EAP protocol names are the ones specified in: +-- http://www.iana.org/assignments/eap-numbers/eap-numbers.xml +-- +-- Scripts can use the library to start an eap session and then to +-- send identity and nak responses to identity and authentication +-- requests made by AP authenticators to analyze their behaviour. +-- +-- The following sample code illustrates how to respond to an identity +-- request: +-- +-- +-- pcap:pcap_open(iface.device, 512, true, "ether proto 0x888e") +-- ... +-- local _, _, l2_data, l3_data, _ = pcap:pcap_receive() +-- local packet = eap.parse(l2_data .. l3_data3) +-- if packet then +-- if packet.eap.type == eap.eap_t.IDENTITY and packet.eap.code == eap.code_t.REQUEST then +-- eap.send_identity_response(iface, packet.eap.id, "anonymous") +-- end +-- end +-- +-- +-- +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- +-- @author "Riccardo Cecolin " +-- + +module(... or "eap", package.seeall) + +-- Created 02/23/2012 - v0.1 + +require 'nmap' +require 'packet' +require 'bin' + + +local ETHER_BROADCAST = "01:80:c2:00:00:03" +local ETHER_TYPE_EAPOL_N = 0x888E +local ETHER_TYPE_EAPOL = bin.pack(">S",ETHER_TYPE_EAPOL_N) +local ETHER_HEADER_SIZE = 14 +local EAPOL_HEADER_SIZE = 4 +local EAP_HEADER_SIZE = 5 + +eapol_t = { + PACKET = 0, + START = 1, + LOGOFF = 2, + KEY = 3, + ASF = 4, +} + +eapol_str = { + [0] = "EAP Packet", + [1] = "EAPOL Start", + [2] = "EAPOL Logoff", + [3] = "EAPOL Key", + [4] = "EAPOL Encapsulated ASF Alert", +} + +code_t = { + REQUEST = 1, + RESPONSE = 2, + SUCCESS = 3, + FAILURE = 4, + INITIATE = 5, + FINISH = 6, +} + +code_str = { + [1] = "Request", + [2] = "Response", + [3] = "Success", + [4] = "Failure", + [5] = "Initiate", + [6] = "Finish", +} + +eap_t = { + IDENTITY = 1, + NAK = 3, + MD5 = 4, + TLS = 13, + TTLS = 21, + PEAP = 25, + MSCHAP = 29, +} + +eap_str = { + [0] = "Reserved", + [1] = "Identity", + [2] = "Notification", + [3] = "Legacy Nak", + [4] = "MD5-Challenge", + [5] = "One-Time Password (OTP)", + [6] = "Generic Token Card (GTC)", + [7] = "Allocated", + [8] = "Allocated", + [9] = "RSA Public Key Authentication", + [10] = "DSS Unilateral", + [11] = "KEA", + [12] = "KEA-VALIDATE", + [13] = "EAP-TLS", + [14] = "Defender Token (AXENT)", + [15] = "RSA Security SecurID EAP", + [16] = "Arcot Systems EAP", + [17] = "EAP-Cisco Wireless", + [18] = "GSM Subscriber Identity Modules (EAP-SIM)", + [19] = "SRP-SHA1", + [20] = "Unassigned", + [21] = "EAP-TTLS", + [22] = "Remote Access Service", + [23] = "EAP-AKA Authentication", + [24] = "EAP-3Com Wireless", + [25] = "PEAP", + [26] = "MS-EAP-Authentication", + [27] = "Mutual Authentication w/Key Exchange (MAKE)", + [28] = "CRYPTOCard", + [29] = "EAP-MSCHAP-V2", + [30] = "DynamID", + [31] = "Rob EAP", + [32] = "Protected One-Time Password", + [33] = "MS-Authentication-TLV", + [34] = "SentriNET", + [35] = "EAP-Actiontec Wireless", + [36] = "Cogent Systems Biometrics Authentication EAP", + [37] = "AirFortress EAP", + [38] = "EAP-HTTP Digest", + [39] = "SecureSuite EAP", + [40] = "DeviceConnect EAP", + [41] = "EAP-SPEKE", + [42] = "EAP-MOBAC", + [43] = "EAP-FAST", + [44] = "ZoneLabs EAP (ZLXEAP)", + [45] = "EAP-Link", + [46] = "EAP-PAX", + [47] = "EAP-PSK", + [48] = "EAP-SAKE", + [49] = "EAP-IKEv2", + [50] = "EAP-AKA'", + [51] = "EAP-GPSK", + [52] = "EAP-pwd", + [53] = "EAP-EKE Version 1", + -- 54-253 Unassigned + [254] = "Reserved for the Expanded Type", + [255] = "Experimental", +} + +local make_eapol = function (arg) + if not arg.type then arg.type = eapol_types.PACKET end + if not arg.version then arg.version = 1 end + if not arg.payload then arg.payload = "" end + if not arg.src then return nil end + + local p = packet.Frame:new() + p.mac_src = arg.src + p.mac_dst = packet.mactobin(ETHER_BROADCAST) + p.ether_type = ETHER_TYPE_EAPOL + + local bin_payload = bin.pack(">A",arg.payload) + p.buf = bin.pack("C",arg.version) .. bin.pack("C",arg.type) .. bin.pack(">S",bin_payload:len()).. bin_payload + p:build_ether_frame() + return p.frame_buf +end + +local make_eap = function (arg) + + if not arg.code then arg.code = code_t.REQUEST end + if not arg.id then arg.id = math.random(0,255) end + if not arg.type then arg.type = eap_t.IDENTITY end + if not arg.payload then arg.payload = "" end + if not arg.header then return nil end + + local bin_payload = bin.pack(">A",arg.payload) + arg.header.payload = bin.pack("C",arg.code) .. bin.pack("C",arg.id) .. bin.pack(">S",bin_payload:len() + EAP_HEADER_SIZE).. bin.pack("C",arg.type) .. bin_payload + + local v = make_eapol(arg.header) + stdnse.print_debug(2, "make eapol %s", arg.header.src) + + return v +end + +parse = function (packet) + local tb = {} + local _ + + stdnse.print_debug(2, "packet size: 0x%x", #packet ) + + -- parsing ethernet header + _, tb.mac_src, tb.mac_dst, tb.ether_type = bin.unpack(">A6A6S", packet) + _, tb.mac_src_str, tb.mac_dst_str = bin.unpack(">H6H6", packet) + + -- parsing eapol header + _, tb.version, tb.type, tb.length = bin.unpack(">CCS", packet, ETHER_HEADER_SIZE + 1) + + stdnse.print_debug(1, "mac_src: %s, mac_dest: %s, ether_type: 0x%X", + tb.mac_src_str, tb.mac_dst_str, tb.ether_type) + + if tb.ether_type ~= ETHER_TYPE_EAPOL_N then return nil, "not an eapol packet" end + + stdnse.print_debug(2, "version: %X, type: %s, length: 0x%X", + tb.version, eapol_str[tb.type] or "unknown", + tb.length) + + tb.eap = {} + + if tb.length > 0 then + -- parsing body + + _, tb.eap.code, tb.eap.id, tb.eap.length, tb.eap.type = bin.unpack(">CCSC", packet, + ETHER_HEADER_SIZE + EAPOL_HEADER_SIZE + 1) + stdnse.print_debug(2, "code: %s, id: 0x%X, length: 0x%X, type: %s", + code_str[tb.eap.code] or "unknown", + tb.eap.id, tb.eap.length, eap_str[tb.eap.type] or "unknown" ) + if tb.length ~= tb.eap.length then + stdnse.print_debug(1, "WARNING length mismatch: 0x%X and 0x%X", tb.length, tb.eap.length ) + end + end + + tb.eap.body = {} + + -- parsing payload + if tb.length > 5 and tb.eap.type == eap_t.IDENTITY then + _, tb.eap.body.identity = bin.unpack("z", packet, + ETHER_HEADER_SIZE + EAPOL_HEADER_SIZE + EAP_HEADER_SIZE + 1) + stdnse.print_debug(1, "identity: %s", tb.eap.body.identity ) + end + + if tb.length > 5 and tb.eap.type == eap_t.MD5 then + _, tb.eap.body.challenge = bin.unpack("p", packet, ETHER_HEADER_SIZE + EAPOL_HEADER_SIZE + EAP_HEADER_SIZE + 1) + end + + return tb +end + +send_identity_response = function (iface, id, identity) + + if not iface then + stdnse.print_debug(1, "no interface given") + return + end + + local dnet = nmap.new_dnet() + local tb = {src = iface.mac, type = eap.eapol_t.PACKET} + local response = make_eap{header = tb, code = code_t.RESPONSE, type = eap_t.IDENTITY, id = id, payload = identity} + + dnet:ethernet_open(iface.device) + dnet:ethernet_send(response) + dnet:ethernet_close() +end + +send_nak_response = function (iface, id, auth) + + if not iface then + stdnse.print_debug(1, "no interface given") + return + end + + local dnet = nmap.new_dnet() + local tb = {src = iface.mac, type = eap.eapol_t.PACKET} + local response = make_eap{header = tb, code = code_t.RESPONSE, type = eap_t.NAK, id = id, payload = bin.pack("C",auth)} + + dnet:ethernet_open(iface.device) + dnet:ethernet_send(response) + dnet:ethernet_close() +end + + +send_start = function (iface) + + if not iface then + stdnse.print_debug(1, "no interface given") + return + end + + local dnet = nmap.new_dnet() + local start = make_eapol{type = eap.eapol_t.START, src = iface.mac} + + dnet:ethernet_open(iface.device) + dnet:ethernet_send(start) + dnet:ethernet_close() + +end \ No newline at end of file diff --git a/scripts/eap-info.nse b/scripts/eap-info.nse new file mode 100644 index 000000000..8acd74a54 --- /dev/null +++ b/scripts/eap-info.nse @@ -0,0 +1,194 @@ +description = [[ +Enumerates the authentication methods offered by an EAP authenticator for a +given identity or for the anonymous identity if no argument is passed. +]] + +--- +-- @usage +-- nmap -e interface --script eap-info [--script-args="eap-info.identity=0-user,eap-info.scan={13,50}"] +-- +-- @output +-- Pre-scan script results: +-- | eap-info: +-- | Available authentication methods with identity="anonymous" on interface eth2 +-- | true PEAP +-- | true EAP-TTLS +-- | false EAP-TLS +-- |_ false EAP-MSCHAP-V2 +-- +-- @args identity Identity to use for the first step of the authentication methods (if omitted "anonymous" will be used). +-- @args scan Table of authentication methods to test, e.g. { 4, 13, 25 } for MD5, TLS and PEAP. Default: TLS, TTLS, PEAP, MSCHAP. +-- @args interface Network interface to use for the scan, overrides "-e". +-- @args timeout Maximum time allowed for the scan, in seconds. Methods not tested because of timeout will be listed as "unknown". + +author = "Riccardo Cecolin" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +categories = { "broadcast", "safe" } + +require 'nmap' +require 'packet' +require 'bin' +require 'stdnse' +require 'eap' + +prerule = function() + return nmap.is_privileged() +end + +local default_scan = { + eap.eap_t.TLS, + eap.eap_t.TTLS, + eap.eap_t.PEAP, + eap.eap_t.MSCHAP, +} + +local UNKNOWN = "unknown" + +action = function() + + local arg_interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface") + local arg_identity = stdnse.get_script_args(SCRIPT_NAME .. ".identity") + local arg_scan = stdnse.get_script_args(SCRIPT_NAME .. ".scan") + local arg_timeout = stdnse.get_script_args(SCRIPT_NAME .. ".timeout") + local iface + + -- trying with provided interface name + if arg_interface then + iface = nmap.get_interface_info(arg_interface) + end + + -- trying with default nmap interface + if not iface then + local iname = nmap.get_interface() + if iname then + iface = nmap.get_interface_info(iname) + end + end + + -- failed + if not iface then + return "please specify an interface with -e" + end + stdnse.print_debug(1, "iface: %s", iface.device) + + timeout = 10 * 1000 + if arg_timeout then + timeout = arg_timeout * 1000 + end + + stdnse.print_debug(2, "timeout: %s", timeout) + + local pcap = nmap.new_socket() + pcap:pcap_open(iface.device, 512, true, "ether proto 0x888e") + + + local identity = { name="anonymous", auth = {}, probe = -1 } + + if arg_identity then + identity.name = tostring(arg_identity) + end + + local scan + if arg_scan == nil or type(arg_scan) ~= "table" or #arg_scan == 0 then + scan = default_scan + else + scan = arg_scan + end + + local valid = false + for i,v in ipairs(scan) do + v = tonumber(v) + if v ~= nil and v < 256 and v > 3 then + stdnse.print_debug(1, "selected: %s", eap.eap_str[v] or "unassigned" ) + identity.auth[v] = UNKNOWN + valid = true + end + end + + if not valid then + return "no valid scan methods provided" + end + + local tried_all = false + + local start_time = nmap.clock_ms() + eap.send_start(iface) + + while(nmap.clock_ms() - start_time < timeout) and not tried_all do + local status, plen, l2_data, l3_data, time = pcap:pcap_receive() + if (status) then + stdnse.print_debug(2, "packet size: 0x%x", plen ) + local packet = eap.parse(l2_data .. l3_data) + + if packet then + stdnse.print_debug(2, "packet valid") + + -- respond to identity requests, using the same session id + if packet.eap.type == eap.eap_t.IDENTITY and packet.eap.code == eap.code_t.REQUEST then + stdnse.print_debug(1, "server identity: %s",packet.eap.body.identity) + eap.send_identity_response(iface, packet.eap.id, identity.name) + end + + -- respond with NAK to every auth request to enumerate them until we get a failure + if packet.eap.type ~= eap.eap_t.IDENTITY and packet.eap.code == eap.code_t.REQUEST then + stdnse.print_debug(1, "auth request: %s",eap.eap_str[packet.eap.type]) + identity.auth[packet.eap.type] = true + + identity.probe = -1 + for i,v in pairs(identity.auth) do + stdnse.print_debug(1, "identity.auth: %d %s",i,tostring(v)) + if v == UNKNOWN then + identity.probe = i + eap.send_nak_response(iface, packet.eap.id, i) + break + end + end + if identity.probe == -1 then tried_all = true end + end + + -- retry on failure + if packet.eap.code == eap.code_t.FAILURE then + stdnse.print_debug(1, "auth failure") + identity.auth[identity.probe] = false + + -- don't give up at the first failure! + -- mac spoofing to avoid to wait too much + local d = string.byte(iface.mac,6) + d = (d + 1) % 256 + iface.mac = iface.mac:sub(1,5) .. bin.pack("C",d) + + tried_all = true + for i,v in pairs(identity.auth) do + if v == UNKNOWN then + tried_all = false + break + end + end + if not tried_all then + eap.send_start(iface) + end + end + + else + stdnse.print_debug(1, "packet invalid! wrong filter?") + end + end + end + + local results = { ["name"] = ("Available authentication methods with identity=\"%s\" on interface %s"):format(identity.name, iface.device) } + for i,v in pairs(identity.auth) do + if v== true then + table.insert(results, 1, ("%-8s %s"):format(tostring(v), eap.eap_str[i] or "unassigned" )) + else + table.insert(results, ("%-8s %s"):format(tostring(v), eap.eap_str[i] or "unassigned" )) + end + end + + for i,v in ipairs(results) do + stdnse.print_debug(1, "%s", tostring(v)) + end + + return stdnse.format_output(true, results) +end + diff --git a/scripts/script.db b/scripts/script.db index 987dac830..54c86cd86 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -77,6 +77,7 @@ Entry { filename = "domino-enum-users.nse", categories = { "auth", "intrusive", Entry { filename = "dpap-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "drda-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "drda-info.nse", categories = { "discovery", "safe", "version", } } +Entry { filename = "eap-info.nse", categories = { "broadcast", "safe", } } Entry { filename = "epmd-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "finger.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "firewalk.nse", categories = { "discovery", "safe", } }