Land #16788, SCADA scanner module for BACnet protocol
This commit is contained in:
commit
fd2b325e44
|
@ -0,0 +1,74 @@
|
||||||
|
## Vulnerable Application
|
||||||
|
BACnet is a Data Communication Protocol for Building Automation and Control Networks.
|
||||||
|
Developed under the auspices of the American Society of Heating,
|
||||||
|
Refrigerating and Air-Conditioning Engineers (ASHRAE), BACnet is an American national standard,
|
||||||
|
a European standard, a national standard in more than 30 countries, and an ISO global standard.
|
||||||
|
The protocol is supported and maintained by ASHRAE Standing Standard Project Committee 135
|
||||||
|
|
||||||
|
This script polls bacnet devices with a l3 broadcast Who-is message
|
||||||
|
and for each reply communicates further to discover more data and saves the data into metasploit.
|
||||||
|
Each bacnet device responds with this data:
|
||||||
|
- It's IP address, and BACnet/IP address (if the device is nested).
|
||||||
|
- It's device number.
|
||||||
|
- Model name.
|
||||||
|
- Application software version.
|
||||||
|
- Firmware revision.
|
||||||
|
- Device description.
|
||||||
|
## Verification Steps
|
||||||
|
|
||||||
|
1. Start msfconsole.
|
||||||
|
2. Do: `use auxiliary/scanner/scada/bacnet_l3`.
|
||||||
|
3. Do: `set INTERFACE`.
|
||||||
|
5. Do: `run`.
|
||||||
|
6. Devices running the BACnet protocol should respond with data.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
A user can choose between the interfaces of his host (e.g. eth1, ens192...),
|
||||||
|
the number of Who-is packets to send - for reliability purposes, the time (in seconds) to wait for packets to arrive
|
||||||
|
and the UDP port, the default is 47808.
|
||||||
|
|
||||||
|
The user can always check these options via the `show options` command.
|
||||||
|
|
||||||
|
```
|
||||||
|
msf auxiliary(profinet_siemens) > show options
|
||||||
|
|
||||||
|
Module options (auxiliary/scanner/scada/bacnet_l3):
|
||||||
|
|
||||||
|
Name Current Setting Required Description
|
||||||
|
---- --------------- -------- -----------
|
||||||
|
COUNT 1 yes The number of times to send each packet
|
||||||
|
INTERFACE eth1 yes The interface to scan from
|
||||||
|
PORT 47808 yes BACnet/IP UDP port to scan (usually between 47808-47817)
|
||||||
|
TIMEOUT 1 yes The socket connect timeout in seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scenarios
|
||||||
|
|
||||||
|
The following demonstrates a basic scenario, we "detect" two devices:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
msf > use auxiliary/scanner/scada/bacnet_l3
|
||||||
|
msf auxiliary(auxiliary/scanner/scada/bacnet_l3) > run
|
||||||
|
|
||||||
|
[*] Broadcasting Who-is via eth1
|
||||||
|
[*] found 2 devices
|
||||||
|
[*] Querying device number 826001 in ip 192.168.13.11
|
||||||
|
[*] Querying device number 4194303 in ip 192.168.13.12
|
||||||
|
[*] Done scanning
|
||||||
|
[+] for asset number 826001:
|
||||||
|
model name: iSMA-B-4U4A-H-IP
|
||||||
|
firmware revision: 6.2
|
||||||
|
application software version: GC5 6.2
|
||||||
|
description: BACnet iSMA-B-4U4A-H-IP Module
|
||||||
|
|
||||||
|
[+] for asset number 4194303:
|
||||||
|
model name: PXG3.L-1
|
||||||
|
firmware revision: FW=01.21.30.38;WPC=1.4.131;SVS-300:SBC=13.21;
|
||||||
|
application software version:
|
||||||
|
description: BacnetRouter
|
||||||
|
|
||||||
|
[+] Successfully saved data to local store named bacnet-discovery.xml
|
||||||
|
[*] Done.
|
||||||
|
[*] Auxiliary module execution completed
|
||||||
|
```
|
|
@ -0,0 +1,417 @@
|
||||||
|
class MetasploitModule < Msf::Auxiliary
|
||||||
|
include Msf::Auxiliary::Report
|
||||||
|
include Msf::Exploit::Capture
|
||||||
|
include Rex::Socket::Udp
|
||||||
|
|
||||||
|
FILE_NAME = 'bacnet-discovery'.freeze
|
||||||
|
DEFAULT_SERVER_TIMEOUT = 1
|
||||||
|
DEFAULT_SEND_COUNT = 1
|
||||||
|
DEFAULT_SLEEP = 1
|
||||||
|
|
||||||
|
BACNET_ASHARE_STANDARD = "\x01".freeze
|
||||||
|
BACNETIP_CONSTANT = "\x81".freeze
|
||||||
|
BACNET_LLC = "\x82\x82\x03".freeze
|
||||||
|
BACNET_BVLC = "\x81\x0b\x00\x0c".freeze
|
||||||
|
BACNET_BVLC_LEN = BACNET_BVLC.length
|
||||||
|
|
||||||
|
BACNET_WHOIS_APDU_NPDU = "\x01\x20\xff\xff\x00\xff\x10\x08".freeze
|
||||||
|
|
||||||
|
# Building Automation and Control Network APDU
|
||||||
|
# 0001 .... = APDU Type: Unconfirmed-REQ (1)
|
||||||
|
# Unconfirmed Service Choice: i-Am (0)
|
||||||
|
# ObjectIdentifier: device
|
||||||
|
BACNET_UNCOFIRMED_REQ_I_AM_OBJ_DEVICE_PREFIX = "\x10\x00\xc4\x02".freeze
|
||||||
|
DEFAULT_BACNET_PORT = 47808
|
||||||
|
DISCOVERY_MESSAGE_L3 = BACNET_BVLC + BACNET_WHOIS_APDU_NPDU
|
||||||
|
DISCOVERY_MESSAGE_L2 = BACNET_LLC + BACNET_WHOIS_APDU_NPDU
|
||||||
|
DISCOVERY_MESSAGE_L2_LEN = Array[DISCOVERY_MESSAGE_L2.length].pack('n')
|
||||||
|
|
||||||
|
READ_MULTIPLE_DEVICES_PROP = "\x1e\x09\x08\x1f".freeze
|
||||||
|
READ_MODEL_NAME_PROP = "\x19\x46".freeze
|
||||||
|
READ_FIRMWARE_VERSION_PROP = "\x19\x2c".freeze
|
||||||
|
READ_APP_SOFT_VERSION_PROP = "\x19\x0c".freeze
|
||||||
|
READ_DESCRIPTION_PROP = "\x19\x1c".freeze
|
||||||
|
|
||||||
|
GET_PROPERTY_MESSAGES_L3_SIMPLE = [
|
||||||
|
"\x81\n\u0000\u0011\u0001\u0004\u0002\u0002\u0000\f\f\u0002{object_identifier}#{READ_MODEL_NAME_PROP}", # model-name
|
||||||
|
"\x81\n\u0000\u0011\u0001\u0004\u0002\u0002\u0000\f\f\u0002{object_identifier}#{READ_FIRMWARE_VERSION_PROP}", # firmware-revision
|
||||||
|
"\x81\n\u0000\u0011\u0001\u0004\u0002\u0002\u0000\f\f\u0002{object_identifier}#{READ_APP_SOFT_VERSION_PROP}", # application-software-version
|
||||||
|
"\x81\n\u0000\u0011\u0001\u0004\u0002\u0002\u0000\f\f\u0002{object_identifier}#{READ_DESCRIPTION_PROP}"
|
||||||
|
].freeze # description
|
||||||
|
|
||||||
|
GET_PROPERTY_MESSAGES_L3_NESTED = [
|
||||||
|
"\u0001${dest_net_id}{dadr_len}{dadr}\xFF\u0002\u0002\u0002\f\f\u0002{object_identifier}#{READ_MODEL_NAME_PROP}",
|
||||||
|
"\u0001${dest_net_id}{dadr_len}{dadr}\xFF\u0002\u0002\u0002\f\f\u0002{object_identifier}#{READ_FIRMWARE_VERSION_PROP}",
|
||||||
|
"\u0001${dest_net_id}{dadr_len}{dadr}\xFF\u0002\u0002\u0002\f\f\u0002{object_identifier}#{READ_APP_SOFT_VERSION_PROP}",
|
||||||
|
"\u0001${dest_net_id}{dadr_len}{dadr}\xFF\u0002\u0002\u0002\f\f\u0002{object_identifier}#{READ_DESCRIPTION_PROP}"
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
super(
|
||||||
|
'Name' => 'BACnet Scanner',
|
||||||
|
'Description' => '
|
||||||
|
Discover BACnet devices by broadcasting Who-is message, then poll
|
||||||
|
discovered devices for properties including model name,
|
||||||
|
software version, firmware revision and description.
|
||||||
|
',
|
||||||
|
'Author' => ['Paz @ SCADAfence'],
|
||||||
|
'License' => MSF_LICENSE,
|
||||||
|
'Notes' => {
|
||||||
|
'Stability' => [CRASH_SAFE],
|
||||||
|
'Reliability' => [UNRELIABLE_SESSION],
|
||||||
|
'SideEffects' => [SCREEN_EFFECTS]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
register_options(
|
||||||
|
[
|
||||||
|
OptInt.new('TIMEOUT', [true, 'The socket connect timeout in seconds', DEFAULT_SERVER_TIMEOUT]),
|
||||||
|
OptInt.new('COUNT', [true, 'The number of times to send each packet', DEFAULT_SEND_COUNT]),
|
||||||
|
OptPort.new('PORT', [true, 'BACnet/IP UDP port to scan (usually between 47808-47817)', DEFAULT_BACNET_PORT]),
|
||||||
|
OptString.new('INTERFACE', [true, 'The interface to scan from', 'eth1'])
|
||||||
|
], self.class
|
||||||
|
)
|
||||||
|
deregister_options('RHOSTS', 'FILTER', 'PCAPFILE', 'LHOST')
|
||||||
|
end
|
||||||
|
|
||||||
|
def hex_to_bin(str)
|
||||||
|
str.scan(/../).map { |x| x.hex.chr }.join
|
||||||
|
end
|
||||||
|
|
||||||
|
def bin_to_hex(str)
|
||||||
|
str.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if device is nested and extract relevant data
|
||||||
|
def parse_npdu(data)
|
||||||
|
is_nested = false
|
||||||
|
if data.start_with? BACNET_ASHARE_STANDARD
|
||||||
|
control = data[1].unpack1('C*')
|
||||||
|
src_specifier = control & (1 << 3) != 0 # check if 4th bit is set
|
||||||
|
dst_specifier = control & (1 << 5) != 0 # check if 6th bit is set
|
||||||
|
|
||||||
|
idx = 2
|
||||||
|
if dst_specifier
|
||||||
|
dst_len = data[idx + 2].ord
|
||||||
|
idx += 3 + dst_len
|
||||||
|
end
|
||||||
|
if src_specifier
|
||||||
|
src_net_id = data[idx..idx + 1]
|
||||||
|
sadr_len = data[idx + 2]
|
||||||
|
sadr = data[idx + 3..idx + 2 + sadr_len.unpack1('C*')]
|
||||||
|
is_nested = true
|
||||||
|
end
|
||||||
|
|
||||||
|
# if no network address specified - set as broadcast network address
|
||||||
|
src_net_id ||= '\x00'
|
||||||
|
end
|
||||||
|
[is_nested, src_net_id, sadr_len, sadr]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Extracting index to start handling the data from
|
||||||
|
def extract_index(data)
|
||||||
|
if data.start_with? BACNET_ASHARE_STANDARD
|
||||||
|
begin
|
||||||
|
control = data[1].unpack1('C*')
|
||||||
|
src_specifier = control & (1 << 3) != 0 # check if 4th bit is set
|
||||||
|
dst_specifier = control & (1 << 5) != 0 # check if 6th bit is set
|
||||||
|
idx = 2
|
||||||
|
if dst_specifier
|
||||||
|
idx += 3 + dst_len
|
||||||
|
end
|
||||||
|
if src_specifier
|
||||||
|
sadr_len = data[idx + 2]
|
||||||
|
idx += 3 + sadr_len.unpack1('C*')
|
||||||
|
end
|
||||||
|
idx += 1 if dst_specifier # increase index if both specifiers exist
|
||||||
|
idx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Broadcasting Who-is and returns a capture with the responses.
|
||||||
|
def broadcast_who_is
|
||||||
|
begin
|
||||||
|
broadcast_addr = get_ipv4_broadcast(datastore['INTERFACE'])
|
||||||
|
interface_addr = get_ipv4_addr(datastore['INTERFACE'])
|
||||||
|
rescue StandardError
|
||||||
|
raise StandardError, "Interface #{datastore['INTERFACE']} is down"
|
||||||
|
end
|
||||||
|
cap = []
|
||||||
|
|
||||||
|
# Create a socket for broadcast response and a socket for unicast response.
|
||||||
|
lsocket = Rex::Socket::Udp.create({
|
||||||
|
'LocalHost' => broadcast_addr,
|
||||||
|
'LocalPort' => datastore['PORT'],
|
||||||
|
'Context' => { 'Msf' => framework, 'MsfExploit' => self }
|
||||||
|
})
|
||||||
|
ssocket = Rex::Socket::Udp.create({
|
||||||
|
'LocalHost' => interface_addr,
|
||||||
|
'LocalPort' => datastore['PORT'],
|
||||||
|
'Context' => { 'Msf' => framework, 'MsfExploit' => self }
|
||||||
|
})
|
||||||
|
datastore['COUNT'].times { lsocket.sendto(DISCOVERY_MESSAGE_L3, '255.255.255.255', datastore['PORT'], 0) }
|
||||||
|
|
||||||
|
# Collect responses with unicast or broadcast destination.
|
||||||
|
loop do
|
||||||
|
data, host, port = lsocket.recvfrom(65535, datastore['TIMEOUT'])
|
||||||
|
data2, host2, port2 = ssocket.recvfrom(65535, datastore['TIMEOUT'])
|
||||||
|
break if (host.nil? && host2.nil?)
|
||||||
|
|
||||||
|
cap << [data, host, port] if host
|
||||||
|
cap << [data2, host2, port2] if host2
|
||||||
|
end
|
||||||
|
lsocket.close
|
||||||
|
cap
|
||||||
|
end
|
||||||
|
|
||||||
|
# Analyze I-am packets,and prepare read-property messages for each.
|
||||||
|
def analyze_i_am_devices(capture)
|
||||||
|
devices_data = {}
|
||||||
|
instance_numbers = []
|
||||||
|
capture.each do |cap|
|
||||||
|
data = cap[0]
|
||||||
|
ip = cap[1]
|
||||||
|
next unless data[0] == BACNETIP_CONSTANT # If communication is not a bacnet/ip
|
||||||
|
|
||||||
|
data = data[4..]
|
||||||
|
index = data.index(BACNET_UNCOFIRMED_REQ_I_AM_OBJ_DEVICE_PREFIX)
|
||||||
|
next unless index # If cap has no I-am object
|
||||||
|
|
||||||
|
raw_instance_number = bin_to_hex(data[(index + BACNET_UNCOFIRMED_REQ_I_AM_OBJ_DEVICE_PREFIX.length)..(index + BACNET_UNCOFIRMED_REQ_I_AM_OBJ_DEVICE_PREFIX.length + 2)]).to_i(16) & 0x3fffff
|
||||||
|
instance_number = raw_instance_number.to_s(16).rjust(6, '0')
|
||||||
|
next if instance_numbers.include? instance_number # Pass if we already analysed this instance number
|
||||||
|
|
||||||
|
devices_data[[instance_number, ip]] = data unless devices_data[[instance_number, ip]]
|
||||||
|
end
|
||||||
|
devices_data
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_messages_for_devices(devices_data)
|
||||||
|
messages = {}
|
||||||
|
devices_data.each do |key, data|
|
||||||
|
instance_number = hex_to_bin(key[0])
|
||||||
|
items = parse_npdu(data) # Get specifier data
|
||||||
|
# Check if device is nested and create messages accordingly
|
||||||
|
if items[0] == true
|
||||||
|
messages[key] = create_nested_messages(instance_number, items)
|
||||||
|
else
|
||||||
|
messages[key] = create_simple_messages(instance_number)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
messages
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create messages for nested device and return them in array.
|
||||||
|
def create_nested_messages(instance_number, items)
|
||||||
|
nested_messages = []
|
||||||
|
GET_PROPERTY_MESSAGES_L3_NESTED.each do |msg_base|
|
||||||
|
msg = msg_base
|
||||||
|
.sub('{object_identifier}', instance_number)
|
||||||
|
.sub('{dest_net_id}', items[1])
|
||||||
|
.sub('{dadr_len}', items[2])
|
||||||
|
.sub('{dadr}', items[3])
|
||||||
|
length = Array(msg.length + BACNET_BVLC_LEN).pack('n*')
|
||||||
|
msg = "\x81\n#{length}#{msg}"
|
||||||
|
nested_messages.append(msg)
|
||||||
|
end
|
||||||
|
nested_messages
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create messages for non-nested device and return them in array.
|
||||||
|
def create_simple_messages(instance_number)
|
||||||
|
simple_messages = []
|
||||||
|
GET_PROPERTY_MESSAGES_L3_SIMPLE.each do |msg_base|
|
||||||
|
msg = msg_base.sub('{object_identifier}', instance_number)
|
||||||
|
simple_messages.append(msg)
|
||||||
|
end
|
||||||
|
simple_messages
|
||||||
|
end
|
||||||
|
|
||||||
|
# Loop on recorded packets and extract data from read-property messages
|
||||||
|
def extract_data(capture)
|
||||||
|
asset_data = {}
|
||||||
|
capture.each do |packet|
|
||||||
|
data = packet[0][4..]
|
||||||
|
items = parse_npdu(data)
|
||||||
|
index = extract_index(data)
|
||||||
|
asset_data['sadr'] = bin_to_hex(items[3]) if items[0] == true
|
||||||
|
type = data[index + 8..index + 9]
|
||||||
|
attribute = ''
|
||||||
|
case type
|
||||||
|
when READ_MODEL_NAME_PROP
|
||||||
|
attribute = 'model-name'
|
||||||
|
when READ_DESCRIPTION_PROP
|
||||||
|
attribute = 'description'
|
||||||
|
when READ_APP_SOFT_VERSION_PROP
|
||||||
|
attribute = 'application-software-version'
|
||||||
|
when READ_FIRMWARE_VERSION_PROP
|
||||||
|
attribute = 'firmware-revision'
|
||||||
|
else
|
||||||
|
raise "undefined attribute for property number #{bin_to_hex(type)}."
|
||||||
|
end
|
||||||
|
value = bin_to_hex(data[index + 9..])[/3e(.*?)3f/m, 1]
|
||||||
|
value = hex_to_bin(value)
|
||||||
|
value = (value[value.index(hex_to_bin('00')) + 1..]).force_encoding('UTF-8') # parsing the needed text
|
||||||
|
asset_data[attribute] = value
|
||||||
|
end
|
||||||
|
asset_data
|
||||||
|
end
|
||||||
|
|
||||||
|
# Gets properties from devices and returns a hash with the details of each device.
|
||||||
|
def get_properties_from_devices(messages)
|
||||||
|
devices_by_ip = {}
|
||||||
|
messages.each do |key, message_block|
|
||||||
|
instance_number = key[0].to_i(16)
|
||||||
|
ip = key[1]
|
||||||
|
|
||||||
|
capture = send_read_properties(message_block, ip, instance_number)
|
||||||
|
begin
|
||||||
|
device = extract_data(capture)
|
||||||
|
raise StandardError if device.empty?
|
||||||
|
|
||||||
|
device['instance-number'] = instance_number.to_s
|
||||||
|
devices_by_ip[ip] = [] unless devices_by_ip[ip]
|
||||||
|
devices_by_ip[ip].append(device)
|
||||||
|
rescue StandardError
|
||||||
|
print_bad("Couldn't collect data for asset number #{instance_number}.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
devices_by_ip
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sending read-property packets and returns a pcap with the responses.
|
||||||
|
def send_read_properties(messages, ip, instance_number)
|
||||||
|
cap = []
|
||||||
|
ssocket = Rex::Socket::Udp.create({
|
||||||
|
'PeerHost' => ip,
|
||||||
|
'PeerPort' => datastore['PORT'],
|
||||||
|
'Context' => { 'Msf' => framework, 'MsfExploit' => self }
|
||||||
|
})
|
||||||
|
print_status("Querying device number #{instance_number} in ip #{ip}")
|
||||||
|
messages.each do |message|
|
||||||
|
ssocket.sendto(message, ip, datastore['PORT'], 0)
|
||||||
|
loop do
|
||||||
|
data, host, port = ssocket.recvfrom(65535, datastore['TIMEOUT'])
|
||||||
|
break if host.nil?
|
||||||
|
|
||||||
|
cap << [data, host, port]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ssocket.close
|
||||||
|
cap
|
||||||
|
end
|
||||||
|
|
||||||
|
# Iterates over all the devices and prints the details to the user.
|
||||||
|
def output_results(devices_by_ip)
|
||||||
|
devices_by_ip.each_value do |ip_group|
|
||||||
|
ip_group.each do |asset|
|
||||||
|
sadr = ''
|
||||||
|
if asset['sadr']
|
||||||
|
sadr = "sadr: #{asset['sadr']}\n"
|
||||||
|
end
|
||||||
|
print_good(<<~OUTPUT)
|
||||||
|
for asset number #{asset['instance-number']}:
|
||||||
|
\tmodel name: #{asset['model-name']}
|
||||||
|
\tfirmware revision: #{asset['firmware-revision']}
|
||||||
|
\tapplication software version: #{asset['application-software-version']}
|
||||||
|
\tdescription: #{asset['description']}
|
||||||
|
\t#{sadr}
|
||||||
|
OUTPUT
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Convert data values to xml format.
|
||||||
|
def parse_data_to_xml(raw_data)
|
||||||
|
data = ''
|
||||||
|
raw_data.each do |ip, devices|
|
||||||
|
chunk = <<~IP.chomp
|
||||||
|
<ip>
|
||||||
|
<value> #{ip} </value>
|
||||||
|
IP
|
||||||
|
devices.each do |device|
|
||||||
|
sadr = ''
|
||||||
|
if device['sadr']
|
||||||
|
sadr = "
|
||||||
|
<sadr> #{device['sadr']} </sadr>"
|
||||||
|
end
|
||||||
|
chunk = <<~XML.chomp
|
||||||
|
#{chunk}
|
||||||
|
<asset>
|
||||||
|
<instance-number> #{device['instance-number']} </instance-number>
|
||||||
|
<model-name> #{device['model-name']} </model-name>
|
||||||
|
<application-software-version> #{device['application-software-version']} </application-software-version>
|
||||||
|
<firmware-revision> #{device['firmware-revision']} </firmware-revision>
|
||||||
|
<description> #{device['description']} </description>#{sadr}
|
||||||
|
</asset>
|
||||||
|
XML
|
||||||
|
end
|
||||||
|
chunk += <<~IP
|
||||||
|
|
||||||
|
</ip>
|
||||||
|
IP
|
||||||
|
data += chunk
|
||||||
|
end
|
||||||
|
data
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_device_array(devices_by_ip)
|
||||||
|
devices = []
|
||||||
|
devices_by_ip.each do |ip, batch|
|
||||||
|
batch.each do |device|
|
||||||
|
device['ip'] = ip
|
||||||
|
devices << device
|
||||||
|
end
|
||||||
|
end
|
||||||
|
devices
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
# Validate user input
|
||||||
|
raise Msf::OptionValidateError, ['TIMEOUT'] if datastore['TIMEOUT'].negative?
|
||||||
|
raise Msf::OptionValidateError, ['COUNT'] if datastore['COUNT'] < 1
|
||||||
|
raise Msf::OptionValidateError, ['INTERFACE'] if datastore['INTERFACE'].empty?
|
||||||
|
|
||||||
|
begin
|
||||||
|
# Broadcast who-is and create request-property messages for detected devices.
|
||||||
|
print_status "Broadcasting Who-is via #{datastore['INTERFACE']}"
|
||||||
|
capture = broadcast_who_is
|
||||||
|
devices_data = analyze_i_am_devices(capture)
|
||||||
|
messages = create_messages_for_devices(devices_data)
|
||||||
|
|
||||||
|
# If there are messages to send
|
||||||
|
if !messages.empty?
|
||||||
|
print_status "found #{messages.length} devices"
|
||||||
|
sleep(DEFAULT_SLEEP)
|
||||||
|
devices_by_ip = get_properties_from_devices(messages)
|
||||||
|
print_status 'Done collecting data'
|
||||||
|
sleep(DEFAULT_SLEEP)
|
||||||
|
output_results(devices_by_ip)
|
||||||
|
else
|
||||||
|
fail_with(Failure::NotFound, 'No devices found. Exiting.')
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
fail_with(Failure::Unknown, e.message)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
begin
|
||||||
|
data = parse_data_to_xml(devices_by_ip)
|
||||||
|
begin
|
||||||
|
store_local('bacnet.devices.info'.dup, 'text/xml', data, FILE_NAME)
|
||||||
|
print_good("Successfully saved data to local store named #{FILE_NAME}.xml")
|
||||||
|
rescue StandardError # If there are no privileges to save a file
|
||||||
|
devices = get_device_array(devices_by_ip)
|
||||||
|
report_note(
|
||||||
|
ips: devices_by_ip.keys,
|
||||||
|
devices: devices,
|
||||||
|
proto: 'udp'
|
||||||
|
)
|
||||||
|
print_good('Successfully reported data')
|
||||||
|
end
|
||||||
|
print_status('Done.')
|
||||||
|
rescue StandardError => e
|
||||||
|
fail_with(Failure::Unknown, e.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue