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