1local io = require "io" 2local nmap = require "nmap" 3local string = require "string" 4local shortport = require "shortport" 5local sip = require "sip" 6local stdnse = require "stdnse" 7local math = require "math" 8local brute = require "brute" 9local creds = require "creds" 10local unpwdb = require "unpwdb" 11 12description = [[ 13Enumerates a SIP server's valid extensions (users). 14 15The script works by sending REGISTER SIP requests to the server with the 16specified extension and checking for the response status code in order 17to know if an extension is valid. If a response status code is 401 or 18407, it means that the extension is valid and requires authentication. If the 19response status code is 200, it means that the extension exists and doesn't 20require any authentication while a 403 response status code means that 21extension exists but access is forbidden. To skip false positives, the script 22begins by sending a REGISTER request for a random extension and checking for 23response status code. 24]] 25 26--- 27--@args sip-enum-users.minext Extension value to start enumeration from. 28-- Defaults to <code>0</code>. 29-- 30--@args sip-enum-users.maxext Extension value to end enumeration at. 31-- Defaults to <code>999</code>. 32-- 33--@args sip-enum-users.padding Number of digits to pad zeroes up to. 34-- Defaults to <code>0</code>. No padding if this is set to zero. 35-- 36--@args sip-enum-users.users If set, will also enumerate users 37-- from <code>userslist</code> file. 38-- 39--@args sip-enum-users.userslist Path to list of users. 40-- Defaults to <code>nselib/data/usernames.lst</code>. 41-- 42--@usage 43-- nmap --script=sip-enum-users -sU -p 5060 <targets> 44-- 45-- nmap --script=sip-enum-users -sU -p 5060 <targets> --script-args 46-- 'sip-enum-users.padding=4, sip-enum-users.minext=1000, 47-- sip-enum-users.maxext=9999' 48-- 49--@output 50-- 5060/udp open sip 51-- | sip-enum-users: 52-- | Accounts 53-- | 101: Auth required 54-- | 120: No auth 55-- | Statistics 56-- |_ Performed 1000 guesses in 50 seconds, average tps: 20 57 58 59author = "Hani Benhabiles" 60 61license = "Same as Nmap--See https://nmap.org/book/man-legal.html" 62 63categories = {"auth", "intrusive"} 64 65 66portrule = shortport.port_or_service(5060, "sip", {"tcp", "udp"}) 67 68--- Function that sends register sip request with provided extension 69-- using the specified session. 70-- @arg sess session to use. 71-- @arg ext Extension to send register request to. 72-- @return status true on success, false on failure. 73-- @return Response instance on success, error string on failure. 74local registerext = function(sess, ext) 75 -- set session values 76 local request = sip.Request:new(sip.Method.REGISTER) 77 78 request:setUri("sip:" .. sess.sessdata:getServer()) 79 sess.sessdata:setUsername(ext) 80 request:setSessionData(sess.sessdata) 81 82 return sess:exch(request) 83end 84 85--- Function that returns a number as string with a number of zeroes padded to 86-- the left. 87-- @arg num Number to be padded. 88-- @arg padding number of digits to pad up to. 89-- @return string of padded number. 90local padnum = function(num, padding) 91 -- How many zeroes do we need to add 92 local n = #tostring(num) 93 if n >= padding then 94 return tostring(num) 95 end 96 n = padding - n 97 98 return string.rep(tostring(0), n) .. tostring(num) 99end 100 101--- Iterator function that returns values from a lower value up to a greater 102-- value with zeroes padded up to padding argument. 103-- @arg minval Start value. 104-- @arg maxval End value. 105-- @arg padding number of digits to pad up to. 106-- @return string current value. 107local numiterator = function(minval, maxval, padding) 108 local i = minval - 1 109 return function() 110 i = i + 1 111 if i <= maxval then return padnum(i, padding), '' end 112 end 113end 114 115--- Iterator function that returns lines from a file 116-- @arg userslist Path to file list in data location. 117-- @return status false if error. 118-- @return string current line. 119local useriterator = function(list) 120 local f = nmap.fetchfile(list) or list 121 if not f then 122 return false, ("Couldn't find %s"):format(list) 123 end 124 f = io.open(f) 125 if ( not(f) ) then 126 return false, ("Failed to open %s"):format(list) 127 end 128 return function() 129 for line in f:lines() do 130 return line 131 end 132 end 133end 134 135--- function that tests for 404 status code when sending a REGISTER request 136-- with a random sip extension. 137-- @arg host Target host table. 138-- @arg port Target port table. 139local test404 = function(host, port) 140 local session, status, randext, response 141 -- Random extension 142 randext = math.random(1234567,987654321) 143 144 session = sip.Session:new(host, port) 145 status = session:connect() 146 if not status then 147 return false, "Failed to connect to the SIP server." 148 end 149 150 status, response = registerext(session, randext) 151 if not status then 152 return false, "No response from the SIP server." 153 end 154 if response:getErrorCode() ~= 404 then 155 return false, "Server not returning 404 for random extension." 156 end 157 return true 158 159end 160 161Driver = { 162 163 new = function(self, host, port) 164 local o = {} 165 setmetatable(o, self) 166 self.__index = self 167 o.host = host 168 o.port = port 169 return o 170 end, 171 172 connect = function( self ) 173 self.session = sip.Session:new(self.host, self.port) 174 local status = self.session:connect() 175 if ( not(status) ) then 176 return false, brute.Error:new( "Couldn't connect to host" ) 177 end 178 return true 179 end, 180 181 login = function( self, username, password) 182 -- We are using the "password" values instead of the "username" so we 183 -- could benefit from brute.lua passonly option and setPasswordIterator 184 -- function, as we are doing usernames enumeration only and not 185 -- credentials brute forcing. 186 local status, response, responsecode 187 -- Send REGISTER request for each extension 188 status, response = registerext(self.session, password) 189 if status then 190 responsecode = response:getErrorCode() 191 -- If response status code is 401 or 407, then extension exists but 192 -- requires authentication 193 if responsecode == sip.Error.UNAUTHORIZED or 194 responsecode == sip.Error.PROXY_AUTH_REQUIRED then 195 return true, creds.Account:new(password, " Auth required", '') 196 197 -- If response status code is 200, then extension exists 198 -- and requires no authentication 199 elseif responsecode == sip.Error.OK then 200 return true, creds.Account:new(password, " No auth", '') 201 -- If response status code is 200, then extension exists 202 -- but access is forbidden. 203 204 elseif responsecode == sip.Error.FORBIDDEN then 205 return true, creds.Account:new(password, " Forbidden", '') 206 end 207 return false,brute.Error:new( "Not found" ) 208 else 209 return false,brute.Error:new( "No response" ) 210 end 211 end, 212 213 disconnect = function(self) 214 self.session:close() 215 return true 216 end, 217} 218 219local function fail (err) return stdnse.format_output(false, err) end 220 221action = function(host, port) 222 local result, lthreads = {}, {} 223 local status, err 224 local minext = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".minext")) or 0 225 local minext = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".minext")) or 0 226 local maxext = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".maxext")) or 999 227 local padding = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".padding")) or 0 228 local users = stdnse.get_script_args(SCRIPT_NAME .. ".users") 229 local usersfile = stdnse.get_script_args(SCRIPT_NAME .. ".userslist") 230 or "nselib/data/usernames.lst" 231 232 -- min extension should be less than max extension. 233 if minext > maxext then 234 return fail("maxext should be greater or equal than minext.") 235 end 236 -- If not set to zero, number of digits to pad up to should have less or 237 -- equal the number of digits of max extension. 238 if padding ~= 0 and #tostring(maxext) > padding then 239 return fail("padding should be greater or equal to number of digits of maxext.") 240 end 241 242 -- We test for false positives by sending a request for a random extension 243 -- and checking if it did return a 404. 244 status, err = test404(host, port) 245 if not status then 246 return fail(err) 247 end 248 249 local engine = brute.Engine:new(Driver, host, port) 250 engine.options.script_name = SCRIPT_NAME 251 252 local iterator = numiterator(minext, maxext, padding) 253 if users then 254 local usernames, err = useriterator(usersfile) 255 if not usernames then 256 return fail(err) 257 end 258 -- Concat numbers and users iterators 259 iterator = unpwdb.concat_iterators(iterator, usernames) 260 end 261 engine:setPasswordIterator(iterator) 262 engine.options.passonly = true 263 status, result = engine:start() 264 265 return result 266end 267