diff --git a/CHANGELOG b/CHANGELOG index 75234d179..0caf54907 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,14 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] New script: ipmi-version retrieves protocol version and authentication + options from ASF-RMCP (IPMI) services. [Claudiu Perta] + +o [NSE] New script: ipmi-cipher-zero checks IPMI services for Cipher Zero + support, which allows connection without a password. [Claudiu Perta] + +o [NSE] New script: ipmi-brute performs authentication brute-forcing on IPMI + services. [Claudiu Perta] + o [NSE][GH#352] New script: mqtt-subscribe connects to a MQTT broker, subscribes to topics, and lists the messages received. [Mak Kolybabi] diff --git a/nselib/ipmi.lua b/nselib/ipmi.lua new file mode 100644 index 000000000..976e13a12 --- /dev/null +++ b/nselib/ipmi.lua @@ -0,0 +1,307 @@ +--- +-- A module implementing IPMI protocol (the code is a porting of the Metasploit ipmi scanner: +-- https://github.com/rapid7/metasploit-framework/tree/master/modules/auxiliary/scanner/ipmi) +-- +-- @class module +-- @name ipmi +-- @author "Claudiu Perta " +local bin = require "bin" +local bit = require "bit" +local math = require "math" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" + +_ENV = stdnse.module("ipmi", stdnse.seeall) + +local HAVE_SSL, openssl = pcall(require,"openssl") + +PAYLOADS = { + ["IPMI"] = 0, + ["PAYLOAD_SOL"] = 1, + ["RMCPPLUSOPEN_REQ"] = 0x10, + ["RMCPPLUSOPEN_REP"] = 0x11, + ["RAKP1"] = 0x12, + ["RAKP2"] = 0x13, + ["RAKP3"] = 0x14, + ["RAKP4"] = 0x15, +} + +RMCP_ERRORS = { + [1] = "Insufficient resources to create new session \ + (wait for existing sessions to timeout)", + + -- Shouldn't occur. + [2] = "Invalid Session ID", + + -- Shouldn't occur. + [3] = "Invalid payload type", + + -- If these happen, we need to enhance our mechanism for detecting + -- supported auth algorithms. + [4] = "Invalid authentication algorithm", + [5] = "Invalid integrity algorithm", + + [6] = "No matching authentication payload", + [7] = "No matching integrity payload", + + -- This suggests the session was timed out while trying to negotiate, + -- shouldn't happen. + [8] = "Inactive Session ID", + + [9] = "Invalid role", + [0xa] = "Unauthorised role or privilege level requested", + [0xb] = "Insufficient resources to create a session at the requested role", + [0xc] = "Invalid username length", + [0xd] = "Unauthorized name", + [0xe] = "Unauthorized GUID", + [0xf] = "Invalid integrity check value", + [0x10] = "Invalid confidentiality algorithm", + [0x11] = "No cipher suite match with proposed security algorithms", + + -- Never observed, most likely a bug in xCAT or IPMI device. + [0x12] = "Illegal or unrecognized parameter", +} + +channel_auth_request = function() + return ( + "\x06\x00\xff\x07" .. -- Header + "\x00\x00\x00\x00" .. + "\x00\x00\x00\x00\x00\x09\x20\x18" .. + "\xc8\x81\x00\x38\x8e\x04\xb5" + ) +end + +rmcpplus_header = function (payload_type) + return ( + "\x06\x00\xff\x07" .. -- RMCP Header + "\x06" .. -- RMCP+ Authentication Type + string.char(PAYLOADS[payload_type]) .. -- Payload Type + "\x00\x00\x00\x00" .. -- Session ID + "\x00\x00\x00\x00" -- Sequence Number + ) +end + +-- Open rmcpplus_request +session_open_request = function(console_session_id) + local data = ( + "\x00\x00" .. -- Maximum Access + "\x00\x00" .. -- Reserved + console_session_id .. + "\x00\x00\x00\x08" .. + "\x01\x00\x00\x00" .. + "\x01\x00\x00\x08" .. + "\x01\x00\x00\x00" .. -- HMAC-SHA1 + "\x02\x00\x00\x08" .. + "\x01\x00\x00\x00" -- AES Encryption + ) + + return bin.pack(" +-- +-- @output +-- PORT STATE SERVICE REASON +-- 623/udp open|filtered unknown +-- | ipmi-brute: +-- | Accounts +-- |_ admin:admin => Valid credentials +-- + +author = "Claudiu Perta" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "brute"} + +portrule = shortport.port_or_service(623, "asf-rmcp", "udp", {"open", "open|filtered"}) + +Driver = { + + new = function(self, host, port) + local o = {} + setmetatable(o, self) + self.__index = self + o.host = host + o.port = port + return o + end, + + connect = function(self) + self.socket = nmap.new_socket() + self.socket:set_timeout( + ((self.host.times and self.host.times.timeout) or 8) * 1000) + self.socket:connect(self.host, self.port, "udp") + + return true + end, + + login = function(self, username, password) + local console_session_id = stdnse.generate_random_string(4) + local console_random_id = stdnse.generate_random_string(16) + + local request = ipmi.session_open_request(console_session_id) + local status, reply + + self.socket:send(request) + status, reply = self.socket:receive() + + if not status then + return false, brute.Error:new( + "No response to IPMI open session request") + end + + local session = ipmi.parse_open_session_reply(reply) + if session["session_payload_type"] ~= ipmi.PAYLOADS["RMCPPLUSOPEN_REP"] then + return false, brute.Error:new("Unknown response to open session request") + end + + if session["error_code"] ~= 0 then + return false, brute.Error:new(ipmi.RMCP_ERRORS[session.error_code] or "Unknown error") + end + local bmc_session_id = session["bmc_session_id"] + local rakp1_request = ipmi.rakp_1_request( + bmc_session_id, console_random_id, username) + + self.socket:send(rakp1_request) + status, reply = self.socket:receive() + + if not status then + return false, brute.Error:new("No response to RAKP1 message") + end + + local rakp2_message = ipmi.parse_rakp_1_reply(reply) + if rakp2_message["session_payload_type"] ~= ipmi.PAYLOADS["RAKP2"] then + return false, brute.Error:new("Unknown response to RAPK1 request") + end + + if rakp2_message["error_code"] ~= 0 then + return false, brute.Error:new( + ipmi.RMCP_ERRORS[rakp2_message["error_code"]]) + end + + local hmac_salt = ipmi.rakp_hmac_sha1_salt( + console_session_id, + session["bmc_session_id"], + console_random_id, + rakp2_message["bmc_random_id"], + rakp2_message["bmc_guid"], + 0x14, + username + ) + + local found = ipmi.verify_rakp_hmac_sha1( + hmac_salt, rakp2_message["hmac_sha1"], password) + + if found then + return true, brute.Account:new(username, password, creds.State.VALID) + else + return false, brute.Error:new("Incorrect password") + end + + end, + + disconnect = function(self) + self.socket:close() + end, + + check = function(host, port) + return true + end +} + +action = function(host, port) + local status, result + local engine = brute.Engine:new(Driver, host, port) + + engine.options.script_name = SCRIPT_NAME + status, result = engine:start() + return result +end diff --git a/scripts/ipmi-cipher-zero.nse b/scripts/ipmi-cipher-zero.nse new file mode 100644 index 000000000..d57ef3639 --- /dev/null +++ b/scripts/ipmi-cipher-zero.nse @@ -0,0 +1,104 @@ +local ipmi = require "ipmi" +local nmap = require "nmap" +local shortport = require "shortport" +local stdnse = require "stdnse" +local string = require "string" +local vulns = require "vulns" + +description = [[ + IPMI 2.0 Cipher Zero Authentication Bypass Scanner. This module identifies IPMI 2.0 + compatible systems that are vulnerable to an authentication bypass vulnerability + through the use of cipher zero. +]] + +--- +-- @usage +-- nmap -sU --script ipmi-cipher-zero -p 623 +-- +-- @output +---PORT STATE SERVICE REASON +-- 623/udp open|filtered unknown no-response +-- | ipmi-cipher-zero: +-- | VULNERABLE: +-- | IPMI 2.0 RAKP Cipher Zero Authentication Bypass +-- | State: VULNERABLE +-- | Risk factor: High +-- | Description: +-- | +-- | The issue is due to the vendor shipping their devices with the +-- | cipher suite '0' (aka 'cipher zero') enabled. This allows a +-- | remote attacker to authenticate to the IPMI interface using +-- | an arbitrary password. The only information required is a valid +-- | account, but most vendors ship with a default 'admin' account. +-- | This would allow an attacker to have full control over the IPMI +-- | functionality. +-- | +-- | References: +-- | http://fish2.com/ipmi/cipherzero.html +-- | http://osvdb.org/show/osvdb/93039 +-- |_ http://osvdb.org/show/osvdb/93040 +-- + +author = "Claudiu Perta " +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"vuln", "safe"} + +portrule = shortport.port_or_service(623, "asf-rmcp", "udp", {"open", "open|filtered"}) + +action = function(host, port) + + local vuln_table = { + title = "IPMI 2.0 RAKP Cipher Zero Authentication Bypass", + state = vulns.STATE.NOT_VULN, + risk_factor = "High", + description = [[ + +The issue is due to the vendor shipping their devices with the +cipher suite '0' (aka 'cipher zero') enabled. This allows a +remote attacker to authenticate to the IPMI interface using +an arbitrary password. The only information required is a valid +account, but most vendors ship with a default 'admin' account. +This would allow an attacker to have full control over the IPMI +functionality + ]], + references = { + 'http://fish2.com/ipmi/cipherzero.html', + 'http://osvdb.org/show/osvdb/93040', + 'http://osvdb.org/show/osvdb/93039', + } + } + + local report = vulns.Report:new(SCRIPT_NAME, host, port) + + local request = ipmi.session_open_cipher_zero_request() + + local socket = nmap.new_socket() + socket:set_timeout( + ((host.times and host.times.timeout) or 8) * 1000) + socket:connect(host, port, "udp") + + -- Send 3 probes + local tries = 3 + repeat + socket:send(request) + tries = tries - 1 + until tries == 0 + + local status, reply = socket:receive() + socket:close() + + if not status then + stdnse.debug1(string.format("No response (%s)", reply)) + return nil + end + + nmap.set_port_state(host, port, "open") + + local info = ipmi.parse_open_session_reply(reply) + if info["session_payload_type"] == ipmi.PAYLOADS["RMCPPLUSOPEN_REP"] then + vuln_table.state = vulns.STATE.VULN + end + + return report:make_output(vuln_table) + +end diff --git a/scripts/ipmi-version.nse b/scripts/ipmi-version.nse new file mode 100644 index 000000000..9d2895763 --- /dev/null +++ b/scripts/ipmi-version.nse @@ -0,0 +1,170 @@ +local ipmi = require "ipmi" +local nmap = require "nmap" +local shortport = require "shortport" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" + +description = [[ + Performs IPMI Information Discovery through Channel Auth probes. +]] + +--- +-- @usage +-- nmap -sU --script ipmi-version -p 623 +-- +-- @output +-- PORT STATE SERVICE REASON +-- 623/udp open|filtered unknown +-- | ipmi-version: +-- | Version: IPMI-2.0 +-- | UserAuth: password, md5, md2 +-- | PassAuth: null_user +-- |_ Level: 1.2,2.0 +-- +-- @xmloutput +-- +--
+-- IPMI-2.0 +--
+-- +-- +-- password +-- md5 +-- md2 +--
+-- +-- +-- kg_default +-- null_user +--
+-- +-- +-- 1.2 +-- 2.0 +--
+-- +-- + +author = "Claudiu Perta " +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +portrule = shortport.version_port_or_service(623, "asf-rmcp", "udp", {"open", "open|filtered"}) + +local comma_separated = { + __tostring = function(t) return table.concat(t, ", ") end +} + +action = function(host, port) + + local request = ipmi.channel_auth_request() + local socket = nmap.new_socket() + + socket:set_timeout( + ((host.times and host.times.timeout) or 8) * 1000) + socket:connect(host, port, "udp") + + -- Send 3 probes + local tries = 3 + repeat + socket:send(request) + tries = tries - 1 + until tries == 0 + + local status, reply = socket:receive() + socket:close() + + if not status then + stdnse.debug1(string.format("No response (%s)", reply)) + return nil + end + + nmap.set_port_state(host, port, "open") + + -- Invalid reply + local info = ipmi.parse_channel_auth_reply(reply) + if info["ipmi_command"] ~= 56 then + return "IPMI - Invalid response" + end + + -- Valid reply + local Version = {} + if info["ipmi_compat_20"] then + table.insert(Version, "IPMI-2.0") + else + table.insert(Version, "IPMI-1.5") + end + + local UserAuth = {} + setmetatable(UserAuth, comma_separated) + + if info["ipmi_compat_oem_auth"] then + table.insert(UserAuth, "oem_auth") + end + + if info["ipmi_compat_password"] then + table.insert(UserAuth, "password") + end + + if info["ipmi_compat_md5"] then + table.insert(UserAuth, "md5") + end + + if info["ipmi_compat_md2"] then + table.insert(UserAuth, "md2") + end + + if info["ipmi_compat_none"] then + table.insert(UserAuth, "null") + end + + local PassAuth = {} + setmetatable(PassAuth, comma_separated) + + if info["ipmi_compat_20"] and info["ipmi_user_kg"] then + table.insert(PassAuth, "kg_default") + end + + if not info["ipmi_user_disable_message_auth"] then + table.insert(PassAuth, "auth_msg") + end + + if not info["ipmi_user_disable_user_auth"] then + table.insert(PassAuth, "auth_user") + end + + if info["ipmi_user_non_null"] then + table.insert(PassAuth, "non_null_user") + end + + if info["ipmi_user_null"] then + table.insert(PassAuth, "null_user") + end + + if info["ipmi_user_anonymous"] then + table.insert(PassAuth, "anonymous_user") + end + + local ConnInfo = {} + setmetatable(ConnInfo, comma_separated) + + if info["ipmi_conn_15"] then + table.insert(ConnInfo, "1.5") + end + + if info["ipmi_conn_20"] then + table.insert(ConnInfo, "2.0") + end + + local output = stdnse.output_table() + output["Version"] = Version + output["UserAuth"] = UserAuth + output["PassAuth"] = PassAuth + output["Level"] = ConnInfo + if info["ipmi_oem_id"] ~= 0 then + output["OEMID"] = info["ipmi_oem_id"] + end + + return output +end diff --git a/scripts/script.db b/scripts/script.db index a5959bb23..c96a2f51a 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -278,6 +278,9 @@ Entry { filename = "ip-geolocation-ipinfodb.nse", categories = { "discovery", "e Entry { filename = "ip-geolocation-maxmind.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "ip-https-discover.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "ipidseq.nse", categories = { "discovery", "safe", } } +Entry { filename = "ipmi-brute.nse", categories = { "brute", "intrusive", } } +Entry { filename = "ipmi-cipher-zero.nse", categories = { "safe", "vuln", } } +Entry { filename = "ipmi-version.nse", categories = { "discovery", "safe", } } Entry { filename = "ipv6-multicast-mld-list.nse", categories = { "broadcast", "discovery", } } Entry { filename = "ipv6-node-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "ipv6-ra-flood.nse", categories = { "dos", "intrusive", } }