description = [[ Attempts to extract information from IBM DB2 Server instances. The script sends a DB2 EXCSAT (exchange server attributes) command packet and parses the response. ]] -- rev 1.3 (2009-12-16) author = "Tom Sellers" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"safe", "discovery", "version"} require "stdnse" require "shortport" portrule = shortport.port_or_service({50000,60000},"ibm-db2", "tcp", {"open", "open|filtered"}) -- This function processes a section of the EXCSAT response packet --@param response This is the data returned from the server as a result of the client query. --@param position This is the position within the response that this function will start processing from. --@param ebcdic2ascii This is a table containing a conversion chart for returning the ASCII value of EBCDIC encoded HEX value. --@return section_length This is the length of the currect section. Will be used to move position for processing next section. --@return data_string This string contains the data pulled from this section of the server response. local function process_block(response, position, ebcdic2ascii) -- This fuction assumes that the current position is the start of a section within -- the DB2 EXCSAT response packet -- Get the length of this section of the response packet local section_length = string.format("%d",string.byte(response,position +1)) .. string.format("%d",string.byte(response,position + 2)) position = position + 2 -- locate the data string and convert it from EBCDIC to ASCII local i = 0 local data_string = "" for i = (position + 3),(position + section_length -2 ),1 do -- stdnse.print_debug("%s","INFO: Current postion (i) = " .. i) -- stdnse.print_debug("%s","INFO: Hex value = " .. string.format("%x",string.byte(response,i))) -- stdnse.print_debug("%s","INFO: Current data_string = " .. data_string) if string.format("%x",string.byte(response,i)) == "0" then break end data_string = data_string .. ebcdic2ascii[string.format("%x",string.byte(response,i))] end return section_length, data_string end -- fuction process_block action = function(host, port) local ebcdic2ascii = { -- The following reference was used for this table: http://www.simotime.com/asc2ebc1.htm ["00"] = string.format("%c", 00), ["40"] = " ", ["81"] = "a", ["82"] = "b", ["83"] = "c", ["84"] = "d", ["85"] = "e", ["86"] = "f", ["87"] = "g", ["88"] = "h", ["89"] = "i", ["91"] = "j", ["92"] = "k", ["93"] = "l", ["94"] = "m", ["95"] = "n", ["96"] = "o", ["97"] = "p", ["98"] = "q", ["99"] = "r", ["a2"] = "s", ["a3"] = "t", ["a4"] = "u", ["a5"] = "v", ["a6"] = "w", ["a7"] = "x", ["a8"] = "y", ["a9"] = "z", ["c1"] = "A", ["c2"] = "B", ["c3"] = "C", ["c4"] = "D", ["c5"] = "E", ["c6"] = "F", ["c7"] = "G", ["c8"] = "H", ["c9"] = "I", ["d1"] = "J", ["d2"] = "K", ["d3"] = "L", ["d4"] = "M", ["d5"] = "N", ["d6"] = "O", ["d7"] = "P", ["d8"] = "Q", ["d9"] = "R", ["e2"] = "S", ["e3"] = "T", ["e4"] = "U", ["e5"] = "V", ["e6"] = "W", ["e7"] = "X", ["e8"] = "Y", ["e9"] = "Z", ["f0"] = 0, ["f1"] = 1, ["f2"] = 2, ["f3"] = 3, ["f4"] = 4, ["f5"] = 5, ["f6"] = 6, ["f7"] = 7, ["f8"] = 8, ["f9"] = 9, ["4b"] = ".", ["4c"] = "<", ["4d"] = "(", ["4e"] = "+", ["4f"] = "|", ["5a"] = "!", ["5b"] = "$", ["5c"] = "*", ["5d"] = ")", ["5e"] = ";", ["60"] = "-", ["61"] = "/", ["6b"] = ",", ["6c"] = "%", ["6d"] = "_", ["6e"] = ">", ["6f"] = "?", ["79"] = "`", ["7a"] = ":", ["7b"] = "#", ["7c"] = "@", ["7d"] = "'", ["7e"] = "=", ["7f"] = "\"", ["a1"] = "~", ["ba"] = "[", ["bb"] = "]", ["c0"] = "{", ["d0"] = "}", ["e0"] = "\\" -- escape the \ character } -- ebcdic2ascii does not contain all value, set a default value -- to improve stability. setmetatable(ebcdic2ascii, { __index = function() return " " end }) -- create the socket used for our connection local socket = nmap.new_socket() -- set a reasonable timeout value socket:set_timeout(10000) -- do some exception handling / cleanup local catch = function() stdnse.print_debug("%s", "db2-info: ERROR communicating with " .. host.ip .. " on port " .. port.number .. "/" .. port.protocol) socket:close() end local try = nmap.new_try(catch) try(socket:connect(host.ip, port.number, "tcp")) -- Build DB2 EXCSAT (exchange server attributes) command packet local query = string.char(0x00, 0x98, 0xd0, 0x41, 0x00, 0x01, 0x00, 0x92, 0x10, 0x41) -- Header -- NOTE: The server's response packet is in the same format at the client query packet being built -- in the section below. -- External Name section: first two bytes (00,48) are section length in HEX, next bytes (11,5e) are section identifier for External Name -- In this packet the external name is 'db2jcc_application JCC03570300' encoded in EBCDIC query = query .. string.char(0x00, 0x48, 0x11, 0x5e, 0x84, 0x82, 0xf2, 0x91, 0x83, 0x83, 0x6d, 0x81, 0x97, 0x97, 0x93, 0x89) query = query .. string.char(0x83, 0x81, 0xa3, 0x89, 0x96, 0x95, 0x40, 0x40, 0xd1, 0xc3, 0xc3, 0xf0, 0xf3, 0xf5, 0xf7, 0xf0) query = query .. string.char(0xf3, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) query = query .. string.char(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) query = query .. string.char(0x00, 0x00, 0x00, 0x60, 0xf0, 0xf0, 0xf0, 0xf1) -- Client Name section: first two bytes (00,16) are section length in HEX, next two bytes (11,6d) are section identifier for Server Name -- In the request packet Server Name = client name. The value here is all spaces, encoded in EBCDIC query = query .. string.char(0x00, 0x16, 0x11, 0x6d, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40) query = query .. string.char(0x40, 0x40, 0x40, 0x40, 0x40, 0x40) -- Product Release Level section: This section is not as important in the client query as it is in the server response. -- The first two bytes (00,0c) are section length in HEX, next two bytes (11,5a) are section identifier for Product Release Level -- The value here is 'JCC03570' encoded in EBCDIC query = query .. string.char(0x00, 0x0c, 0x11, 0x5a, 0xd1, 0xc3, 0xc3, 0xf0, 0xf3, 0xf5, 0xf7, 0xf0) -- Manager level section: first two bytes (00,18) are section length in HEX, next two bytes (14,04) are section identifier for Manager-Level List query = query .. string.char(0x00, 0x18, 0x14, 0x04, 0x14, 0x03, 0x00, 0x07, 0x24, 0x07, 0x00, 0x0a, 0x24, 0x0f, 0x00, 0x08) query = query .. string.char(0x14, 0x40, 0x00, 0x09, 0x14, 0x74, 0x00, 0x08) -- Server Class section: first two bytes (00,0c) are section length in HEX, next two bytes (11,47) are section identifier for Server Class Name -- This section is essentially platform software information. The value here is 'QDB2/JBM' encoded in EBCDIC) query = query .. string.char(0x00, 0x0c, 0x11, 0x47, 0xd8, 0xc4, 0xc2, 0xf2, 0x61, 0xd1, 0xe5, 0xd4) -- Access Security section query = query .. string.char(0x00, 0x26, 0xd0, 0x01, 0x00, 0x02, 0x00, 0x20, 0x10, 0x6d, 0x00, 0x06, 0x11, 0xa2, 0x00, 0x03) -- Database name section: This is the client's query for a specific database. A DB2 default database name, 'db2insta1', was chosen. -- It is encoded below in EBCDIC. The first two bytes (00,16) are section length in HEX, next two bytes (21,10) are section identifier -- for Relational Database Name query = query .. string.char(0x00, 0x16, 0x21, 0x10, 0x84, 0x82, 0xf2, 0x89, 0x95, 0xa2, 0xa3, 0xf1, 0x40, 0x40, 0x40, 0x40) query = query .. string.char(0x40, 0x40, 0x40, 0x40, 0x40, 0x40) try(socket:send(query)) local status local response -- read in any response we might get status, response = socket:receive() socket:close() if (not status) or (response == "TIMEOUT") or (response == nil) then stdnse.print_debug("%s","db2-info: ERROR: No data, ending communications with " .. host.ip .. ":" .. port.number .. "/" .. port.protocol) return end local position = 0 -- Check to see if the data is actually a DB2 DDM EXCSAT response. -- 0d in the 3rd byte of the data section seems to be a reliable test. if string.format("%x",string.byte(response,position + 3)) ~= "d0" then return end local bytes = " " local len_response = string.len(response) - 2 -- Parse response until the EXCSAT identifier is found. From here we should -- be able to find everything else. while (bytes ~= "1443") and (position <= len_response) do bytes = string.format("%x",string.byte(response,position +1)) .. string.format("%x",string.byte(response,position + 2)) if bytes == nil then return end position = position + 2 end if position >= len_response then -- If this section is true then this either not a valid response or -- it is in a format that we have not seen. Exit cleanly. return end -- **************************************************************************** -- Process the Server class section of the response packet -- **************************************************************************** local len_external_name, external_name = process_block(response, position, ebcdic2ascii) -- **************************************************************************** -- Process the Manager Level section of the response packet -- **************************************************************************** -- Move the position to the beginning of the current section position = position + len_external_name -- Get the length of the next block, Wireshark calls this "Manager-Level list" -- We are going to skip over this section local len_manager_level = string.format("%d",string.byte(response, position +1)) .. string.format("%d",string.byte(response,position + 2)) -- **************************************************************************** -- Process the Server class section of the response packet -- **************************************************************************** -- Move the position to the beginning of the current section position = position + len_manager_level local len_server_class, server_class = process_block(response, position, ebcdic2ascii) -- **************************************************************************** -- Process the Server name section of the response packet -- **************************************************************************** -- Move the position to the beginning of the current section position = position + len_server_class local len_server_name, server_name = process_block(response, position, ebcdic2ascii) -- **************************************************************************** -- Process the Server version section of the response packet -- **************************************************************************** -- Move the position to the beginning of the current section position = position + len_server_name local len_server_version, server_version = process_block(response, position, ebcdic2ascii) if string.sub(server_version,1,3) == "SQL" then local major_version = string.sub(server_version,4,5) -- strip the leading 0 from the major version, for consistency with -- nmap-service-probes results if string.sub(major_version,1,1) == "0" then major_version = string.sub(major_version,2) end local minor_version = string.sub(server_version,6,7) local hotfix = string.sub(server_version,8) server_version = major_version .. "." .. minor_version .. "." .. hotfix end -- Try to determine which of the two values (probe version vs script) has more -- precision. A couple DB2 versions send DB2 UDB 7.1 vs SQL090204 (9.02.04) local _ local current_count = 0 if port.version.version ~= nil then _, current_count = string.gsub(port.version.version, "%.", "%.") end local new_count = 0 if server_version ~= nil then _, new_count = string.gsub(server_version, "%.", "%.") end if current_count < new_count then port.version.version = server_version end -- Set port information port.version.name = "ibm-db2" port.version.product = "IBM DB2 Database Server" port.version.name_confidence = 100 nmap.set_port_state(host, port, "open") if server_class ~= nil then port.version.extrainfo = server_class end nmap.set_port_version(host, port, "hardmatched") -- Generate results local results = "DB2 Version: " .. server_version .. "\n" results = results .. "Server Platform: " .. server_class .. "\n" results = results .. "Instance Name: " .. server_name .. "\n" results = results .. "External Name: " .. external_name return results end