278 lines
10 KiB
Plaintext
278 lines
10 KiB
Plaintext
|
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 <code>nselib/data/vhosts-default.lst</code>.
|
||
|
--@args vtam-enum.commands Commands in a semi-colon separated list needed
|
||
|
-- to access VTAM. Defaults to <code>nothing</code>.
|
||
|
--@args vtam-enum.path Folder used to store valid transaction id 'screenshots'
|
||
|
-- Defaults to <code>None</code> and doesn't store anything.
|
||
|
--@args vtam-enum.macros When set to true does not prepend the application ID
|
||
|
-- with 'logon applid()'. Default is <code>false</code>.
|
||
|
--
|
||
|
--@usage
|
||
|
-- nmap --script vtam-enum -p 23 <targets>
|
||
|
--
|
||
|
-- 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 <targets>
|
||
|
--
|
||
|
--@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(<appname>)' 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(<id>) 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(<id>)'
|
||
|
-- 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
|