---
-- Username/password database library.
--
-- The usernames
and passwords
functions return
-- multiple values for use with exception handling via
-- nmap.new_try
. The first value is the Boolean success
-- indicator, the second value is the closure.
--
-- The closures can take an argument of "reset"
to rewind the list
-- to the beginning.
--
-- To avoid taking a long time against slow services, the closures will
-- stop returning values (start returning nil
) after a
-- certain time. The time depends on the timing template level, and is
-- * -T3
or less: 10 minutes
-- * -T4
: 5 minutes
-- * -T5
: 3 minutes
-- Time limits are increased by 50% if a custom username or password
-- database is used with the userdb
or passdb
-- script arguments. You can control the time limit directly with the
-- unpwdb.timelimit
script argument. Use
-- unpwdb.timelimit=0
to disable the time limit.
--
-- You can select your own username and/or password database to read from with
-- the script arguments userdb
and passdb
,
-- respectively. Comments are allowed in these files, prefixed with
-- "#!comment:"
. Comments cannot be on the same line as a
-- username or password because this leaves too much ambiguity, e.g. does the
-- password in "mypass #!comment: blah"
contain a space, two
-- spaces, or do they just separate the password from the comment?
--
-- @usage
-- require("unpwdb")
--
-- local usernames, passwords
-- local try = nmap.new_try()
--
-- usernames = try(unpwdb.usernames())
-- passwords = try(unpwdb.passwords())
--
-- for password in passwords do
-- for username in usernames do
-- -- Do something with username and password.
-- end
-- usernames("reset")
-- end
--
-- @usage
-- nmap --script-args userdb=/tmp/user.lst
-- nmap --script-args unpwdb.timelimit=10m
--
-- @args userdb The filename of an alternate username database. Default: nselib/data/usernames.lst
-- @args passdb The filename of an alternate password database. Default: nselib/data/passwords.lst
-- @args unpwdb.userlimit The maximum number of usernames
-- usernames
will return (default unlimited).
-- @args unpwdb.passlimit The maximum number of passwords
-- passwords
will return (default unlimited).
-- @args unpwdb.timelimit The maximum amount of time that any iterator will run
-- before stopping. The value is in seconds by default and you can follow it
-- with ms
, s
, m
, or h
for
-- milliseconds, seconds, minutes, or hours. For example,
-- unpwdb.timelimit=30m
or unpwdb.timelimit=.5h
for
-- 30 minutes. The default depends on the timing template level (see the module
-- description). Use the value 0
to disable the time limit.
-- @author Kris Katterjohn 06/2008
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
local io = require "io"
local nmap = require "nmap"
local os = require "os"
local stdnse = require "stdnse"
local datetime = require "datetime"
_ENV = stdnse.module("unpwdb", stdnse.seeall)
local usertable = {}
local passtable = {}
local customdata = false
-- So I don't have to type as much :)
local args = nmap.registry.args
local userfile = function()
if args.userdb then
customdata = true
return args.userdb
end
return nmap.fetchfile("nselib/data/usernames.lst")
end
local passfile = function()
if args.passdb then
customdata = true
return args.passdb
end
return nmap.fetchfile("nselib/data/passwords.lst")
end
local filltable = function(filename, table)
if #table ~= 0 then
return true
end
local file, err = io.open(filename, "r")
if not file then
return false, err
end
for l in file:lines() do
-- Comments takes up a whole line
if not l:match("#!comment:") then
table[#table + 1] = l
end
end
file:close()
return true
end
table_iterator = function(table)
local i = 1
return function(cmd)
if cmd == "reset" then
i = 1
return
end
local elem = table[i]
if elem then i = i + 1 end
return elem
end
end
--- Returns the suggested number of seconds to attempt a brute force attack
--
-- Based on the unpwdb.timelimit
script argument, Nmap's timing
-- values (-T4
etc.) and whether or not a user-defined list is
-- used.
--
-- You can use the script argument notimelimit
to make this
-- function return nil
, which means the brute-force should run
-- until the list is empty. If notimelimit
is not used, be sure to
-- still check for nil
return values on the above two functions in
-- case you finish before the time limit is up.
timelimit = function()
-- If we're reading from a user-defined username or password list,
-- we'll give them a timeout 1.5x the default. If the "notimelimit"
-- script argument is used, we return nil.
-- Easy enough
if args.notimelimit then
return nil
end
if args["unpwdb.timelimit"] then
local limit, err = stdnse.parse_timespec(args["unpwdb.timelimit"])
if not limit then
error(err)
end
return limit
end
local t = nmap.timing_level()
if t <= 3 then
return (customdata and 900) or 600
elseif t == 4 then
return (customdata and 450) or 300
elseif t == 5 then
return (customdata and 270) or 180
end
end
--- Returns a function closure which returns a new username with every call
-- until the username list is exhausted (in which case it returns
-- nil
).
-- @return boolean Status.
-- @return function The usernames iterator.
local usernames_raw = function()
local path = userfile()
if not path then
return false, "Cannot find username list"
end
local status, err = filltable(path, usertable)
if not status then
return false, ("Error parsing username list: %s"):format(err)
end
return true, table_iterator(usertable)
end
--- Returns a function closure which returns a new password with every call
-- until the password list is exhausted (in which case it returns
-- nil
).
-- @return boolean Status.
-- @return function The passwords iterator.
local passwords_raw = function()
local path = passfile()
if not path then
return false, "Cannot find password list"
end
local status, err = filltable(path, passtable)
if not status then
return false, ("Error parsing password list: %s"):format(err)
end
return true, table_iterator(passtable)
end
--- Wraps time and count limits around an iterator.
--
-- When either limit expires, starts returning nil
. Calling the
-- iterator with an argument of "reset" resets the count.
-- @param time_limit Time limit in seconds. Use 0 or nil
for no limit.
-- @param count_limit Count limit in seconds. Use 0 or nil
for no limit.
-- @param label A string describing the iterator, to be used in verbose print messages.
-- @return boolean Status.
-- @return function The wrapped iterator.
limited_iterator = function(iterator, time_limit, count_limit, label)
time_limit = (time_limit and time_limit > 0) and time_limit
count_limit = (count_limit and count_limit > 0) and count_limit
local start = os.time()
local count = 0
label = label or "limited_iterator"
return function(cmd)
if cmd == "reset" then
count = 0
else
count = count + 1
end
if count_limit and count > count_limit then
stdnse.verbose1("%s: Count limit %d exceeded.", label, count_limit)
return
end
if time_limit and os.time() - start >= time_limit then
stdnse.verbose1("%s: Time limit %s exceeded.", label, datetime.format_time(time_limit))
return
end
return iterator(cmd)
end
end
--- Returns a function closure which returns a new password with every call
-- until the username list is exhausted or either limit expires (in which cases
-- it returns nil
).
-- @param time_limit Time limit in seconds. Use 0 for no limit.
-- @param count_limit Count limit in seconds. Use 0 for no limit.
-- @return boolean Status.
-- @return function The usernames iterator.
usernames = function(time_limit, count_limit)
local status, iterator
status, iterator = usernames_raw()
if not status then
return false, iterator
end
time_limit = time_limit or timelimit()
if not count_limit and args["unpwdb.userlimit"] then
count_limit = tonumber(args["unpwdb.userlimit"])
end
return true, limited_iterator(iterator, time_limit, count_limit, "usernames")
end
--- Returns a function closure which returns a new password with every call
-- until the password list is exhausted or either limit expires (in which cases
-- it returns nil
).
-- @param time_limit Time limit in seconds. Use 0 for no limit.
-- @param count_limit Count limit in seconds. Use 0 for no limit.
-- @return boolean Status.
-- @return function The passwords iterator.
passwords = function(time_limit, count_limit)
local status, iterator
status, iterator = passwords_raw()
if not status then
return false, iterator
end
time_limit = time_limit or timelimit()
if not count_limit and args["unpwdb.passlimit"] then
count_limit = tonumber(args["unpwdb.passlimit"])
end
return true, limited_iterator(iterator, time_limit, count_limit, "passwords")
end
--- Returns a new iterator that iterates through its consecutive iterators,
-- basically concatenating them.
-- @param iter1 First iterator to concatenate.
-- @param iter2 Second iterator to concatenate.
-- @return function The concatenated iterators.
function concat_iterators (iter1, iter2)
local function helper (next_iterator, command, first, ...)
if first ~= nil then
return first, ...
elseif next_iterator ~= nil then
return helper(nil, command, next_iterator(command))
end
end
local function iterator (command)
if command == "reset" then
iter1 "reset"
iter2 "reset"
else
return helper(iter2, command, iter1(command))
end
end
return iterator
end
--- Returns a new iterator that filters its results based on the filter.
-- @param iterator Iterator that needs to be filtered
-- @param filter Function that returns bool, which serves as a filter
-- @return function The filtered iterator.
function filter_iterator (iterator, filter)
return function (command)
if command == "reset" then
iterator "reset"
else
local val = iterator(command)
while val and not filter(val) do
val = iterator(command)
end
return val
end
end
end
return _ENV;