1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
|
local io = require "io"
local nmap = require "nmap"
local string = require "string"
local shortport = require "shortport"
local sip = require "sip"
local stdnse = require "stdnse"
local math = require "math"
local brute = require "brute"
local creds = require "creds"
local unpwdb = require "unpwdb"
description = [[
Enumerates a SIP server's valid extensions (users).
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.
]]
---
--@args sip-enum-users.minext Extension value to start enumeration from.
-- Defaults to <code>0</code>.
--
--@args sip-enum-users.maxext Extension value to end enumeration at.
-- 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:
-- | Accounts
-- | 101: Auth required
-- | 120: No auth
-- | Statistics
-- |_ Performed 1000 guesses in 50 seconds, average tps: 20
author = "Hani Benhabiles"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"auth", "intrusive"}
portrule = shortport.port_or_service(5060, "sip", {"tcp", "udp"})
--- 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)
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
return string.rep(tostring(0), n) .. tostring(num)
end
--- 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) or list
if not f then
return false, ("Couldn't find %s"):format(list)
end
f = io.open(f)
if ( not(f) ) then
return false, ("Failed to open %s"):format(list)
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, "Failed to connect to the SIP server."
end
status, response = registerext(session, randext)
if not status then
return false, "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 false, brute.Error:new( "Couldn't connect to host" )
end
return true
end,
login = function( self, username, password)
-- We are using the "password" values instead of the "username" so we
-- could benefit 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, creds.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, creds.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, creds.Account:new(password, " Forbidden", '')
end
return false,brute.Error:new( "Not found" )
else
return false,brute.Error:new( "No response" )
end
end,
disconnect = function(self)
self.session:close()
return true
end,
}
local function fail (err) return stdnse.format_output(false, err) end
action = function(host, port)
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"
-- min extension should be less than max extension.
if minext > maxext then
return fail("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 fail("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 fail(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
local usernames, err = useriterator(usersfile)
if not usernames then
return fail(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()
return result
end
|