Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 62 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: ci
name: CI

on:
push:
Expand All @@ -8,26 +8,81 @@ on:
permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
tests:
test:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12"]
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Check out
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: requirements-dev.txt

- name: Install test dependencies
run: python -m pip install --upgrade pip -r requirements-dev.txt

- name: Lint
run: ruff check tools tests

- name: Compile Python
run: python -m compileall -q tools tests

- name: Check shell syntax
run: bash -n lab/fpm/build_php.sh lab/fpm/build_nginx.sh lab/fpm/build_all.sh

- name: Unit tests
run: PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s tests -v

- name: Validate build profiles against schema
run: |
python - <<'PY'
import json
from pathlib import Path
import jsonschema

schema = json.loads(Path("profiles/schema.json").read_text())
for path in sorted(Path("profiles").glob("*.json")):
if path.name in ("schema.json", "fpm-schema.json"):
continue
jsonschema.validate(json.loads(path.read_text()), schema)
print(f"{path}: valid")
PY

- name: Validate PHP-FPM profiles against the FPM schema
run: |
python - <<'PY'
import json
from pathlib import Path
import jsonschema

schema = json.loads(Path("profiles/fpm-schema.json").read_text())
for path in sorted(Path("profiles").glob("*-fpm.json")):
jsonschema.validate(json.loads(path.read_text()), schema)
print(f"{path}: valid")
PY

secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check out
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2

- name: Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 changes: 52 additions & 0 deletions .github/workflows/live-fpm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Live FPM lab

on:
workflow_dispatch:

permissions:
contents: read

jobs:
live:
name: Build and exploit PHP ${{ matrix.version }}
runs-on: ubuntu-latest
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
version: ["8.4.25", "8.4.26"]
steps:
- name: Check out
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential curl tar libxml2-dev

- name: Build nginx
run: lab/fpm/build_nginx.sh

- name: Build PHP ${{ matrix.version }}
run: lab/fpm/build_php.sh ${{ matrix.version }}

- name: Start lab
run: python3 tools/php2shell_fpm_labs.py up --only ${{ matrix.version }}

- name: Enable ptrace for the oracle test
run: sudo sysctl -w kernel.yama.ptrace_scope=0

- name: Run live tests
env:
PHP2SHELL_FPM_LAB: "1"
PHP2SHELL_FPM_VERSION: ${{ matrix.version }}
run: python3 -m unittest tests.test_fpm.FpmLabIntegrationTests -v

- name: Stop lab
if: always()
run: python3 tools/php2shell_fpm_labs.py down
21 changes: 21 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Release

on:
push:
tags: ["v*"]

permissions:
contents: write

jobs:
release:
name: Publish GitHub release
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4

- name: Create release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ __pycache__/
vendor/
lab/wordpress/wp/
lab/rce/out/
lab/fpm/runtime/
lab/fpm/webroot/out/
120 changes: 99 additions & 21 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,40 +1,118 @@
.PHONY: bootstrap test labs-up labs-down labs-status labs-test lint clean
.PHONY: help test lint secrets ci bootstrap inventory validate-profiles validate-fpm \
build-nginx build-php build-all profile-draft calibrate-remote remote matrix \
labs-up labs-down labs-status labs-test \
labs-fpm-up labs-fpm-down labs-fpm-status labs-fpm-smoke labs-fpm-ports labs-fpm-test \
clean

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

test: ## Run the unit test suite
python3 -m unittest discover -s tests -v

lint: ## Run gitleaks to detect leaked secrets
gitleaks detect --source . --no-git --redact -v
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -v

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

labs-down: ## Stop all local laboratories
python3 tools/php2shell_labs.py down
secrets: ## Run gitleaks to detect leaked secrets
gitleaks detect --source . --no-git --redact -v

labs-status: ## Show laboratory container status
python3 tools/php2shell_labs.py status
ci: lint test validate-profiles validate-fpm ## Run the fast CI checks locally

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

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

validate-profiles: ## Validate profiles (requires jsonschema)
validate-profiles: ## Validate every build profile against the generic schema
python3 -c "import json, jsonschema; \
schema = json.load(open('profiles/schema.json')); \
from pathlib import Path; \
[jsonschema.validate(json.loads(p.read_text()), schema) or print(f' ok {p.name}') \
for p in sorted(Path('profiles').glob('*.json')) if p.name != 'schema.json']"
for p in sorted(Path('profiles').glob('*.json')) \
if p.name not in ('schema.json', 'fpm-schema.json')]"

