2012-06-05 16:36:45 +00:00
|
|
|
##
|
2014-10-17 16:47:33 +00:00
|
|
|
# This module requires Metasploit: http://metasploit.com/download
|
2013-10-15 18:50:46 +00:00
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
2012-05-29 18:34:33 +00:00
|
|
|
##
|
2012-06-05 16:36:45 +00:00
|
|
|
|
2012-05-29 18:34:33 +00:00
|
|
|
require 'msf/core'
|
2012-06-05 20:06:05 +00:00
|
|
|
|
2016-03-07 19:19:55 +00:00
|
|
|
class Metasploit3 < Msf::Auxiliary
|
2012-06-05 20:06:05 +00:00
|
|
|
|
2013-08-30 21:28:54 +00:00
|
|
|
include Msf::Exploit::Remote::Tcp
|
2012-06-05 20:06:05 +00:00
|
|
|
|
2013-08-30 21:28:54 +00:00
|
|
|
def initialize(info = {})
|
|
|
|
super(update_info(info,
|
2014-04-29 14:09:47 +00:00
|
|
|
'Name' => 'Modbus Client Utility',
|
2014-04-28 13:29:55 +00:00
|
|
|
'Description' => %q{
|
|
|
|
This module allows reading and writing data to a PLC using the Modbus protocol.
|
2014-05-01 14:52:55 +00:00
|
|
|
This module is based on the 'modiconstop.rb' Basecamp module from DigitalBond,
|
|
|
|
as well as the mbtget perl script.
|
2013-08-30 21:28:54 +00:00
|
|
|
},
|
2014-04-28 13:29:55 +00:00
|
|
|
'Author' =>
|
2013-08-30 21:28:54 +00:00
|
|
|
[
|
2014-04-29 14:09:47 +00:00
|
|
|
'EsMnemon <esm[at]mnemonic.no>', # original write-only module
|
|
|
|
'Arnaud SOULLIE <arnaud.soullie[at]solucom.fr>' # new code that allows read/write
|
2013-08-30 21:28:54 +00:00
|
|
|
],
|
|
|
|
'License' => MSF_LICENSE,
|
2014-05-01 14:52:55 +00:00
|
|
|
'Actions' =>
|
|
|
|
[
|
|
|
|
['READ_COIL', { 'Description' => 'Read one bit from a coil' } ],
|
|
|
|
['WRITE_COIL', { 'Description' => 'Write one bit to a coil' } ],
|
|
|
|
['READ_REGISTER', { 'Description' => 'Read one word from a register' } ],
|
2014-05-08 15:44:53 +00:00
|
|
|
['WRITE_REGISTER', { 'Description' => 'Write one word to a register' } ]
|
|
|
|
],
|
|
|
|
'DefaultAction' => 'READ_REGISTER'
|
2014-04-28 13:29:55 +00:00
|
|
|
))
|
2014-05-01 14:52:55 +00:00
|
|
|
|
2014-04-28 13:29:55 +00:00
|
|
|
register_options(
|
|
|
|
[
|
|
|
|
Opt::RPORT(502),
|
2014-04-29 14:09:47 +00:00
|
|
|
OptInt.new('DATA', [false, "Data to write (WRITE_COIL and WRITE_REGISTER modes only)"]),
|
|
|
|
OptInt.new('DATA_ADDRESS', [true, "Modbus data address"]),
|
2014-04-28 13:47:10 +00:00
|
|
|
OptInt.new('UNIT_NUMBER', [false, "Modbus unit number", 1]),
|
2014-04-28 13:29:55 +00:00
|
|
|
], self.class)
|
|
|
|
|
2013-08-30 21:28:54 +00:00
|
|
|
end
|
2012-06-05 20:06:05 +00:00
|
|
|
|
2014-04-28 13:29:55 +00:00
|
|
|
# a wrapper just to be sure we increment the counter
|
2014-04-29 14:09:47 +00:00
|
|
|
def send_frame(payload)
|
2014-04-28 13:29:55 +00:00
|
|
|
sock.put(payload)
|
2014-04-29 14:09:47 +00:00
|
|
|
@modbus_counter += 1
|
2014-06-28 20:24:02 +00:00
|
|
|
sock.get_once(-1, sock.def_read_timeout)
|
2014-04-28 13:29:55 +00:00
|
|
|
end
|
|
|
|
|
2014-05-01 15:23:33 +00:00
|
|
|
def make_payload(payload)
|
|
|
|
packet_data = [@modbus_counter].pack("n")
|
|
|
|
packet_data += "\x00\x00\x00" #dunno what these are
|
|
|
|
packet_data += [payload.size].pack("c") # size byte
|
|
|
|
packet_data += payload
|
|
|
|
|
|
|
|
packet_data
|
|
|
|
end
|
|
|
|
|
2014-04-28 13:29:55 +00:00
|
|
|
def make_read_payload
|
2014-05-01 14:52:55 +00:00
|
|
|
payload = [datastore['UNIT_NUMBER']].pack("c")
|
2014-04-28 13:29:55 +00:00
|
|
|
payload += [@function_code].pack("c")
|
|
|
|
payload += [datastore['DATA_ADDRESS']].pack("n")
|
|
|
|
payload += [1].pack("n")
|
2014-06-28 20:24:02 +00:00
|
|
|
make_payload(payload)
|
2014-04-28 13:29:55 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def make_write_coil_payload(data)
|
2014-05-01 14:52:55 +00:00
|
|
|
payload = [datastore['UNIT_NUMBER']].pack("c")
|
2014-04-28 13:29:55 +00:00
|
|
|
payload += [@function_code].pack("c")
|
|
|
|
payload += [datastore['DATA_ADDRESS']].pack("n")
|
|
|
|
payload += [data].pack("c")
|
|
|
|
payload += "\x00"
|
|
|
|
|
2014-05-01 15:23:33 +00:00
|
|
|
packet_data = make_payload(payload)
|
2014-04-28 13:29:55 +00:00
|
|
|
|
2014-05-01 14:52:55 +00:00
|
|
|
packet_data
|
2014-04-28 13:29:55 +00:00
|
|
|
end
|
2012-06-05 20:06:05 +00:00
|
|
|
|
2014-05-01 16:04:29 +00:00
|
|
|
def make_write_register_payload(data)
|
|
|
|
payload = [datastore['UNIT_NUMBER']].pack("c")
|
2014-04-28 13:29:55 +00:00
|
|
|
payload += [@function_code].pack("c")
|
|
|
|
payload += [datastore['DATA_ADDRESS']].pack("n")
|
|
|
|
payload += [data].pack("n")
|
|
|
|
|
2014-06-28 20:24:02 +00:00
|
|
|
make_payload(payload)
|
2014-04-28 13:29:55 +00:00
|
|
|
end
|
|
|
|
|
2014-05-05 21:21:54 +00:00
|
|
|
def handle_error(response)
|
|
|
|
case response.reverse.unpack("c")[0].to_i
|
|
|
|
when 1
|
|
|
|
print_error("Error : ILLEGAL FUNCTION")
|
|
|
|
when 2
|
|
|
|
print_error("Error : ILLEGAL DATA ADDRESS")
|
|
|
|
when 3
|
|
|
|
print_error("Error : ILLEGAL DATA VALUE")
|
|
|
|
when 4
|
|
|
|
print_error("Error : SLAVE DEVICE FAILURE")
|
|
|
|
when 6
|
|
|
|
print_error("Error : SLAVE DEVICE BUSY")
|
|
|
|
else
|
|
|
|
print_error("Unknown error")
|
|
|
|
end
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2014-05-01 16:04:29 +00:00
|
|
|
def read_coil
|
2014-05-08 15:44:53 +00:00
|
|
|
@function_code = 0x1
|
2014-05-01 16:05:55 +00:00
|
|
|
print_status("Sending READ COIL...")
|
2014-05-01 16:04:29 +00:00
|
|
|
response = send_frame(make_read_payload)
|
|
|
|
if response.nil?
|
|
|
|
print_error("No answer for the READ COIL")
|
|
|
|
return
|
2014-05-08 15:44:53 +00:00
|
|
|
elsif response.unpack("C*")[7] == (0x80 | @function_code)
|
2014-05-05 21:21:54 +00:00
|
|
|
handle_error(response)
|
2014-05-08 15:44:53 +00:00
|
|
|
elsif response.unpack("C*")[7] == @function_code
|
|
|
|
value = response[9].unpack("c")[0]
|
|
|
|
print_good("Coil value at address #{datastore['DATA_ADDRESS']} : #{value}")
|
2014-05-05 21:21:54 +00:00
|
|
|
else
|
2014-05-08 15:44:53 +00:00
|
|
|
print_error("Unknown answer")
|
2014-05-01 16:04:29 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def read_register
|
|
|
|
@function_code = 3
|
2014-05-01 16:05:55 +00:00
|
|
|
print_status("Sending READ REGISTER...")
|
2014-05-01 16:04:29 +00:00
|
|
|
response = send_frame(make_read_payload)
|
|
|
|
if response.nil?
|
|
|
|
print_error("No answer for the READ REGISTER")
|
2014-05-08 15:44:53 +00:00
|
|
|
elsif response.unpack("C*")[7] == (0x80 | @function_code)
|
2014-05-05 21:21:54 +00:00
|
|
|
handle_error(response)
|
2014-05-08 15:44:53 +00:00
|
|
|
elsif response.unpack("C*")[7] == @function_code
|
|
|
|
value = response[9..10].unpack("n")[0]
|
|
|
|
print_good("Register value at address #{datastore['DATA_ADDRESS']} : #{value}")
|
2014-05-05 21:21:54 +00:00
|
|
|
else
|
2014-05-08 15:44:53 +00:00
|
|
|
print_error("Unknown answer")
|
2014-05-05 21:21:54 +00:00
|
|
|
end
|
2014-05-01 16:04:29 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def write_coil
|
|
|
|
@function_code = 5
|
|
|
|
if datastore['DATA'] == 0
|
|
|
|
data = 0
|
|
|
|
elsif datastore['DATA'] == 1
|
|
|
|
data = 255
|
|
|
|
else
|
|
|
|
print_error("Data value must be 0 or 1 in WRITE_COIL mode")
|
|
|
|
return
|
|
|
|
end
|
2014-05-01 16:05:55 +00:00
|
|
|
print_status("Sending WRITE COIL...")
|
2014-05-01 16:04:29 +00:00
|
|
|
response = send_frame(make_write_coil_payload(data))
|
|
|
|
if response.nil?
|
|
|
|
print_error("No answer for the WRITE COIL")
|
2014-05-08 15:44:53 +00:00
|
|
|
elsif response.unpack("C*")[7] == (0x80 | @function_code)
|
2014-05-05 21:21:54 +00:00
|
|
|
handle_error(response)
|
2014-05-08 15:44:53 +00:00
|
|
|
elsif response.unpack("C*")[7] == @function_code
|
2014-05-05 21:21:54 +00:00
|
|
|
print_good("Value #{datastore['DATA']} successfully written at coil address #{datastore['DATA_ADDRESS']}")
|
2014-05-08 15:44:53 +00:00
|
|
|
else
|
|
|
|
print_error("Unknown answer")
|
2014-05-01 16:04:29 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def write_register
|
|
|
|
@function_code = 6
|
|
|
|
if datastore['DATA'] < 0 || datastore['DATA'] > 65535
|
|
|
|
print_error("Data to write must be an integer between 0 and 65535 in WRITE_REGISTER mode")
|
|
|
|
return
|
|
|
|
end
|
2014-05-01 16:05:55 +00:00
|
|
|
print_status("Sending WRITE REGISTER...")
|
2014-05-01 16:04:29 +00:00
|
|
|
response = send_frame(make_write_register_payload(datastore['DATA']))
|
|
|
|
if response.nil?
|
|
|
|
print_error("No answer for the WRITE REGISTER")
|
2014-05-08 15:44:53 +00:00
|
|
|
elsif response.unpack("C*")[7] == (0x80 | @function_code)
|
2014-05-05 21:21:54 +00:00
|
|
|
handle_error(response)
|
2014-05-08 15:44:53 +00:00
|
|
|
elsif response.unpack("C*")[7] == @function_code
|
2014-05-05 21:21:54 +00:00
|
|
|
print_good("Value #{datastore['DATA']} successfully written at registry address #{datastore['DATA_ADDRESS']}")
|
2014-05-08 15:44:53 +00:00
|
|
|
else
|
|
|
|
print_error("Unknown answer")
|
2014-05-01 16:04:29 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-04-28 13:29:55 +00:00
|
|
|
def run
|
2014-04-29 14:09:47 +00:00
|
|
|
@modbus_counter = 0x0000 # used for modbus frames
|
2014-04-28 13:29:55 +00:00
|
|
|
connect
|
2014-05-08 15:44:53 +00:00
|
|
|
case action.name
|
2014-04-28 13:29:55 +00:00
|
|
|
when "READ_COIL"
|
2014-05-01 16:04:29 +00:00
|
|
|
read_coil
|
2014-04-28 13:29:55 +00:00
|
|
|
when "READ_REGISTER"
|
2014-05-01 16:04:29 +00:00
|
|
|
read_register
|
2014-04-28 13:29:55 +00:00
|
|
|
when "WRITE_COIL"
|
2014-05-01 16:04:29 +00:00
|
|
|
write_coil
|
2014-04-28 13:29:55 +00:00
|
|
|
when "WRITE_REGISTER"
|
2014-05-01 16:04:29 +00:00
|
|
|
write_register
|
2014-04-28 13:29:55 +00:00
|
|
|
else
|
2014-04-29 14:09:47 +00:00
|
|
|
print_error("Invalid ACTION")
|
2014-04-28 13:29:55 +00:00
|
|
|
end
|
2014-05-01 14:52:55 +00:00
|
|
|
disconnect
|
2014-04-28 13:29:55 +00:00
|
|
|
end
|
2014-06-17 19:03:18 +00:00
|
|
|
end
|