printerxpl-forge 6.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. nse/README.md +204 -0
  2. nse/__init__.py +6 -0
  3. nse/install_nse.py +412 -0
  4. nse/lib/printerxpl.lua +238 -0
  5. nse/scripts/cups-info.nse +74 -0
  6. nse/scripts/cups-queue-info.nse +43 -0
  7. nse/scripts/hp-printers-cve-2022-1026.nse +121 -0
  8. nse/scripts/http-device-mac.nse +107 -0
  9. nse/scripts/http-hp-ilo-info.nse +121 -0
  10. nse/scripts/http-info-xerox-enum.nse +101 -0
  11. nse/scripts/http-vuln-cve2022-1026.nse +158 -0
  12. nse/scripts/lexmark-config.nse +89 -0
  13. nse/scripts/pjl-ready-message.nse +106 -0
  14. nse/scripts/printer-banner.nse +217 -0
  15. nse/scripts/printer-cups-rce.nse +189 -0
  16. nse/scripts/printer-cve-detect.nse +279 -0
  17. nse/scripts/printer-discover.nse +205 -0
  18. nse/scripts/printer-firmware-exposed.nse +219 -0
  19. nse/scripts/printer-hp-pjl.nse +192 -0
  20. nse/scripts/printer-http-ews.nse +293 -0
  21. nse/scripts/printer-ipp-info.nse +235 -0
  22. nse/scripts/printer-lexmark-ipp.nse +203 -0
  23. nse/scripts/printer-passback.nse +204 -0
  24. nse/scripts/printer-pjl-info.nse +146 -0
  25. nse/scripts/printer-printnightmare.nse +211 -0
  26. nse/scripts/printer-snmp-info.nse +176 -0
  27. nse/scripts/printer-vuln-check.nse +256 -0
  28. nse/scripts/snmp-device-mac.nse +93 -0
  29. nse/scripts/snmp-info.nse +146 -0
  30. nse/scripts/snmp-sysdescr.nse +70 -0
  31. printerxpl_forge-6.2.0.dist-info/METADATA +919 -0
  32. printerxpl_forge-6.2.0.dist-info/RECORD +97 -0
  33. printerxpl_forge-6.2.0.dist-info/WHEEL +5 -0
  34. printerxpl_forge-6.2.0.dist-info/entry_points.txt +4 -0
  35. printerxpl_forge-6.2.0.dist-info/licenses/LICENSE +21 -0
  36. printerxpl_forge-6.2.0.dist-info/top_level.txt +4 -0
  37. src/assets/fonts/gunplay.pfa +1671 -0
  38. src/assets/fonts/kshandwrt.pfa +315 -0
  39. src/assets/fonts/laksoner.pfa +2402 -0
  40. src/assets/fonts/paintcans.pfa +9699 -0
  41. src/assets/fonts/stencilod.pfa +4076 -0
  42. src/assets/fonts/takecover.pfa +26138 -0
  43. src/assets/fonts/topsecret.pfa +6652 -0
  44. src/assets/fonts/whoa.pfa +773 -0
  45. src/assets/mibs/HOST-RESOURCES-MIB +1540 -0
  46. src/assets/mibs/Printer-MIB +4389 -0
  47. src/assets/mibs/README.md +9 -0
  48. src/assets/mibs/SNMPv2-MIB +854 -0
  49. src/assets/overlays/hacker.eps +596 -0
  50. src/assets/overlays/smiley.eps +214 -0
  51. src/assets/overlays/smiley2.eps +240 -0
  52. src/core/attack_orchestrator.py +1025 -0
  53. src/core/capabilities.py +323 -0
  54. src/core/destructive_audit.py +430 -0
  55. src/core/discovery.py +488 -0
  56. src/core/osdetect.py +74 -0
  57. src/core/poly_runner.py +579 -0
  58. src/core/printer.py +1426 -0
  59. src/main.py +2134 -0
  60. src/modules/install_printer.py +318 -0
  61. src/modules/login_bruteforce.py +852 -0
  62. src/modules/pcl.py +506 -0
  63. src/modules/pjl.py +3575 -0
  64. src/modules/print_job.py +1290 -0
  65. src/modules/ps.py +1102 -0
  66. src/payloads/__init__.py +98 -0
  67. src/payloads/assets/overlays/notice.eps +9 -0
  68. src/protocols/__init__.py +19 -0
  69. src/protocols/firmware.py +738 -0
  70. src/protocols/ipp.py +216 -0
  71. src/protocols/ipp_attacks.py +609 -0
  72. src/protocols/lpd.py +141 -0
  73. src/protocols/network_map.py +1004 -0
  74. src/protocols/raw.py +173 -0
  75. src/protocols/smb.py +359 -0
  76. src/protocols/ssrf_pivot.py +427 -0
  77. src/protocols/storage.py +587 -0
  78. src/ui/__init__.py +6 -0
  79. src/ui/interactive.py +742 -0
  80. src/ui/spinner.py +112 -0
  81. src/ui/tables.py +132 -0
  82. src/utils/banner_grabber.py +852 -0
  83. src/utils/codebook.py +456 -0
  84. src/utils/config.py +522 -0
  85. src/utils/cve_loader.py +158 -0
  86. src/utils/default_creds.py +134 -0
  87. src/utils/discovery_online.py +1327 -0
  88. src/utils/exploit_manager.py +805 -0
  89. src/utils/fuzzer.py +220 -0
  90. src/utils/helper.py +732 -0
  91. src/utils/local_printers.py +307 -0
  92. src/utils/ml_engine.py +491 -0
  93. src/utils/operators.py +474 -0
  94. src/utils/ports.py +234 -0
  95. src/utils/vuln_scanner.py +823 -0
  96. src/utils/wordlist_loader.py +412 -0
  97. src/version.py +36 -0
