180 lines
6.3 KiB
Lua

local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local stringaux = require "stringaux"
local tab = require "tab"
local table = require "table"
description = [[
Retrieves version and database information from a SAP Max DB database.
]]
---
-- @usage
-- nmap -p 7210 --script maxdb-info <ip>
--
-- @output
-- PORT STATE SERVICE REASON
-- 7210/tcp open maxdb syn-ack
-- | maxdb-info:
-- | Version: 7.8.02
-- | Build: DBMServer 7.8.02 Build 021-121-242-175
-- | OS: UNIX
-- | Instroot: /opt/sdb/MaxDB
-- | Sysname: Linux 3.0.0-12-generic #20-Ubuntu SMP Fri Oct 7 14:56:25 UTC 2011
-- | Databases
-- | instance path version kernel state
-- | MAXDB /opt/sdb/MaxDB 7.8.02.21 fast running
-- | MAXDB /opt/sdb/MaxDB 7.8.02.21 quick offline
-- | MAXDB /opt/sdb/MaxDB 7.8.02.21 slow offline
-- |_ MAXDB /opt/sdb/MaxDB 7.8.02.21 test offline
--
author = "Patrik Karlsson"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = { "default", "version", "safe" }
portrule = shortport.version_port_or_service(7210, "maxdb", "tcp")
-- Sends and receive a MaxDB packet
-- @param socket already connected to the server
-- @param packet string containing the data to send
-- @return status true on success, false on failure
-- @return data string containing the raw response from the server
local function exchPacket(socket, packet)
local status, err = socket:send(packet)
if ( not(status) ) then
stdnse.debug2("Failed to send packet to server")
return false, "Failed to send packet to server"
end
local data
status, data= socket:receive()
if ( not(status) ) then
stdnse.debug2("Failed to read packet from server")
return false, "Failed to read packet from server"
end
local len = string.unpack("<I2", data)
-- make sure we've got it all
if ( len ~= #data ) then
local tmp
status, tmp = socket:receive_bytes(len - #data)
if ( not(status) ) then
stdnse.debug2("Failed to read packet from server")
return false, "Failed to read packet from server"
end
data = data .. tmp
end
return true, data
end
-- Sends and receives a MaxDB command and does some very basic checks of the
-- response.
-- @param socket already connected to the server
-- @param packet string containing the data to send
-- @return status true on success, false on failure
-- @return data string containing the raw response from the server
local function exchCommand(socket, packet)
local status, data = exchPacket(socket, packet)
if( status ) then
if ( #data < 26 ) then
return false, "Response to short"
end
if ( "OK" ~= data:sub(25, 26) ) then
return false, "Incorrect response from server (no OK found)"
end
end
return status, data
end
-- Parses and decodes the raw version response from the server
-- @param data string containing the raw response
-- @return version_info table containing a number of dynamic fields based on
-- the response from the server. The fields typically include:
-- <code>VERSION</code>, <code>BUILD</code>, <code>OS</code>,
-- <code>INSTROOT</code>,<code>LOGON</code>, <code>CODE</code>,
-- <code>SWAP</code>, <code>UNICODE</code>, <code>INSTANCE</code>,
-- <code>SYSNAME</code>, <code>MASKING</code>,
-- <code>REPLYTREATMENT</code> and <code>SDBDBM_IPCLOCATION</code>
local function parseVersion(data)
local version_info = {}
if ( #data > 27 ) then
for _, line in ipairs(stringaux.strsplit("\n", data:sub(28))) do
local key, val = line:match("^(%S+)%s-=%s(.*)%s*$")
if ( key ) then version_info[key] = val end
end
end
return version_info
end
-- Parses and decodes the raw database response from the server
-- @param data string containing the raw response
-- @return result string containing a table of database instance information
local function parseDatabases(data)
local result = tab.new(5)
tab.addrow(result, "instance", "path", "version", "kernel", "state")
for _, line in ipairs(stringaux.strsplit("\n", data:sub(28))) do
local cols = {}
cols.instance, cols.path, cols.ver, cols.kernel,
cols.state = line:match("^(.-)%s*\t(.-)%s*\t(.-)%s*\t(.-)%s-\t(.-)%s-$")
if ( cols.instance ) then
tab.addrow(result, cols.instance, cols.path, cols.ver, cols.kernel, cols.state)
end
end
return tab.dump(result)
end
local function fail (err) return stdnse.format_output(false, err) end
action = function(host, port)
-- this could really be more elegant, but it has to do for now
local handshake = "5a000000035b000001000000ffffffff000004005a000000000242000409000000400000d03f00000040000070000000000500000004000000020000000300000749343231360004501c2a035201037201097064626d73727600"
local dbm_version = "28000000033f000001000000ac130000000004002800000064626d5f76657273696f6e2020202020"
local db_enum = "20000000033f000001000000ac130000000004002000000064625f656e756d20"
local socket = nmap.new_socket()
socket:set_timeout(10000)
local status, err = socket:connect(host, port)
local data
status, data = exchPacket(socket, stdnse.fromhex( handshake))
if ( not(status) ) then
return fail("Failed to perform handshake with MaxDB server")
end
status, data = exchPacket(socket, stdnse.fromhex( dbm_version))
if ( not(status) ) then
return fail("Failed to request version information from server")
end
local version_info = parseVersion(data)
if ( not(version_info) ) then
return fail("Failed to parse version information from server")
end
local result, filter = {}, {"Version", "Build", "OS", "Instroot", "Sysname"}
for _, f in ipairs(filter) do
table.insert(result, ("%s: %s"):format(f, version_info[f:upper()]))
end
status, data = exchCommand(socket, stdnse.fromhex( db_enum))
socket:close()
if ( not(status) ) then
return fail("Failed to request version information from server")
end
local dbs = parseDatabases(data)
table.insert(result, { name = "Databases", dbs } )
-- set the version information
port.version.name = "maxdb"
port.version.product = "SAP MaxDB"
port.version.version = version_info.VERSION
port.version.ostype = version_info.SYSNAME
nmap.set_port_version(host, port)
return stdnse.format_output(true, result)
end