description = [[
Guesses Oracle instance/sid names against the TNS-listener
]]

--
-- @usage
-- nmap --script=oracle-sid-brute --script-args=oraclesids=/path/to/sidfile -p 1521-1560 <host>
-- nmap --script=oracle-sid-brute -p 1521-1560 <host>
--
-- If no oraclesids file is specified, it falls back to the default oracle-sids file
-- License to use the oracle-sids file was granted by the author (Alexander Kornbrust) 
-- Ref: http://seclists.org/nmap-dev/2009/q4/645
--
-- @output
-- PORT     STATE SERVICE REASON
-- 1521/tcp open  oracle  syn-ack
-- | oracle-sid-brute:  
-- |   orcl
-- |   prod
-- |_  devel
--
---

-- Version 0.3

-- Created 12/10/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 12/11/2009 - v0.2 - Added tns_type, split packet creation to header & data
-- Revised 12/14/2009 - v0.3 - Fixed ugly file_exist kludge

author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"intrusive", "auth"}

require 'comm'
require 'datafiles'
require 'shortport'

portrule = shortport.port_or_service(1521, 'oracle-tns')

-- A table containing the different TNS types ... not complete :)
local tns_type = {CONNECT=1, REFUSE=4, REDIRECT=5, RESEND=11}

--- Creates a TNS header
-- A lot of values are still hardcoded ...
--
-- @param packetType string containing the type of TNS packet
-- @param packetLength number defining the length of the DATA segment of the packet
--
-- @return string with the raw TNS header
--
local function create_tns_header(packetType, packetLength)

	local request = bin.pack( ">SSCCS", 
					packetLength + 34, -- Packet Length
					0, -- Packet Checksum
					tns_type[packetType], -- Packet Type
					0, -- Reserved Byte
					0 -- Header Checksum
					)
  
	return request

end

--- Creates a TNS connect packet
--
-- @param host_ip string containing the IP of the remote host
-- @param port_no number containing the remote port of the Oracle instance
-- @param sid string containing the SID against which to attempt to connect
--
-- @return string containing the raw TNS packet
--
local function create_connect_packet( host_ip, port_no, sid )
	
	local connect_data =  "(DESCRIPTION=(CONNECT_DATA=(SID=" .. sid .. ")"
	connect_data = connect_data .. "(CID=(PROGRAM=)(HOST=__jdbc__)(USER=)))"
	connect_data = connect_data .. "(ADDRESS=(PROTOCOL=tcp)(HOST=" .. host_ip .. ")"
	connect_data = connect_data .. "(PORT=" .. port_no .. ")))"
	
	local data = bin.pack(">SSSSSSSSSSICCA",
							308, -- Version
							300, -- Version (Compatibility)
							0, -- Service Options
							2048, -- Session Data Unit Size
							32767, -- Maximum Transmission Data Unit Size
							20376, -- NT Protocol Characteristics
							0, -- Line Turnaround Value
							1, -- Value of 1 in Hardware
							connect_data:len(), -- Length of connect data
							34, -- Offset to connect data
							0, -- Maximum Receivable Connect Data
							1, -- Connect Flags 0
							1, -- Connect Flags 1 
							connect_data
							)
	

	local header = create_tns_header("CONNECT", connect_data:len() )

	return header .. data
	
end

--- Process a TNS response and extracts Length, Checksum and Type
--
-- @param packet string as a raw TNS response 
-- @return table with Length, Checksum and Type set
--
local function process_tns_packet( packet )

	local tnspacket = {}
	
	-- just pull out the bare minimum to be able to match
	_, tnspacket.Length, tnspacket.Checksum, tnspacket.Type = bin.unpack(">SSC", packet)
	
	return tnspacket
	
end

action = function(host, port)

	local found_sids = {}
	local socket = nmap.new_socket()
	local catch = function() socket:close() end
  	local try = nmap.new_try(catch)
	local request, response, tns_packet
	local sidfile
	
	socket:set_timeout(5000)

	-- open the sid file specified by the user or fallback to the default oracle-sids file
	sidfilename = nmap.registry.args.oraclesids or nmap.fetchfile("nselib/data/oracle-sids")
	
	sidfile = io.open(sidfilename)
	
	if not sidfile then
		return
	end
	
	-- read sids line-by-line from the sidfile
	for sid in sidfile:lines() do
		
		-- check for comments
		if not sid:match("#!comment:") then

			try(socket:connect(host.ip, port.number))
			request = create_connect_packet( host.ip, port.number, sid )
			try(socket:send(request))
			response = try(socket:receive_bytes(1))
			tns_packet = process_tns_packet(response)

			-- If we get anything other than REFUSE consider it as a valid SID
			if tns_packet.Type ~= tns_type.REFUSE then
				table.insert(found_sids, sid)
			end

			try(socket:close())
		
		end

	end

	sidfile:close()

	return stdnse.format_output(true, found_sids)

end
