summaryrefslogtreecommitdiffstats
path: root/scripts/s7-info.nse
blob: 38980aec35e4c57f78f475c5094bac95d8e13b08 (plain)
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"


description = [[
Enumerates Siemens S7 PLC Devices and collects their device information. This
script is based off PLCScan that was developed by Positive Research and
Scadastrangelove (https://code.google.com/p/plcscan/). This script is meant to
provide the same functionality as PLCScan inside of Nmap. Some of the
information that is collected by PLCScan was not ported over; this
information can be parsed out of the packets that are received.

Thanks to Positive Research, and Dmitry Efanov for creating PLCScan
]]

author = "Stephen Hilt (Digital Bond)"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "version"}

---
-- @usage
-- nmap --script s7-info.nse -p 102 <host/s>
--
-- @output
--102/tcp open  Siemens S7 PLC
--| s7-info:
--|   Basic Hardware: 6ES7 315-2AG10-0AB0
--|   System Name: SIMATIC 300(1)
--|   Copyright: Original Siemens Equipment
--|   Version: 2.6.9
--|   Module Type: CPU 315-2 DP
--|   Module: 6ES7 315-2AG10-0AB0
--|_  Serial Number: S C-X4U421302009
--
--
-- @xmloutput
--<elem key="Basic Hardware">6ES7 315-2AG10-0AB0</elem>
--<elem key="System Name">SIMATIC 300(1)</elem>
--<elem key="Copyright">Original Siemens Equipment</elem>
--<elem key="Version">2.6.9</elem>
--<elem key="Object Name">SimpleServer</elem>
--<elem key="Module Type">CPU 315-2 DP</elem>
--<elem key="Module">6ES7 315-2AG10-0AB0</elem>
--<elem key="Serial Number">S C-X4U421302009</elem>
--<elm key="Plant Identification"></elem>


-- port rule for devices running on TCP/102
portrule = shortport.version_port_or_service(102, "iso-tsap", "tcp")

---
-- Function to send and receive the S7COMM Packet
--
-- First argument is the socket that was created inside of the main Action
-- this will be utilized to send and receive the packets from the host.
-- the second argument is the query to be sent, this is passed in and is created
-- inside of the main action.
-- @param socket the socket that was created in Action.
-- @param query the specific query that you want to send/receive on.
-- @param bytes how many bytes (minimum) you expect back
local function send_receive(socket, query, bytes)
  local sendstatus, senderr = socket:send(query)
  if(sendstatus == false) then
    return "Error Sending S7COMM"
  end
  -- receive response
  local rcvstatus, response = socket:receive_bytes(bytes)
  if(rcvstatus == false) then
    return "Error Reading S7COMM"
  end
  return response
end

