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