1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
|
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
|