Skip to content

Commit d7e1615

Browse files
committed
Wire the PHP-FPM toolchain into the build and CI
Makefile: add targets for building nginx/PHP (build-nginx, build-php, build-all), drafting a profile (profile-draft), remote calibration (calibrate-remote), the remote chain (remote) and the unpatched matrix (matrix). Split the rootless FPM lab targets (labs-fpm-*) from the Docker labs, add labs-fpm-smoke/ports, and split profile validation into the generic schema and the strict FPM schema. `lint` now runs ruff (matching CI) and the gitleaks scan moves to its own `secrets` target. CI: lint with ruff, compile the tools, check shell syntax, run the unit suite, and validate both the generic and FPM profile schemas, alongside the existing gitleaks secrets job. Add the manual live-lab workflow (build + exploit 8.4.x) and the tag-triggered release workflow, plus a dev requirements file (jsonschema, ruff) the CI installs. pyproject: keep the php2shell identity but modernize the build backend (setuptools>=77, build_meta, PEP 639 license-files) and add the php-fpm keyword.
1 parent 04eea6b commit d7e1615

6 files changed

Lines changed: 231 additions & 35 deletions

File tree

‎.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: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
- name: Enable ptrace for the oracle test
42+
run: sudo sysctl -w kernel.yama.ptrace_scope=0
43+
44+
- name: Run live tests
45+
env:
46+
PHP2SHELL_FPM_LAB: "1"
47+
PHP2SHELL_FPM_VERSION: ${{ matrix.version }}
48+
run: python3 -m unittest tests.test_fpm.FpmLabIntegrationTests -v
49+
50+
- name: Stop lab
51+
if: always()
52+
run: python3 tools/php2shell_fpm_labs.py down

‎.github/workflows/release.yml‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ["v*"]
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
release:
12+
name: Publish GitHub release
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Check out
16+
uses: actions/checkout@v4
17+
18+
- name: Create release
19+
uses: softprops/action-gh-release@v2
20+
with:
21+
generate_release_notes: true

‎Makefile‎

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,97 @@
1-
.PHONY: bootstrap test labs-up labs-down labs-status labs-test labs-fpm-up labs-fpm-down labs-fpm-status labs-fpm-test lint clean
1+
.PHONY: help test lint secrets ci bootstrap inventory validate-profiles validate-fpm \
2+
build-nginx build-php build-all profile-draft calibrate-remote remote matrix \
3+
labs-up labs-down labs-status labs-test \
4+
labs-fpm-up labs-fpm-down labs-fpm-status labs-fpm-smoke labs-fpm-ports labs-fpm-test \
5+
clean
26

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

611
test: ## Run the unit test suite
7-
python3 -m unittest discover -s tests -v
12+
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -v
13+
14+
lint: ## Run ruff on the Python tools and tests
15+
ruff check tools tests
816

9-
lint: ## Run gitleaks to detect leaked secrets
17+
secrets: ## Run gitleaks to detect leaked secrets
1018
gitleaks detect --source . --no-git --redact -v
1119

12-
labs-up: ## Start all local laboratories (ports 8100-8106)
20+
ci: lint test validate-profiles validate-fpm ## Run the fast CI checks locally
21+
22+
bootstrap: ## Fetch and pin the upstream exploit
23+
python3 tools/bootstrap_upstream.py
24+
25+
inventory: ## Print the profile and offset inventory
26+
python3 tools/php2shell_inventory.py
27+
28+
validate-profiles: ## Validate every build profile against the generic schema
29+
python3 -c "import json, jsonschema; \
30+
schema = json.load(open('profiles/schema.json')); \
31+
from pathlib import Path; \
32+
[jsonschema.validate(json.loads(p.read_text()), schema) or print(f' ok {p.name}') \
33+
for p in sorted(Path('profiles').glob('*.json')) \
34+
if p.name not in ('schema.json', 'fpm-schema.json')]"
35+
36+
validate-fpm: ## Validate the PHP-FPM profiles against the FPM schema
37+
python3 -c "import glob, json, jsonschema; \
38+
s = json.load(open('profiles/fpm-schema.json')); \
39+
[jsonschema.validate(json.load(open(p)), s) or print(f' ok {p}') \
40+
for p in sorted(glob.glob('profiles/*-fpm.json'))]"
41+
42+
# --------------------------------------------------------------------------- #
43+
# PHP-FPM laboratory (nginx + PHP-FPM, user prefix, no Docker)
44+
# --------------------------------------------------------------------------- #
45+
46+
build-nginx: ## Build nginx into lab/fpm/runtime
47+
lab/fpm/build_nginx.sh
48+
49+
build-php: ## Build a PHP version into lab/fpm/runtime (VERSION=8.4.26)
50+
@test -n "$(VERSION)" || \
51+
(echo "usage: make build-php VERSION=8.4.26"; exit 2)
52+
lab/fpm/build_php.sh "$(VERSION)"
53+
54+
build-all: ## Build several PHP versions (VERSIONS="8.4.25 8.4.26")
55+
@test -n "$(VERSIONS)" || \
56+
(echo "usage: make build-all VERSIONS=\"8.4.25 8.4.26\""; exit 2)
57+
lab/fpm/build_all.sh $(VERSIONS)
58+
59+
profile-draft: ## Generate a draft FPM profile (PHP_FPM=... VERSION=...)
60+
@test -n "$(PHP_FPM)" -a -n "$(VERSION)" || \
61+
(echo "usage: make profile-draft PHP_FPM=/path/to/php-fpm VERSION=8.4.25"; exit 2)
62+
python3 tools/php2shell_fpm_profile.py --php-fpm "$(PHP_FPM)" --version "$(VERSION)"
63+
64+
calibrate-remote: ## Discover a profile over HTTP only (VERSION=... BASE=... BUILDER=...)
65+
@test -n "$(VERSION)" || \
66+
(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)
67+
python3 tools/php2shell_fpm.py --version "$(VERSION)" \
68+
--base "$(BASE)" --builder "$(BUILDER)" calibrate
69+
70+
remote: ## Run the 100%-remote chain (VERSION=... BASE=... BUILDER=...)
71+
@test -n "$(VERSION)" || \
72+
(echo "usage: make remote VERSION=8.2.34 BASE=http://127.0.0.1:8406 BUILDER=http://127.0.0.1:8407"; exit 2)
73+
python3 tools/php2shell_fpm.py --version "$(VERSION)" \
74+
--base "$(BASE)" --builder "$(BUILDER)" \
75+
--command "id>out/remote-$(VERSION)" --proof-file "out/remote-$(VERSION)" \
76+
no-oracle
77+
78+
matrix: ## Verify the remote chain on every unpatched build
79+
python3 tools/php2shell_fpm_matrix.py
80+
81+
# --------------------------------------------------------------------------- #
82+
# Laboratories
83+
# --------------------------------------------------------------------------- #
84+
85+
labs-up: ## Start all local Docker laboratories (ports 8100-8106)
1386
python3 tools/php2shell_labs.py up
1487

