diff --git a/scripts/modbus-discover.nse b/scripts/modbus-discover.nse index 705b03866..24fb7b6dc 100644 --- a/scripts/modbus-discover.nse +++ b/scripts/modbus-discover.nse @@ -1,5 +1,15 @@ description = [[ - Attempts to find valid sid for tcp modbus server and try to get device info. +Enumerates Modbus slave ids (sids) and gets their device information. + +Modbus is one of the popular SCADA protocols. This script does Modbus device +information disclosure. It tries to find legal sids (slave ids) of Modbus +devices and to get additional information about the vendor and firmware. This +script is improvement of modscan python utility written by Mark Bristow. + +Information about MODBUS protocol and security issues: +* MODBUS application protocol specification: http://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf +* Defcon 16 Modscan presentation: https://www.defcon.org/images/defcon-16/dc16-presentations/defcon-16-bristow.pdf +* Modscan utility is hosted at google code: http://code.google.com/p/modscan/ ]] --- @@ -16,22 +26,9 @@ description = [[ -- | SLAVE ID DATA: \xFA\xFFPM710PowerMeter -- | DEVICE IDENTIFICATION: Schneider Electric PM710 v03.110 -- |_ Positive error response for sid = 0x96 (GATEWAY TARGET DEVICE FAILED TO RESPONSE) --- + -- Version 0.2 - /12.12.10/ - script cleanup -- Version 0.3 - /13.12.10/ - several bugfixes --- --- MODBUS is one of the popular SCADA protocols. --- This script intended for modbus device information disclosure. --- It tries to find legal sid (slave id) of modbus device(s) and --- if success try to get additional information about vendor and firmware. --- This script is improvement of modscan python utility written by --- Mark Bristow. --- --- Information about MODBUS protocol and security issues: --- * MODBUS application protocol specification: http://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf --- * Defcon 16 Modscan presentation: https://www.defcon.org/images/defcon-16/dc16-presentations/defcon-16-bristow.pdf --- * Modscan utility is hosted at google code: http://code.google.com/p/modscan/ ---- author = "Alexander Rudakov" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" @@ -45,58 +42,58 @@ require "shortport" portrule = shortport.portnumber(502, "tcp") local form_rsid = function(sid, functionId, data) - local payload_len = 2 - if ( #data > 0 ) then - payload_len = payload_len + #data - end - return bin.pack('CCCCC', 0x00, 0x00, 0x00, 0x00, 0x00) .. bin.pack('C', payload_len) .. bin.pack('C', sid) .. bin.pack('C', functionId) .. data + local payload_len = 2 + if ( #data > 0 ) then + payload_len = payload_len + #data + end + return bin.pack('CCCCC', 0x00, 0x00, 0x00, 0x00, 0x00) .. bin.pack('C', payload_len) .. bin.pack('C', sid) .. bin.pack('C', functionId) .. data end discover_device_id_recursive = function(host, port, sid, start_id) - local rsid = form_rsid(sid, 0x2B, bin.pack('H', "0E 01")..bin.pack('C', start_id)) - local status, result = comm.exchange(host, port, rsid) - local objects_table = {} - if ( status and (#result >= 8)) then - local ret_code = string.byte(result, 8) - if ( ret_code == 0x2B and #result >= 15 ) then - local more_follows = string.byte(result, 12) - local next_object_id = string.byte(result, 13) - local number_of_objects = string.byte(result, 14) - stdnse.print_debug(1, ("more = 0x%x, next_id = 0x%x, obj_number = 0x%x"):format(more_follows, next_object_id, number_of_objects)) - local offset = 15 - for i = start_id, (number_of_objects - 1) do - local object_id = string.byte(result, offset) - local object_len = string.byte(result, offset + 1) - -- error data format -- - if object_len == nil then break end - local object_value = string.sub(result, offset + 2, offset + 1 + object_len) - stdnse.print_debug(1, ("Object id = 0x%x, value = %s"):format(object_id, object_value)) - table.insert(objects_table, object_id + 1, object_value) - offset = offset + 2 + object_len - end - if ( more_follows == 0xFF and next_object_id ~= 0x00 ) then - stdnse.print_debug(1, "Has more objects") - local recursive_table = discover_device_id_recursive(host, port, sid, next_object_id) - for k,v in pairs(recursive_table) do - table.insert(objects_table, k, v) - end - end - end - end - return objects_table + local rsid = form_rsid(sid, 0x2B, bin.pack('H', "0E 01")..bin.pack('C', start_id)) + local status, result = comm.exchange(host, port, rsid) + local objects_table = {} + if ( status and (#result >= 8)) then + local ret_code = string.byte(result, 8) + if ( ret_code == 0x2B and #result >= 15 ) then + local more_follows = string.byte(result, 12) + local next_object_id = string.byte(result, 13) + local number_of_objects = string.byte(result, 14) + stdnse.print_debug(1, ("more = 0x%x, next_id = 0x%x, obj_number = 0x%x"):format(more_follows, next_object_id, number_of_objects)) + local offset = 15 + for i = start_id, (number_of_objects - 1) do + local object_id = string.byte(result, offset) + local object_len = string.byte(result, offset + 1) + -- error data format -- + if object_len == nil then break end + local object_value = string.sub(result, offset + 2, offset + 1 + object_len) + stdnse.print_debug(1, ("Object id = 0x%x, value = %s"):format(object_id, object_value)) + table.insert(objects_table, object_id + 1, object_value) + offset = offset + 2 + object_len + end + if ( more_follows == 0xFF and next_object_id ~= 0x00 ) then + stdnse.print_debug(1, "Has more objects") + local recursive_table = discover_device_id_recursive(host, port, sid, next_object_id) + for k,v in pairs(recursive_table) do + table.insert(objects_table, k, v) + end + end + end + end + return objects_table end local discover_device_id = function(host, port, sid) - return discover_device_id_recursive(host, port, sid, 0x0) + return discover_device_id_recursive(host, port, sid, 0x0) end local form_device_id_string = function(device_table) local ret_string = "DEVICE IDENTIFICATION: " for i = 1, #device_table do - if ( device_table[i] ~= nil ) then - ret_string = ret_string..device_table[i] - if ( i < #device_table ) then ret_string = ret_string.." " end - end + if ( device_table[i] ~= nil ) then + ret_string = ret_string..device_table[i] + if ( i < #device_table ) then ret_string = ret_string.." " end + end end return ret_string end @@ -121,52 +118,52 @@ modbus_exception_codes = { } action = function(host, port) - local aggressive = false -- stop on first founded sid - - if (nmap.registry.args['modbus-discover.aggressive']) then - aggressive = nmap.registry.args['modbus-discover.aggressive'] - end - - local opts = {timeout=2000} - local results = {} - - for sid = 1, 246 do - stdnse.print_debug(3, "Sending command with sid = %d", sid) - rsid = form_rsid(sid, 0x11, "") + local aggressive = false -- stop on first founded sid - local status, result = comm.exchange(host, port, rsid, opts) - if ( status and (#result >= 8) ) then - local ret_code = string.byte(result, 8) - if ( ret_code == (0x11) or ret_code == (0x11 + 128) ) then - local sid_table = {} - if ret_code == (0x11) then - table.insert(results, ("Positive response for sid = 0x%x"):format(sid)) - local slave_id = extract_slave_id(result) - if ( slave_id ~= nil ) then table.insert(sid_table, "SLAVE ID DATA: "..slave_id) end - elseif ret_code == (0x11 + 128) then - local exception_code = string.byte(result, 9) - local exception_string = modbus_exception_codes[exception_code] - if ( exception_string == nil ) then exception_string = "UNKNOWN EXCEPTION" end - table.insert(results, ("Positive error response for sid = 0x%x (%s)"):format(sid, exception_string)) - end + if (nmap.registry.args['modbus-discover.aggressive']) then + aggressive = nmap.registry.args['modbus-discover.aggressive'] + end - local device_table = discover_device_id(host, port, sid) - if ( #device_table > 0 ) then - table.insert(sid_table, form_device_id_string(device_table)) - end - if ( #sid_table > 0 ) then - table.insert(results, sid_table) - end - if ( not aggressive ) then break end - end - end - end + local opts = {timeout=2000} + local results = {} - if ( #results > 0 ) then - port.state = "open" - port.version.name = "modbus" - nmap.set_port_version(host, port, "hardmatched") - end - - return stdnse.format_output(true, results) + for sid = 1, 246 do + stdnse.print_debug(3, "Sending command with sid = %d", sid) + rsid = form_rsid(sid, 0x11, "") + + local status, result = comm.exchange(host, port, rsid, opts) + if ( status and (#result >= 8) ) then + local ret_code = string.byte(result, 8) + if ( ret_code == (0x11) or ret_code == (0x11 + 128) ) then + local sid_table = {} + if ret_code == (0x11) then + table.insert(results, ("Positive response for sid = 0x%x"):format(sid)) + local slave_id = extract_slave_id(result) + if ( slave_id ~= nil ) then table.insert(sid_table, "SLAVE ID DATA: "..slave_id) end + elseif ret_code == (0x11 + 128) then + local exception_code = string.byte(result, 9) + local exception_string = modbus_exception_codes[exception_code] + if ( exception_string == nil ) then exception_string = "UNKNOWN EXCEPTION" end + table.insert(results, ("Positive error response for sid = 0x%x (%s)"):format(sid, exception_string)) + end + + local device_table = discover_device_id(host, port, sid) + if ( #device_table > 0 ) then + table.insert(sid_table, form_device_id_string(device_table)) + end + if ( #sid_table > 0 ) then + table.insert(results, sid_table) + end + if ( not aggressive ) then break end + end + end + end + + if ( #results > 0 ) then + port.state = "open" + port.version.name = "modbus" + nmap.set_port_version(host, port, "hardmatched") + end + + return stdnse.format_output(true, results) end