From 6023e253dcbb776cd1681956a578d6c64b5184e6 Mon Sep 17 00:00:00 2001 From: kroosec Date: Wed, 18 Jul 2012 12:06:33 +0000 Subject: [PATCH] Changed sip-enum-users which now uses brute.lua for extensions enumeration and supports iteration over custom lists and numeric ranges. --- CHANGELOG | 7 +- scripts/sip-enum-users.nse | 332 +++++++++++++++++++++++++------------ 2 files changed, 231 insertions(+), 108 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 37963517d..4eaa719a7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ # Nmap Changelog ($Id$); -*-text-*- + +o [NSE] Complete change to sip-enum-users script which now uses brute.lua for + enumeration and supports iterating over custom username lists and numeric + ranges. [Hani Benhabiles] + o Added protocol specific payloads for IPv6 hop-by-hop (0x00), routing (0x2b), -fragment (0x2c), and destination (0x3c). [Sean Rivera] + fragment (0x2c), and destination (0x3c). [Sean Rivera] o [NSE] Added http-slowloris script which performes a slowloris DoS attack against a Web server and reports if it's vulnerable or not. [Aleksandar Nikolic] diff --git a/scripts/sip-enum-users.nse b/scripts/sip-enum-users.nse index 6cee2ab8d..1a71801f4 100644 --- a/scripts/sip-enum-users.nse +++ b/scripts/sip-enum-users.nse @@ -2,143 +2,261 @@ local shortport = require "shortport" local sip = require "sip" local stdnse = require "stdnse" local table = require "table" +local math = require "math" +local brute = require "brute" +local creds = require "creds" local unpwdb = require "unpwdb" description = [[ -Attempts to enumerate valid user account using SIP (Session Initiation -Protocol - http://en.wikipedia.org/wiki/Session_Initiation_Protocol). -This protocol is most commonly associated with VoIP -sessions. Currently only the SIP server Asterisk is supported. +Enumerates a SIP server's valid extensions (users). -* Asterisk - - The script enumerates valid accounts by checking the SIP servers response - to the REGISTER request. If TRYING is returned, the account does not - exist. If REGISTER is returned the account is valid. +The script works by sending REGISTER SIP requests to the server with the +specified extension and checking for the response status code in order +to know if an extension is valid. If a response status code is 401 or +407, it means that the extension is valid and requires authentication. If the +response status code is 200, it means that the extension exists and doesn't +require any authentication while a 403 response status code means that +extension exists but access is forbidden. To skip false positives, the script +begins by sending a REGISTER request for a random extension and checking for +response status code. ]] --- --- @usage --- nmap -sU -p 5060 --script=sip-enum-users +--@args sip-enum-users.minext Extension value to start enumeration from. +-- Defaults to 0. +-- +--@args sip-enum-users.maxext Extension value to end enumeration at. +-- Defaults to 999. -- --- PORT STATE SERVICE --- 5060/udp open|filtered sip +--@args sip-enum-users.padding Number of digits to pad zeroes up to. +-- Defaults to 0. No padding if this is set to zero. +-- +--@args sip-enum-users.users If set, will also enumerate users +-- from userslist file. +-- +--@args sip-enum-users.userslist Path to list of users. +-- Defaults to nselib/data/usernames.lst. +-- +--@usage +-- nmap --script=sip-enum-users -sU -p 5060 +-- +-- nmap --script=sip-enum-users -sU -p 5060 --script-args +-- 'sip-enum-users.padding=4, sip-enum-users.minext=1000, +-- sip-enum-users.maxext=9999' +-- +--@output +-- 5060/udp open sip -- | sip-enum-users: --- | Valid SIP accounts --- | 1000 --- |_ 1001 +-- | Accounts +-- | 101: Auth required +-- | 120: No auth +-- | Statistics +-- |_ Performed 1000 guesses in 50 seconds, average tps: 20 --- Version 0.1 --- Created 04/03/2011 - v0.1 - created by Patrik Karlsson -author = "Patrik Karlsson" +author = "Hani Benhabiles" + license = "Same as Nmap--See http://nmap.org/book/man-legal.html" -categories = {"intrusive", "auth"} + +categories = {"auth", "intrusive"} portrule = shortport.port_or_service(5060, "sip", {"tcp", "udp"}) --- Send a register request to the server and returned the unparsed response --- @param session instance of Session class --- @param username string containing the name of the user --- @param Used protocol, could be "UDP" or "TCP" --- @return status true on success false on failure --- @return response instance of sip.Response (on success) --- @return err string containing the error message (on failure) -local function register(session, username, protocol) - local request = sip.Request:new(sip.Method.REGISTER, protocol) +--- Function that sends register sip request with provided extension +-- using the specified session. +-- @arg sess session to use. +-- @arg ext Extension to send register request to. +-- @return status true on success, false on failure. +-- @return Response instance on success, error string on failure. +local registerext = function(sess, ext) + -- set session values + local request = sip.Request:new(sip.Method.REGISTER) - session.sessdata:setUsername(username) - request:setUri("sip:" .. session.sessdata:getServer() ) - request:setSessionData(session.sessdata) - - local status, response = session:exch(request) - if (not(status)) then return false, response end - - return true, response + request:setUri("sip:" .. sess.sessdata:getServer()) + sess.sessdata:setUsername(ext) + request:setSessionData(sess.sessdata) + + return sess:exch(request) end +--- Function that returns a number as string with a number of zeroes padded to +-- the left. +-- @arg num Number to be padded. +-- @arg padding number of digits to pad up to. +-- @return string of padded number. +local padnum = function(num, padding) + -- How many zeroes do we need to add + local n = #tostring(num) + if n >= padding then + return tostring(num) + end + n = padding - n --- Confirm the server is a valid and supported one --- @param host table as passed to the action method --- @param port table as passed to the action method --- @return status true on success, false on failure --- @return header string containing the server name -local function confirmServer(host, port) - local user = "nmap_banner_check" - local session = sip.Session:new( host, port ) + return string.rep(tostring(0), n) .. tostring(num) +end - local status = session:connect() +--- Iterator function that returns values from a lower value up to a greater +-- value with zeroes padded up to padding argument. +-- @arg minval Start value. +-- @arg maxval End value. +-- @arg padding number of digits to pad up to. +-- @return string current value. +local numiterator = function(minval, maxval, padding) + local i = minval - 1 + return function() + i = i + 1 + if i <= maxval then return padnum(i, padding), '' end + end +end + +--- Iterator function that returns lines from a file +-- @arg userslist Path to file list in data location. +-- @return status false if error. +-- @return string current line. +local useriterator = function(list) + local f = nmap.fetchfile(list) + if not f then + return false, ("\n ERROR: Couldn't find %s"):format(list) + end + f = io.open(f) + if ( not(f) ) then + return false, ("\n ERROR: Failed to open %s"):format(DEFAULT_ACCOUNTS) + end + return function() + for line in f:lines() do + return line + end + end +end + +--- function that tests for 404 status code when sending a REGISTER request +-- with a random sip extension. +-- @arg host Target host table. +-- @arg port Target port table. +local test404 = function(host, port) + local session, status, randext, response + -- Random extension + randext = math.random(1234567,987654321) + + session = sip.Session:new(host, port) + status = session:connect() + if not status then + return false, "ERROR: Failed to connect to the SIP server." + end + + status, response = registerext(session, randext) + if not status then + return false, "ERROR: No response from the SIP server." + end + if response:getErrorCode() ~= 404 then + return false, "Server not returning 404 for random extension." + end + return true + +end + +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.session = sip.Session:new(self.host, self.port) + local status = self.session:connect() if ( not(status) ) then - return "ERROR: Failed to connect to the SIP server" + return false, brute.Error:new( "Couldn't connect to host: " .. err ) end + return true + end, - local response - status, response = register(session, user, port.protocol:upper()) - if ( status ) then - return true, ( - response:getHeader("User-Agent") or - response:getHeader("Server") - ) + login = function( self, username, password) + -- We are using the "password" values instead of the "username" so we + -- could benifit from brute.lua passonly option and setPasswordIterator + -- function, as we are doing usernames enumeration only and not + -- credentials brute forcing. + local status, response, responsecode + -- Send REGISTER request for each extension + status, response = registerext(self.session, password) + if status then + responsecode = response:getErrorCode() + -- If response status code is 401 or 407, then extension exists but + -- requires authentication + if responsecode == sip.Error.UNAUTHORIZED or + responsecode == sip.Error.PROXY_AUTH_REQUIRED then + return true, brute.Account:new(password, " Auth required", '') + + -- If response status code is 200, then extension exists + -- and requires no authentication + elseif responsecode == sip.Error.OK then + return true, brute.Account:new(password, " No auth", '') + -- If response status code is 200, then extension exists + -- but access is forbidden. + + elseif responsecode == sip.Error.FORBIDDEN then + return true, brute.Account:new(password, " Forbidden", '') + end + return false,brute.Error:new( "Not found" ) + else + return false,brute.Error:new( "No response" ) end + end, - return false -end - --- Asterisk specific function used to check for valid usernames --- @param session instance of SIP Session --- @param username string containing the SIP username --- @param Used protocol, could be "UDP" or "TCP" --- @return status true on success, false on failure --- @return err on failure -local function checkAsteriskUsername(session, username, protocol) - local status, response = register(session, username, protocol) - if ( status and response:getErrorCode() == 401 ) then - return true, "SUCCESS" - end - return false, "FAILURE" -end - --- Table containing a server match and corresponding check function -local detectiontbl = { - { name="^Asterisk PBX", func=checkAsteriskUsername } + disconnect = function(self) + self.session:close() + return true + end, } action = function(host, port) - local accounts = {} - local status, usernames = unpwdb.usernames() - if ( not(status) ) then return false, "Failed to load usernames" end + local result, lthreads = {}, {} + local status, err + local minext = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".minext")) or 0 + local minext = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".minext")) or 0 + local maxext = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".maxext")) or 999 + local padding = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".padding")) or 0 + local users = stdnse.get_script_args(SCRIPT_NAME .. ".users") + local usersfile = stdnse.get_script_args(SCRIPT_NAME .. ".userslist") + or "nselib/data/usernames.lst" - local server - status, server = confirmServer( host, port ) - if ( not(status) ) then - return "ERROR: Failed to determine server version" + -- min extension should be less than max extension. + if minext > maxext then + return "ERROR: maxext should be greater or equal than minext." + end + -- If not set to zero, number of digits to pad up to should have less or + -- equal the number of digits of max extension. + if padding ~= 0 and #tostring(maxext) > padding then + return "ERROR: padding should be greater or equal to number of digits of maxext." + end + + -- We test for false positives by sending a request for a random extension + -- and checking if it did return a 404. + status, err = test404(host, port) + if not status then + return err + end + + local engine = brute.Engine:new(Driver, host, port) + engine.options.script_name = SCRIPT_NAME + + local iterator = numiterator(minext, maxext, padding) + if users then + usernames, err = useriterator(usersfile) + if not usernames then + return err end + -- Concat numbers and users iterators + iterator = unpwdb.concat_iterators(iterator, usernames) + end + engine:setPasswordIterator(iterator) + engine.options.passonly = true + status, result = engine:start() - local checkUsername - for _, item in ipairs( detectiontbl ) do - if ( server and server:match( item.name ) ) then - checkUsername = item.func - break - end - end - - if ( not(checkUsername) ) then return ("ERROR: Unsupported server (%s)"):format((server or "")) end - - for username in usernames do - local session = sip.Session:new( host, port ) - - local status = session:connect() - if ( not(status) ) then - return "ERROR: Failed to connect to the SIP server" - end - - local status, err = checkUsername( session, username, port.protocol:upper() ) - if ( status ) then table.insert( accounts, username ) end - - session:close() - end - - accounts.name = "Valid SIP accounts" - return stdnse.format_output(true, { accounts } ) - + return result end