Skip to content

Commit e074669

Browse files
author
Brent Cook
committed
Land rapid7#7296, Added a SCADA module for detecting Profinet devices, e.g. Siemens controllers
2 parents 7e2e98f + 2fab62b commit e074669

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
Siemens Industrial controllers and most other industrial OEMs
2+
use a proprietary protocol to discover their devices accross a network.
3+
In the case of Siemens this is called the Profinet Discover Protocol.
4+
Known in Wireshark as PN_DCP
5+
6+
It works purely on Layer 2 (Ethernet addresses) and sends out a single
7+
multicast packet (making it safe to use in sensitive networks).
8+
Each profinet enabled responds with an array of information:
9+
- Its IP address, Subnetmask and Gateway
10+
- Its Profinet Devicename ('Station Name')
11+
- The Type of station
12+
- A Vendor ID (e.g. '002a'), signifing the vendor (e.g. 'Siemens')
13+
- A Device Role (e.g. '01'), signifing the type of device (e.g. 'IO-Controller')
14+
- A Device ID (e.g. '010d'), signifing the device type (e.g. 'S7-1200')
15+
16+
## Vulnerable Application
17+
18+
This is a hardware choice of design, and as such CANNOT be changed without
19+
loss of compatibility.
20+
Possible mitigations include: pulling the plug (literally), using network isolation
21+
(Firewall, Router, IDS, IPS, network segmentation, etc...) or not allowing bad
22+
people on your network.
23+
24+
Most, if not all, PLC's (computers that control engines, robots, conveyor
25+
belts, sensors, camera's, doorlocks, CRACs ...) have vulnerabilities where,
26+
using their own tools, remote configuration and programming can be done
27+
*WITHOUT* authentication. Investigators and underground hackers are just now
28+
creating simple tools to convert the, often proprietary, protocols into simple
29+
scripts. The operating word here is "proprietary". Right now, the only thing
30+
stopping very bad stuff from happening.
31+
32+
## Verification Steps
33+
34+
The following demonstrates a basic scenario, we "detect" two devices:
35+
36+
```
37+
msf > search profinet
38+
msf > use auxiliary/scanner/scada/profinet_siemens
39+
msf auxiliary(profinet_siemens) > run
40+
41+
[*] Sending packet out to eth0
42+
[+] Parsing packet from 00:0e:8c:cf:7b:1a
43+
Type of station: ET200S CPU
44+
Name of station: pn-io-1
45+
Vendor and Device Type: Siemens, ET200S
46+
Device Role: IO-Controller
47+
IP, Subnetmask and Gateway are: 172.16.108.11, 255.255.0.0, 172.16.108.11
48+
49+
[+] Parsing packet from 00:50:56:b6:fe:b6
50+
Type of station: SIMATIC-PC
51+
Name of station: nm
52+
Vendor and Device Type: Siemens, PC Simulator
53+
Device Role: IO-Controller
54+
IP, Subnetmask and Gateway are: 172.16.30.102, 255.255.0.0, 172.16.0.1
55+
56+
[+] I found 2 devices for you!
57+
[*] Auxiliary module execution completed
58+
```
59+
60+
## Module Options
61+
```
62+
msf auxiliary(profinet_siemens) > show options
63+
64+
Module options (auxiliary/scanner/scada/profinet_siemens):
65+
66+
Name Current Setting Required Description
67+
---- --------------- -------- -----------
68+
INTERFACE eth0 yes Set an interface
69+
TIMEOUT 2 yes Seconds to wait, set longer on slower networks
70+
```
71+
72+
By default, the module uses interface 'eth0', there is a check to see if it is live.
73+
74+
The module will send out an ethernet packet and wait for responses.
75+
By default, it will wait 2 seconds for any responses, this is long enough for most networks.
76+
Increase this on larger and/or slower networks, it just increases the wait time.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'packetfu'
8+
9+
class MetasploitModule < Msf::Auxiliary
10+
def initialize
11+
super(
12+
'Name' => 'Siemens Profinet Scanner',
13+
'Description' => %q{
14+
This module will use Layer2 packets, known as Profinet Discovery packets,
15+
to detect all Siemens (and sometimes other) devices on a network.
16+
It is perfectly SCADA-safe, as there will only be ONE single packet sent out.
17+
Devices will respond with their IP configuration and hostnames.
18+
Created by XiaK Industrial Security Research Center (www[dot]xiak[dot]be))
19+
},
20+
'References' =>
21+
[
22+
[ 'URL', 'https://wiki.wireshark.org/PROFINET/DCP' ],
23+
[ 'URL', 'https://github.com/tijldeneut/ICSSecurityScripts' ]
24+
],
25+
'Author' => 'Tijl Deneut <tijl.deneut[at]howest.be>',
26+
'License' => MSF_LICENSE
27+
)
28+
29+
register_options(
30+
[
31+
OptString.new('INTERFACE', [ true, 'Set an interface', 'eth0' ]),
32+
OptInt.new('ANSWERTIME', [ true, 'Seconds to wait for answers, set longer on slower networks', 2 ])
33+
], self.class
34+
)
35+
end
36+
37+
def hex_to_bin(s)
38+
s.scan(/../).map { |x| x.hex.chr }.join
39+
end
40+
41+
def bin_to_hex(s)
42+
s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
43+
end
44+
45+
def hexint_to_str(s)
46+
s.to_i(16).to_s
47+
end
48+
49+
def hex_to_address(s)
50+
hexint_to_str(s[0..1]) + '.' + hexint_to_str(s[2..3]) + '.' + hexint_to_str(s[4..5]) + '.' + hexint_to_str(s[6..7])
51+
end
52+
53+
def parse_devicerole(role)
54+
arr = { "01" => "IO-Device", "02" => "IO-Controller", "04" => "IO-Multidevice", "08" => "PN-Supervisor" }
55+
return arr[role] unless arr[role].nil?
56+
'Unknown'
57+
end
58+
59+
def parse_vendorid(id)
60+
return 'Siemens' if id == '002a'
61+
'Unknown'
62+
end
63+
64+
def parse_deviceid(id)
65+
arr = { "0a01" => "Switch", "0202" => "PC Simulator", "0203" => "S7-300 CPU", \
66+
"0101" => "S7-300", "010e" => "S7-1500", "010d" => "S7-1200", "0301" => "HMI", \
67+
"0403" => "HMI", "010b" => "ET200S" }
68+
return arr[id] unless arr[id].nil?
69+
'Unknown'
70+
end
71+
72+
def parse_block(block, block_length)
73+
block_id = block[0..2 * 2 - 1]
74+
case block_id
75+
when '0201'
76+
type_of_station = hex_to_bin(block[4 * 2..4 * 2 + block_length * 2 - 1])
77+
print_line("Type of station: #{type_of_station}")
78+
when '0202'
79+
name_of_station = hex_to_bin(block[4 * 2..4 * 2 + block_length * 2 - 1])
80+
print_line("Name of station: #{name_of_station}")
81+
when '0203'
82+
vendor_id = parse_vendorid(block[6 * 2..8 * 2 - 1])
83+
device_id = parse_deviceid(block[8 * 2..10 * 2 - 1])
84+
print_line("Vendor and Device Type: #{vendor_id}, #{device_id}")
85+
when '0204'
86+
device_role = parse_devicerole(block[6 * 2..7 * 2 - 1])
87+
print_line("Device Role: #{device_role}")
88+
when '0102'
89+
ip = hex_to_address(block[6 * 2..10 * 2 - 1])
90+
snm = hex_to_address(block[10 * 2..14 * 2 - 1])
91+
gw = hex_to_address(block[14 * 2..18 * 2 - 1])
92+
print_line("IP, Subnetmask and Gateway are: #{ip}, #{snm}, #{gw}")
93+
end
94+
end
95+
96+
def parse_profinet(data)
97+
data_to_parse = data[24..-1]
98+
99+
until data_to_parse.empty?
100+
block_length = data_to_parse[2 * 2..4 * 2 - 1].to_i(16)
101+
block = data_to_parse[0..(4 + block_length) * 2 - 1]
102+
103+
parse_block(block, block_length)
104+
105+
padding = block_length % 2
106+
data_to_parse = data_to_parse[(4 + block_length + padding) * 2..-1]
107+
end
108+
end
109+
110+
def receive(iface, answertime)
111+
capture = PacketFu::Capture.new(iface: iface, start: true, filter: 'ether proto 0x8892')
112+
sleep answertime
113+
capture.save
114+
i = 0
115+
capture.array.each do |packet|
116+
data = bin_to_hex(packet).downcase
117+
mac = data[12..13] + ':' + data[14..15] + ':' + data[16..17] + ':' + data[18..19] + ':' + data[20..21] + ':' + data[22..23]
118+
next unless data[28..31] == 'feff'
119+
print_good("Parsing packet from #{mac}")
120+
parse_profinet(data[28..-1])
121+
print_line('')
122+
i += 1
123+
end
124+
if i.zero?
125+
print_warning('No devices found, maybe you are running virtually?')
126+
else
127+
print_good("I found #{i} devices for you!")
128+
end
129+
end
130+
131+
def run
132+
iface = datastore['INTERFACE']
133+
answertime = datastore['ANSWERTIME']
134+
packet = "\x00\x00\x88\x92\xfe\xfe\x05\x00\x04\x00\x00\x03\x00\x80\x00\x04\xff\xff\x00\x00\x00\x00"
135+
packet += "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
136+
137+
eth_pkt = PacketFu::EthPacket.new
138+
begin
139+
eth_pkt.eth_src = PacketFu::Utils.whoami?(iface: iface)[:eth_src]
140+
rescue
141+
print_error("Error: interface #{iface} not active?")
142+
return
143+
end
144+
eth_pkt.eth_daddr = "01:0e:cf:00:00:00"
145+
eth_pkt.eth_proto = 0x8100
146+
eth_pkt.payload = packet
147+
print_status("Sending packet out to #{iface}")
148+
eth_pkt.to_w(iface)
149+
150+
receive(iface, answertime)
151+
end
152+
end

0 commit comments

Comments
 (0)