local bin = require "bin" local nmap = require "nmap" local shortport = require "shortport" local stdnse = require "stdnse" local string = require "string" description = [[ Connects to the IBM DB2 Administration Server (DAS) on TCP or UDP port 523 and exports the server profile. No authentication is required for this request. The script will also set the port product and version if a version scan is requested. ]] -- rev 1.1 (2010-01-28) --- -- @output -- PORT STATE SERVICE VERSION -- 523/tcp open ibm-db2 IBM DB2 Database Server 9.07.0 -- | db2-das-info: DB2 Administration Server Settings -- | ;DB2 Server Database Access Profile -- | ;Use BINARY file transfer -- | ;Comment lines start with a ";" -- | ;Other lines must be one of the following two types: -- | ;Type A: [section_name] -- | ;Type B: keyword=value -- | -- | [File_Description] -- | Application=DB2/LINUX 9.7.0 -- | Platform=18 -- | File_Content=DB2 Server Definitions -- | File_Type=CommonServer -- | File_Format_Version=1.0 -- | DB2System=MYBIGDATABASESERVER -- | ServerType=DB2LINUX -- | -- | [adminst>dasusr1] -- | NodeType=1 -- | DB2Comm=TCPIP -- | Authentication=SERVER -- | HostName=MYBIGDATABASESERVER -- | PortNumber=523 -- | IpAddress=127.0.1.1 -- | -- | [inst>db2inst1] -- | NodeType=1 -- | DB2Comm=TCPIP -- | Authentication=SERVER -- | HostName=MYBIGDATABASESERVER -- | ServiceName=db2c_db2inst1 -- | PortNumber=50000 -- | IpAddress=127.0.1.1 -- | QuietMode=No -- | TMDatabase=1ST_CONN -- | -- | [db>db2inst1:TOOLSDB] -- | DBAlias=TOOLSDB -- | DBName=TOOLSDB -- | Drive=/home/db2inst1 -- | Dir_entry_type=INDIRECT -- |_Authentication=NOTSPEC author = "Patrik Karlsson, Tom Sellers" license = "Same as Nmap--See https://nmap.org/book/man-legal.html" categories = {"safe", "discovery", "version"} --- Research Notes: -- -- Little documentation on the protocol used to communicate with the IBM DB2 Admin Server -- service exists. The packets and methods here were developed based on data captured -- in the wild. Interviews with knowledgeable individuals indicates that the following -- information can be used to recreate the traffic. -- -- Requirements: -- IBM DB2 Administrative Server (DAS) version >= 7.x instance, typically on port 523 tcp or udp -- IBM DB2 Control Center (Java application, workings on Linux, Windows, etc) -- -- Steps to reproduce: -- Ensure network connectivity from test host to DB2 DAS instance on 523 -- In the Control Center, right click on All Systems and click Add -- Enter the DB2 server IP or hostname in the System Name field and click OK -- Start packet capture -- Under All Systems right click on your DB2 server, choose export profile, enter file location, click OK -- Stop packet capture -- -- Details on how to reproduce these steps with the CLI are welcome. portrule = shortport.version_port_or_service({523}, nil, {"tcp","udp"}, {"open", "open|filtered"}) --- Extracts the server profile from an already parsed db2 packet -- -- This function assumes that the data contains the server profile and does -- no attempts to verify whether it does or not. The response from the function -- is simply a substring starting at offset 37. -- -- @param data string containing the "info" section as parsed by parse_db2_packet -- @return string containing the complete server profile function extract_server_profile(data) local server_profile_offset = 37 if server_profile_offset > data:len() then return end return data:sub(server_profile_offset) end --- Does *very* basic parsing of a DB2 packet -- -- Due to the limited documentation of the protocol this function is guesswork -- The section called info is essentially the data part of the db2das data response -- The length of this section is found at offset 158 in the db2das.data section -- -- -- @param packet table as returned from read_db2_packet -- @return table with parsed data function parse_db2_packet(packet) local info_length_offset = 158 local info_offset = 160 local version_offset = 97 local response = {} if packet.header.data_len < info_length_offset then stdnse.debug1("packet too short to be DB2 response...") return end local _, len = bin.unpack(">S", packet.data:sub(info_length_offset, info_length_offset + 1)) _, response.version = bin.unpack("z", packet.data:sub(version_offset) ) response.info_length = len - 4 response.info = packet.data:sub(info_offset, info_offset + response.info_length - (info_offset-info_length_offset)) if(nmap.debugging() > 3) then stdnse.debug1("version: %s", response.version) stdnse.debug1("info_length: %d", response.info_length) stdnse.debug1("response.info:len(): %d", response.info:len()) end return response end --- Reads a DB2 packet from the socket -- -- Due to the limited documentation of the protocol this function is guesswork -- The first 41 bytes of the db2das response are considered to be the header -- The bytes following the header are considered to be the data -- -- Offset 38 of the header contains an integer with the length of the data section -- The length of the data section can unfortunately be of either endianness -- There's -- -- @param socket connected to the server -- @return table with header and data function read_db2_packet(socket) local packet = {} local header_len = 41 local total_len = 0 local buf local DATA_LENGTH_OFFSET = 38 local ENDIANESS_OFFSET = 23 local catch = function() stdnse.debug1("ERROR communicating with DB2 server") socket:close() end local try = nmap.new_try(catch) packet.header = {} buf = try( socket:receive_bytes(header_len) ) packet.header.raw = buf:sub(1, header_len) if packet.header.raw:sub(1, 10) == "\x00\x00\x00\x00\x44\x42\x32\x44\x41\x53" then stdnse.debug1("Got DB2DAS packet") local _, endian = bin.unpack( "A2", packet.header.raw, ENDIANESS_OFFSET ) if endian == "9z" then _, packet.header.data_len = bin.unpack("I", packet.header.raw, DATA_LENGTH_OFFSET ) else _, packet.header.data_len = bin.unpack(">I", packet.header.raw, DATA_LENGTH_OFFSET ) end total_len = header_len + packet.header.data_len if(nmap.debugging() > 3) then stdnse.debug1("data_len: %d", packet.header.data_len) stdnse.debug1("buf_len: %d", buf:len()) stdnse.debug1("total_len: %d", total_len) end -- do we have all data as specified by data_len? while total_len > buf:len() do -- if not read additional bytes if(nmap.debugging() > 3) then stdnse.debug1("Reading %d additional bytes", total_len - buf:len()) end local tmp = try( socket:receive_bytes( total_len - buf:len() ) ) if(nmap.debugging() > 3) then stdnse.debug1("Read %d bytes", tmp:len()) end buf = buf .. tmp end packet.data = buf:sub(header_len + 1) else stdnse.debug1("Unknown packet, aborting ...") return end return packet end --- Sends a db2 packet table over the wire -- -- @param socket already connected to the server -- @param packet table as returned from create_das_packet -- function send_db2_packet( socket, packet ) local catch = function() stdnse.debug1("ERROR communicating with DB2 server") socket:close() end local try = nmap.new_try(catch) local buf = packet.header.raw .. packet.data try( socket:send(buf) ) end --- Creates a db2 packet table using the magic byte and data -- -- The function returns a db2 packet table: -- packet.header - contains header specific values -- packet.header.raw - contains the complete un-parsed header (string) -- packet.header.data_len - contains the length of the data block -- packet.data - contains the complete un-parsed data block (string) -- -- @param magic byte containing a value of unknown function (could be type) -- @param data string containing the db2 packet data -- @return table as described above -- function create_das_packet( magic, data ) local packet = {} local data_len = data:len() packet.header = {} packet.header.raw = "\x00\x00\x00\x00\x44\x42\x32\x44\x41\x53\x20\x20\x20\x20\x20\x20" .. "\x01\x04\x00\x00\x00\x10\x39\x7a\x00\x05\x00\x00\x00\x00\x00\x00" .. "\x00\x00\x00\x00" .. bin.pack("C", magic) .. bin.pack("S", data_len) .. "\x00\x00" packet.header.data_len = data_len packet.data = data return packet end action = function(host, port) -- 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.debug1("ERROR communicating with " .. host.ip .. " on port " .. port.number) socket:close() end local try = nmap.new_try(catch) try(socket:connect(host, port)) local query -- ************************************************************************************ -- Transaction block 1 -- ************************************************************************************ local data = "\x00\x00\x00\x0d\x00\x00\x00\x0c\x00\x00\x00\x4a\x00" --try(socket:send(query)) local db2packet = create_das_packet(0x02, data) send_db2_packet( socket, db2packet ) read_db2_packet( socket ) -- ************************************************************************************ -- Transaction block 2 -- ************************************************************************************ data = "\x00\x00\x00\x2c\x00\x00\x00" .. "\x0c\x00\x00\x00\x08\x59\xe7\x1f\x4b\x79\xf0\x90\x72\x85\xe0\x8f" .. "\x3e\x38\x45\x38\xe3\xe5\x12\xc4\x3b\xe9\x7d\xe2\xf5\xf0\x78\xcc" .. "\x81\x6f\x87\x5f\x91" db2packet = create_das_packet(0x05, data) send_db2_packet( socket, db2packet ) read_db2_packet( socket ) -- ************************************************************************************ -- Transaction block 3 -- ************************************************************************************ data = "\x00\x00\x00\x0d\x00\x00\x00\x0c\x00\x00\x00\x4a\x01\x00\x00\x00" .. "\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\xff\xff\xff\xff\x00\x00\x00" .. "\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x04\xb8\x64\x62\x32" .. "\x64\x61\x73\x4b\x6e\x6f\x77\x6e\x44\x73\x63\x76\x00\x00\x00\x00" .. "\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x04\xb8\x64\x62\x32" .. "\x4b\x6e\x6f\x77\x6e\x44\x73\x63\x76\x53\x72\x76\x00" db2packet = create_das_packet(0x0a, data) send_db2_packet( socket, db2packet ) read_db2_packet( socket ) -- ************************************************************************************ -- Transaction block 4 -- ************************************************************************************ data = "\x00\x00\x00\x0d\x00\x00\x00\x0c\x00\x00\x00\x4a\x01\x00\x00\x00" .. "\x20\x00\x00\x00\x0c\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x03" .. "\x48\x00\x00\x00\x00\x4a\xfb\x42\x90\x00\x00\x24\x93\x00\x00\x00" .. "\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\xff\xff\xff\xff\x00\x00\x00" .. "\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\xff\xff\xff\xff\x00\x00\x00" .. "\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x04\xb8\x64\x62\x32" .. "\x4b\x6e\x6f\x77\x6e\x44\x73\x63\x76\x53\x72\x76\x00\x00\x00\x00" .. "\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x04\xb8\x64\x62\x32" .. "\x64\x61\x73\x4b\x6e\x6f\x77\x6e\x44\x73\x63\x76\x00\x00\x00\x00" .. "\x0c\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x00\x10\x00\x00\x00" .. "\x0c\x00\x00\x00\x4c\xff\xff\xff\xff\x00\x00\x00\x10\x00\x00\x00" .. "\x0c\x00\x00\x00\x4c\xff\xff\xff\xff\x00\x00\x00\x11\x00\x00\x00" .. "\x0c\x00\x00\x00\x04\x00\x00\x04\xb8\x00" db2packet = create_das_packet(0x06, data) send_db2_packet( socket, db2packet ) data = "\x00\x00\x00\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00" .. "\x00\x04\xb8\x64\x62\x32\x64\x61\x73\x4b\x6e\x6f\x77\x6e\x44\x73" .. "\x63\x76\x00\x00\x00\x00\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00" .. "\x00\x04\xb8\x64\x62\x32\x4b\x6e\x6f\x77\x6e\x44\x73\x63\x76\x53" .. "\x72\x76\x00\x00\x00\x00\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\x00" .. "\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\x00" .. "\x00\x00\x01\x00\x00\x00\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\x00" .. "\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x0c\x00\x00\x00\x08\x00" .. "\x00\x00\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\x00\x00\x00\x01\x00" .. "\x00\x00\x18\x00\x00\x00\x0c\x00\x00\x00\x08\x00\x00\x00\x0c\x00" .. "\x00\x00\x0c\x00\x00\x00\x18" db2packet = create_das_packet(0x06, data) send_db2_packet( socket, db2packet ) local packet = read_db2_packet( socket ) local db2response = parse_db2_packet(packet) socket:close() -- The next block of code is essentially the version extraction code from db2-info.nse local server_version if string.sub(db2response.version,1,3) == "SQL" then local major_version = string.sub(db2response.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(db2response.version,6,7) local hotfix = string.sub(db2response.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 local result = false local db2profile = extract_server_profile( db2response.info ) if (db2profile ~= nil ) then result = "DB2 Administration Server Settings\r\n" .. extract_server_profile( db2response.info ) -- Set port information port.version.name = "ibm-db2" port.version.product = "IBM DB2 Database Server" port.version.name_confidence = 10 nmap.set_port_version(host, port) nmap.set_port_state(host, port, "open") end return result end