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", } }