mirror of
https://github.com/nmap/nmap.git
synced 2025-12-15 04:09:01 +00:00
http://seclists.org/nmap-dev/2013/q3/249 - Multi-threaded (thanks to nselib/brute.lua) - Can automatically reduce number of threads if it senses that the target supports less than what brute.lua wants to use. Without this feature the script tends to bail out because brute.lua default of 10 threads is too much for a lot of telnet targets. This saves the user the trouble of finding out how much the target can take before launching the script. - Uses connection pooling for sending multiple login attempts across the same connection. This significantly improves performance. - Supports password-only logins. Other changes: - Fixed support for Windows telnet service. Added support for Netgear RM356. - Improved accuracy of target state detection. Tested on: - Cisco IOS - Linux telnetd - Windows telnet service - Digital Sprite 2 - Nortel Contivity - Netgear RM356 - Hummingbird telnetd
689 lines
18 KiB
Lua
689 lines
18 KiB
Lua
local comm = require "comm"
|
|
local shortport = require "shortport"
|
|
local stdnse = require "stdnse"
|
|
local strbuf = require "strbuf"
|
|
local string = require "string"
|
|
local brute = require "brute"
|
|
local pcre = require "pcre"
|
|
|
|
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"
|
|
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
|
|
|
|
local pcreptn = {} -- cache of compiled PCRE patterns
|
|
|
|
|
|
---
|
|
-- 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
|
|
|
|
|
|
---
|
|
-- 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)
|
|
pcreptn.username_prompt = pcreptn.username_prompt
|
|
or pcre.new("\\b(?:username|login)\\s*:\\s*$",
|
|
pcre.flags().CASELESS, "C")
|
|
return pcreptn.username_prompt: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)
|
|
pcreptn.password_prompt = pcreptn.password_prompt
|
|
or pcre.new("\\bpass(?:word|code)\\s*:\\s*$",
|
|
pcre.flags().CASELESS, "C")
|
|
return pcreptn.password_prompt: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)
|
|
pcreptn.login_success = pcreptn.login_success
|
|
or pcre.new("[/>%$#]\\s*$" -- general prompt
|
|
.. "|^Last login\\s*:" -- linux telnetd
|
|
.. "|^(?-i:[A-Z]):\\\\" -- Windows telnet
|
|
.. "|Main(?:\\s|\\x1B\\[\\d+;\\d+H)Menu\\b" -- Netgear RM356
|
|
.. "|^Enter Terminal Emulation:\\s*$", -- Hummingbird telnetd
|
|
pcre.flags().CASELESS, "C")
|
|
return pcreptn.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)
|
|
pcreptn.login_failure = pcreptn.login_failure
|
|
or pcre.new("\\b(?:incorrect|failed|denied|invalid|bad)\\b",
|
|
pcre.flags().CASELESS, "C")
|
|
return pcreptn.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 the calling thread up 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)
|
|
-- (INIT, EXEC, PARK)
|
|
-- @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")
|
|
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
|