File: modbus-discover.nse

package info (click to toggle)
nmap 6.47-3%2Bdeb8u2
  • links: PTS, VCS
  • area: main
  • in suites: jessie
  • size: 44,788 kB
  • ctags: 25,108
  • sloc: ansic: 89,741; cpp: 62,412; sh: 19,492; python: 17,323; xml: 11,413; perl: 2,529; makefile: 2,503; yacc: 608; lex: 469; asm: 372; java: 45
file content (170 lines) | stat: -rw-r--r-- 6,400 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
local bin = require "bin"
local comm = require "comm"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"

description = [[
Enumerates SCADA Modbus slave ids (sids) and collects 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/
]]

---
-- @usage
-- nmap --script modbus-discover.nse --script-args='modbus-discover.aggressive=true' -p 502 <host>
--
-- @args aggressive - boolean value defines find all or just first sid
--
-- @output
-- PORT    STATE SERVICE
-- 502/tcp open  modbus
-- | modbus-discover:
-- |   Positive response for sid = 0x64
-- |     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

author = "Alexander Rudakov"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "intrusive"}


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
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", 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", 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)
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
  end
  return ret_string
end

local extract_slave_id = function(response)
  local byte_count = string.byte(response, 9)
  if ( byte_count == nil or byte_count == 0) then return nil end
  local offset, slave_id = bin.unpack("A"..byte_count, response, 10)
  return slave_id
end

modbus_exception_codes = {
  [1]  = "ILLEGAL FUNCTION",
  [2]  = "ILLEGAL DATA ADDRESS",
  [3]  = "ILLEGAL DATA VALUE",
  [4]  = "SLAVE DEVICE FAILURE",
  [5]  = "ACKNOWLEDGE",
  [6]  = "SLAVE DEVICE BUSY",
  [8]  = "MEMORY PARITY ERROR",
  [10] = "GATEWAY PATH UNAVAILABLE",
  [11] = "GATEWAY TARGET DEVICE FAILED TO RESPOND"
}

action = function(host, port)
  -- If false, stop after first sid.
  local aggressive = stdnse.get_script_args('modbus-discover.aggressive')

  local opts = {timeout=2000}
  local results = {}

  for sid = 1, 246 do
    stdnse.print_debug(3, "Sending command with sid = %d", sid)
    local 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)
  end

  return stdnse.format_output(true, results)
end