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