mirror of
https://github.com/nmap/nmap.git
synced 2025-12-06 04:31:29 +00:00
701 lines
19 KiB
Lua
701 lines
19 KiB
Lua
local comm = require "comm"
|
|
local coroutine = require "coroutine"
|
|
local nmap = require "nmap"
|
|
local re = require "re"
|
|
local U = require "lpeg.utility"
|
|
local shortport = require "shortport"
|
|
local stdnse = require "stdnse"
|
|
local strbuf = require "strbuf"
|
|
local string = require "string"
|
|
local brute = require "brute"
|
|
|
|
local P = lpeg.P;
|
|
local R = lpeg.R;
|
|
local S = lpeg.S;
|
|
local V = lpeg.V;
|
|
local C = lpeg.C;
|
|
local Cb = lpeg.Cb;
|
|
local Cc = lpeg.Cc;
|
|
local Cf = lpeg.Cf;
|
|
local Cg = lpeg.Cg;
|
|
local Ct = lpeg.Ct;
|
|
|
|
description = [[
|
|
Performs brute-force password auditing against telnet servers.
|
|
]]
|
|
|
|
---
|
|
-- @usage
|
|
-- nmap -p 23 --script telnet-brute \
|
|
-- --script-args userdb=myusers.lst,passdb=mypwds.lst \
|
|
-- --script-args telnet-brute.timeout=8s \
|
|
-- <target>
|
|
--
|
|
-- @output
|
|
-- 23/tcp open telnet
|
|
-- | telnet-brute:
|
|
-- | Accounts
|
|
-- | wkurtz:colonel
|
|
-- | Statistics
|
|
-- |_ Performed 15 guesses in 19 seconds, average tps: 0
|
|
--
|
|
-- @args telnet-brute.timeout Connection time-out timespec (default: "5s")
|
|
-- @args telnet-brute.autosize Whether to automatically reduce the thread
|
|
-- count based on the behavior of the target
|
|
-- (default: "true")
|
|
|
|
author = "nnposter, Patrick Donnelly"
|
|
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
|
categories = {'brute', 'intrusive'}
|
|
|
|
portrule = shortport.port_or_service(23, 'telnet')
|
|
|
|
|
|
-- Miscellaneous script-wide parameters and constants
|
|
local arg_timeout = stdnse.get_script_args(SCRIPT_NAME .. ".timeout") or "5s"
|
|
local arg_autosize = stdnse.get_script_args(SCRIPT_NAME .. ".autosize") or "true"
|
|
|
|
local telnet_timeout -- connection timeout (in ms), from arg_timeout
|
|
local telnet_autosize -- whether to auto-size the execution, from arg_autosize
|
|
local telnet_eol = "\r\n" -- termination string for sent lines
|
|
local conn_retries = 2 -- # of retries when attempting to connect
|
|
local critical_debug = 1 -- debug level for printing critical messages
|
|
local login_debug = 2 -- debug level for printing attempted credentials
|
|
local detail_debug = 3 -- debug level for printing individual login steps
|
|
-- and thread-level info
|
|
|
|
---
|
|
-- Print debug messages, prepending them with the script name
|
|
--
|
|
-- @param level Verbosity level (mandatory, unlike stdnse.print_debug).
|
|
-- @param fmt Format string.
|
|
-- @param ... Arguments to format.
|
|
local print_debug = function (level, fmt, ...)
|
|
stdnse.print_debug(level, "%s: " .. fmt, SCRIPT_NAME, ...)
|
|
end
|
|
|
|
local patt_login = U.atwordboundary(re.compile [[([uU][sS][eE][rR][nN][aA][mM][eE] / [lL][oO][gG][iI][nN]) %s* ':' %s* !.]])
|
|
|
|
local patt_password = U.atwordboundary(re.compile [[[pP][aA][sS][sS] ([wW][oO][rR][dD] / [cC][oO][dD][eE]) %s* ':' %s* !.]])
|
|
|
|
local patt_login_success = re.compile([[
|
|
prompt <- [/>%$#] \ -- general prompt
|
|
[lL][aA][sS][tT] %s+ [lL][oO][gG][iI][nN] %s* ':' \ -- linux telnetd
|
|
[A-Z] ':\\' \ -- Windows telnet
|
|
'Main' (%s \ %ESC '[' %d+ ';' %d+ 'H') 'Menu' \ -- Netgear RM356
|
|
[mM][aA][iI][nN] (%s \ '\x1B' ) [mM][eE][nN][uU] ! %a \ -- Netgear RM356
|
|
[eE][nN][tT][eE][rR] %s+ [tT][eE][rR][mM][iI][nN][aA][lL] %s+ [eE][mM][uU][lL][aA][tT][iI][oO][nN] %s* ':' -- Hummingbird telnetd
|
|
]], {ESC = "\x1B"})
|
|
|
|
local patt_login_failure = U.atwordboundary(U.caseless "incorrect" + U.caseless "failed" + U.caseless "denied" + U.caseless "invalid" + U.caseless "bad")
|
|
|
|
---
|
|
-- Decide whether a given string (presumably received from a telnet server)
|
|
-- represents a username prompt
|
|
--
|
|
-- @param str The string to analyze
|
|
-- @return Verdict (true or false)
|
|
local is_username_prompt = function (str)
|
|
return not not login_patt:match(str)
|
|
end
|
|
|
|
|
|
---
|
|
-- Decide whether a given string (presumably received from a telnet server)
|
|
-- represents a password prompt
|
|
--
|
|
-- @param str The string to analyze
|
|
-- @return Verdict (true or false)
|
|
local is_password_prompt = function (str)
|
|
return not not password_patt:match(str)
|
|
end
|
|
|
|
|
|
---
|
|
-- Decide whether a given string (presumably received from a telnet server)
|
|
-- indicates a successful login
|
|
--
|
|
-- @param str The string to analyze
|
|
-- @return Verdict (true or false)
|
|
local is_login_success = function (str)
|
|
return not not password_login_success:match(str)
|
|
end
|
|
|
|
|
|
---
|
|
-- Decide whether a given string (presumably received from a telnet server)
|
|
-- indicates a failed login
|
|
--
|
|
-- @param str The string to analyze
|
|
-- @return Verdict (true or false)
|
|
local is_login_failure = function (str)
|
|
return not not patt_login_failure:match(str)
|
|
end
|
|
|
|
|
|
---
|
|
-- Simple class to encapsulate connection operations
|
|
local Connection = { methods = {} }
|
|
|
|
|
|
---
|
|
-- Initialize a connection object
|
|
--
|
|
-- @param host Telnet host
|
|
-- @param port Telnet port
|
|
-- @return Connection object or nil (if the operation failed)
|
|
Connection.new = function (host, port, proto)
|
|
local soc = nmap.new_socket(proto)
|
|
if not soc then return nil end
|
|
return setmetatable( {
|
|
socket = soc,
|
|
isopen = false,
|
|
buffer = nil,
|
|
error = nil,
|
|
host = host,
|
|
port = port,
|
|
proto = proto
|
|
},
|
|
{
|
|
__index = Connection.methods,
|
|
__gc = Connection.methods.close
|
|
} )
|
|
end
|
|
|
|
|
|
---
|
|
-- Open the connection
|
|
--
|
|
-- @param self Connection object
|
|
-- @return Status (true or false)
|
|
-- @return nil if the operation was successful; error code otherwise
|
|
Connection.methods.connect = function (self)
|
|
local status
|
|
local wait = 1
|
|
|
|
self.buffer = ""
|
|
|
|
for tries = 0, conn_retries do
|
|
self.socket:set_timeout(telnet_timeout)
|
|
status, self.error = self.socket:connect(self.host, self.port, self.proto)
|
|
if status then break end
|
|
stdnse.sleep(wait)
|
|
wait = 2 * wait
|
|
end
|
|
|
|
self.isopen = status
|
|
return status, self.error
|
|
end
|
|
|
|
|
|
---
|
|
-- Close the connection
|
|
--
|
|
-- @param self Connection object
|
|
-- @return Status (true or false)
|
|
-- @return nil if the operation was successful; error code otherwise
|
|
Connection.methods.close = function (self)
|
|
if not self.isopen then return true, nil end
|
|
local status
|
|
self.isopen = false
|
|
self.buffer = nil
|
|
status, self.error = self.socket:close()
|
|
return status, self.error
|
|
end
|
|
|
|
|
|
---
|
|
-- Send one line through the connection to the server
|
|
--
|
|
-- @param self Connection object
|
|
-- @param line Characters to send, will be automatically terminated
|
|
-- @return Status (true or false)
|
|
-- @return nil if the operation was successful; error code otherwise
|
|
Connection.methods.send_line = function (self, line)
|
|
local status
|
|
status, self.error = self.socket:send(line .. telnet_eol)
|
|
return status, self.error
|
|
end
|
|
|
|
|
|
---
|
|
-- Add received data to the connection buffer while taking care
|
|
-- of telnet option signalling
|
|
--
|
|
-- @param self Connection object
|
|
-- @param data Data string to add to the buffer
|
|
-- @return Number of characters in the connection buffer
|
|
Connection.methods.fill_buffer = function (self, data)
|
|
local outbuf = strbuf.new(self.buffer)
|
|
local optbuf = strbuf.new()
|
|
local oldpos = 0
|
|
|
|
while true do
|
|
-- look for IAC (Interpret As Command)
|
|
local newpos = data:find('\255', oldpos)
|
|
if not newpos then break end
|
|
|
|
outbuf = outbuf .. data:sub(oldpos, newpos - 1)
|
|
local opttype = data:byte(newpos + 1)
|
|
local opt = data:byte(newpos + 2)
|
|
|
|
if opttype == 251 or opttype == 252 then
|
|
-- Telnet Will / Will Not
|
|
-- regarding ECHO or GO-AHEAD, agree with whatever the
|
|
-- server wants (or not) to do; otherwise respond with
|
|
-- "don't"
|
|
opttype = (opt == 1 or opt == 3) and opttype + 2 or 254
|
|
elseif opttype == 253 or opttype == 254 then
|
|
-- Telnet Do / Do not
|
|
-- I will not do whatever the server wants me to
|
|
opttype = 252
|
|
end
|
|
|
|
optbuf = optbuf .. string.char(255)
|
|
.. string.char(opttype)
|
|
.. string.char(opt)
|
|
oldpos = newpos + 3
|
|
end
|
|
|
|
self.buffer = strbuf.dump(outbuf) .. data:sub(oldpos)
|
|
self.socket:send(strbuf.dump(optbuf))
|
|
return self.buffer:len()
|
|
end
|
|
|
|
|
|
---
|
|
-- Return leading part of the connection buffer, up to a line termination,
|
|
-- and refill the buffer as needed
|
|
--
|
|
-- @param self Connection object
|
|
-- @param normalize whether the returned line is normalized (default: false)
|
|
-- @return String representing the first line in the buffer
|
|
Connection.methods.get_line = function (self)
|
|
if self.buffer:len() == 0 then
|
|
-- refill the buffer
|
|
local status, data = self.socket:receive_buf("[\r\n:>%%%$#\255].*", true)
|
|
if not status then
|
|
-- connection error
|
|
self.error = data
|
|
return nil
|
|
end
|
|
|
|
self:fill_buffer(data)
|
|
end
|
|
return self.buffer:match('^[^\r\n]*')
|
|
end
|
|
|
|
|
|
---
|
|
-- Discard leading part of the connection buffer, up to and including
|
|
-- one or more line terminations
|
|
--
|
|
-- @param self Connection object
|
|
-- @return Number of characters remaining in the connection buffer
|
|
Connection.methods.discard_line = function (self)
|
|
self.buffer = self.buffer:gsub('^[^\r\n]*[\r\n]*', '', 1)
|
|
return self.buffer:len()
|
|
end
|
|
|
|
|
|
---
|
|
-- Ghost connection object
|
|
Connection.GHOST = {}
|
|
|
|
|
|
---
|
|
-- Simple class to encapsulate target properties, including thread-specific data
|
|
-- persisted across Driver instances
|
|
local Target = { methods = {} }
|
|
|
|
|
|
---
|
|
-- Initialize a target object
|
|
--
|
|
-- @param host Telnet host
|
|
-- @param port Telnet port
|
|
-- @return Target object or nil (if the operation failed)
|
|
Target.new = function (host, port)
|
|
local soc, _, proto = comm.tryssl(host, port, "\n", {timeout=telnet_timeout})
|
|
if not soc then return nil end
|
|
soc:close()
|
|
return setmetatable({
|
|
host = host,
|
|
port = port,
|
|
proto = proto,
|
|
workers = setmetatable({}, { __mode = "k" })
|
|
},
|
|
{ __index = Target.methods } )
|
|
end
|
|
|
|
|
|
---
|
|
-- Set up the calling thread as one of the worker threads
|
|
--
|
|
-- @param self Target object
|
|
Target.methods.worker = function (self)
|
|
local thread = coroutine.running()
|
|
self.workers[thread] = self.workers[thread] or {}
|
|
end
|
|
|
|
|
|
---
|
|
-- Provide the calling worker thread with an open connection to the target.
|
|
-- The state of the connection is at the beginning of the login flow.
|
|
--
|
|
-- @param self Target object
|
|
-- @return Status (true or false)
|
|
-- @return Connection if the operation was successful; error code otherwise
|
|
Target.methods.attach = function (self)
|
|
local worker = self.workers[coroutine.running()]
|
|
local conn = worker.conn
|
|
or Connection.new(self.host, self.port, self.proto)
|
|
if not conn then return false, "Unable to allocate connection" end
|
|
worker.conn = conn
|
|
|
|
if conn.error then conn:close() end
|
|
if not conn.isopen then
|
|
local status, err = conn:connect()
|
|
if not status then return false, err end
|
|
end
|
|
|
|
return true, conn
|
|
end
|
|
|
|
|
|
---
|
|
-- Recover a connection used by the calling worker thread
|
|
--
|
|
-- @param self Target object
|
|
-- @return Status (true or false)
|
|
-- @return nil if the operation was successful; error code otherwise
|
|
Target.methods.detach = function (self)
|
|
local conn = self.workers[coroutine.running()].conn
|
|
local status, response = true, nil
|
|
if conn and conn.error then status, response = conn:close() end
|
|
return status, response
|
|
end
|
|
|
|
|
|
---
|
|
-- Set the state of the calling worker thread
|
|
--
|
|
-- @param self Target object
|
|
-- @param inuse Whether the worker is in use (true or false)
|
|
-- @return inuse
|
|
Target.methods.inuse = function (self, inuse)
|
|
self.workers[coroutine.running()].inuse = inuse
|
|
return inuse
|
|
end
|
|
|
|
|
|
---
|
|
-- Decide whether the target is still being worked on
|
|
--
|
|
-- @param self Target object
|
|
-- @return Verdict (true or false)
|
|
Target.methods.idle = function (self)
|
|
local idle = true
|
|
for t, w in pairs(self.workers) do
|
|
idle = idle and (not w.inuse or coroutine.status(t) == "dead")
|
|
end
|
|
return idle
|
|
end
|
|
|
|
|
|
---
|
|
-- Class that can be used as a "driver" by brute.lua
|
|
local Driver = { methods = {} }
|
|
|
|
|
|
---
|
|
-- Initialize a driver object
|
|
--
|
|
-- @param host Telnet host
|
|
-- @param port Telnet port
|
|
-- @param target instance of a Target class
|
|
-- @return Driver object or nil (if the operation failed)
|
|
Driver.new = function (self, host, port, target)
|
|
assert(host == target.host and port == target.port, "Target mismatch")
|
|
target:worker()
|
|
return setmetatable({
|
|
target = target,
|
|
connect = telnet_autosize
|
|
and Driver.methods.connect_autosize
|
|
or Driver.methods.connect_simple,
|
|
thread_exit = nmap.condvar(target)
|
|
},
|
|
{ __index = Driver.methods } )
|
|
end
|
|
|
|
|
|
---
|
|
-- Connect the driver to the target (when auto-sizing is off)
|
|
--
|
|
-- @param self Driver object
|
|
-- @return Status (true or false)
|
|
-- @return nil if the operation was successful; error code otherwise
|
|
Driver.methods.connect_simple = function (self)
|
|
assert(not self.conn, "Multiple connections attempted")
|
|
local status, response = self.target:attach()
|
|
if status then
|
|
self.conn = response
|
|
response = nil
|
|
end
|
|
return status, response
|
|
end
|
|
|
|
|
|
---
|
|
-- Connect the driver to the target (when auto-sizing is on)
|
|
--
|
|
-- @param self Driver object
|
|
-- @return Status (true or false)
|
|
-- @return nil if the operation was successful; error code otherwise
|
|
Driver.methods.connect_autosize = function (self)
|
|
assert(not self.conn, "Multiple connections attempted")
|
|
self.target:inuse(true)
|
|
local status, response = self.target:attach()
|
|
if status then
|
|
-- connected to the target
|
|
self.conn = response
|
|
if self:prompt() then
|
|
-- successfully reached login prompt
|
|
return true, nil
|
|
end
|
|
-- connected but turned away
|
|
self.target:detach()
|
|
end
|
|
-- let's park the thread here till all the functioning threads finish
|
|
self.target:inuse(false)
|
|
print_debug(detail_debug, "Retiring %s", tostring(coroutine.running()))
|
|
while not self.target:idle() do self.thread_exit("wait") end
|
|
-- pretend that it connected
|
|
self.conn = Connection.GHOST
|
|
return true, nil
|
|
end
|
|
|
|
|
|
---
|
|
-- Disconnect the driver from the target
|
|
--
|
|
-- @param self Driver object
|
|
-- @return Status (true or false)
|
|
-- @return nil if the operation was successful; error code otherwise
|
|
Driver.methods.disconnect = function (self)
|
|
assert(self.conn, "Attempt to disconnect non-existing connection")
|
|
if self.conn.isopen and not self.conn.error then
|
|
-- try to reach new login prompt
|
|
self:prompt()
|
|
end
|
|
self.conn = nil
|
|
return self.target:detach()
|
|
end
|
|
|
|
|
|
---
|
|
-- Attempt to reach telnet login prompt on the target
|
|
--
|
|
-- @param self Driver object
|
|
-- @return line Reached prompt or nil
|
|
Driver.methods.prompt = function (self)
|
|
assert(self.conn, "Attempt to use disconnected driver")
|
|
local conn = self.conn
|
|
local line
|
|
repeat
|
|
line = conn:get_line()
|
|
until not line
|
|
or is_username_prompt(line)
|
|
or is_password_prompt(line)
|
|
or not conn:discard_line()
|
|
return line
|
|
end
|
|
|
|
|
|
---
|
|
-- Attempt to establish authenticated telnet session on the target
|
|
--
|
|
-- @param self Driver object
|
|
-- @return Status (true or false)
|
|
-- @return instance of brute.Account if the operation was successful;
|
|
-- instance of brute.Error otherwise
|
|
Driver.methods.login = function (self, username, password)
|
|
assert(self.conn, "Attempt to use disconnected driver")
|
|
local sent_username = self.target.passonly
|
|
local sent_password = false
|
|
local conn = self.conn
|
|
|
|
local loc = " in " .. tostring(coroutine.running())
|
|
|
|
local connection_error = function (msg)
|
|
print_debug(detail_debug, msg .. loc)
|
|
local err = brute.Error:new(msg)
|
|
err:setRetry(true)
|
|
return false, err
|
|
end
|
|
|
|
local passonly_error = function ()
|
|
local msg = "Password prompt encountered"
|
|
print_debug(critical_debug, msg .. loc)
|
|
local err = brute.Error:new(msg)
|
|
err:setAbort(true)
|
|
return false, err
|
|
end
|
|
|
|
local username_error = function ()
|
|
local msg = "Invalid username encountered"
|
|
print_debug(detail_debug, msg .. loc)
|
|
local err = brute.Error:new(msg)
|
|
err:setInvalidAccount(username)
|
|
return false, err
|
|
end
|
|
|
|
local login_error = function ()
|
|
local msg = "Login failed"
|
|
print_debug(detail_debug, msg .. loc)
|
|
return false, brute.Error:new(msg)
|
|
end
|
|
|
|
local login_success = function ()
|
|
local msg = "Login succeeded"
|
|
print_debug(detail_debug, msg .. loc)
|
|
return true, brute.Account:new(username, password, "OPEN")
|
|
end
|
|
|
|
local login_no_password = function ()
|
|
local msg = "Login succeeded without password"
|
|
print_debug(detail_debug, msg .. loc)
|
|
return true, brute.Account:new(username, "<none>", "OPEN")
|
|
end
|
|
|
|
print_debug(detail_debug, "Login attempt %s:%s%s", username, password, loc)
|
|
|
|
if conn == Connection.GHOST then
|
|
-- reached when auto-sizing is enabled and all worker threads
|
|
-- failed
|
|
return connection_error("Service unreachable")
|
|
end
|
|
|
|
-- username has not yet been sent
|
|
while not sent_username do
|
|
local line = conn:get_line()
|
|
if not line then
|
|
-- stopped receiving data
|
|
return connection_error("Login prompt not reached")
|
|
end
|
|
|
|
if is_username_prompt(line) then
|
|
-- being prompted for a username
|
|
conn:discard_line()
|
|
print_debug(detail_debug, "Sending username" .. loc)
|
|
if not conn:send_line(username) then
|
|
return connection_error(conn.error)
|
|
end
|
|
sent_username = true
|
|
if conn:get_line() == username then
|
|
-- ignore; remote echo of the username in effect
|
|
conn:discard_line()
|
|
end
|
|
|
|
elseif is_password_prompt(line) then
|
|
-- looks like 'password only' support
|
|
return passonly_error()
|
|
|
|
else
|
|
-- ignore; insignificant response line
|
|
conn:discard_line()
|
|
end
|
|
end
|
|
|
|
-- username has been already sent
|
|
while not sent_password do
|
|
local line = conn:get_line()
|
|
if not line then
|
|
-- remote host disconnected
|
|
return connection_error("Password prompt not reached")
|
|
end
|
|
|
|
if is_login_success(line) then
|
|
-- successful login without a password
|
|
conn:close()
|
|
return login_no_password()
|
|
|
|
elseif is_password_prompt(line) then
|
|
-- being prompted for a password
|
|
conn:discard_line()
|
|
print_debug(detail_debug, "Sending password" .. loc)
|
|
if not conn:send_line(password) then
|
|
return connection_error(conn.error)
|
|
end
|
|
sent_password = true
|
|
|
|
elseif is_login_failure(line) then
|
|
-- failed login without a password; explicitly told so
|
|
conn:discard_line()
|
|
return username_error()
|
|
|
|
elseif is_username_prompt(line) then
|
|
-- failed login without a password; prompted again for a username
|
|
return username_error()
|
|
|
|
else
|
|
-- ignore; insignificant response line
|
|
conn:discard_line()
|
|
end
|
|
|
|
end
|
|
|
|
-- password has been already sent
|
|
while true do
|
|
local line = conn:get_line()
|
|
if not line then
|
|
-- remote host disconnected
|
|
return connection_error("Login not completed")
|
|
end
|
|
|
|
if is_login_success(line) then
|
|
-- successful login
|
|
conn:close()
|
|
return login_success()
|
|
|
|
elseif is_login_failure(line) then
|
|
-- failed login; explicitly told so
|
|
conn:discard_line()
|
|
return login_error()
|
|
|
|
elseif is_password_prompt(line) or is_username_prompt(line) then
|
|
-- failed login; prompted again for credentials
|
|
return login_error()
|
|
|
|
else
|
|
-- ignore; insignificant response line
|
|
conn:discard_line()
|
|
end
|
|
|
|
end
|
|
|
|
-- unreachable code
|
|
assert(false, "Reached unreachable code")
|
|
end
|
|
|
|
|
|
action = function (host, port)
|
|
local ts, tserror = stdnse.parse_timespec(arg_timeout)
|
|
if not ts then
|
|
return stdnse.format_output(false, "Invalid timeout value: " .. tserror)
|
|
end
|
|
telnet_timeout = 1000 * ts
|
|
telnet_autosize = arg_autosize:lower() == "true"
|
|
|
|
local target = Target.new(host, port)
|
|
if not target then
|
|
return stdnse.format_output(false, "Unable to connect to the target")
|
|
end
|
|
|
|
local engine = brute.Engine:new(Driver, host, port, target)
|
|
engine.options.script_name = SCRIPT_NAME
|
|
target.passonly = engine.options.passonly
|
|
local _, result = engine:start()
|
|
return result
|
|
end
|