Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Magento Arbitrary File Read (CVE-2024-34102) + PHP Buffer Overflow iconv() of GLIBC (CVE-2024-2961) #19544

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

jheysel-r7
Copy link
Contributor

@jheysel-r7 jheysel-r7 commented Oct 9, 2024

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

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
  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.
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
  2. Do: use
  3. Set the RHOST, SRVHOST and LHOST options
  4. Run the module
  5. Receive 3 Meterpreter sessions as the daemon user.

Copy link
Contributor

@jvoisin jvoisin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CC @cfreal

'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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cfreal: do you know what's happening here?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A guess: you hang every worker? Try reproducing and then trying to reach a normal page. It may come from the fact that you don't kill the worker.

Copy link
Contributor Author

@jheysel-r7 jheysel-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review @jvoisin, much appreciated 🙇 Changes have been pushed in dab5d66

@dledda-r7 dledda-r7 self-assigned this Oct 10, 2024
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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the 3 meterpreter executions are coming from the free of the different chunks?

Comment on lines +509 to +512
command = payload.encoded

command = (command + "\x00").b
command = command.ljust(step4_use_custom_heap_size, "\x00".b)
Copy link
Contributor

@dledda-r7 dledda-r7 Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we should prevent to free (aka system) the other chunks that contains random data.
From the original PoC:

# We make sure that the "system" command kills the current process 
# to avoid other system() calls with random chunk data, leading to undefined behaviour.
...
 COMMAND = self.command
 COMMAND = f"kill -9 $PPID; {COMMAND}"
  if self.sleep:
      COMMAND = f"sleep {self.sleep}; {COMMAND}"
 COMMAND = COMMAND.encode() + b"\x00"

The most elegant way would be to reset the free to the __libc_free instead of system but I am not sure if we can do it in an easy way.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,

In the current state of affairs, it is very hard to make the process go on without crashing. That's why I kill it. Here's why.

The exploit overwrites zend_mm_heap.custom_heap function pointers, then overwrites zend_mm_heap.use_custom_heap to make PHP actually use the modified funcs. At this point, if we manage to make PHP call efree(some_buffer_we_control), we get RCE, because it calls zend_mm_heap.custom_heap.efree (which is system@libc). However, before this happens, PHP will want to allocate some structures (for instance, a php_stream_bucket). We cannot avoid this. Therefore, we need to provide a way for PHP to allocate before we can reach the efree() call. I used malloc@libc because it was a way to get memory, not because they were the "complement" of free@libc. Now, in the original exploit, as soon as the system() calls comes in, the worker process gets killed before it can do nasty things. However, if we let it live, he'd have some chunks allocated on its real, PHP, heap, and others in the libc heap. We'd not know which free() function needs to be called for each.

To let the process live, we could ROP, but that would (to me) break the beauty of the exploit, which lies in its heavy compatibility.

Not quite sure I'm clear, so please ask if not!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was unable to return a session (had tried cmd/linux/http/x64/meterpreter/reverse_tcp, cmd/unix/reverse_bash, cmd/unix/reverse_netcat) while prepending sleep 1; kill -9 $PPID to the payload.

However when using cmd/unix/generic with CMD => 'touch /tmp/vuln with sleep 1; kill -9 $PPID prepended, the payload gets delivered successfully and the file gets written to disk.

I meant to mention this and go back and do some investigating with a process monitor to try and deduce why I wasn't getting a session when attempting to kill the parent process/ why 3 were coming back when I wasn't though I was eager to get this posted my apologies.

I'll take a look today and let you know what I find. If I can't get it working I'll consider your more elegant solution. Thank you @dledda-r7

Copy link

@cfreal cfreal Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jheysel-r7 just to be clear, the sleep 1 does not do anything, it's just there to induce lag and confirm the code actually triggered. Also (but I think our messages crossed) the solution suggested by @dledda-r7 would not work, sadly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

4 participants