From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- scripts/irc-sasl-brute.nse | 204 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 scripts/irc-sasl-brute.nse (limited to 'scripts/irc-sasl-brute.nse') diff --git a/scripts/irc-sasl-brute.nse b/scripts/irc-sasl-brute.nse new file mode 100644 index 0000000..90acbfc --- /dev/null +++ b/scripts/irc-sasl-brute.nse @@ -0,0 +1,204 @@ +local base64 = require "base64" +local brute = require "brute" +local comm = require "comm" +local creds = require "creds" +local sasl = require "sasl" +local irc = require "irc" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" + +description=[[ +Performs brute force password auditing against IRC (Internet Relay Chat) servers supporting SASL authentication. +]] + +-- You can read more about sasl here: +-- https://github.com/atheme/charybdis/blob/master/doc/sasl.txt +-- http://www.leeh.co.uk/draft-mitchell-irc-capabilities-02.html +-- the first link also explains the meaning of constants used in +-- this script. + +--- +-- @usage +-- nmap --script irc-sasl-brute -p 6667 +-- +-- @output +-- PORT STATE SERVICE REASON +-- 6667/tcp open irc syn-ack +-- | irc-sasl-brute: +-- | Accounts +-- | root:toor - Valid credentials +-- | Statistics +-- |_ Performed 60 guesses in 29 seconds, average tps: 2 +-- +-- @args irc-sasl-brute.threads the number of threads to use while brute-forcing. +-- Defaults to 2. + + + +author = "Piotr Olma" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories={"brute","intrusive"} + +portrule = irc.portrule + +local dbg = stdnse.debug + +-- some parts of the following class are taken from irc-brute written by Patrik +Driver = { + + new = function(self, host, port, saslencoder) + local o = { host = host, port = port, saslencoder = saslencoder} + setmetatable(o, self) + self.__index = self + return o + end, + + connect = function(self) + -- the high timeout should take delays from ident into consideration + local s, r, opts, _ = comm.tryssl(self.host, + self.port, + "CAP REQ sasl\r\n", + { timeout = 10000 } ) + if ( not(s) ) then + return false, "Failed to connect to server" + end + if string.find(r:lower(), "throttled") then + -- we were reconnecting too fast + dbg(2, "throttled.") + return false, "We got throttled." + end + local status, _ = s:send("CAP END\r\n") + if not status then return false, "Send failed." end + local response + repeat + status, response = s:receive_lines(1) + if not status then return false, "Receive failed." end + if string.find(response, "ACK") then status = false end + until (not status) + self.socket = s + return true + end, + + login = function(self, username, password) + self.socket:send("AUTHENTICATE ".. self.saslencoder:get_mechanism() .."\r\n") + local status, response, challenge + repeat + status, response = self.socket:receive_lines(1) + if not status then + local err = brute.Error:new(response) + err:setRetry(true) + return false, err + end + challenge = string.match(response, "AUTHENTICATE (.*)") + dbg(3, "challenge found: %s", tostring(challenge)) + if challenge then status = false end + until (not status) + local msg = self.saslencoder:encode(username, password, challenge) + + -- SASL PLAIN is supposed to be plaintext, but freenode actually wants it to be base64 encoded + if self.saslencoder:get_mechanism() == "PLAIN" then + msg = base64.enc(msg) + end + + local status, data = self.socket:send("AUTHENTICATE "..msg.."\r\n") + local success = false + + if ( not(status) ) then + local err = brute.Error:new( data ) + -- This might be temporary, set the retry flag + err:setRetry( true ) + return false, err + end + + repeat + status, response = self.socket:receive_lines(1) + if ( status and string.find(response, "90[45]") ) then + status = false + end + if ( status and string.find(response, "90[03]") ) then + success = true + status = false + end + until (not status) + + if (success) then + return true, creds.Account:new(username, password, creds.State.VALID) + end + return false, brute.Error:new("Incorrect username or password") + end, + + disconnect = function(self) return self.socket:close() end, +} + +-- checks if server supports sasl authentication and if it does, also checks for supported +-- mechanisms +local function check_sasl(host, port) + local s, r, opts, _ = comm.tryssl(host, port, "CAP REQ sasl\r\n", { timeout = 15000 } ) + + repeat + local status, lines = s:receive_lines(1) + if string.find(lines, "ACK") then status = false end + if string.find(lines, "NAK") then + s:close() + return false + end + until (not status) + + -- we know that sasl is supported, now check which mechanisms can be used + local to_check = {"PLAIN", "DH-BLOWFISH", "NTLM", "CRAM-MD5", "DIGEST-MD5"} + local supported = {} + for _,m in ipairs(to_check) do + s:send("AUTHENTICATE "..m.."\r\n") + dbg(3, "checking mechanism %s", m) + repeat + local status, lines = s:receive_lines(1) + if string.find(lines, "AUTHENTICATE") then + s:send("AUTHENTICATE abort\r\n") -- it's not a real command, just to break the process + -- wait till we get a message indicating failed authentication + repeat + status, lines = s:receive_lines(1) + if string.find(lines, "90[45]") then status = false end + until (not status) + table.insert(supported, m) + status = false + elseif string.find(lines, "90[45]") then + status = false + break + end + until (not status) + end + s:close() + return true, supported +end + +action = function(host, port) + local sasl_supported, mechs = check_sasl(host, port) + if not sasl_supported then + return stdnse.format_output(false, "Server doesn't support SASL authentication.") + end + + local saslencoder = sasl.Helper:new() + local sasl_mech + + -- check if the library supports any of the mechanisms we identified + for _,m in ipairs(mechs) do + if saslencoder:set_mechanism(m) then + sasl_mech = m + dbg(2, "supported mechanism found: %s", m) + break + end + end + local engine = brute.Engine:new(Driver, host, port, saslencoder) + engine.options.script_name = SCRIPT_NAME + engine.options.firstonly = true + -- irc servers seem to be restrictive about too many connection attempts + -- in a short time thus we need to limit the number of threads + local threads = stdnse.get_script_args(("%s.threads"):format(SCRIPT_NAME)) + threads = tonumber(threads) or 2 + engine:setMaxThreads(threads) + local status, accounts = engine:start() + return accounts +end + + -- cgit v1.2.3