From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- scripts/telnet-brute.nse | 697 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 697 insertions(+) create mode 100644 scripts/telnet-brute.nse (limited to 'scripts/telnet-brute.nse') 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 +-- +-- @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 [ +-- 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 -- cgit v1.2.3