#!/usr/bin/env python3
#php2shell scanner
import argparse
import csv
import random
import re
import sys
import time
import urllib3
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import urljoin
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# ============================================================
# KONFIGURASI
# ============================================================
FIXED = {
(8, 2): 35,
(8, 3): 36,
(8, 4): 27,
(8, 5): 12,
}
# Path phpinfo umum (GET only, read-only)
PHPINFO_PATHS = [
"/phpinfo.php", "/info.php", "/test.php", "/php.php",
"/phpinfo", "/i.php", "/p.php",
]
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Mobile/15E148 Safari/604.1",
]
PHP_VER_RE = re.compile(r"PHP/(\d+)\.(\d+)\.(\d+)", re.I)
PHPINFO_RE = re.compile(r"PHP\s+Version\s*\s*
]*>\s*(\d+)\.(\d+)\.(\d+)", re.I | re.S)
GENERIC_RE = re.compile(r"\bPHP\s+(\d+)\.(\d+)\.(\d+)", re.I)
def headers():
return {
"User-Agent": random.choice(USER_AGENTS),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Connection": "keep-alive",
}
def fetch(session, url, timeout, verify, method="GET", **kwargs):
try:
time.sleep(random.uniform(0.15, 0.4))
if method == "POST":
return session.post(url, headers=headers(), timeout=timeout,
verify=verify, allow_redirects=True, **kwargs)
return session.get(url, headers=headers(), timeout=timeout,
verify=verify, allow_redirects=True, **kwargs)
except Exception:
return None
def parse_ver(text):
for rx in (PHP_VER_RE, PHPINFO_RE, GENERIC_RE):
m = rx.search(text or "")
if m:
return tuple(int(x) for x in m.groups())
return None
def verdict(v):
if not v:
return "UNKNOWN", "versi tidak terdeteksi"
maj, mnr, pat = v
br = (maj, mnr)
if br <= (8, 1):
return "EOL_NO_FIX", f"PHP {maj}.{mnr}.{pat} — branch EOL, tidak ada patch upstream"
fixed = FIXED.get(br)
if fixed is None:
return "UNKNOWN", f"PHP {maj}.{mnr}.{pat} — branch tidak dikenal"
if pat >= fixed:
return "PATCHED", f"PHP {maj}.{mnr}.{pat} >= {maj}.{mnr}.{fixed}"
return "VULNERABLE_VERSION", f"PHP {maj}.{mnr}.{pat} < {maj}.{mnr}.{fixed}"
def fingerprint(session, base, timeout, verify):
"""Kumpulkan kandidat versi dari header + phpinfo."""
cands = []
r = fetch(session, base, timeout, verify)
if r is None:
return cands, None
for h in ("X-Powered-By", "Server"):
val = r.headers.get(h, "")
if "PHP" in val.upper():
v = parse_ver(val)
if v:
cands.append((f"header:{h}", v, val))
v = parse_ver(r.text)
if v:
cands.append(("body", v, "generator/meta"))
for p in PHPINFO_PATHS:
u = urljoin(base.rstrip("/") + "/", p.lstrip("/"))
rr = fetch(session, u, timeout, verify)
if rr is None or rr.status_code >= 400:
continue
v = parse_ver(rr.text)
if v:
cands.append((f"phpinfo:{p}", v, u))
break
return cands, r
def probe_upload_progress(session, base, timeout, verify):
"""
Kirim multipart POST kecil dengan field PHP_SESSION_UPLOAD_PROGRESS.
Tidak mengirim file, tidak mengirim payload heap-groom.
Jika server membalas Set-Cookie: PHPSESSID, fitur aktif.
"""
url = base.rstrip("/") + "/index.php"
try:
time.sleep(random.uniform(0.2, 0.5))
r = session.post(
url,
headers=headers(),
files={"PHP_SESSION_UPLOAD_PROGRESS": (None, "probe")},
timeout=timeout,
verify=verify,
allow_redirects=False,
)
except Exception:
return None
if r is None:
return None
# Cek Set-Cookie PHPSESSID
for cookie in r.headers.get("Set-Cookie", "").split(","):
if "PHPSESSID" in cookie:
return "ACTIVE (Set-Cookie PHPSESSID)"
# Cek Set-Cookie umum (beberapa framework pakai nama lain)
if "Set-Cookie" in r.headers:
return f"COOKIE_SET ({r.headers['Set-Cookie'][:60]}...)"
return "NO_COOKIE"
def scan_one(raw, args):
if not raw.startswith(("http://", "https://")):
raw = "https://" + raw
base = raw.rstrip("/")
res = {
"target": raw,
"base": base,
"reachable": False,
"php_version": "",
"verdict": "UNKNOWN",
"note": "",
"upload_progress": "",
"sources": "",
}
s = requests.Session()
s.headers.update(headers())
cands, root = fingerprint(s, base, args.timeout, not args.no_verify)
res["reachable"] = root is not None
if cands:
# Prioritas: header X-Powered-By > phpinfo > header Server > body
def prio(c):
if c[0] == "header:X-Powered-By": return 0
if c[0].startswith("phpinfo"): return 1
if c[0] == "header:Server": return 2
return 3
cands.sort(key=prio)
src, ver, rawv = cands[0]
res["php_version"] = ".".join(map(str, ver))
res["sources"] = "; ".join(f"{c[0]}={'.'.join(map(str,c[1]))}" for c in cands)
v, note = verdict(ver)
res["verdict"] = v
res["note"] = note
elif res["reachable"]:
res["verdict"] = "UNKNOWN"
res["note"] = "PHP terdeteksi tapi versi tidak terbaca (expose_php mungkin Off)"
if args.probe_upload and res["reachable"]:
res["upload_progress"] = probe_upload_progress(
s, base, args.timeout, not args.no_verify
) or "PROBE_FAILED"
return res
def print_row(r):
icon = {
"PATCHED": "✅",
"VULNERABLE_VERSION": "🚨",
"EOL_NO_FIX": "⛔",
"UNKNOWN": "❓",
}.get(r["verdict"], "•")
print(f"\n{'='*72}")
print(f"TARGET : {r['target']}")
print(f"AKSES : {'OK' if r['reachable'] else 'GAGAL'}")
if r["php_version"]:
print(f"PHP : {r['php_version']} ({r['sources']})")
print(f"VERDICT: {icon} {r['verdict']}")
print(f" {r['note']}")
else:
print(f"PHP : tidak terdeteksi")
print(f"VERDICT: {icon} UNKNOWN — {r['note']}")
if r["upload_progress"]:
print(f"UPLOAD : {r['upload_progress']}")
# Peringatan backport Ubuntu
if r["verdict"] == "VULNERABLE_VERSION" and r["php_version"].startswith("8.3.6"):
print(" ⚠️ Peringatan: ini mungkin false positive. Ubuntu 24.04 backport patch")
print(" tanpa menaikkan versi. Cek `dpkg -l | grep php8.3` di server.")
def save_csv(rows, path):
with open(path, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["target", "base", "reachable", "php_version", "verdict",
"upload_progress", "note", "sources"])
for r in rows:
w.writerow([r["target"], r["base"], r["reachable"], r["php_version"],
r["verdict"], r["upload_progress"], r["note"], r["sources"]])
def main():
ap = argparse.ArgumentParser()
ap.add_argument("-l", "--list", default="targets.txt")
ap.add_argument("-t", "--threads", type=int, default=6)
ap.add_argument("--timeout", type=int, default=12)
ap.add_argument("--no-verify", action="store_true")
ap.add_argument("--probe-upload", action="store_true",
help="kirim multipart POST kecil untuk cek upload_progress")
ap.add_argument("-o", "--output", default="php2shell_report.csv")
args = ap.parse_args()
try:
with open(args.list, "r", encoding="utf-8") as f:
targets = [l.strip() for l in f
if l.strip() and not l.strip().startswith("#")]
except FileNotFoundError:
print(f"[!] File tidak ditemukan: {args.list}")
sys.exit(1)
print(f"[*] Target : {len(targets)}")
print(f"[*] Threads : {args.threads}")
print(f"[*] Timeout : {args.timeout}s")
print(f"[*] Probe : {'ON (multipart upload_progress)' if args.probe_upload else 'OFF'}")
print("[!] Fingerprint versi TIDAK bisa membedakan backport Ubuntu.\n")
rows = []
with ThreadPoolExecutor(max_workers=args.threads) as ex:
fut = {ex.submit(scan_one, t, args): t for t in targets}
for f in as_completed(fut):
try:
r = f.result()
except Exception as e:
print(f"[!] {fut[f]}: error {e}")
continue
rows.append(r)
print_row(r)
save_csv(rows, args.output)
print("\n" + "=" * 72)
print("SUMMARY")
print("=" * 72)
buckets = {}
for r in rows:
buckets.setdefault(r["verdict"], []).append(r["target"])
for k in ("VULNERABLE_VERSION", "EOL_NO_FIX", "UNKNOWN", "PATCHED"):
lst = buckets.get(k, [])
if lst:
print(f" {k:<20}: {len(lst)}")
for t in lst:
print(f" - {t}")
print(f"\n[✓] CSV: {args.output}")
print("\n[!] Untuk kepastian di Ubuntu, jalankan di tiap server:")
print(" dpkg -l | grep php8.3")
print(" (patched jika versi paket >= 8.3.6-0ubuntu0.24.04.13)")
if __name__ == "__main__":
main() |