summaryrefslogtreecommitdiffstats
path: root/scripts/lu-enum.nse
blob: 965f3942343381c4c338de2dc4ba0716be421782 (plain)
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
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 = [[
Attempts to enumerate Logical Units (LU) of TN3270E servers.

When connecting to a TN3270E server you are assigned a Logical Unit (LU) or you can tell
the TN3270E server which LU you'd like to use. Typically TN3270E servers are configured to 
give you an LU from a pool of LUs. They can also have LUs set to take you to a specific
application. This script attempts to guess valid LUs that bypass the default LUs you are
assigned. For example, if a TN3270E server sends you straight to TPX you could use this
script to find LUs that take you to TSO, CICS, etc.
]]

---
--@args lulist Path to list of Logical Units to test.
--  Defaults the initial Logical Unit TN3270E provides, replacing the 
--  last two characters with <code>00-99</code>.
--@args lu-enum.path Folder used to store valid logical unit 'screenshots'
--  Defaults to <code>None</code> and doesn't store anything. This stores 
--  all valid logical units.
--@usage
-- nmap --script lu-enum -p 23 <targets>
--
--@usage
-- nmap --script lu-enum --script-args lulist=lus.txt,
-- lu-enum.path="/home/dade/screenshots/" -p 23 -sV <targets>
--
--@output
-- PORT     STATE SERVICE REASON  VERSION
-- 23/tcp   open  tn3270  syn-ack IBM Telnet TN3270 (TN3270E)
-- | lu-enum: 
-- |   Logical Units: 
-- |     LU:BSLVLU69 - Valid credentials
-- |_  Statistics: Performed 7 guesses in 7 seconds, average tps: 1.0
-- 
-- @changelog
-- 2019-02-04 - v0.1 - created by Soldier of Fortran

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 TN3270E terminal screen 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()
    return o
  end,
  connect = function( self )
    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['path']
    local original = self.options['no_lu']
    local threshold = 90
    stdnse.verbose(2,"Trying Logical Unit: %s", pass)
    self.tn3270:set_lu(pass)
    local status, err = self.tn3270:initiate(self.host,self.port)
    if not status then
      stdnse.debug(2,"Could not initiate TN3270: %s", err )
      stdnse.verbose(2, "Invalid LU: %s",string.upper(pass))
      return false,  brute.Error:new( "Invalid Logical Unit" )
    end
    self.tn3270:get_all_data()
    self.tn3270:get_screen_debug(2)
    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

    stdnse.debug(3, "compare results: %s ", tostring(screen_diff(original, self.tn3270:get_screen_raw())))
    if screen_diff(original, self.tn3270:get_screen_raw()) > threshold then
      stdnse.verbose(2,'Same Screen for LU: %s',string.upper(pass))
      return false,  brute.Error:new( "Invalid Logical Unit" )
    else
      stdnse.verbose(2,"Valid Logical Unit: %s",string.upper(pass))
      return true, creds.Account:new("LU", string.upper(pass), creds.State.VALID)
    end
  end
}

--- Tests the target to see if we can connect with TN3270E
--
-- @param host host NSE object
-- @param port port NSE object
-- @return status true on success, false on failure
local function lu_test( host, port )
  local tn = tn3270.Telnet:new()
  local status, err = tn:initiate(host,port)
  
  if not status then
    stdnse.debug(1,"[lu_test] Could not initiate TN3270: %s", err )
    return false
  end

  stdnse.debug(2,"[lu_test] Displaying initial TN3270 Screen:")
  tn:get_screen_debug(2) -- prints TN3270 screen to debug
  if tn.state == tn.TN3270E_DATA then -- Could make a function in the library 'istn3270e'
    stdnse.debug(1,"[lu_test] Orig screen: %s", tn:get_screen_raw())
    return true, tn:get_lu(), tn:get_screen_raw()
  else 
    return false, 'Not in TN3270E Mode. LU not supported.', ''
  end

end

-- Checks if it's a valid Logical Unit name
local valid_lu = function(x)
  return (string.len(x) <= 8 and string.match(x,"[%w@#%$]"))
end

-- iterator function
function iter(t)
  local i, val
  return function()
    i, val = next(t, i)
    return val
  end
end

action = function(host, port)
  local lu_id_file = stdnse.get_script_args("lulist")
  local path = stdnse.get_script_args(SCRIPT_NAME .. '.path') -- Folder for screen grabs
  local logical_units = {}
  lu_id_file = ((lu_id_file and nmap.fetchfile(lu_id_file)) or lu_id_file) 

  local status, lu, orig_screen = lu_test( host, port )
  if status then
      
    
    if not lu_id_file then
      -- we have to do this here because we don't have an LU to use for the template until now
      stdnse.debug(3, "No LU list provided, auto generating a list using template: %s##", lu:sub(1, (#lu-2)))
      for i=1,99 do
        table.insert(logical_units, lu:sub(1, (#lu-2)) .. string.format("%02d", i))
        end
    else 
      for l in io.lines(lu_id_file) do
        local cleaned_line = string.gsub(l,"[\r\n]","")
        if not cleaned_line:match("#!comment:") then
          table.insert(logical_units, cleaned_line)
        end
      end
    end
    
    
    -- Make sure we pass the original screen we got to the brute 
    local options = { no_lu = orig_screen, path = path }
    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(logical_units), valid_lu))
    engine.options.passonly = true
    engine.options:setTitle("Logical Units")
    local status, result = engine:start()
    return result
  else
    stdnse.debug(1,"Not in TN3270E mode, LU not supported.")
    return lu
  end

end