15-
labs-down: ## Stop all local laboratories
88+
labs-down: ## Stop all local Docker laboratories
1689
python3 tools/php2shell_labs.py down
1790

18-
labs-status: ## Show laboratory container status
91+
labs-status: ## Show Docker laboratory container status
1992
python3 tools/php2shell_labs.py status
2093

21-
labs-test: ## Run the dispatcher against each lab
94+
labs-test: ## Run the dispatcher against each Docker lab
2295
python3 tools/php2shell_labs.py test
2396

2497
labs-fpm-up: ## Start the nginx + PHP-FPM laboratories (no Docker)
@@ -30,23 +103,16 @@ labs-fpm-down: ## Stop the nginx + PHP-FPM laboratories
30103
labs-fpm-status: ## Show FPM laboratory status
31104
python3 tools/php2shell_fpm_labs.py status
32105

33-
labs-fpm-test: ## Run primitive + endgame against the FPM lab
34-
python3 tools/php2shell_fpm_labs.py test
106+
labs-fpm-smoke: ## Check the FPM lab endpoints
107+
python3 tools/php2shell_fpm_labs.py smoke
35108

36-
inventory: ## Print the profile and offset inventory
37-
python3 tools/php2shell_inventory.py
109+
labs-fpm-ports: ## Print the FPM lab port map
110+
python3 tools/php2shell_fpm_labs.py ports
38111

39-
validate-profiles: ## Validate profiles (requires jsonschema)
40-
python3 -c "import json, jsonschema; \
41-
schema = json.load(open('profiles/schema.json')); \
42-
from pathlib import Path; \
43-
[jsonschema.validate(json.loads(p.read_text()), schema) or print(f' ok {p.name}') \
44-
for p in sorted(Path('profiles').glob('*.json')) if p.name != 'schema.json']"
112+
labs-fpm-test: ## Run primitive + endgame against the FPM lab
113+
python3 tools/php2shell_fpm_labs.py test
45114

46115
clean: ## Remove generated files and vendor checkout
47116
rm -rf vendor/php2shell-upstream
48117
rm -rf lab/rce/out/
49-
50-
help: ## Show this help
51-
@grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \
52-
awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
118+
rm -rf lab/fpm/webroot/out/

‎pyproject.toml‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
[project]
22
name = "php2shell"
33
version = "1.0.0"
4-
description = "Pre-auth RCE research toolkit for CVE-2026-109618 / CVE-2026-109619"
4+
description = "Pre-auth RCE research toolkit for CVE-2026-109618 / CVE-2026-109619 (Apache mod_php and nginx + PHP-FPM chains)"
55
readme = "README.md"
66
license = "MIT"
7+
license-files = ["LICENSE"]
78
requires-python = ">=3.11"
89
authors = [{ name = "devploit" }]
9-
keywords = ["security", "research", "php", "rce", "exploit"]
10+
keywords = ["security", "research", "php", "php-fpm", "rce", "exploit"]
1011
classifiers = [
1112
"Development Status :: 4 - Beta",
1213
"Intended Audience :: Information Technology",
1314
"Intended Audience :: Science/Research",
14-
"License :: OSI Approved :: MIT License",
1515
"Programming Language :: Python :: 3",
1616
"Programming Language :: Python :: 3.11",
1717
"Programming Language :: Python :: 3.12",
@@ -24,8 +24,8 @@ Repository = "https://github.com/devploit/php2shell"
2424
Documentation = "https://github.com/devploit/php2shell/blob/main/docs/php2shell-analysis.md"
2525

2626
[build-system]
27-
requires = ["setuptools>=68.0"]
28-
build-backend = "setuptools.backends._legacy:_Backend"
27+
requires = ["setuptools>=77.0"]
28+
build-backend = "setuptools.build_meta"
2929

3030
[tool.setuptools]
3131
packages = []

‎requirements-dev.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
jsonschema>=4.20
2+
ruff>=0.6

0 commit comments

Comments
 (0)