# php2shell — PHP 8.4.26 Session Vulnerability Reproduction
[中文](#中文) | [English](#english)
---
## English
Under the constraints of "completely remote, HTTP only, target page without `session_start()`, no PHP written on the target side," use **a single `exploit.py`** to achieve RCE on PHP 8.4.26 + Apache prefork + mod_php. The target page contains only `<?php echo "OK\n"; ?>`.
### Supported Scope (Read First)
> **The only supported runtime environment is the official `php:8.4.26-apache` image** (PHP 8.4.26 / Debian 12 / Apache prefork + mod_php) with configurations under [docker/](docker/) in this repository.
This is not a generic ASLR bypass and does not disable ASLR:
- The exploitation process leaks only the **Zend MM chunk base** and **libphp base** through two out-of-bounds reads; it does not leak libc, stack, or complete memory maps.
- The address space remains randomized by ASLR. The host kernel ASLR (kASLR), container runtime, kernel `mmap` behavior, allocator, and the specific build of PHP/Apache all affect heap layout and leak results.
- The heap geometry, calibration values, and libphp offsets in the chain are based on measurements from the official image and do not include address scanning or brute force enumeration. Changing the image, PHP build, or distribution may invalidate the object forging layout even if addresses can still be leaked.
Apache also has strict prerequisites:
- Must be **prefork MPM + single worker + mod_php**. After a child process aborts, the parent process directly `fork`s a new worker without `exec`, preserving the libphp mapping and address space dependencies at each stage.
- `docker/apache.conf` fixes `StartServers/MinSpareServers/MaxSpareServers/MaxRequestWorkers/ServerLimit = 1`, `MaxConnectionsPerChild = 0`, and disables KeepAlive.
- Multiple workers, event/worker MPM, Apache restart/graceful restart, worker re-`exec`, container rebuild, or parent process exit can re-randomize addresses or route requests to different processes, breaking the entire chain.
- If leaking fails at any stage, ASLR remains effective; retrying only switches to a different worker and does not "defeat" ASLR.
The current RCE also requires: `display_errors=1`, `memory_limit=8M`, `max_input_vars=1000`, `session.use_cookies=1`, `session.use_strict_mode=0`, `session.upload_progress.enabled=1`.
### One-Command Reproduction
This repository does not compile PHP/Apache and has no `docker build`: the environment is simply the official `php:8.4.26-apache` image with configurations from `docker/` mounted.
```bash
# Dependencies: 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) Pull the official image (PHP 8.4.26 / Debian 12 / Apache prefork)
docker compose -f docker/docker-compose.yml pull
# 2) Start target: single worker, display_errors=On, memory_limit=8M
docker compose -f docker/docker-compose.yml up -d
# 3) Wait for Apache to be ready and confirm normal page access
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) Complete the entire exploitation with one command; first run automatically reads libphp offsets
python3 exploit.py -u http://127.0.0.1:8088