summaryrefslogtreecommitdiffstats
path: root/nselib/creds.lua
blob: 7a3a55526db6851d195fd8b5b48cb26f94b819b9 (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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
--- The credential class stores found credentials in the Nmap registry
--
-- The credentials library may be used by scripts to store credentials in
-- a common format in the nmap registry. The Credentials class serves as
-- a primary interface for scripts to the library.
--
-- The State table keeps track of possible account states and a corresponding
-- message to return for each state.
--
-- The following code illustrates how a script may add discovered credentials
-- to the database:
-- <code>
-- local c = creds.Credentials:new( {"myapp"}, host, port )
-- c:add("patrik", "secret", creds.State.VALID )
-- </code>
--
-- The following code illustrates how a script can return a table of discovered
-- credentials at the end of execution:
-- <code>
-- return tostring(creds.Credentials:new({"myapp"}, host, port))
-- </code>
--
-- Another script can iterate over credential already discovered by other
-- scripts just by referring to the same tag:
-- <code>
-- local c = creds.Credentials:new({"myapp", "yourapp"}, host, port)
-- for cred in c:getCredentials(creds.State.VALID) do
--   showContentForUser(cred.user, cred.pass)
-- end
-- </code>
--
-- The following code illustrates how a script may iterate over all discovered
-- credentials:
-- <code>
-- local c = creds.Credentials:new(creds.ALL_DATA, host, port)
-- for cred in c:getCredentials(creds.State.VALID) do
--   showContentForUser(cred.user, cred.pass)
-- end
-- </code>
--
-- The library also enables users to add credentials through script arguments
-- either globally or per service. These credentials may be retrieved by script
-- through the same functions as any other discovered credentials. Arguments
-- passed using script arguments will be added with the PARAM state. The
-- following code may be used by a scripts to retrieve these credentials:
-- <code>
-- local c = creds.Credentials:new(creds.ALL_DATA, host, port)
-- for cred in c:getCredentials(creds.State.PARAM) do
--   ... do something ...
-- end
-- </code>
--
-- Any globally added credentials will be made available to all scripts,
-- regardless of what service is being filtered through the host and port
-- arguments when instantiating the Credentials class. Service specific
-- arguments will only be made available to scripts with ports matching
-- the service name. The following two examples illustrate how credentials are
-- added globally and for the http service:
-- <code>
-- --script-args creds.global='admin:nimda'
-- --script-args creds.http='webadmin:password'
-- </code>
--
-- The service name at this point may be anything and the entry is created
-- dynamically without validating whether the service exists or not.
--
-- The credential argument is not documented in this library using the <at>args
-- function as the argument would incorrectly show up in all scripts making use
-- of this library. This would show that credentials could be added to scripts
-- that do not make use of this function. Therefore any scripts that make use
-- of the credentials passing arguments need to have appropriate documentation
-- added to them.
--
--
-- The following code illustrates how a script may save its discovered credentials
-- to a file:
-- <code>
-- local c = creds.Credentials:new( SCRIPT_NAME, host, port )
-- c:add("patrik", "secret", creds.State.VALID )
-- status, err = c:saveToFile("outputname","csv")
-- </code>
--
-- Supported output formats are CSV, verbose and plain.  In both verbose and plain
-- records are separated by colons.  The difference between the two is that verbose
-- includes the credential state.  The file extension is automatically added to
-- the filename based on the type requested.
--
-- @args creds.global Credentials to be returned by Credentials.getCredentials
--                    regardless of the service.
-- @args creds.[service] Credentials to be returned by
--                       Credentials.getCredentials for [service]. E.g.
--                       creds.http=admin:password
--
-- @author Patrik Karlsson <patrik@cqure.net>
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html

-- Version 0.5
-- Created 2011/02/06 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 2011/27/06 - v0.2 - revised by Patrik Karlsson <patrik@cqure.net>
--                * added documentation
--                * added getCredentials function
--
-- Revised 2011/05/07 - v0.3 - revised by Patrik Karlsson <patrik@cqure.net>
--                * modified getCredentials to return an iterator
--                * added support for adding credentials as
--                  script arguments
--
-- Revised 2011/09/04 - v0.4 - revised by Tom Sellers
--                * added saveToFile function for saving credential
--                * table to file in CSV or text formats
--
-- Revised 2015/19/08 - v0.5 - Gioacchino Mazzurco <gmazzurco89@gmail.com>
--                * added multitag support to share credential easier across
--                  scripts
--

local coroutine = require "coroutine"
local io = require "io"
local ipOps = require "ipOps"
local nmap = require "nmap"
local stdnse = require "stdnse"
local stringaux = require "stringaux"
local table = require "table"
local tableaux = require "tableaux"
_ENV = stdnse.module("creds", stdnse.seeall)


--- Table mapping the different account states to their number
--
-- Also available is the <code>StateMsg</code> table, used to map these numbers
-- to a description.
-- @class table
-- @name State
-- @field LOCKED Account is locked
-- @field VALID Valid credentials
-- @field DISABLED Account is disabled
-- @field CHANGEPW Valid credentials, password must be changed at next logon
-- @field PARAM Credentials passed to script during Nmap execution
-- @field EXPIRED Valid credentials, account expired
-- @field TIME_RESTRICTED Valid credentials, account cannot log in at current time
-- @field HOST_RESTRICTED Valid credentials, account cannot log in from current host
-- @field LOCKED_VALID Valid credentials, account locked
-- @field DISABLED_VALID Valid credentials, account disabled
-- @field HASHED Hashed valid or invalid credentials
State = {
  LOCKED = 1,
  VALID = 2,
  DISABLED = 4,
  CHANGEPW = 8,
  PARAM = 16,
  EXPIRED = 32,
  TIME_RESTRICTED = 64,
  HOST_RESTRICTED = 128,
  LOCKED_VALID = 256,
  DISABLED_VALID = 512,
  HASHED = 1024,
}

StateMsg = {
  [State.LOCKED]    = 'Account is locked',
  [State.VALID]     = 'Valid credentials',
  [State.DISABLED]  = 'Account is disabled',
  [State.CHANGEPW]  = 'Valid credentials, password must be changed at next logon',
  [State.PARAM]  = 'Credentials passed to script during Nmap execution',
  [State.EXPIRED]   = 'Valid credentials, account expired',
  [State.TIME_RESTRICTED] = 'Valid credentials, account cannot log in at current time',
  [State.HOST_RESTRICTED] = 'Valid credentials, account cannot log in from current host',
  [State.LOCKED_VALID]    = 'Valid credentials, account locked',
  [State.DISABLED_VALID]  = 'Valid credentials, account disabled',
  [State.HASHED]  = 'Hashed valid or invalid credentials',
}


ALL_DATA = {}

-- The RegStorage class
RegStorage = {

  --- Creates a new RegStorage instance
  --
  -- @return a new instance
  -- @name RegStorage.new
  new = function(self)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.filter = {}
    return o
  end,

  --- Add credentials to storage
  --
  -- @param tags a table containing tags associated with the credentials
  -- @param host host table, name or ip
  -- @param port number containing the port of the service
  -- @param service the name of the service
  -- @param user the name of the user
  -- @param pass the password of the user
  -- @param state of the account
  -- @name RegStorage.add
  add = function( self, tags, host, port, service, user, pass, state )
    local cred = {
      tags = tags,
      host = host,
      port = port,
      service = service,
      user = user,
      pass = pass,
      state = state
    }
    nmap.registry.creds = nmap.registry.creds or {}
    table.insert( nmap.registry.creds, cred )
  end,

  --- Sets the storage filter
  --
  -- @param host table containing the host
  -- @param port table containing the port
  -- @param state table containing the account state
  -- @name RegStorage.setFilter
  setFilter = function( self, host, port, state )
    self.filter.host = host
    self.filter.port = port
    self.filter.state = state
  end,

  --- Returns a credential iterator matching the selected filters
  --
  -- @return a credential iterator
  -- @name RegStorage.getAll
  getAll = function( self )
    local function get_next()
      local host, port = self.filter.host, self.filter.port

      if ( not(nmap.registry.creds) ) then return end

      for _, v in pairs(nmap.registry.creds) do
        local h = ( v.host.ip or v.host )
        if ( not(host) and not(port) ) then
          if ( not(self.filter.state) or ( v.state == self.filter.state ) ) then
            coroutine.yield(v)
          end
        elseif ( not(host) and ( port == v.port ) ) then
          if ( not(self.filter.state) or ( v.state == self.filter.state ) ) then
            coroutine.yield(v)
          end
        elseif ( ( host and ( h == host or h == host.ip ) ) and not(port) ) then
          if ( not(self.filter.state) or ( v.state == self.filter.state ) ) then
            coroutine.yield(v)
          end
        elseif ( ( host and ( h == host or h == host.ip ) ) and port.number == v.port ) then
          if ( not(self.filter.state) or ( v.state == (self.filter.state & v.state) ) ) then
            coroutine.yield(v)
          end
        end
      end
    end
    return coroutine.wrap(get_next)
  end,

}

Account = {
  --- Creates a new instance of the Account class
  --
  -- @param username containing the user's name
  -- @param password containing the user's password
  -- @param state A <code>creds.State</code> account state
  -- @return A new <code>creds.Account</code> object
  -- @name Account.new
  new = function(self, username, password, state)
    local o = { username = username, password = password, state = StateMsg[state] or state }
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  --- Converts an account object to a printable script
  --
  -- @return string representation of object
  -- @name Account.__tostring
  __tostring = function( self )
    return (
      (self.username and self.username .. ":" or "") ..
      (self.password ~= "" and self.password or "<empty>") ..
      (self.state and " - " .. self.state or "")
      )
  end,

  --- Less-than operation for sorting
  --
  -- Lexicographic comparison by user, pass, and state
  -- @name Account.__lt
  __lt = function (a, b)
    if a.user and b.user and a.user >= b.user then
      return false
    elseif a.pass and b.pass and a.pass >= b.pass then
      return false
    elseif a.state and b.state and a.state >= b.state then
      return false
    end
    return true
  end,
}


-- Return a function suitable for use as a __pairs metamethod
-- which will cause the table to yield its values sorted by key.
local function sorted_pairs (sortby)
  return function (t)
    local order = tableaux.keys(t)
    table.sort(order, sortby)
    return coroutine.wrap(function()
        for i,k in ipairs(order) do
          coroutine.yield(k, t[k])
        end
      end)
  end
end

-- The credentials class
Credentials = {

  --- Creates a new instance of the Credentials class
  -- @param tags a table containing tags associated with the credentials
  -- @param host table as received by the scripts action method
  -- @param port table as received by the scripts action method
  -- @name Credentials.new
  new = function(self, tags, host, port)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.storage = RegStorage:new()
    o.storage:setFilter(host, port)
    o.host = host
    o.port = ( port and port.number ) and port.number
    o.service = ( port and port.service ) and port.service
    if ( type(tags) ~= "table" ) then tags = {tags} end
    o.tags = tags
    return o
  end,

  --- Add a discovered credential
  --
  -- @param user the name of the user
  -- @param pass the password of the user
  -- @param state of the account
  -- @name Credentials.add
  add = function( self, user, pass, state )
    local pass = ( pass and #pass > 0 ) and pass or "<empty>"
    assert( self.host, "No host supplied" )
    assert( self.port, "No port supplied" )
    assert( state, "No state supplied")
    assert( self.tags, "No tags supplied")

    -- there are cases where we will only get a user or password
    -- so as long we have one of them, we're good
    if ( user or pass ) then
      self.storage:add( self.tags, self.host, self.port, self.service, user, pass, state )
    end
  end,

  --- Returns a credential iterator
  --
  -- @see State
  -- @param state mask containing values from the <code>State</code> table
  -- @return credential iterator, returning a credential each time it's
  --         called. Unless filtered by the state mask all credentials
  --         for the host, port match are iterated over.
  --         The credential table has the following fields:
  --         <code>host</code> - table as received by the action function
  --         <code>port</code> - number containing the port number
  --         <code>user</code> - string containing the user name
  --         <code>pass</code> - string containing the user password
  --         <code>state</code> - a state number
  --         <code>service</code> - string containing the name of the service
  --         <code>tags</code> - table containing tags associated with
  --                             the credential
  -- @name Credentials.getCredentials
  getCredentials = function(self, state)
    local function next_credential()
      if ( state ) then
        self.storage:setFilter(self.host, { number=self.port, service = self.service }, state)
      end

      for cred in self.storage:getAll() do
        if ( self.tags == ALL_DATA ) then
          coroutine.yield(cred)
        end
        for _,stag in pairs(self.tags) do
          for _,ctag in pairs(cred.tags) do
            if(stag == ctag) then
              coroutine.yield(cred)
            end
          end
        end
      end

      if ( state and State.PARAM == (state & State.PARAM) ) then
        local creds_global = stdnse.get_script_args('creds.global')
        local creds_service
        local creds_params

        if ( self.service ) then
          creds_service = stdnse.get_script_args('creds.' .. self.service )
        end

        if ( creds_service ) then creds_params = creds_service end
        if ( creds_global and creds_service ) then
          creds_params = creds_params .. ',' .. creds_global
        elseif ( creds_global ) then
          creds_params = creds_global
        end

        if ( not(creds_params) ) then return end

        for _, cred in ipairs(stringaux.strsplit(",", creds_params)) do
          -- if the credential contains a ':' we have a user + pass pair
          -- if not, we only have a user with an empty password
          local user, pass
          if ( cred:match(":") ) then
            user, pass = cred:match("^(.-):(.-)$")
          else
            user = cred:match("^(.*)$")
          end
          coroutine.yield( { host = self.host,
          port = self.port,
          user = user,
          pass = pass,
          state = State.PARAM,
          service = self.service } )
        end
      end
    end
    return coroutine.wrap( next_credential )
  end,

  --- Returns a table of credentials
  --
  -- @return tbl table containing the discovered credentials
  -- @name Credentials.getTable
  getTable = function(self)
    local result = {}

    for v in self:getCredentials() do
      local h = ( v.host.ip or v.host )
      assert(type(h)=="string", "Could not determine a valid host")
      local svc = ("%s/%s"):format(v.port,v.service)

      result[h] = result[h] or {}
      result[h][svc] = result[h][svc] or {}
      table.insert( result[h][svc], Account:new(
          v.user ~= "" and v.user or nil,
          v.pass,
          v.state
          )
        )
    end

    for _, host_tbl in pairs(result) do
      for _, svc_tbl in pairs(host_tbl) do
        -- sort the accounts
        table.sort( svc_tbl )
      end
      -- sort the services
      setmetatable(host_tbl, {
          __pairs = sorted_pairs( function(a,b)
              return tonumber(a:match("^(%d+)")) < tonumber(b:match("^(%d+)"))
            end )
        })
    end

    -- sort the IP addresses
    setmetatable(result, {
        __pairs = sorted_pairs( function(a, b)
          return ipOps.compare_ip(a, "le", b)
        end )
      })

    local _
    if ( self.host and next(result) ) then
      _, result = next(result)
    end
    if ( self.host and self.port and next(result) ) then
      _, result = next(result)
    end
    return next(result) and result
  end,

  -- Saves credentials in the current object to file
  -- @param filename string name of the file
  -- @param fileformat string file format type, values = csv | verbose | plain (default)
  -- @return status true on success, false on failure
  -- @return err string containing the error if status is false
  saveToFile = function(self, filename, fileformat)

    if ( fileformat == 'csv' ) then
      filename = filename .. '.csv'
    else
      filename = filename .. '.txt'
    end

    local f = io.open( filename, "w")
    local output = nil

    if ( not(f) ) then
      return false, ("ERROR: Failed to open file (%s)"):format(filename)
    end

    for account in self:getCredentials() do
      if ( fileformat == 'csv' ) then
        output = "\"" .. account.user .. "\",\"" .. account.pass .. "\",\"" .. StateMsg[account.state] .. "\""
      elseif ( fileformat == 'verbose') then
        output = account.user .. ":" .. account.pass .. ":" .. StateMsg[account.state]
      else
        output = account.user .. ":" .. account.pass
      end
      if ( not(f:write( output .."\n" ) ) ) then
        return false, ("ERROR: Failed to write file (%s)"):format(filename)
      end
    end

    f:close()
    return true
  end,

  --- Get credentials with optional host and port filter
  -- If no filters are supplied all records are returned
  --
  -- @param host table or string containing the host to filter
  -- @param port number containing the port to filter
  -- @return table suitable from <code>stdnse.format_output</code>
  -- @name Credentials.__tostring
  __tostring = function(self)
    local all = self:getTable()
    if ( all ) then return tostring(all) end
  end,

}

return _ENV;