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
|