local nmap = require "nmap" local string = require "string" local stringaux = require "stringaux" local stdnse = require "stdnse" local shortport = require "shortport" local tn3270 = require "tn3270" local brute = require "brute" local creds = require "creds" local unpwdb = require "unpwdb" description = [[ CICS User ID brute forcing script for the CESL login screen. ]] --- -- @args cics-user-brute.commands Commands in a semi-colon separated list needed -- to access CICS. Defaults to CICS. -- -- @usage -- nmap --script=cics-user-brute -p 23 -- -- nmap --script=cics-user-brute --script-args userdb=users.txt, -- cics-user-brute.commands="exit;logon applid(cics42)" -p 23 -- -- @output -- PORT STATE SERVICE -- 23/tcp open tn3270 -- | cics-user-brute: -- | Accounts: -- | PLAGUE: Valid - CICS User ID -- |_ Statistics: Performed 31 guesses in 114 seconds, average tps: 0 -- @changelog -- 2016-08-29 - v0.1 - created by Soldier of Fortran -- 2016-10-26 - v0.2 - Added RACF support -- 2017-01-23 - v0.3 - Rewrote script to use fields and skip enumeration to speed up testing -- 2019-02-01 - v0.4 - Disabled new TN3270E support 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") --- Registers User IDs that no longer need to be tested -- -- @param username to stop checking local function register_invalid( username ) if nmap.registry.cicsinvalid == nil then nmap.registry.cicsinvalid = {} end stdnse.debug(2,"Registering %s", username) nmap.registry.cicsinvalid[username] = true 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(brute.new_socket()) o.tn3270:disable_tn3270e() return o end, connect = function( self ) local status, err = self.tn3270:initiate(self.host,self.port) self.tn3270:get_screen_debug(2) if not status then stdnse.debug("Could not initiate TN3270: %s", err ) return false end stdnse.debug(2,"Connect Successful") return true end, disconnect = function( self ) self.tn3270:disconnect() self.tn3270 = nil return true end, login = function (self, user, pass) local commands = self.options['key1'] local timeout = 300 local max_blank = 1 local loop = 1 local err stdnse.debug(2,"Getting to CICS") local run = stringaux.strsplit(";%s*", commands) for i = 1, #run do stdnse.debug(2,"Issuing Command (#%s of %s): %s", i, #run ,run[i]) self.tn3270:send_cursor(run[i]) self.tn3270:get_all_data() self.tn3270:get_screen_debug(2) end -- Are we at the logon transaction? if not (self.tn3270:find('SIGN ON TO CICS') or self.tn3270:find("Signon to CICS")) then -- We might be at some weird screen, lets try and exit it then clear it out stdnse.debug(2,"Sending: F3") self.tn3270:send_pf(3) -- send F3 self.tn3270:get_all_data() stdnse.debug(2,"Clearing the Screen") self.tn3270:send_clear() self.tn3270:get_all_data() self.tn3270:get_screen_debug(2) stdnse.debug(2,"Sending 'CESL'") self.tn3270:send_cursor('CESL') self.tn3270:get_all_data() -- Have we encoutered a slow system? if self.tn3270:isClear() then self.tn3270:get_all_data(1000) end self.tn3270:get_screen_debug(2) end -- At this point we MUST be at CESL to try accounts. -- If we're not then we quit with an error if not (self.tn3270:find('SIGN ON TO CICS') or self.tn3270:find("Signon to CICS")) then local err = brute.Error:new( "Can't get to CESL") err:setRetry( true ) return false, err end -- Ok we're good we're at CESL. Send the Userid and Password. local fields = self.tn3270:writeable() -- Get the writeable field areas local user_loc = {fields[2][1],user} -- This is the 'UserID:' field local pass_loc = {fields[4][1],pass} -- This is the 'Password:' field ([2] is a group ID) stdnse.verbose('[BRUTE] Trying CICS: ' .. user ..' : ' .. pass) stdnse.debug(3,"[BRUTE] Location:" .. fields[2][1] .. " x " .. fields[4][1]) self.tn3270:send_locations({user_loc,pass_loc}) self.tn3270:get_all_data() stdnse.debug(2,"Screen Received for User ID: %s/%s", user, pass) self.tn3270:get_screen_debug(2) local loop = 1 while loop < 300 and self.tn3270:find('DFHCE3520') do -- still at Enter UserID message? stdnse.verbose('Trying CICS: ' .. user ..' : ' .. pass) self.tn3270:send_locations({user_loc,pass_loc}) self.tn3270:get_all_data() stdnse.debug(2,"Screen Received for User ID: %s/%s", user, pass) self.tn3270:get_screen_debug(2) loop = loop + 1 -- We don't want this to loop forever end if loop == 300 then local err = brute.Error:new( "Error with UserID entry") err:setRetry( true ) return false, err end -- Now check what we received if its valid or not if self.tn3270:find('TSS7101E') or self.tn3270:find('DFHCE3530') or self.tn3270:find('DFHCE3532') then -- Top Secret: -- TSS7101E Password is Incorrect -- RACF: -- DFHCE3530/2 Your userid or password is invalid. Please retype both. return false, brute.Error:new( "Incorrect password" ) elseif self.tn3270:find('TSS7145E') or self.tn3270:find("TSS714[0-3]E") or self.tn3270:find('TSS7160E') or self.tn3270:find('TSS7120E') then -- Top Secret: -- TSS7140E Accessor ID Has Expired: No Longer Valid -- TSS7141E Use of Accessor ID Suspended -- TSS7142E Accessor ID Not Yet Available for Use - Still Inactive -- TSS7143E Accessor ID Has Been Inactive Too Long -- TSS7120E PASSWORD VIOLATION THRESHOLD EXCEEDED -- TSS7145E ACCESSOR ID NOT DEFINED TO SECURITY -- TSS7160E Facility Not Authorized for Your Use -- Store the invalid ID in the registry so we don't keep trying it with subsequent passwords -- when using default brute library. register_invalid(user) return false, brute.Error:new( "User ID Not Able to Use CICS" ) elseif self.tn3270:find("DFHCE3549") or self.tn3270:find('TSS7000I') or self.tn3270:find('TSS7110E Password Has Expired. New Password Missing') or self.tn3270:find('TSS7001I') then stdnse.verbose("Valid CICS UserID / Password: " .. user .. "/" .. pass) return true, creds.Account:new(user, pass, creds.State.VALID) else -- ok whoa, something happened, print the screen but don't store as valid stdnse.verbose("Valid(?) user/pass with current output " .. user .. "/" .. pass .. "\n" .. self.tn3270:get_screen()) return false, brute.Error:new( "Incorrect password" ) end return false, brute.Error:new("Something went wrong, we didn't get a proper response") end } --- Tests the target to see if we can even get to CICS -- -- @param host host NSE object -- @param port port NSE object -- @param commands optional script-args of commands to use to get to CICS -- @return status true on success, false on failure local function cics_test( host, port, commands ) stdnse.verbose(2,"Checking for CICS Login Page") local tn = tn3270.Telnet:new() tn:disable_tn3270e() local status, err = tn:initiate(host,port) local cesl = false -- initially we're not at CICS if not status then stdnse.debug("Could not initiate TN3270: %s", err ) return false end tn:get_screen_debug(2) -- prints TN3270 screen to debug stdnse.debug(2,"Getting to CICS") local run = stringaux.strsplit(";%s*", commands) for i = 1, #run do stdnse.debug(2,"Issuing Command (#%s of %s): %s", i, #run ,run[i]) tn:send_cursor(run[i]) tn:get_all_data() tn:get_screen_debug(2) end tn:get_all_data() tn:get_screen_debug(2) -- for debug purposes -- We should now be at CICS. Check if we're already at the logon screen if tn:find('SIGN ON TO CICS') and tn:find("Signon to CICS") then stdnse.verbose(2,"At CICS Login Transaction (CESL)") tn:disconnect() return true end -- Uh oh. We're not at the logon screen. Now we need to send: -- * F3 to exit the CICS program -- * CLEAR (a tn3270 command) to clear the screen. -- (you need to clear before sending a transaction ID) -- * a known default CICS transaction ID with predictable outcome -- (CESF with 'Sign-off is complete.' as the result) -- to confirm that we were in CICS. If so we return true -- otherwise we return false local count = 1 while not tn:isClear() and count < 6 do -- some systems will just kick you off others are slow in responding -- this loop continues to try getting out of whatever transaction 5 times. If it can't -- then we probably weren't in CICS to begin with. stdnse.debug(2,"Sending: F3") tn:send_pf(3) -- send F3 tn:get_all_data() stdnse.debug(2,"Clearing the Screen") tn:send_clear() tn:get_all_data() tn:get_screen_debug(2) count = count + 1 end if count == 5 then return false, 'Could not get to CICS after 5 attempts. Is this even CICS?' end stdnse.debug(2,"Sending 'CESL'") tn:send_cursor('CESL') tn:get_all_data() if tn:isClear() then tn:get_all_data(1000) end tn:get_screen_debug(2) if tn:find("Signon to CICS") then stdnse.verbose(2,"At CICS Login Transaction (CESL)") tn:disconnect() return true end tn:disconnect() return false, 'Could not get to CESL (CICS Logon Screen)' end -- Filter iterator for unpwdb -- IDs are limited to 8 alpha numeric and @, #, $ and can't start with a number -- pattern: -- ^%D = The first char must NOT be a digit -- [%w@#%$] = All letters including the special chars @, #, and $. local valid_name = function(x) if nmap.registry.tsoinvalid and nmap.registry.tsoinvalid[x] then return false end return (string.len(x) <= 8 and string.match(x,"^%D+[%w@#%$]")) end -- Checks string to see if it follows valid password limitations local valid_pass = function(x) return (string.len(x) <= 8 ) end action = function(host, port) local commands = stdnse.get_script_args(SCRIPT_NAME .. '.commands') or "cics" local cicstst, err = cics_test(host, port, commands) if cicstst then local options = { key1 = commands } local engine = brute.Engine:new(Driver, host, port, options) stdnse.debug(2,"Starting CICS Brute Forcing") engine:setUsernameIterator(unpwdb.filter_iterator(brute.usernames_iterator(),valid_name)) engine:setPasswordIterator(unpwdb.filter_iterator(brute.passwords_iterator(),valid_pass)) engine.options.script_name = SCRIPT_NAME engine.options:setTitle("CICS User Accounts") local status, result = engine:start() return result else return err end end