summaryrefslogtreecommitdiffstats
path: root/scripts/telnet-brute.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/telnet-brute.nse')
-rw-r--r--scripts/telnet-brute.nse697
1 files changed, 697 insertions, 0 deletions
diff --git a/scripts/telnet-brute.nse b/scripts/telnet-brute.nse
new file mode 100644
index 0000000..5b220a1
--- /dev/null
+++ b/scripts/telnet-brute.nse
@@ -0,0 +1,697 @@
+local comm = require "comm"
+local coroutine = require "coroutine"
+local creds = require "creds"
+local match = require "match"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local strbuf = require "strbuf"
+local string = require "string"
+local brute = require "brute"
+
+description = [[
+Performs brute-force password auditing against telnet servers.
+]]
+
+---
+-- @usage
+-- nmap -p 23 --script telnet-brute --script-args userdb=myusers.lst,passdb=mypwds.lst,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 https://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
+-- @param fmt Format string.
+-- @param ... Arguments to format.
+local debug = stdnse.debug
+
+---
+-- 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)
+ local lcstr = str:lower()
+ return lcstr:find("%f[%w]username%s*:%s*$")
+ or lcstr:find("%f[%w]login%s*:%s*$")
+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)
+ local lcstr = str:lower()
+ return lcstr:find("%f[%w]password%s*:%s*$")
+ or lcstr:find("%f[%w]passcode%s*:%s*$")
+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)
+ if str:find("^[A-Z]:\\") then -- Windows telnet
+ return true
+ end
+ local lcstr = str:lower()
+ return lcstr:find("[/>%%%$#]%s*$") -- general prompt
+ or lcstr:find("^last login%s*:") -- linux telnetd
+ or lcstr:find("%f[%w]main%smenu%f[%W]") -- Netgear RM356
+ or lcstr:find("^enter terminal emulation:%s*$") -- Hummingbird telnetd
+ or lcstr:find("%f[%w]select an option%f[%W]") -- Zebra PrintServer
+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)
+ local lcstr = str:lower()
+ return lcstr:find("%f[%w]incorrect%f[%W]")
+ or lcstr:find("%f[%w]failed%f[%W]")
+ or lcstr:find("%f[%w]denied%f[%W]")
+ or lcstr:find("%f[%w]invalid%f[%W]")
+ or lcstr:find("%f[%w]bad%f[%W]")
+end
+
+
+---
+-- Strip off ANSI escape sequences (terminal codes) that start with <esc>[
+-- and replace them with white space, namely the VT character (0x0B).
+-- This way their new representation can be naturally matched with pattern %s.
+--
+-- @param str The string that needs to be strained
+-- @return The same string without the escape sequences
+local remove_termcodes = function (str)
+ local mark = '\x0B'
+ return str:gsub('\x1B%[%??%d*%a', mark)
+ :gsub('\x1B%[%??%d*;%d*%a', mark)
+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 = brute.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, true)
+ if not newpos then break end
+
+ outbuf = outbuf .. data:sub(oldpos, newpos - 1)
+ local opttype, opt = data:byte(newpos + 1, 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, opttype, 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(match.pattern_limit("[\r\n:>%%%$#\255].*", 2048), true)
+ if not status then
+ -- connection error
+ self.error = data
+ return nil
+ end
+
+ self:fill_buffer(data)
+ end
+ return remove_termcodes(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)
+ 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 creds.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)
+ 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"
+ 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"
+ 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"
+ debug(detail_debug, msg .. loc)
+ return false, brute.Error:new(msg)
+ end
+
+ local login_success = function ()
+ local msg = "Login succeeded"
+ debug(detail_debug, msg .. loc)
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+
+ local login_no_password = function ()
+ local msg = "Login succeeded without password"
+ debug(detail_debug, msg .. loc)
+ return true, creds.Account:new(username, "", creds.State.VALID)
+ end
+
+ 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()
+ 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()
+ 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