summaryrefslogtreecommitdiffstats
path: root/nselib/creds.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/creds.lua')
-rw-r--r--nselib/creds.lua541
1 files changed, 541 insertions, 0 deletions
diff --git a/nselib/creds.lua b/nselib/creds.lua
new file mode 100644
index 0000000..7a3a555
--- /dev/null
+++ b/nselib/creds.lua
@@ -0,0 +1,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;