local stdnse = require "stdnse" local shortport = require "shortport" local tn3270 = require "tn3270" local brute = require "brute" local creds = require "creds" local unpwdb = require "unpwdb" local io = require "io" local nmap = require "nmap" local string = require "string" local stringaux = require "stringaux" local table = require "table" description = [[ Many mainframes use VTAM screens to connect to various applications (CICS, IMS, TSO, and many more). This script attempts to brute force those VTAM application IDs. This script is based on mainframe_brute by Dominic White (https://github.com/sensepost/mainframe_brute). However, this script doesn't rely on any third party libraries or tools and instead uses the NSE TN3270 library which emulates a TN3270 screen in lua. Application IDs only allows for 8 byte IDs, that is the only specific rule found for application IDs. ]] --- --@args idlist Path to list of application IDs to test. -- Defaults to nselib/data/vhosts-default.lst. --@args vtam-enum.commands Commands in a semi-colon separated list needed -- to access VTAM. Defaults to nothing. --@args vtam-enum.path Folder used to store valid transaction id 'screenshots' -- Defaults to None and doesn't store anything. --@args vtam-enum.macros When set to true does not prepend the application ID -- with 'logon applid()'. Default is false. -- --@usage -- nmap --script vtam-enum -p 23 -- -- nmap --script vtam-enum --script-args idlist=defaults.txt, -- vtam-enum.command="exit;logon applid(logos)",vtam-enum.macros=true -- vtam-enum.path="/home/dade/screenshots/" -p 23 -sV -- --@output -- PORT STATE SERVICE VERSION -- 23/tcp open tn3270 IBM Telnet TN3270 -- | vtam-enum: -- | VTAM Application ID: -- | applid:TSO - Valid credentials -- | applid:CICSTS51 - Valid credentials -- |_ Statistics: Performed 14 guesses in 5 seconds, average tps: 2 -- -- @changelog -- 2015-07-04 - v0.1 - created by Soldier of Fortran -- 2015-11-04 - v0.2 - significant upgrades and speed increases -- 2015-11-14 - v0.3 - rewrote iterator -- 2017-01-13 - v0.4 - Fixed 'macros' bug with default vtam screen and test -- and added threshold for macros screen checking -- 2019-02-01 - v0.5 - Disabling Enhanced mode author = "Philip Young aka Soldier of Fortran" license = "Same as Nmap--See https://nmap.org/book/man-legal.html" categories = {"intrusive", "brute"} portrule = shortport.port_or_service({23,992}, "tn3270") --- Saves the Screen generated by the VTAM command to disk -- -- @param filename string containing the name and full path to the file -- @param data contains the data -- @return status true on success, false on failure -- @return err string containing error message if status is false local function save_screens( filename, data ) local f = io.open( filename, "w") if not f then return false, ("Failed to open file (%s)"):format(filename) end if not(f:write(data)) then return false, ("Failed to write file (%s)"):format(filename) end f:close() return true end --- Compares two screens and returns the difference as a percentage -- -- @param1 the original screen -- @param2 the screen to compare to local function screen_diff( orig_screen, current_screen ) if orig_screen == current_screen then return 100 end if #orig_screen == 0 or #current_screen == 0 then return 0 end local m = 1 for i =1 , #orig_screen do if orig_screen:byte(i) == current_screen:byte(i) then m = m + 1 end end return (m/1920)*100 end Driver = { new = function(self, host, port, options) local o = {} setmetatable(o, self) self.__index = self o.host = host o.port = port o.options = options o.tn3270 = tn3270.Telnet:new() o.tn3270:disable_tn3270e() return o end, connect = function( self ) local status, err = self.tn3270:initiate(self.host,self.port) if not status then stdnse.debug2("Could not initiate TN3270: %s", err ) return false end return true end, disconnect = function( self ) self.tn3270:disconnect() self.tn3270 = nil end, login = function (self, user, pass) -- pass is actually the username we want to try local path = self.options['key2'] local macros = self.options['key3'] local cmdfmt = "logon applid(%s)" local type = "applid" local threshold = 75 -- instead of sending 'logon applid()' when macros=true -- we try to logon with just the command if macros then cmdfmt = "%s" type ="macro" threshold = 90 -- sometimes the screen barely changes end stdnse.verbose(2,"Trying VTAM ID: %s", pass) local previous_screen = self.tn3270:get_screen_raw() self.tn3270:send_cursor(cmdfmt:format(pass)) self.tn3270:get_all_data() self.tn3270:get_screen_debug(2) local current_screen = self.tn3270:get_screen_raw() if (self.tn3270:find('UNABLE TO ESTABLISH SESSION') or -- thanks goes to Dominic White for creating these self.tn3270:find('COMMAND UNRECOGNI[SZ]ED') or self.tn3270:find('USSMSG0[1-4]') or self.tn3270:find('SESSION NOT BOUND') or self.tn3270:find('INVALID COMMAND') or self.tn3270:find('PARAMETER OMITTED') or self.tn3270:find('REQUERIDO PARAMETRO PERDIDO') or self.tn3270:find('Your command is unrecognized') or self.tn3270:find('invalid command or syntax') or self.tn3270:find('UNSUPPORTED FUNCTION') or self.tn3270:find('REQSESS error') or self.tn3270:find('syntax invalid') or self.tn3270:find('INVALID SYSTEM') or self.tn3270:find('NOT VALID') or self.tn3270:find('INVALID USERID, APPLID') ) or self.tn3270:find('UNABLE TO CONNECT TO THE REQUESTED APPLICATION') or screen_diff(previous_screen, current_screen) > threshold then -- Looks like an invalid APPLID. stdnse.verbose(2,'Invalid Application ID: %s',string.upper(pass)) return false, brute.Error:new( "Invalid VTAM Application ID" ) else stdnse.verbose(2,"Valid Application ID: %s",string.upper(pass)) if path ~= nil then stdnse.verbose(2,"Writting screen to: %s", path..string.upper(pass)..".txt") local status, err = save_screens(path..string.upper(pass)..".txt",self.tn3270:get_screen()) if not status then stdnse.verbose(2,"Failed writting screen to: %s", path..string.upper(pass)..".txt") end end return true, creds.Account:new(type,string.upper(pass), creds.State.VALID) end end } --- Tests the target to see if we can use logon applid() for enumeration -- -- @param host host NSE object -- @param port port NSE object -- @param commands optional script-args of commands to use to get to VTAM -- @return status true on success, false on failure local function vtam_test( host, port, commands, macros) local tn = tn3270.Telnet:new() tn:disable_tn3270e() local status, err = tn:initiate(host,port) stdnse.debug1("Testing if VTAM and 'logon applid' command supported") stdnse.debug2("Connecting TN3270 to %s:%s", host.targetname or host.ip, port.number) if not status then stdnse.debug1("Could not initiate TN3270: %s", err ) return false end stdnse.debug2("Displaying initial TN3270 Screen:") tn:get_screen_debug(2) -- prints TN3270 screen to debug if commands ~= nil then local run = stringaux.strsplit(";%s*", commands) for i = 1, #run do stdnse.debug(2,"Issuing Command (#%s of %s) or %s", i, #run ,run[i]) tn:send_cursor(run[i]) tn:get_screen_debug(2) end end stdnse.debug2("Sending VTAM command: IBMTEST") tn:send_cursor('IBMTEST') tn:get_all_data() tn:get_screen_debug(2) local isVTAM = false if not macros and tn:find('IBMECHO ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') then stdnse.debug2("IBMTEST Returned: IBMECHO ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.") stdnse.debug1("VTAM Test Success!") isVTAM = true elseif macros then isVTAM = true end if not macros then -- now testing if we can send 'logon applid()' -- certain systems interpret 'logon' as the tso logon tn:send_cursor('LOGON APPLID(FAKE)') tn:get_all_data() tn:get_screen_debug(2) if tn:find('INVALID USERID') then isVTAM = false end tn:disconnect() end return isVTAM end -- Checks if it's a valid VTAM name local valid_vtam = function(x) return (string.len(x) <= 8 and string.match(x,"[%w@#%$]")) end function iter(t) local i, val return function() i, val = next(t, i) return val end end action = function(host, port) local vtam_id_file = stdnse.get_script_args("idlist") local path = stdnse.get_script_args(SCRIPT_NAME .. '.path') -- Folder for screen grabs local macros = stdnse.get_script_args(SCRIPT_NAME .. '.macros') or false -- if set to true, doesn't prepend the commands with 'logon applid' local commands = stdnse.get_script_args(SCRIPT_NAME .. '.commands') -- Commands to send to get to VTAM local vtam_ids = {"tso", "CICS", "IMS", "NETVIEW", "TPX"} -- these are defaults usually seen vtam_id_file = ( (vtam_id_file and nmap.fetchfile(vtam_id_file)) or vtam_id_file ) or nmap.fetchfile("nselib/data/vhosts-default.lst") for l in io.lines(vtam_id_file) do local cleaned_line = string.gsub(l,"[\r\n]","") if not cleaned_line:match("#!comment:") then table.insert(vtam_ids, cleaned_line) end end if vtam_test(host, port, commands, macros) then local options = { key1 = commands, key2 = path, key3=macros } stdnse.verbose("Starting VTAM Application ID Enumeration") if path ~= nil then stdnse.verbose(2,"Saving Screenshots to: %s", path) end local engine = brute.Engine:new(Driver, host, port, options) engine.options.script_name = SCRIPT_NAME engine:setPasswordIterator(unpwdb.filter_iterator(iter(vtam_ids), valid_vtam)) engine.options.passonly = true engine.options:setTitle("VTAM Application ID") local status, result = engine:start() return result else return "Not VTAM or 'logon applid' command not accepted. Try with script arg 'vtam-enum.macros=true'" end end