summaryrefslogtreecommitdiffstats
path: root/scripts/sip-enum-users.nse
blob: 04faad9fcb4df3c00c7877a433425100032e8601 (plain)
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