Skip to content

Commit 19a518c

Browse files
authored
Merge pull request #3 from login-securite/0.2.0
0.2.0
2 parents e97d1aa + 2797969 commit 19a518c

20 files changed

Lines changed: 2063 additions & 874 deletions

LICENSE

Lines changed: 0 additions & 21 deletions
This file was deleted.

Makefile

Lines changed: 0 additions & 21 deletions
This file was deleted.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,5 @@ conpass -d domain -u pixis -p P4ssw0rd -P /tmp/passwords.txt
4545
```
4646

4747
All passwords and NT hashes provided in `/tmp/passwords.txt` will be added to a testing Queue, and will be tested against all users, whenever it is possible without locking users out.
48+
49+
![conpass](https://github.com/user-attachments/assets/ca33e9ae-0cbb-4689-b5ff-c434f386ea22)

conpass/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
__version__ = '0.0.1'
1+
from conpass.core import ThreadPool
2+
3+
__all__ = ["ThreadPool"]

conpass/console.py

Lines changed: 228 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,239 @@
1-
import argparse
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
from typing import Annotated
5+
6+
import typer
7+
import re
8+
from rich.console import Console
29

3-
from conpass import __version__
410
from conpass.core import ThreadPool
11+
from conpass.utils import blocks
12+
from conpass.utils.logger import get_logger
13+
14+
app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]})
15+
16+
17+
def complete_path(): # Typer bug : https://github.com/fastapi/typer/issues/951
18+
return []
19+
20+
21+
@app.command(
22+
help='Spray given passwords to all Active Directory users and taking password policies into account',
23+
)
24+
def spray(
25+
26+
domain: Annotated[
27+
str,
28+
typer.Option("--domain", "-d", help="Domain name", rich_help_panel="Authentication"),
29+
],
30+
password_file: Annotated[
31+
Path | None,
32+
typer.Option(
33+
"--password-file",
34+
"-P",
35+
exists=True,
36+
file_okay=True,
37+
readable=True,
38+
resolve_path=True,
39+
help="File containing passwords to test",
40+
autocompletion=complete_path,
41+
rich_help_panel="Spray",
42+
),
43+
] = None,
44+
username: Annotated[
45+
str | None,
46+
typer.Option("--username", "-u", help="Domain user", rich_help_panel="Authentication"),
47+
] = None,
48+
password: Annotated[
49+
str | None,
50+
typer.Option(
51+
"--password",
52+
"-p",
53+
help="Domain password",
54+
rich_help_panel="Authentication"
55+
),
56+
] = None,
57+
hashes: Annotated[
58+
str | None,
59+
typer.Option(
60+
"--hashes",
61+
"-H",
62+
help="NTLM hashes, format is LMHASH:NTHASH",
63+
rich_help_panel="Authentication",
64+
),
65+
] = None,
66+
user_file: Annotated[
67+
Path | None,
68+
typer.Option(
69+
"--user-file",
70+
"-U",
71+
exists=True,
72+
file_okay=True,
73+
readable=True,
74+
resolve_path=True,
75+
help="File containing users to test",
76+
autocompletion=complete_path,
77+
rich_help_panel="Spray",
78+
),
79+
] = None,
80+
lockout_threshold: Annotated[
81+
int | None,
82+
typer.Option(
83+
"--lockout-threshold",
84+
"-t",
85+
help="Manually provide lockout threshold (Necessary when users list if provided)",
86+
rich_help_panel="Spray",
87+
),
88+
] = None,
89+
lockout_observation_window: Annotated[
90+
int | None,
91+
typer.Option(
92+
"--lockout-observation-window",
93+
"-o",
94+
help="Manually provide lockout observation window in seconds (Necessary when users list if provided)",
95+
rich_help_panel="Spray",
96+
),
97+
] = None,
98+
user_as_pass: Annotated[
99+
bool | None,
100+
typer.Option(
101+
"--user-as-pass",
102+
"-a",
103+
help="Enables user-as-pass for each user",
104+
rich_help_panel="Spray",
105+
),
106+
] = False,
107+
security_threshold: Annotated[
108+
int | None,
109+
typer.Option(
110+
"--security-threshold",
111+
"-s",
112+
help="Specifies the number of remaining attempts allowed before reaching the lockout threshold",
113+
rich_help_panel="Spray",
114+
),
115+
] = 2,
116+
max_threads: Annotated[
117+
int | None,
118+
typer.Option(
119+
"--max-threads",
120+
"-m",
121+
help="Max threads number",
122+
rich_help_panel="Spray",
123+
),
124+
] = 10,
125+
limit_memory: Annotated[
126+
bool | None,
127+
typer.Option(
128+
"--limit-memory",
129+
"-l",
130+
help="Limit the size of internal queues. Could be useful for 10k users and more",
131+
rich_help_panel="Spray",
132+
),
133+
] = False,
134+
disable_spray: Annotated[
135+
bool | None,
136+
typer.Option(
137+
"--disable-spray",
138+
help="Disable password spraying. Useful to only retrieve PSO details",
139+
rich_help_panel="Spray",
140+
),
141+
] = False,
142+
dc_ip: Annotated[
143+
str | None,
144+
typer.Option("--dc-ip", "-D", help="Domain Controller IP address", rich_help_panel="Authentication"),
145+
] = None,
146+
dc_host: Annotated[
147+
str | None,
148+
typer.Option(
149+
"--dc-host",
150+
help="Hostname of the domain controller. If omitted it uses the --dc-ip or --domain",
151+
rich_help_panel="Authentication",
152+
),
153+
] = None,
5154

155+
use_ssl: Annotated[
156+
bool,
157+
typer.Option(
158+
"--use-ssl",
159+
help="Uses LDAP over SSL/TLS (port 636)",
160+
rich_help_panel="Authentication",
161+
),
162+
] = False,
6163

7-
def main():
8-
"""
9-
Command line function to call conpass
10-
"""
11-
version = __version__
12-
parser = argparse.ArgumentParser(
13-
prog="conpass",
14-
description='conpass v{} - Continuous password spraying tool'.format(__version__)
15-
)
164+
use_kerberos: Annotated[
165+
bool,
166+
typer.Option(
167+
"--kerberos",
168+
"-k",
169+
help="Uses kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters",
170+
rich_help_panel="Authentication",
171+
),
172+
] = False,
173+
aes_key: Annotated[
174+
str | None,
175+
typer.Option(
176+
"--aes-key",
177+
"-a",
178+
help="AES key to use for Kerberos Authentication (128 or 256 bits)",
179+
rich_help_panel="Authentication",
180+
),
181+
] = None,
16182

17-
group_auth = parser.add_argument_group('Authentication')
18-
group_auth.add_argument('-u', '--username', action='store', help='Username', required=True)
19-
group_auth.add_argument('-p', '--password', action='store', help='Plaintext password', required=True)
20-
group_auth.add_argument('-d', '--domain', default="", action='store', help='Domain name', required=True)
21-
group_auth.add_argument('-dc-ip', action='store', metavar="ip address",
22-
help='IP Address of the primary domain controller.')
23183

24-
group_spray = parser.add_argument_group('Spray')
25-
group_spray.add_argument('-P', '--password-file', action='store', help='File containing passwords to test', required=True)
26-
group_spray.add_argument('-U', '--user-file', action='store', help='File containing usernames to test (Default all domain users)', required=False)
27-
group_spray.add_argument('-S', '--security-threshold', default=2, type=int, action='store', help='Specifies the number of remaining attempts allowed before reaching the lockout threshold (Default: 2)')
28-
group_auth.add_argument('--threads', default=10, type=int, action='store', help='Threads number (Default 10)')
29-
group_auth.add_argument('--limit-memory', action='store_true', help='Limit the size of internal queues. Could be useful for 10k users and more')
184+
):
185+
console = Console()
186+
logger = get_logger(console)
187+
if username is None and user_file is None:
188+
logger.error("Either --username or --users-file is required")
189+
raise typer.Exit(code=1)
190+
if '.' not in domain:
191+
logger.error("Provide fully qualified domain name (e.g. domain.local instead of DOMAIN)")
192+
raise typer.Exit(code=1)
30193

31-
group_info = parser.add_argument_group('Info')
32-
group_info.add_argument('-v', action='count', default=0, help='Verbosity level (-v or -vv)')
33-
group_info.add_argument('-V', '--version', action='version', version='%(prog)s (version {})'.format(version))
194+
if username is not None and password is None and hashes is None and (use_kerberos is False or aes_key is None):
195+
logger.error(f"--password or --hashes{' or --aes-key' if use_kerberos else ''} is required for authentication")
196+
raise typer.Exit(code=1)
34197

35-
args = parser.parse_args()
198+
if len([c for c in (password, hashes, aes_key) if c is not None]) > 1:
199+
logger.error("Only one secret can be provided")
200+
raise typer.Exit(code=1)
36201

37-
ThreadPool(args).run()
202+
if username is None and user_file and (not lockout_threshold or not lockout_observation_window):
203+
logger.error("When using --users-file, --lockout-threshold and --lockout-observation-window are required")
204+
raise typer.Exit(code=1)
38205

206+
if password_file is not None:
207+
with open(password_file) as f:
208+
nb_passwords = sum(bl.count("\n") for bl in blocks(f))
209+
if nb_passwords > 100:
210+
res = console.input(f"[yellow]The password file has {nb_passwords} passwords. It will take a very long time to try them all[/yellow]\nDo you want to continue? \\[y/N] ")
211+
if not res.lower().startswith('y'):
212+
raise typer.Exit(code=1)
213+
else:
214+
disable_spray = True
39215

40-
if __name__ == "__main__":
41-
main()
216+
try:
217+
thread_pool = ThreadPool(
218+
username,
219+
password,
220+
domain,
221+
use_ssl,
222+
dc_ip,
223+
dc_host,
224+
password_file,
225+
user_file,
226+
lockout_threshold,
227+
lockout_observation_window,
228+
user_as_pass,
229+
security_threshold,
230+
max_threads,
231+
limit_memory,
232+
disable_spray,
233+
console
234+
)
235+
thread_pool.run()
236+
except Exception as e:
237+
logger.critical(e)
238+
console.print_exception()
239+
raise typer.Exit(code=1) from None

0 commit comments

Comments
 (0)