Repository navigation
Expand file tree
/
Copy pathphp2shell_labs.py
More file actions
executable file
·154 lines (129 loc) · 5.2 KB
/
Copy pathphp2shell_labs.py
File metadata and controls
executable file
·154 lines (129 loc) · 5.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
php2shell_labs.py — disposable local laboratories for the PHP builds in the
profile/offset inventory.
Each lab is an official PHP Apache image with:
- display_errors=On, memory_limit=8M, post_max_size=64M (lab/rce/php.ini)
- one prefork worker (lab/rce/apache.conf)
- a minimal webroot (lab/rce/webroot)
- a per-lab /out mount for the proof file
Commands:
python3 tools/php2shell_labs.py up
python3 tools/php2shell_labs.py status
python3 tools/php2shell_labs.py test
python3 tools/php2shell_labs.py down
`test` runs tools/php2shell_auto.py against every lab and reports RCE/no-RCE.
Only builds with a verified RCE profile will execute a command; the rest are
refused by the dispatcher.
"""
from __future__ import annotations
from php2shell_paths import default_repo_dir, prepare_lab_out
import argparse
import os
import subprocess
import sys
import time
import urllib.request
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
LAB = os.path.join(ROOT, "lab", "rce")
OUT = os.path.join(LAB, "out")
REPO = default_repo_dir()
BUILDS = [
("7.4.33", "php:7.4-apache", 8100),
("8.0.30", "php:8.0-apache", 8101),
("8.1.34", "php:8.1-apache", 8102),
("8.2.34", "php:8.2.34-apache", 8103),
("8.3.35", "php:8.3.35-apache", 8104),
("8.4.26", "php:8.4.26-apache", 8105),
("8.5.11", "php:8.5.11-apache", 8106),
]
def docker(*args, timeout=900, capture=True):
return subprocess.run(["docker", *args], capture_output=capture, text=True, timeout=timeout)
def name_for(version: str) -> str:
return "p2s-lab-" + version.replace(".", "")
def wait_http(url: str, timeout: float = 40.0) -> bool:
deadline = time.time() + timeout
while time.time() < deadline:
try:
with urllib.request.urlopen(url, timeout=2) as resp:
if resp.status == 200:
return True
except Exception:
time.sleep(0.5)
return False
def up(only=None):
prepare_lab_out(OUT)
for version, image, port in BUILDS:
if only and version not in only:
continue
name = name_for(version)
outdir = prepare_lab_out(os.path.join(OUT, version))
docker("rm", "-f", name)
print(f"[*] {version}: starting {image} on :{port}", flush=True)
result = docker("run", "-d", "--name", name,
"-p", f"127.0.0.1:{port}:80",
"-v", f"{LAB}/webroot:/var/www/html:ro",
"-v", f"{LAB}/php.ini:/usr/local/etc/php/conf.d/zz-lab.ini:ro",
"-v", f"{LAB}/apache.conf:/etc/apache2/conf-enabled/zz-lab.conf:ro",
"-v", f"{outdir}:/out",
image)
if result.returncode != 0:
print(f" failed: {result.stderr.strip()[:200]}")
continue
ok = wait_http(f"http://127.0.0.1:{port}/index.php")
print(f" {'ready' if ok else 'NOT responding'} -> http://127.0.0.1:{port}/")
def down():
for version, _image, _port in BUILDS:
docker("rm", "-f", name_for(version))
def status():
for version, image, port in BUILDS:
result = docker("inspect", "-f", "{{.State.Status}}", name_for(version))
state = result.stdout.strip() if result.returncode == 0 else "absent"
print(f"{version:8} {image:24} :{port} {state}")
def test():
results = []
for version, _image, port in BUILDS:
name = name_for(version)
state = docker("inspect", "-f", "{{.State.Status}}", name).stdout.strip()
if state != "running":
results.append((version, "not running", ""))
continue
proof = os.path.join(OUT, version, "p1")
try:
os.unlink(proof)
except OSError:
pass
cmd = [sys.executable, os.path.join(HERE, "php2shell_auto.py"),
"-u", f"http://127.0.0.1:{port}/index.php",
"-c", "id>/out/p1", "--repo-dir", REPO,
"--proof-file", proof, "--memory-limit-mb", "8"]
print(f"\n=== {version} :{port} ===", flush=True)
result = subprocess.run(cmd, capture_output=True, text=True, timeout=900)
output = (result.stdout + result.stderr).strip().splitlines()
print("\n".join(output[-6:]), flush=True)
if result.returncode == 0 and os.path.isfile(proof):
results.append((version, "RCE", open(proof).read().strip()))
else:
results.append((version, "no RCE (exit %d)" % result.returncode, ""))
print("\n=== summary ===")
for version, status_text, proof in results:
print(f" {version:8} {status_text:24} {proof}")
def main() -> int:
ap = argparse.ArgumentParser(description="Local PHP2Shell laboratories.")
ap.add_argument("command", choices=("up", "down", "status", "test"))
ap.add_argument("--only", default=None, help="comma-separated versions for 'up'")
args = ap.parse_args()
only = set(args.only.split(",")) if args.only else None
if args.command == "up":
up(only)
elif args.command == "down":
down()
elif args.command == "status":
status()
else:
test()
return 0
if __name__ == "__main__":
sys.exit(main())