mirror of
https://github.com/nmap/nmap.git
synced 2025-12-07 13:11:28 +00:00
Changed sip-enum-users which now uses brute.lua for extensions enumeration and supports iteration over custom lists and numeric ranges.
This commit is contained in:
@@ -1,4 +1,9 @@
|
|||||||
# Nmap Changelog ($Id$); -*-text-*-
|
# 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),
|
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]
|
||||||
|
|
||||||
|
|||||||
@@ -2,143 +2,261 @@ local shortport = require "shortport"
|
|||||||
local sip = require "sip"
|
local sip = require "sip"
|
||||||
local stdnse = require "stdnse"
|
local stdnse = require "stdnse"
|
||||||
local table = require "table"
|
local table = require "table"
|
||||||
|
local math = require "math"
|
||||||
|
local brute = require "brute"
|
||||||
|
local creds = require "creds"
|
||||||
local unpwdb = require "unpwdb"
|
local unpwdb = require "unpwdb"
|
||||||
|
|
||||||
description = [[
|
description = [[
|
||||||
Attempts to enumerate valid user account using SIP (Session Initiation
|
Enumerates a SIP server's valid extensions (users).
|
||||||
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.
|
|
||||||
|
|
||||||
* Asterisk
|
The script works by sending REGISTER SIP requests to the server with the
|
||||||
- The script enumerates valid accounts by checking the SIP servers response
|
specified extension and checking for the response status code in order
|
||||||
to the REGISTER request. If TRYING is returned, the account does not
|
to know if an extension is valid. If a response status code is 401 or
|
||||||
exist. If REGISTER is returned the account is valid.
|
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
|
--@args sip-enum-users.minext Extension value to start enumeration from.
|
||||||
-- nmap -sU -p 5060 <target> --script=sip-enum-users
|
-- Defaults to <code>0</code>.
|
||||||
--
|
--
|
||||||
-- PORT STATE SERVICE
|
--@args sip-enum-users.maxext Extension value to end enumeration at.
|
||||||
-- 5060/udp open|filtered sip
|
-- Defaults to <code>999</code>.
|
||||||
|
--
|
||||||
|
--@args sip-enum-users.padding Number of digits to pad zeroes up to.
|
||||||
|
-- Defaults to <code>0</code>. No padding if this is set to zero.
|
||||||
|
--
|
||||||
|
--@args sip-enum-users.users If set, will also enumerate users
|
||||||
|
-- from <code>userslist</code> file.
|
||||||
|
--
|
||||||
|
--@args sip-enum-users.userslist Path to list of users.
|
||||||
|
-- Defaults to <code>nselib/data/usernames.lst</code>.
|
||||||
|
--
|
||||||
|
--@usage
|
||||||
|
-- nmap --script=sip-enum-users -sU -p 5060 <targets>
|
||||||
|
--
|
||||||
|
-- nmap --script=sip-enum-users -sU -p 5060 <targets> --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:
|
-- | sip-enum-users:
|
||||||
-- | Valid SIP accounts
|
-- | Accounts
|
||||||
-- | 1000
|
-- | 101: Auth required
|
||||||
-- |_ 1001
|
-- | 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 <patrik@cqure.net>
|
|
||||||
|
|
||||||
author = "Patrik Karlsson"
|
author = "Hani Benhabiles"
|
||||||
|
|
||||||
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
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"})
|
portrule = shortport.port_or_service(5060, "sip", {"tcp", "udp"})
|
||||||
|
|
||||||
-- Send a register request to the server and returned the unparsed response
|
--- Function that sends register sip request with provided extension
|
||||||
-- @param session instance of Session class
|
-- using the specified session.
|
||||||
-- @param username string containing the name of the user
|
-- @arg sess session to use.
|
||||||
-- @param Used protocol, could be "UDP" or "TCP"
|
-- @arg ext Extension to send register request to.
|
||||||
-- @return status true on success false on failure
|
-- @return status true on success, false on failure.
|
||||||
-- @return response instance of sip.Response (on success)
|
-- @return Response instance on success, error string on failure.
|
||||||
-- @return err string containing the error message (on failure)
|
local registerext = function(sess, ext)
|
||||||
local function register(session, username, protocol)
|
-- set session values
|
||||||
local request = sip.Request:new(sip.Method.REGISTER, protocol)
|
local request = sip.Request:new(sip.Method.REGISTER)
|
||||||
|
|
||||||
session.sessdata:setUsername(username)
|
request:setUri("sip:" .. sess.sessdata:getServer())
|
||||||
request:setUri("sip:" .. session.sessdata:getServer() )
|
sess.sessdata:setUsername(ext)
|
||||||
request:setSessionData(session.sessdata)
|
request:setSessionData(sess.sessdata)
|
||||||
|
|
||||||
local status, response = session:exch(request)
|
return sess:exch(request)
|
||||||
if (not(status)) then return false, response end
|
|
||||||
|
|
||||||
return true, response
|
|
||||||
end
|
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
|
return string.rep(tostring(0), n) .. tostring(num)
|
||||||
-- @param host table as passed to the action method
|
end
|
||||||
-- @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 )
|
|
||||||
|
|
||||||
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
|
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
|
end
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
|
||||||
local response
|
login = function( self, username, password)
|
||||||
status, response = register(session, user, port.protocol:upper())
|
-- We are using the "password" values instead of the "username" so we
|
||||||
if ( status ) then
|
-- could benifit from brute.lua passonly option and setPasswordIterator
|
||||||
return true, (
|
-- function, as we are doing usernames enumeration only and not
|
||||||
response:getHeader("User-Agent") or
|
-- credentials brute forcing.
|
||||||
response:getHeader("Server")
|
local status, response, responsecode
|
||||||
)
|
-- Send REGISTER request for each extension
|
||||||
end
|
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", '')
|
||||||
|
|
||||||
return false
|
-- If response status code is 200, then extension exists
|
||||||
end
|
-- 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.
|
||||||
|
|
||||||
-- Asterisk specific function used to check for valid usernames
|
elseif responsecode == sip.Error.FORBIDDEN then
|
||||||
-- @param session instance of SIP Session
|
return true, brute.Account:new(password, " Forbidden", '')
|
||||||
-- @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
|
end
|
||||||
return false, "FAILURE"
|
return false,brute.Error:new( "Not found" )
|
||||||
|
else
|
||||||
|
return false,brute.Error:new( "No response" )
|
||||||
end
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
-- Table containing a server match and corresponding check function
|
disconnect = function(self)
|
||||||
local detectiontbl = {
|
self.session:close()
|
||||||
{ name="^Asterisk PBX", func=checkAsteriskUsername }
|
return true
|
||||||
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
action = function(host, port)
|
action = function(host, port)
|
||||||
local accounts = {}
|
local result, lthreads = {}, {}
|
||||||
local status, usernames = unpwdb.usernames()
|
local status, err
|
||||||
if ( not(status) ) then return false, "Failed to load usernames" end
|
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
|
-- min extension should be less than max extension.
|
||||||
status, server = confirmServer( host, port )
|
if minext > maxext then
|
||||||
if ( not(status) ) then
|
return "ERROR: maxext should be greater or equal than minext."
|
||||||
return "ERROR: Failed to determine server version"
|
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
|
end
|
||||||
|
|
||||||
local checkUsername
|
-- We test for false positives by sending a request for a random extension
|
||||||
for _, item in ipairs( detectiontbl ) do
|
-- and checking if it did return a 404.
|
||||||
if ( server and server:match( item.name ) ) then
|
status, err = test404(host, port)
|
||||||
checkUsername = item.func
|
if not status then
|
||||||
break
|
return err
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if ( not(checkUsername) ) then return ("ERROR: Unsupported server (%s)"):format((server or "")) end
|
local engine = brute.Engine:new(Driver, host, port)
|
||||||
|
engine.options.script_name = SCRIPT_NAME
|
||||||
|
|
||||||
for username in usernames do
|
local iterator = numiterator(minext, maxext, padding)
|
||||||
local session = sip.Session:new( host, port )
|
if users then
|
||||||
|
usernames, err = useriterator(usersfile)
|
||||||
local status = session:connect()
|
if not usernames then
|
||||||
if ( not(status) ) then
|
return err
|
||||||
return "ERROR: Failed to connect to the SIP server"
|
|
||||||
end
|
end
|
||||||
|
-- Concat numbers and users iterators
|
||||||
local status, err = checkUsername( session, username, port.protocol:upper() )
|
iterator = unpwdb.concat_iterators(iterator, usernames)
|
||||||
if ( status ) then table.insert( accounts, username ) end
|
|
||||||
|
|
||||||
session:close()
|
|
||||||
end
|
end
|
||||||
|
engine:setPasswordIterator(iterator)
|
||||||
|
engine.options.passonly = true
|
||||||
|
status, result = engine:start()
|
||||||
|
|
||||||
accounts.name = "Valid SIP accounts"
|
return result
|
||||||
return stdnse.format_output(true, { accounts } )
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user