@@ -0,0 +1,579 @@
1
+ """
2
+ poly_runner — Orquestrador multi-linguagem para execução de exploits em
3
+ qualquer linguagem (Python, C/C++, Ruby/Metasploit, Node.js, Go, Rust, PHP, Perl).
4
+
5
+ Suporta compilação (C/C++/Go/Rust) e execução de runtimes externos
6
+ (Ruby/MSF, Node, PHP, Perl, PowerShell).
7
+
8
+ Todos os builds usam .tmp/build/ do projeto (nunca diretórios externos).
9
+
10
+ Funções públicas principais:
11
+ detect(lang) → caminho do compilador/runtime ou None
12
+ detect_msf() → caminho do msfconsole ou None
13
+ available_langs() → dict {lang: path_or_None} para todas as linguagens
14
+ build(src, lang, ...) → compila source → binário em .tmp/build/
15
+ run(src, lang, ...) → detect + build (se compilado) + executa + retorna dict
16
+ run_msf(module, ...) → gera RC script + msfconsole -r → retorna dict
17
+ run_from_dir(dir, ...) → auto-detecta source no diretório + despacha para run()
18
+
19
+ # Autor: André Henrique (@mrhenrike) | União Geek
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ import re
25
+ import shutil
26
+ import subprocess
27
+ import sys
28
+ import time
29
+ from pathlib import Path
30
+ from typing import Any, Optional
31
+
32
+ # ── Build directory inside the project workspace ──────────────────────────────
33
+ _PROJECT_ROOT = Path(__file__).parent.parent.parent
34
+ _BUILD_TMP = _PROJECT_ROOT / ".tmp" / "build"
35
+
36
+ # ── Compiler/runtime detection tables ─────────────────────────────────────────
37
+ _COMPILERS: dict[str, list[str]] = {
38
+ "c": ["gcc", "clang", "cc"],
39
+ "cpp": ["g++", "clang++", "c++"],
40
+ "go": ["go"],
41
+ "rust": ["cargo"],
42
+ }
43
+
44
+ _RUNTIMES: dict[str, list[str]] = {
45
+ "ruby": ["ruby"],
46
+ "node": ["node", "nodejs"],
47
+ "php": ["php"],
48
+ "perl": ["perl"],
49
+ "powershell": ["pwsh", "powershell"],
50
+ "python": [sys.executable],
51
+ "sh": ["sh", "bash"],
52
+ }
53
+
54
+ _MSF_BINARIES = ["msfconsole", "msfexec"]
55
+
56
+ # WSL binary (Windows only) — used as fallback compiler/runtime host
57
+ _WSL_BIN: Optional[str] = shutil.which("wsl") if sys.platform == "win32" else None
58
+
59
+
60
+ # ── Source file extension → language key ──────────────────────────────────────
61
+ _EXT_TO_LANG: dict[str, str] = {
62
+ ".c": "c",
63
+ ".cpp": "cpp",
64
+ ".cc": "cpp",
65
+ ".go": "go",
66
+ ".rs": "rust",
67
+ ".rb": "ruby",
68
+ ".js": "node",
69
+ ".php": "php",
70
+ ".pl": "perl",
71
+ ".ps1": "powershell",
72
+ ".sh": "sh",
73
+ ".py": "python",
74
+ }
75
+
76
+ # Source file names considered as exploit entry points when auto-discovering
77
+ _SOURCE_CANDIDATES = ["source.c", "source.cpp", "exploit.c", "exploit.cpp",
78
+ "exploit.rb", "exploit.go", "exploit.rs",
79
+ "exploit.js", "exploit.php", "exploit.pl", "exploit.ps1"]
80
+
81
+
82
+ # ── Public API ─────────────────────────────────────────────────────────────────
83
+
84
+ def detect(lang: str) -> Optional[str]:
85
+ """Return path to first available compiler/runtime for *lang*, or None.
86
+
87
+ On Windows, if no native compiler is found for C/C++/Go/Rust, falls back
88
+ to checking inside WSL via ``wsl <compiler> --version``. When the WSL
89
+ fallback succeeds, returns the string ``"wsl:<compiler>"`` as a sentinel
90
+ that ``build()`` and ``run()`` recognise and dispatch via ``wsl``.
91
+ """
92
+ lang = lang.lower()
93
+ candidates: list[str] = []
94
+ if lang in _COMPILERS:
95
+ candidates = _COMPILERS[lang]
96
+ elif lang in _RUNTIMES:
97
+ candidates = _RUNTIMES[lang]
98
+ else:
99
+ return None
100
+
101
+ for name in candidates:
102
+ found = shutil.which(name)
103
+ if found:
104
+ return found
105
+
106
+ # WSL fallback on Windows for compiled languages
107
+ if sys.platform == "win32" and _WSL_BIN and lang in _COMPILERS:
108
+ wsl_compiler = _COMPILERS[lang][0] # e.g. "gcc"
109
+ probe = subprocess.run(
110
+ [_WSL_BIN, wsl_compiler, "--version"],
111
+ capture_output=True, text=True,
112
+ )
113
+ if probe.returncode == 0:
114
+ return f"wsl:{wsl_compiler}"
115
+
116
+ return None
117
+
118
+
119
+ def detect_msf() -> Optional[str]:
120
+ """Return path to msfconsole binary, or None if Metasploit is not installed."""
121
+ for name in _MSF_BINARIES:
122
+ found = shutil.which(name)
123
+ if found:
124
+ return found
125
+ return None
126
+
127
+
128
+ def available_langs() -> dict[str, Optional[str]]:
129
+ """Return a mapping of every supported language to its detected binary path.
130
+
131
+ Keys include all compiler languages (c, cpp, go, rust) and all runtime
132
+ languages (ruby, node, php, perl, powershell, python, sh). The value is
133
+ the resolved binary path or ``None`` when not installed.
134
+
135
+ Useful for ``--xpl-list`` output to flag exploits with unmet dependencies
136
+ and for the exploit_manager dependency check.
137
+
138
+ Returns:
139
+ dict: ``{"c": "/usr/bin/gcc", "ruby": None, ...}``
140
+ """
141
+ result: dict[str, Optional[str]] = {}
142
+ for lang in list(_COMPILERS) + list(_RUNTIMES):
143
+ result[lang] = detect(lang)
144
+ return result
145
+
146
+
147
+ def run_from_dir(
148
+ module_dir: Path,
149
+ host: str,
150
+ port: int,
151
+ timeout: float = 30.0,
152
+ dry_run: bool = False,
153
+ extra_args: Optional[list[str]] = None,
154
+ build_flags: Optional[list[str]] = None,
155
+ ) -> dict[str, Any]:
156
+ """Auto-detect a non-Python source file in *module_dir* and execute it.
157
+
158
+ Searches for candidate source files in *module_dir* following the priority
159
+ order defined in ``_SOURCE_CANDIDATES``. The first match determines the
160
+ language and is passed to :func:`run`.
161
+
162
+ This helper lets ``exploit.py`` wrappers delegate execution to poly_runner
163
+ without needing to know the exact source filename or language key::
164
+
165
+ from src.core import poly_runner
166
+ from pathlib import Path
167
+
168
+ _DIR = Path(__file__).parent
169
+
170
+ def run(host, port=9100, timeout=15, dry_run=True, **opts):
171
+ return poly_runner.run_from_dir(_DIR, host, port,
172
+ timeout=timeout, dry_run=dry_run)
173
+
174
+ Parameters
175
+ ----------
176
+ module_dir : Directory that contains the source file (usually the same
177
+ directory as the calling ``exploit.py``).
178
+ host : Target host.
179
+ port : Target port.
180
+ timeout : Execution timeout in seconds.
181
+ dry_run : If True, return metadata without executing.
182
+ extra_args : Extra CLI arguments forwarded to the exploit binary/script.
183
+ build_flags : Compiler flags forwarded to :func:`build` (compiled langs).
184
+
185
+ Returns
186
+ -------
187
+ dict with keys: success, vulnerable, output, evidence, error, lang, runner.
188
+ Returns an error dict when no source file is found.
189
+ """
190
+ module_dir = Path(module_dir)
191
+
192
+ for candidate in _SOURCE_CANDIDATES:
193
+ src = module_dir / candidate
194
+ if src.exists():
195
+ lang = _EXT_TO_LANG.get(src.suffix.lower(), "")
196
+ if not lang:
197
+ return _error_result("unknown",
198
+ f"run_from_dir: unsupported extension '{src.suffix}'")
199
+ artifact = module_dir.name or src.stem
200
+ return run(
201
+ src=src,
202
+ lang=lang,
203
+ host=host,
204
+ port=port,
205
+ timeout=timeout,
206
+ dry_run=dry_run,
207
+ extra_args=extra_args,
208
+ build_flags=build_flags,
209
+ artifact_name=artifact,
210
+ )
211
+
212
+ return _error_result(
213
+ "unknown",
214
+ f"run_from_dir: no supported source file found in '{module_dir}' "
215
+ f"(searched: {_SOURCE_CANDIDATES})",
216
+ )
217
+
218
+
219
+ def build(
220
+ src: Path,
221
+ lang: str,
222
+ artifact_name: str,
223
+ build_flags: Optional[list[str]] = None,
224
+ ) -> Path:
225
+ """
226
+ Compile *src* → .tmp/build/<artifact_name>/exploit (or .exe on Windows).
227
+
228
+ Uses a **compilation cache**: if the binary already exists and its
229
+ modification time is newer than *src*, the existing binary is returned
230
+ immediately without recompiling.
231
+
232
+ Parameters
233
+ ----------
234
+ src : Path to source file (.c, .cpp, .go, etc.)
235
+ lang : Language key — "c", "cpp", "go", "rust"
236
+ artifact_name : Directory name under .tmp/build/ for isolation
237
+ build_flags : Extra compiler flags (e.g. ["-lpthread"])
238
+
239
+ Returns
240
+ -------
241
+ Path to the compiled binary.
242
+
243
+ Raises
244
+ ------
245
+ RuntimeError : If compiler not found or compilation fails.
246
+ """
247
+ lang = lang.lower()
248
+ if lang not in _COMPILERS:
249
+ raise RuntimeError(f"poly_runner: no compiler registered for lang='{lang}'")
250
+
251
+ compiler = detect(lang)
252
+ if not compiler:
253
+ raise RuntimeError(
254
+ f"poly_runner: no compiler found for lang='{lang}' "
255
+ f"(tried: {_COMPILERS[lang]})"
256
+ )
257
+
258
+ out_dir = _BUILD_TMP / artifact_name
259
+ out_dir.mkdir(parents=True, exist_ok=True)
260
+
261
+ binary_name = "exploit.exe" if sys.platform == "win32" else "exploit"
262
+ binary_path = out_dir / binary_name
263
+ flags = build_flags or []
264
+
265
+ # Compilation cache: skip rebuild if binary is newer than source
266
+ if binary_path.exists() and src.exists():
267
+ if binary_path.stat().st_mtime >= src.stat().st_mtime:
268
+ return binary_path
269
+
270
+ # Detect WSL-based compilation (compiler path starts with "wsl:")
271
+ using_wsl = compiler.startswith("wsl:")
272
+ wsl_compiler = compiler.split(":", 1)[1] if using_wsl else compiler
273
+
274
+ if using_wsl:
275
+ # Convert Windows path to WSL Linux path (/mnt/X/ convention)
276
+ def _win_to_wsl(p: Path) -> str:
277
+ import re as _re
278
+ s = str(p.resolve()) # always absolute before converting
279
+ m = _re.match(r'^([A-Za-z]):[/\\](.*)', s)
280
+ if m:
281
+ drive = m.group(1).lower()
282
+ rest = m.group(2).replace("\\", "/")
283
+ return f"/mnt/{drive}/{rest}"
284
+ return s.replace("\\", "/")
285
+
286
+ wsl_src = _win_to_wsl(src)
287
+ wsl_out = _win_to_wsl(binary_path)
288
+
289
+ if lang == "go":
290
+ cmd = [_WSL_BIN, wsl_compiler, "build", "-o", wsl_out, wsl_src] # type: ignore[list-item]
291
+ elif lang == "rust":
292
+ cmd = [_WSL_BIN, "rustc", wsl_src, "-o", wsl_out] + flags # type: ignore[list-item]
293
+ else:
294
+ cmd = [_WSL_BIN, wsl_compiler, wsl_src, "-o", wsl_out] + flags # type: ignore[list-item]
295
+ elif lang == "go":
296
+ cmd = [compiler, "build", "-o", str(binary_path), str(src)]
297
+ elif lang == "rust":
298
+ rustc = shutil.which("rustc")
299
+ if not rustc:
300
+ raise RuntimeError("poly_runner: rustc not found")
301
+ cmd = [rustc, str(src), "-o", str(binary_path)] + flags
302
+ else:
303
+ # C / C++
304
+ cmd = [compiler, str(src), "-o", str(binary_path)] + flags
305
+
306
+ result = subprocess.run(
307
+ cmd,
308
+ capture_output=True,
309
+ text=True,
310
+ cwd=str(out_dir),
311
+ )
312
+ if result.returncode != 0:
313
+ raise RuntimeError(
314
+ f"poly_runner: compilation failed ({compiler}):\n"
315
+ f" STDOUT: {result.stdout}\n"
316
+ f" STDERR: {result.stderr}"
317
+ )
318
+
319
+ return binary_path
320
+
321
+
322
+ def run(
323
+ src: Path,
324
+ lang: str,
325
+ host: str,
326
+ port: int,
327
+ timeout: float = 30.0,
328
+ dry_run: bool = False,
329
+ extra_args: Optional[list[str]] = None,
330
+ build_flags: Optional[list[str]] = None,
331
+ artifact_name: Optional[str] = None,
332
+ ) -> dict[str, Any]:
333
+ """
334
+ Full pipeline: detect → build (if compiled) → execute → parse → return result.
335
+
336
+ For compiled languages (C/C++/Go/Rust), the source is compiled first via
337
+ `build()`, then the binary is executed with `host port [extra_args]`.
338
+
339
+ For interpreted languages (Python/Ruby/Node/PHP/Perl), the runtime is
340
+ invoked directly: `runtime src host port [extra_args]`.
341
+
342
+ Parameters
343
+ ----------
344
+ src : Path to source or script file
345
+ lang : Language key
346
+ host : Target host
347
+ port : Target port
348
+ timeout : Execution timeout in seconds
349
+ dry_run : If True, skip actual execution and return metadata only
350
+ extra_args : Additional CLI arguments passed to the exploit
351
+ build_flags : Compiler flags (compiled langs only)
352
+ artifact_name : Override build dir name (defaults to src.stem)
353
+
354
+ Returns
355
+ -------
356
+ dict with keys: success, vulnerable, output, evidence, error, lang, runner
357
+ """
358
+ lang = lang.lower()
359
+ extra_args = extra_args or []
360
+ artifact = artifact_name or src.stem
361
+
362
+ if dry_run:
363
+ return {
364
+ "success": True,
365
+ "vulnerable": None,
366
+ "output": f"[DRY-RUN] poly_runner: would execute {lang} exploit {src.name} against {host}:{port}",
367
+ "evidence": "",
368
+ "error": "",
369
+ "lang": lang,
370
+ "runner": "poly_runner",
371
+ "dry_run": True,
372
+ }
373
+
374
+ try:
375
+ if lang in _COMPILERS:
376
+ binary = build(src, lang, artifact, build_flags)
377
+ # If compiled via WSL, the binary is an ELF — run it via wsl
378
+ if _WSL_BIN and sys.platform == "win32":
379
+ probe = subprocess.run(
380
+ ["file", str(binary)] if shutil.which("file") else
381
+ [_WSL_BIN, "file", _win_to_wsl_safe(binary)],
382
+ capture_output=True, text=True,
383
+ )
384
+ is_elf = "ELF" in probe.stdout
385
+ else:
386
+ is_elf = False
387
+
388
+ if is_elf and _WSL_BIN:
389
+ wsl_bin_path = _win_to_wsl_safe(binary)
390
+ cmd = [_WSL_BIN, wsl_bin_path, host, str(port)] + extra_args
391
+ else:
392
+ cmd = [str(binary), host, str(port)] + extra_args
393
+ elif lang in _RUNTIMES:
394
+ runtime = detect(lang)
395
+ if not runtime:
396
+ return _error_result(lang, f"runtime not found for '{lang}'")
397
+ cmd = [runtime, str(src), host, str(port)] + extra_args
398
+ else:
399
+ return _error_result(lang, f"unknown language '{lang}'")
400
+
401
+ result = subprocess.run(
402
+ cmd,
403
+ capture_output=True,
404
+ text=True,
405
+ timeout=timeout,
406
+ )
407
+ return _normalize(result.stdout, result.stderr, result.returncode, lang)
408
+
409
+ except subprocess.TimeoutExpired:
410
+ return _error_result(lang, f"timeout after {timeout}s")
411
+ except RuntimeError as exc:
412
+ return _error_result(lang, str(exc))
413
+ except Exception as exc:
414
+ return _error_result(lang, f"unexpected error: {exc}")
415
+ finally:
416
+ _cleanup_build(artifact)
417
+
418
+
419
+ def run_msf(
420
+ msf_module: str,
421
+ host: str,
422
+ port: int,
423
+ payload: Optional[str] = None,
424
+ lhost: Optional[str] = None,
425
+ lport: Optional[int] = None,
426
+ extra_options: Optional[dict[str, str]] = None,
427
+ timeout: float = 120.0,
428
+ dry_run: bool = False,
429
+ ) -> dict[str, Any]:
430
+ """
431
+ Execute a Metasploit module via msfconsole -q -r <rc_script>.
432
+
433
+ Parameters
434
+ ----------
435
+ msf_module : MSF module path (e.g. "exploit/windows/local/cve_2020_1048_printerdemon")
436
+ host : RHOST value
437
+ port : RPORT value
438
+ payload : Optional payload string
439
+ lhost : LHOST for reverse shells
440
+ lport : LPORT for reverse shells
441
+ extra_options : Dict of additional MSF options
442
+ timeout : Execution timeout in seconds
443
+ dry_run : If True, return script without executing
444
+
445
+ Returns
446
+ -------
447
+ dict with keys: success, vulnerable, output, evidence, error, lang, runner
448
+ """
449
+ msf_bin = detect_msf()
450
+
451
+ # Build resource script
452
+ rc_lines = [
453
+ f"use {msf_module}",
454
+ f"set RHOSTS {host}",
455
+ f"set RPORT {port}",
456
+ ]
457
+ if payload:
458
+ rc_lines.append(f"set PAYLOAD {payload}")
459
+ if lhost:
460
+ rc_lines.append(f"set LHOST {lhost}")
461
+ if lport:
462
+ rc_lines.append(f"set LPORT {lport}")
463
+ for k, v in (extra_options or {}).items():
464
+ rc_lines.append(f"set {k} {v}")
465
+ rc_lines.extend(["run", "exit"])
466
+ rc_content = "\n".join(rc_lines)
467
+
468
+ if dry_run or not msf_bin:
469
+ reason = "DRY-RUN" if dry_run else "msfconsole not found"
470
+ return {
471
+ "success": True if dry_run else False,
472
+ "vulnerable": None,
473
+ "output": f"[{reason}] MSF RC script:\n{rc_content}",
474
+ "evidence": "",
475
+ "error": "" if dry_run else "Metasploit not installed",
476
+ "lang": "ruby",
477
+ "runner": "poly_runner_msf",
478
+ "dry_run": dry_run,
479
+ "msf_module": msf_module,
480
+ }
481
+
482
+ # Write RC script to project .tmp (never system /tmp)
483
+ rc_path = _BUILD_TMP / f"msf_{int(time.time())}.rc"
484
+ rc_path.parent.mkdir(parents=True, exist_ok=True)
485
+ rc_path.write_text(rc_content, encoding="utf-8")
486
+
487
+ try:
488
+ result = subprocess.run(
489
+ [msf_bin, "-q", "-r", str(rc_path)],
490
+ capture_output=True,
491
+ text=True,
492
+ timeout=timeout,
493
+ )
494
+ return _normalize_msf(result.stdout, result.stderr, result.returncode)
495
+ except subprocess.TimeoutExpired:
496
+ return _error_result("ruby", f"msfconsole timeout after {timeout}s")
497
+ except Exception as exc:
498
+ return _error_result("ruby", f"msfconsole error: {exc}")
499
+ finally:
500
+ if rc_path.exists():
501
+ rc_path.unlink(missing_ok=True)
502
+
503
+
504
+ # ── Internal helpers ───────────────────────────────────────────────────────────
505
+
506
+ def _win_to_wsl_safe(p: Path) -> str:
507
+ """Convert a Windows path to a WSL Linux path string (best-effort).
508
+
509
+ Converts ``D:\\foo\\bar`` → ``/mnt/d/foo/bar`` using the standard WSL
510
+ drive-mount convention. Falls back to a forward-slash replacement when
511
+ the path does not match a Windows drive letter pattern.
512
+ """
513
+ s = str(p)
514
+ # Windows absolute path: e.g. D:\foo\bar or d:/foo/bar
515
+ import re as _re
516
+ m = _re.match(r'^([A-Za-z]):[/\\](.*)', s)
517
+ if m:
518
+ drive = m.group(1).lower()
519
+ rest = m.group(2).replace("\\", "/")
520
+ return f"/mnt/{drive}/{rest}"
521
+ # Already a WSL/POSIX path or relative
522
+ return s.replace("\\", "/")
523
+
524
+
525
+ def _normalize(stdout: str, stderr: str, rc: int, lang: str) -> dict[str, Any]:
526
+ success = rc == 0
527
+ vulnerable = success and bool(stdout.strip())
528
+ evidence = stdout
529
+ if stderr and rc != 0:
530
+ evidence += f"\n[STDERR]: {stderr}"
531
+ return {
532
+ "success": success,
533
+ "vulnerable": vulnerable,
534
+ "output": stdout,
535
+ "evidence": evidence,
536
+ "error": stderr if rc != 0 else "",
537
+ "returncode": rc,
538
+ "lang": lang,
539
+ "runner": "poly_runner",
540
+ }
541
+
542
+
543
+ def _normalize_msf(stdout: str, stderr: str, rc: int) -> dict[str, Any]:
544
+ """Parse msfconsole output for session/success indicators."""
545
+ lines = stdout.lower()
546
+ session_opened = bool(re.search(r"session \d+ opened", lines))
547
+ exploited = bool(re.search(r"\[\+\]", stdout))
548
+ failed = bool(re.search(r"\[-\].*exploit failed|no session", lines))
549
+ vulnerable = session_opened or exploited
550
+ return {
551
+ "success": rc == 0 and not failed,
552
+ "vulnerable": vulnerable,
553
+ "output": stdout,
554
+ "evidence": stdout if vulnerable else "",
555
+ "error": stderr if rc != 0 else "",
556
+ "returncode": rc,
557
+ "lang": "ruby",
558
+ "runner": "poly_runner_msf",
559
+ "session_opened": session_opened,
560
+ }
561
+
562
+
563
+ def _error_result(lang: str, msg: str) -> dict[str, Any]:
564
+ return {
565
+ "success": False,
566
+ "vulnerable": False,
567
+ "output": "",
568
+ "evidence": "",
569
+ "error": msg,
570
+ "lang": lang,
571
+ "runner": "poly_runner",
572
+ }
573
+
574
+
575
+ def _cleanup_build(artifact_name: str) -> None:
576
+ """Remove compiled artifacts from .tmp/build/<artifact_name>/."""
577
+ build_dir = _BUILD_TMP / artifact_name
578
+ if build_dir.exists():
579
+ shutil.rmtree(build_dir, ignore_errors=True)