|
| 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