---
-- Function to parse the first SZL Request response that was received from the S7 PLCC
--
-- First argument is the socket that was created inside of the main Action
-- this will be utilized to send and receive the packets from the host.
-- the second argument is the query to be sent, this is passed in and is created
-- inside of the main action.
-- @param response Packet response that was received from S7 host.
-- @param host The host hat was passed in via Nmap, this is to change output of host/port
-- @param port The port that was passed in via Nmap, this is to change output of host/port
-- @param output Table used for output for return to Nmap
local function parse_response(response, host, port, output)
  -- unpack the protocol ID
  local value = string.byte(response, 8)
  -- unpack the second byte of the SZL-ID
  local szl_id = string.byte(response, 31)
  -- if the protocol ID is 0x32
  if (value == 0x32 and #response >= 125) then
    -- unpack the module information
    output["Module"] = string.unpack("z", response, 44)
    -- unpack the basic hardware information
    output["Basic Hardware"] = string.unpack("z", response, 72)
    -- parse version number
    local char1, char2, char3 = string.unpack("BBB", response, 123)
    -- concatenate string, or if string is nil make version number 0.0
    output["Version"] = table.concat({char1 or "0.0", char2, char3}, ".")
    -- return the output table
    return output
  else
    return nil
  end
end

---
-- Function to parse the second SZL Request response that was received from the S7 PLC
--
-- First argument is the socket that was created inside of the main Action
-- this will be utilized to send and receive the packets from the host.
-- the second argument is the query to be sent, this is passed in and is created
-- inside of the main action.
-- @param response Packet response that was received from S7 host.
-- @param output Table used for output for return to Nmap
local function second_parse_response(response, output)
  local offset = 0
  -- unpack the protocol ID
  local value = string.byte(response, 8)
  -- unpack the second byte of the SZL-ID
  local szl_id = string.byte(response, 31)
  -- if the protocol ID is 0x32
  if (value == 0x32) then
    -- if the szl-ID is not 0x1c
    if( szl_id ~= 0x1c ) then
      -- change offset to 4, this is where most of valid PLCs will fall
      offset = 4
    end
    -- parse system name
    if #response > 40 + offset then
      output["System Name"] = string.unpack("z", response, 40 + offset)
    end
    -- parse module type
    if #response > 74 + offset then
      output["Module Type"] = string.unpack("z", response, 74 + offset)
    end
    -- parse serial number
    if #response > 176 + offset then
      output["Serial Number"] = string.unpack("z", response, 176 + offset)
    end
    -- parse plant identification
    if #response > 108 + offset then
      output["Plant Identification"] = string.unpack("z", response, 108 + offset)
    end
    -- parse copyright
    if #response > 142 + offset then
      output["Copyright"] = string.unpack("z", response, 142 + offset)
    end

    -- for each element in the table, if it is nil, then remove the information from the table
    for key, value in pairs(output) do
      if(string.len(output[key]) == 0) then
        output[key] = nil
      end
    end
    -- return output
    return output
  else
    return nil
  end
end
---
--  Function to set the nmap output for the host, if a valid S7COMM packet
--  is received then the output will show that the port is open
--  and change the output to reflect an S7 PLC
--
-- @param host Host that was passed in via nmap
-- @param port port that S7COMM is running on
local function set_nmap(host, port)
  --set port Open
  port.state = "open"
  -- set that detected an Siemens S7
  port.version.name = "iso-tsap"
  port.version.devicetype = "specialized"
  port.version.product = "Siemens S7 PLC"
  nmap.set_port_version(host, port)
  nmap.set_port_state(host, port, "open")

end
---
--  Action Function that is used to run the NSE. This function will send the initial query to the
--  host and port that were passed in via nmap. The initial response is parsed to determine if host
--  is a S7COMM device. If it is then more actions are taken to gather extra information.
--
-- @param host Host that was scanned via nmap
-- @param port port that was scanned via nmap
action = function(host, port)
  -- COTP packet with a dst of 102
local COTP = stdnse.fromhex( "0300001611e00000001400c1020100c2020" .. "102" .. "c0010a")
  -- COTP packet with a dst of 200
  local alt_COTP = stdnse.fromhex( "0300001611e00000000500c1020100c2020" .. "200" .. "c0010a")
  -- setup the ROSCTR Packet
  local ROSCTR_Setup = stdnse.fromhex( "0300001902f08032010000000000080000f0000001000101e0")
  -- setup the Read SZL information packet
  local Read_SZL = stdnse.fromhex( "0300002102f080320700000000000800080001120411440100ff09000400110001")
  -- setup the first SZL request (gather the basic hardware and version number)
  local first_SZL_Request = stdnse.fromhex( "0300002102f080320700000000000800080001120411440100ff09000400110001")
  -- setup the second SZL request
  local second_SZL_Request = stdnse.fromhex( "0300002102f080320700000000000800080001120411440100ff090004001c0001")
  -- response is used to collect the packet responses
  local response
  -- output table for Nmap
  local output = stdnse.output_table()
  -- create socket for communications
  local sock = nmap.new_socket()
  -- connect to host
  local constatus, conerr = sock:connect(host, port)
  if not constatus then
    stdnse.debug1('Error establishing connection for %s - %s', host, conerr)
    return nil
  end
  -- send and receive the COTP Packet
  response  = send_receive(sock, COTP, 6)
  -- unpack the PDU Type
  local CC_connect_confirm = string.byte(response, 6)
  -- if PDU type is not 0xd0, then not a successful COTP connection
  if ( CC_connect_confirm ~= 0xd0) then
    sock:close()
    -- create socket for communications
    stdnse.debug1('S7INFO:: CREATING NEW SOCKET')
    sock = nmap.new_socket()
    -- connect to host
    local constatus, conerr = sock:connect(host, port)
    if not constatus then
      stdnse.debug1('Error establishing connection for %s - %s', host, conerr)
      return nil
    end
    response = send_receive(sock, alt_COTP, 6)
    local CC_connect_confirm = string.byte(response, 6)
    if ( CC_connect_confirm ~= 0xd0) then
      stdnse.debug1('S7 INFO:: Could not negotiate COTP')
      return nil
    end
  end
  -- send and receive the ROSCTR Setup Packet
  response  = send_receive(sock, ROSCTR_Setup, 8)
  -- unpack the protocol ID
  local protocol_id = string.byte(response, 8)
  -- if protocol ID is not 0x32 then return nil
  if ( protocol_id ~= 0x32) then
    return nil
  end
  -- send and receive the READ_SZL packet
  response  = send_receive(sock, Read_SZL, 8)
  local protocol_id = string.byte(response, 8)
  -- if protocol ID is not 0x32 then return nil
  if ( protocol_id ~= 0x32) then
    return nil
  end
  -- send and receive the first SZL Request packet
  response  = send_receive(sock, first_SZL_Request, 125)
  -- parse the response for basic hardware information
  output = parse_response(response, host, port, output)
  -- send and receive the second SZL Request packet
  response = send_receive(sock, second_SZL_Request, 180)
  -- parse the response for more information
  output = second_parse_response(response, output)
  -- close the socket
  sock:close()

  -- If we parsed anything, then set the version info for Nmap
  if #output > 0 then
    set_nmap(host, port)
  end
  -- return output to Nmap
  return output

end