validate-fpm: ## Validate the PHP-FPM profiles against the FPM schema
python3 -c "import glob, json, jsonschema; \
s = json.load(open('profiles/fpm-schema.json')); \
[jsonschema.validate(json.load(open(p)), s) or print(f' ok {p}') \
for p in sorted(glob.glob('profiles/*-fpm.json'))]"

# --------------------------------------------------------------------------- #
# PHP-FPM laboratory (nginx + PHP-FPM, user prefix, no Docker)
# --------------------------------------------------------------------------- #

build-nginx: ## Build nginx into lab/fpm/runtime
lab/fpm/build_nginx.sh

build-php: ## Build a PHP version into lab/fpm/runtime (VERSION=8.4.26)
@test -n "$(VERSION)" || \
(echo "usage: make build-php VERSION=8.4.26"; exit 2)
lab/fpm/build_php.sh "$(VERSION)"

build-all: ## Build several PHP versions (VERSIONS="8.4.25 8.4.26")
@test -n "$(VERSIONS)" || \
(echo "usage: make build-all VERSIONS=\"8.4.25 8.4.26\""; exit 2)
lab/fpm/build_all.sh $(VERSIONS)

profile-draft: ## Generate a draft FPM profile (PHP_FPM=... VERSION=...)
@test -n "$(PHP_FPM)" -a -n "$(VERSION)" || \
(echo "usage: make profile-draft PHP_FPM=/path/to/php-fpm VERSION=8.4.25"; exit 2)
python3 tools/php2shell_fpm_profile.py --php-fpm "$(PHP_FPM)" --version "$(VERSION)"

calibrate-remote: ## Discover a profile over HTTP only (VERSION=... BASE=... BUILDER=...)
@test -n "$(VERSION)" || \
(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)
python3 tools/php2shell_fpm.py --version "$(VERSION)" \
--base "$(BASE)" --builder "$(BUILDER)" calibrate

remote: ## Run the 100%-remote chain (VERSION=... BASE=... BUILDER=...)
@test -n "$(VERSION)" || \
(echo "usage: make remote VERSION=8.2.34 BASE=http://127.0.0.1:8406 BUILDER=http://127.0.0.1:8407"; exit 2)
python3 tools/php2shell_fpm.py --version "$(VERSION)" \
--base "$(BASE)" --builder "$(BUILDER)" \
--command "id>out/remote-$(VERSION)" --proof-file "out/remote-$(VERSION)" \
no-oracle

matrix: ## Verify the remote chain on every unpatched build
python3 tools/php2shell_fpm_matrix.py

# --------------------------------------------------------------------------- #
# Laboratories
# --------------------------------------------------------------------------- #

labs-up: ## Start all local Docker laboratories (ports 8100-8106)
python3 tools/php2shell_labs.py up

labs-down: ## Stop all local Docker laboratories
python3 tools/php2shell_labs.py down

labs-status: ## Show Docker laboratory container status
python3 tools/php2shell_labs.py status

labs-test: ## Run the dispatcher against each Docker lab
python3 tools/php2shell_labs.py test

labs-fpm-up: ## Start the nginx + PHP-FPM laboratories (no Docker)
python3 tools/php2shell_fpm_labs.py up

labs-fpm-down: ## Stop the nginx + PHP-FPM laboratories
python3 tools/php2shell_fpm_labs.py down

labs-fpm-status: ## Show FPM laboratory status
python3 tools/php2shell_fpm_labs.py status

labs-fpm-smoke: ## Check the FPM lab endpoints
python3 tools/php2shell_fpm_labs.py smoke

labs-fpm-ports: ## Print the FPM lab port map
python3 tools/php2shell_fpm_labs.py ports

labs-fpm-test: ## Run primitive + endgame against the FPM lab
python3 tools/php2shell_fpm_labs.py test

clean: ## Remove generated files and vendor checkout
rm -rf vendor/php2shell-upstream
rm -rf lab/rce/out/

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
rm -rf lab/fpm/webroot/out/
Loading