diff --git a/modules/auxiliary/scanner/scada/modbusclient.rb b/modules/auxiliary/scanner/scada/modbusclient.rb index 70b5242414..1bb2e6c054 100644 --- a/modules/auxiliary/scanner/scada/modbusclient.rb +++ b/modules/auxiliary/scanner/scada/modbusclient.rb @@ -20,24 +20,31 @@ class Metasploit3 < Msf::Auxiliary 'Author' => [ 'EsMnemon ', # original write-only module - 'Arnaud SOULLIE ' # new code that allows read/write + 'Arnaud SOULLIE ', # code that allows read/write + 'Alexandrine TORRENTS ', # code that allows reading/writing at multiple consecutive addresses + 'Mathieu CHEVALIER ' ], 'License' => MSF_LICENSE, 'Actions' => [ - ['READ_COIL', { 'Description' => 'Read one bit from a coil' } ], + ['READ_COILS', { 'Description' => 'Read bits from several coils' } ], + ['READ_REGISTERS', { 'Description' => 'Read words from several registers' } ], ['WRITE_COIL', { 'Description' => 'Write one bit to a coil' } ], - ['READ_REGISTER', { 'Description' => 'Read one word from a register' } ], - ['WRITE_REGISTER', { 'Description' => 'Write one word to a register' } ] + ['WRITE_REGISTER', { 'Description' => 'Write one word to a register' } ], + ['WRITE_COILS', { 'Description' => 'Write bits to several coils' } ], + ['WRITE_REGISTERS', { 'Description' => 'Write words to several registers' } ] ], - 'DefaultAction' => 'READ_REGISTER' + 'DefaultAction' => 'READ_REGISTERS' )) register_options( [ Opt::RPORT(502), - OptInt.new('DATA', [false, "Data to write (WRITE_COIL and WRITE_REGISTER modes only)"]), OptInt.new('DATA_ADDRESS', [true, "Modbus data address"]), + OptInt.new('NUMBER', [false, "Number of coils/registers to read (READ_COILS ans READ_REGISTERS modes only)", 1]), + OptInt.new('DATA', [false, "Data to write (WRITE_COIL and WRITE_REGISTER modes only)"]), + OptString.new('DATA_COILS', [false, "Data in binary to write (WRITE_COILS mode only) e.g. 0110"]), + OptString.new('DATA_REGISTERS', [false, "Words to write to each register separated with a comma (WRITE_REGISTERS mode only) e.g. 1,2,3,4"]), OptInt.new('UNIT_NUMBER', [false, "Modbus unit number", 1]), ], self.class) @@ -63,7 +70,7 @@ class Metasploit3 < Msf::Auxiliary payload = [datastore['UNIT_NUMBER']].pack("c") payload += [@function_code].pack("c") payload += [datastore['DATA_ADDRESS']].pack("n") - payload += [1].pack("n") + payload += [datastore['NUMBER']].pack("n") make_payload(payload) end @@ -79,6 +86,21 @@ class Metasploit3 < Msf::Auxiliary packet_data end + def make_write_coils_payload(data, byte) + payload = [datastore['UNIT_NUMBER']].pack("c") + payload += [@function_code].pack("c") + payload += [datastore['DATA_ADDRESS']].pack("n") + payload += [datastore['DATA_COILS'].size].pack("n") # bit count + payload += [byte].pack("c") # byte count + for i in 0..(byte-1) + payload += [data[i]].pack("b*") + end + + packet_data = make_payload(payload) + + packet_data + end + def make_write_register_payload(data) payload = [datastore['UNIT_NUMBER']].pack("c") payload += [@function_code].pack("c") @@ -88,6 +110,19 @@ class Metasploit3 < Msf::Auxiliary make_payload(payload) end + def make_write_registers_payload(data, size) + payload = [datastore['UNIT_NUMBER']].pack("c") + payload += [@function_code].pack("c") + payload += [datastore['DATA_ADDRESS']].pack("n") + payload += [size].pack("n") # word count + payload += [2*size].pack("c") # byte count + for i in 0..(size-1) + payload += [data[i]].pack("n") + end + + make_payload(payload) + end + def handle_error(response) case response.reverse.unpack("c")[0].to_i when 1 @@ -106,34 +141,57 @@ class Metasploit3 < Msf::Auxiliary return end - def read_coil + def read_coils + if datastore['NUMBER']+datastore['DATA_ADDRESS'] > 65535 + print_error("Coils addresses go from 0 to 65535. You cannot go beyond.") + return + end @function_code = 0x1 - print_status("Sending READ COIL...") + print_status("Sending READ COILS...") response = send_frame(make_read_payload) + values = [] if response.nil? - print_error("No answer for the READ COIL") + print_error("No answer for the READ COILS") return elsif response.unpack("C*")[7] == (0x80 | @function_code) handle_error(response) elsif response.unpack("C*")[7] == @function_code - value = response[9].unpack("c")[0] - print_good("Coil value at address #{datastore['DATA_ADDRESS']} : #{value}") + loop = (datastore['NUMBER']-1)/8 + for i in 0..loop + bin_value = response[9+i].unpack("b*")[0] + list = bin_value.split("") + for j in 0..7 + list[j] = list[j].to_i + values[i*8 + j] = list[j] + end + end + values = values[0..(datastore['NUMBER']-1)] + print_good("#{datastore['NUMBER']} coil values from address #{datastore['DATA_ADDRESS']} : ") + print_good("#{values}") else print_error("Unknown answer") end end - def read_register + def read_registers + if datastore['NUMBER']+datastore['DATA_ADDRESS'] > 65535 + print_error("Registers addresses go from 0 to 65535. You cannot go beyond.") + return + end @function_code = 3 - print_status("Sending READ REGISTER...") + print_status("Sending READ REGISTERS...") response = send_frame(make_read_payload) + values = [] if response.nil? - print_error("No answer for the READ REGISTER") + print_error("No answer for the READ REGISTERS") elsif response.unpack("C*")[7] == (0x80 | @function_code) handle_error(response) elsif response.unpack("C*")[7] == @function_code - value = response[9..10].unpack("n")[0] - print_good("Register value at address #{datastore['DATA_ADDRESS']} : #{value}") + for i in 0..(datastore['NUMBER']-1) + values.push(response[9+2*i..10+2*i].unpack("n")[0]) + end + print_good("#{datastore['NUMBER']} register values from address #{datastore['DATA_ADDRESS']} : ") + print_good("#{values}") else print_error("Unknown answer") end @@ -162,6 +220,39 @@ class Metasploit3 < Msf::Auxiliary end end + def write_coils + @function_code = 15 + temp = datastore['DATA_COILS'] + check = temp.split("") + if temp.size > 65535 + print_error("DATA_COILS size must be between 0 and 65535") + return + end + for j in check + if j=="0" or j=="1" + else + print_error("DATA_COILS value must only contain 0s and 1s without space") + return + end + end + byte_number = (temp.size-1)/8 + 1 + data = [] + for i in 0..(byte_number-1) + data.push(temp[(i*8+0)..(i*8+7)]) + end + print_status("Sending WRITE COILS...") + response = send_frame(make_write_coils_payload(data, byte_number)) + if response.nil? + print_error("No answer for the WRITE COILS") + elsif response.unpack("C*")[7] == (0x80 | @function_code) + handle_error(response) + elsif response.unpack("C*")[7] == @function_code + print_good("Values #{datastore['DATA_COILS']} successfully written from coil address #{datastore['DATA_ADDRESS']}") + else + print_error("Unknown answer") + end + end + def write_register @function_code = 6 if datastore['DATA'] < 0 || datastore['DATA'] > 65535 @@ -181,18 +272,74 @@ class Metasploit3 < Msf::Auxiliary end end + def write_registers + @function_code = 16 + check = datastore['DATA_REGISTERS'].split("") + for j in 0..(check.size-1) + if check[j] == "0" or check[j]== "1" or check[j]== "2" or check[j]== "3" or check[j]== "4" or check[j]== "5" or check[j]== "6" or check[j]== "7" or check[j]== "8" or check[j]== "9" or check[j]== "," + if check[j] == "," and check[j+1] == "," + print_error("DATA_REGISTERS cannot contain two consecutive commas") + return + end + else + print_error("DATA_REGISTERS value must only contain numbers and commas without space") + return + end + end + list = datastore['DATA_REGISTERS'].split(",") + if list.size+datastore['DATA_ADDRESS'] > 65535 + print_error("Registers addresses go from 0 to 65535. You cannot go beyond.") + return + end + data = [] + for i in 0..(list.size-1) + data[i] = list[i].to_i + end + for j in 0..(data.size-1) + if data[j] < 0 || data[j] > 65535 + print_error("Each word to write must be an integer between 0 and 65535 in WRITE_REGISTERS mode") + return + end + end + print_status("Sending WRITE REGISTERS...") + response = send_frame(make_write_registers_payload(data, data.size)) + if response.nil? + print_error("No answer for the WRITE REGISTERS") + elsif response.unpack("C*")[7] == (0x80 | @function_code) + handle_error(response) + elsif response.unpack("C*")[7] == @function_code + print_good("Values #{datastore['DATA_REGISTERS']} successfully written from registry address #{datastore['DATA_ADDRESS']}") + else + print_error("Unknown answer") + end + end + def run @modbus_counter = 0x0000 # used for modbus frames connect case action.name - when "READ_COIL" - read_coil - when "READ_REGISTER" - read_register + when "READ_COILS" + read_coils + when "READ_REGISTERS" + read_registers when "WRITE_COIL" write_coil when "WRITE_REGISTER" write_register + when "WRITE_COILS" + if datastore['DATA_COILS'] == nil + print_error("The following option is needed in WRITE_COILS mode: DATA_COILS.") + return + else + write_coils + end + when "WRITE_REGISTERS" + if datastore['DATA_REGISTERS'] == nil + print_error("The following option is needed in WRITE_REGISTERS mode: DATA_REGISTERS.") + return + else + write_registers + end else print_error("Invalid ACTION") end