1local io = require "io"
2local string = require "string"
3local stringaux = require "stringaux"
4local table = require "table"
5local nmap = require "nmap"
6local stdnse = require "stdnse"
7local shortport = require "shortport"
8local brute = require "brute"
9local creds = require "creds"
10local unpwdb = require "unpwdb"
11local drda = require "drda"
12local comm = require "comm"
13
14description = [[
15z/OS JES Network Job Entry (NJE) target node name brute force.
16
17NJE node communication is made up of an OHOST and an RHOST. Both fields
18must be present when conducting the handshake. This script attemtps to
19determine the target systems NJE node name.
20
21To initiate NJE the client sends a 33 byte record containing the type of
22record, the hostname (RHOST), IP address (RIP), target (OHOST),
23target IP (OIP) and a 1 byte response value (R) as outlined below:
24
25<code>
260 1 2 3 4 5 6 7 8 9 A B C D E F
27+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
28|  TYPE       |     RHOST     |
29+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
30|  RIP  |  OHOST      | OIP   |
31+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
32| R |
33+-+-+
34</code>
35
36* TYPE: Can either be 'OPEN', 'ACK', or 'NAK', in EBCDIC, padded by spaces to make 8 bytes. This script always send 'OPEN' type.
37* RHOST: Node name of the local machine initiating the connection. Set to 'FAKE'.
38* RIP: Hex value of the local systems IP address. Set to '0.0.0.0'
39* OHOST: The value being enumerated to determine the targets NJE node name.
40* OIP: IP address, in hex, of the target system. Set to '0.0.0.0'.
41* R: The response. NJE will send an 'R' of 0x01 if the OHOST is wrong or 0x04 if the OHOST is correct.
42
43By default this script will attempt the brute force a mainframes OHOST. If supplied with
44the argument <code>nje-node-brute.ohost</code> this script will attempt the bruteforce
45the RHOST, setting OHOST to the value supplied to the argument.
46
47Since most systems will only have one OHOST name, it is recommended to use the
48<code>brute.firstonly</code> script argument.
49]]
50
51
52---
53-- @usage
54-- nmap -sV --script=nje-node-brute <target>
55-- nmap --script=nje-node-brute --script-args=hostlist=nje_names.txt -p 175 <target>
56--
57-- @args nje-node-brute.hostlist The filename of a list of node names to try.
58--                               Defaults to "nselib/data/vhosts-default.lst"
59--
60-- @args nje-node-brute.ohost The target mainframe OHOST. Used to bruteforce RHOST.
61--
62-- @output
63-- PORT    STATE SERVICE REASON
64-- 175/tcp open  nje     syn-ack
65-- | nje-node-brute:
66-- |   Node Name:
67-- |     POTATO:CACTUS - Valid credentials
68-- |_  Statistics: Performed 6 guesses in 14 seconds, average tps: 0
69--
70-- @changelog
71-- 2015-06-15 - v0.1 - created by Soldier of Fortran
72-- 2016-03-22 - v0.2 - Added RHOST Brute forcing.
73
74author = "Soldier of Fortran"
75license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
76categories = {"intrusive", "brute"}
77
78portrule = shortport.port_or_service({175,2252}, "nje")
79
80local openNJEfmt = "\xd6\xd7\xc5\xd5@@@@%s\0\0\0\0%s\0\0\0\0\0"
81
82Driver = {
83  new = function(self, host, port, options)
84    local o = {}
85    setmetatable(o, self)
86    self.__index = self
87    o.host = host
88    o.port = port
89    o.options = options
90    return o
91  end,
92
93  connect = function( self )
94    -- the high timeout should take delays into consideration
95    local s, r, opts, _ = comm.tryssl(self.host, self.port, '', { timeout = 50000 } )
96    if ( not(s) ) then
97      stdnse.debug2("Failed to connect")
98      return false, "Failed to connect to server"
99    end
100    self.socket = s
101    return true
102  end,
103
104  disconnect = function( self )
105    return self.socket:close()
106  end,
107
108  login = function( self, username, password ) -- Technically we're not 'logging in' we're just using password
109    -- Generates an NJE 'OPEN' packet with the node name
110    password = string.upper(password)
111    stdnse.verbose(2,"Trying... %s", password)
112    local openNJE
113    if self.options['ohost'] then
114      -- One RHOST may have many valid OHOSTs
115      if password == self.options['ohost'] then return false, brute.Error:new( "RHOST cannot be OHOST" ) end
116      openNJE = openNJEfmt:format(drda.StringUtil.toEBCDIC(("%-8s"):format(password)),
117        drda.StringUtil.toEBCDIC(("%-8s"):format(self.options['ohost'])) )
118    else
119      openNJE = openNJEfmt:format(drda.StringUtil.toEBCDIC(("%-8s"):format('FAKE')),
120        drda.StringUtil.toEBCDIC(("%-8s"):format(password)) )
121    end
122    local status, err = self.socket:send( openNJE )
123    if not status then return false, "Failed to send" end
124    local status, data = self.socket:receive_bytes(33)
125    if not status then return false, "Failed to receive" end
126    if ( not self.options['ohost'] and ( data:sub(-1) == "\x04" ) ) or
127       ( self.options['ohost'] and ( data:sub(-1) == "\0" ) ) then
128      -- stdnse.verbose(2,"Valid Node Name Found: %s", password)
129      return true, creds.Account:new((self.options['ohost'] or "Node Name"), password, creds.State.VALID)
130    end
131    return false, brute.Error:new( "Invalid Node Name" )
132  end,
133}
134
135-- Checks string to see if it follows node naming limitations
136local valid_name = function(x)
137  local patt = "[%w@#%$]"
138  return (string.len(x) <= 8 and string.match(x,patt))
139end
140
141function iter(t)
142  local i, val
143  return function()
144    i, val = next(t, i)
145    return val
146  end
147end
148
149action = function( host, port )
150  -- Oftentimes the LPAR will be one of the subdomain of a system.
151  local names = host.name and stringaux.strsplit("%.", host.name) or {}
152  local o_host = stdnse.get_script_args('nje-node-brute.ohost') or nil
153  local options = {}
154  if o_host then options = { ohost = o_host:upper() } end
155  if host.targetname then
156    host.targetname:gsub("[^.]+", function(n) table.insert(names, n) end)
157  end
158  local filename = stdnse.get_script_args('nje-node-brute.hostlist')
159  filename = (filename and nmap.fetchfile(filename) or filename) or
160    nmap.fetchfile("nselib/data/vhosts-default.lst")
161  for l in io.lines(filename) do
162    if not l:match("#!comment:") then
163      table.insert(names, l)
164    end
165  end
166  if o_host then stdnse.verbose(2,'RHOST Mode, using OHOST: %s', o_host:upper()) end
167  local engine = brute.Engine:new(Driver, host, port, options)
168  local nodes = unpwdb.filter_iterator(iter(names), valid_name)
169  engine.options:setOption("passonly", true )
170  engine:setPasswordIterator(nodes)
171  engine.options.script_name = SCRIPT_NAME
172  engine.options:setTitle("Node Name(s)")
173  local status, result = engine:start()
174  return result
175end
176