local io = require "io"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"

description = [[
Guesses Oracle instance/SID names against the TNS-listener.

If the <code>oraclesids</code> script argument is not used to specify an
alternate file, the default <code>oracle-sids</code> file will be used.
License to use the <code>oracle-sids</code> file was granted by its
author, Alexander Kornbrust (http://seclists.org/nmap-dev/2009/q4/645).
]]

---
-- @args oraclesids A file containing SIDs to try.
--
-- @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>
--
-- @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 https://nmap.org/book/man-legal.html"
categories = {"intrusive", "brute"}


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 = string.pack( ">I2 I2 BB I2",
    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 = string.format(
    "(DESCRIPTION=(CONNECT_DATA=(SID=%s)(CID=(PROGRAM=)(HOST=__jdbc__)(USER=)))\z
    (ADDRESS=(PROTOCOL=tcp)(HOST=%s)(PORT=%d)))", sid, host_ip, port_no)

  local data = string.pack(">I2 I2 I2 I2 I2 I2 I2 I2 I2 I2 I4 BB",
    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 = string.unpack(">I2I2B", 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
  local 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, port))
      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
