diff --git a/README.md b/README.md index 8958764..4b89598 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Being able to hide or continually rotate the source IP address when making web c ![Black Hills Information Security](https://www.blackhillsinfosec.com/wp-content/uploads/2016/03/BHIS-logo-L-300x300.png "Black Hills Information Security") ## Maintainer -- Follow me on Twitter for more tips, tricks, and tools (or just to say hi)! ([Mike Felch - @ustayready](https://twitter.com/ustayready)) +- Follow me on Twitter for more tips, tricks, and tools (or just to say hi)! ([Mike Felch - @ustayready](https://twitter.com/ustayready)) ### Benefits ## @@ -28,8 +28,8 @@ Being able to hide or continually rotate the source IP address when making web c * All parameters and URI's are passed through * Create, delete, list, or update proxies * Spoof X-Forwarded-For source IP header by requesting with an X-My-X-Forwarded-For header - - + + ### Disclaimers ## * ~~Source IP address is passed to the destination in the X-Forwarded-For header by AWS~~ * ~~($100 to the first person to figure out how to strip it in the AWS config before it reaches the destination LOL!)~~ @@ -39,7 +39,7 @@ Being able to hide or continually rotate the source IP address when making web c * Use of this tool on systems other than those that you own are likely to violate the [AWS Acceptable Use Policy](https://aws.amazon.com/aup/) and could potentially lead to termination or suspension of your AWS account. Further, even use of this tool on systems that you do own, or have explicit permission to perform penetration testing on, is subject to the AWS policy on [penetration testing](https://aws.amazon.com/security/penetration-testing/). ## Credit ## -After releasing FireProx publicly, I learned two others were already using the AWS API Gateway technique. Researching the chain of events and having some great conversations, I came to the realization that the only reason I even knew about it was because of these people. I thought it would be cool to give them a few shout-outs and credit, follow these people -- they are awesome. +After releasing FireProx publicly, I learned two others were already using the AWS API Gateway technique. Researching the chain of events and having some great conversations, I came to the realization that the only reason I even knew about it was because of these people. I thought it would be cool to give them a few shout-outs and credit, follow these people -- they are awesome. Credit goes to [Ryan Hanson - @ryHanson](https://twitter.com/ryHanson) who is the first known source of the API Gateway technique @@ -48,18 +48,19 @@ Shout-out to [Mike Hodges - @rmikehodges](https://twitter.com/rmikehodges) for m Major shout-out, once again, to my good friend [Ralph May - @ralphte1](https://twitter.com/ralphte1) for introducing me to the technique awhile back. ## Basic Usage ## -### Requires AWS access key and secret access key or aws cli configured +##### Requires AWS access key and secret access key or aws cli configured usage: **fire.py** [-h] [--access_key ACCESS_KEY] [--secret_access_key SECRET_ACCESS_KEY] [--region REGION] [--command COMMAND] [--api_id API_ID] [--url URL] FireProx API Gateway Manager ``` -usage: fire.py [-h] [--profile_name PROFILE_NAME] [--access_key ACCESS_KEY] [--secret_access_key SECRET_ACCESS_KEY] [--session_token SESSION_TOKEN] [--region REGION] [--command COMMAND] [--api_id API_ID] [--url URL] +usage: fire.py[-h] [--profile_name PROFILE_NAME] [--access_key ACCESS_KEY] [--secret_access_key SECRET_ACCESS_KEY] [--session_token SESSION_TOKEN] + [--region REGION] [--command COMMAND] [--api_id API_ID] [--url URL] FireProx API Gateway Manager -optional arguments: +options: -h, --help show this help message and exit --profile_name PROFILE_NAME AWS Profile Name to store/retrieve credentials @@ -69,8 +70,8 @@ optional arguments: AWS Secret Access Key --session_token SESSION_TOKEN AWS Session Token - --region REGION AWS Region - --command COMMAND Commands: list, create, delete, update + --region REGION AWS Regions (accepts single region, comma-separated list of regions or file containing regions) + --command COMMAND Commands: list, list_all, create, delete, prune, update --api_id API_ID API ID --url URL URL end-point ``` @@ -78,7 +79,50 @@ optional arguments: * Examples * examples/google.py: Use a FireProx proxy to scrape Google search. * examples/bing.py: Use a FireProx proxy to scrape Bing search. - + +### CLI Usage Examples + +#### List all APIs from default regions using an AWS profile +``` +fire.py --profile_name myprofile --command list_all +``` +Example of output: +``` +Listing API's from ap-south-1... +Listing API's from eu-north-1... +Listing API's from eu-west-3... +Listing API's from eu-west-2... +Listing API's from eu-west-1... +Listing API's from ap-northeast-3... +Listing API's from ap-northeast-2... +Listing API's from ap-northeast-1... +Listing API's from ca-central-1... +Listing API's from sa-east-1... +Listing API's from ap-southeast-1... +Listing API's from ap-southeast-2... +Listing API's from eu-central-1... +Listing API's from us-east-1... +Listing API's from us-east-2... +Listing API's from us-west-1... +Listing API's from us-west-2... +``` + +#### Remove ALL APIs from regions defined in a text file (one per line) +``` +fire.py --command prune --region aws-regions.txt +``` +A confirmation will be required before proceeding: +``` +This will delete ALL APIs from region(s): ap-south-1,eu-north-1,eu-west-3,eu-west-2,eu-west-1, +ap-northeast-3,ap-northeast-2,ap-northeast-1,ca-central-1,sa-east-1,ap-southeast-1, +ap-southeast-2,eu-central-1,us-east-1,us-east-2,us-west-1,us-west-2. Proceed? [y/N] +``` + +#### Create a new API in a random region from a list of regions +``` +fire.py --profile_name myprofile --command create --url https://example.com --region aws-regions.txt +``` + ## Installation ## You can install and run with the following command: diff --git a/aws-regions.txt b/aws-regions.txt new file mode 100644 index 0000000..28ddab0 --- /dev/null +++ b/aws-regions.txt @@ -0,0 +1,17 @@ +ap-south-1 +eu-north-1 +eu-west-3 +eu-west-2 +eu-west-1 +ap-northeast-3 +ap-northeast-2 +ap-northeast-1 +ca-central-1 +sa-east-1 +ap-southeast-1 +ap-southeast-2 +eu-central-1 +us-east-1 +us-east-2 +us-west-1 +us-west-2 diff --git a/fire.py b/fire.py index 6c71359..1af25ad 100755 --- a/fire.py +++ b/fire.py @@ -4,29 +4,51 @@ import shutil import tldextract import boto3 +import botocore import os import sys +import random import datetime import tzlocal import argparse import json import configparser -from typing import Tuple, Callable - +from typing import Tuple, List, Union, Any, Callable +from time import sleep + +class FireProxException(Exception): + pass + +AWS_DEFAULT_REGIONS = [ + "ap-south-1", + "eu-north-1", + "eu-west-3", + "eu-west-2", + "eu-west-1", + "ap-northeast-3", + "ap-northeast-2", + "ap-northeast-1", + "ca-central-1", + "sa-east-1", + "ap-southeast-1", + "ap-southeast-2", + "eu-central-1", + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2" +] class FireProx(object): - def __init__(self, arguments: argparse.Namespace, help_text: str): - self.profile_name = arguments.profile_name - self.access_key = arguments.access_key - self.secret_access_key = arguments.secret_access_key - self.session_token = arguments.session_token - self.region = arguments.region - self.command = arguments.command - self.api_id = arguments.api_id - self.url = arguments.url + def __init__(self, **kwargs): + self.profile_name = None + self.access_key = None + self.secret_access_key = None + self.session_token = None + self.region = None self.api_list = [] self.client = None - self.help = help_text + self.__dict__.update(kwargs) if self.access_key and self.secret_access_key: if not self.region: @@ -35,8 +57,6 @@ def __init__(self, arguments: argparse.Namespace, help_text: str): if not self.load_creds(): self.error('Unable to load AWS credentials') - if not self.command: - self.error('Please provide a valid command') def __str__(self): return 'FireProx()' @@ -76,10 +96,12 @@ def load_creds(self) -> bool: # If profile in files, try it, but flow through if it does not work config_profile_section = f'profile {self.profile_name}' if self.profile_name in credentials: - if config_profile_section not in config: - print(f'Please create a section for {self.profile_name} in your ~/.aws/config file') + if config_profile_section not in config and self.region is None: + self.error(f'Please create a section for {self.profile_name} in your ~/.aws/config file or provide region') return False - self.region = config[config_profile_section].get('region', 'us-east-1') + # if region is not set, load it from config + if self.region is None: + self.region = config[config_profile_section].get('region', 'us-east-1') try: self.client = boto3.session.Session(profile_name=self.profile_name, region_name=self.region).client('apigateway') @@ -98,7 +120,8 @@ def load_creds(self) -> bool: region_name=self.region ) self.client.get_account() - self.region = self.client._client_config.region_name + if self.region is None: + self.region = self.client._client_config.region_name # Save/overwrite config if profile specified if self.profile_name: if config_profile_section not in config: @@ -123,11 +146,9 @@ def load_creds(self) -> bool: return False def error(self, error): - print(self.help) - sys.exit(error) + raise FireProxException(error) - def get_template(self): - url = self.url + def get_template(self, url): if url[-1] == '/': url = url[:-1] @@ -239,9 +260,7 @@ def create_api(self, url): if not url: self.error('Please provide a valid URL end-point') - print(f'Creating => {url}...') - - template = self.get_template() + template = self.get_template(url) response = self.client.import_rest_api( parameters={ 'endpointConfigurationTypes': 'REGIONAL' @@ -249,7 +268,8 @@ def create_api(self, url): body=template ) resource_id, proxy_url = self.create_deployment(response['id']) - self.store_api( + result = {"id":response['id'],"proxy_url":proxy_url} + return result, self.store_api( response['id'], response['name'], response['createdDate'], @@ -259,6 +279,7 @@ def create_api(self, url): proxy_url ) + def update_api(self, api_id, url): if not any([api_id, url]): self.error('Please provide a valid API ID and URL end-point') @@ -268,7 +289,6 @@ def update_api(self, api_id, url): resource_id = self.get_resource(api_id) if resource_id: - print(f'Found resource {resource_id} for {api_id}!') response = self.client.update_integration( restApiId=api_id, resourceId=resource_id, @@ -288,18 +308,40 @@ def update_api(self, api_id, url): def delete_api(self, api_id): if not api_id: self.error('Please provide a valid API ID') - items = self.list_api(api_id) - for item in items: - item_api_id = item['id'] - if item_api_id == api_id: + retry = 3 + sleep_time = 3 + success = False + error_msg = 'Generic error' + while retry > 0 and not success: + try: response = self.client.delete_rest_api( restApiId=api_id ) - return True - return False + except botocore.exceptions.ClientError as err: + if err.response['Error']['Code'] == 'NotFoundException': + error_msg = 'API not found' + break + elif err.response['Error']['Code'] == 'TooManyRequestsException': + error_msg = 'Too many requests' + sleep(sleep_time) + sleep_time *= 2 + else: + error_msg = err.response['Error']['Message'] + except BaseException as e: + error_msg = 'Generic error' + else: + success = True + error_msg = '' + finally: + retry -= 1 + return success, error_msg + - def list_api(self, deleted_api_id=None): + def list_api(self, deleted_api_id=None, deleting=False): + results = [] response = self.client.get_rest_apis() + if deleting: + return response['items'] for item in response['items']: try: created_dt = item['createdDate'] @@ -308,15 +350,15 @@ def list_api(self, deleted_api_id=None): proxy_url = self.get_integration(api_id).replace('{proxy}', '') url = f'https://{api_id}.execute-api.{self.region}.amazonaws.com/fireprox/' if not api_id == deleted_api_id: - print(f'[{created_dt}] ({api_id}) {name}: {url} => {proxy_url}') + results.append(f'[{created_dt}] ({api_id}) {name}: {url} => {proxy_url}') except: pass - return response['items'] + return results def store_api(self, api_id, name, created_dt, version_dt, url, resource_id, proxy_url): - print( + return( f'[{created_dt}] ({api_id}) {name} => {proxy_url} ({url})' ) @@ -373,9 +415,9 @@ def parse_arguments() -> Tuple[argparse.Namespace, str]: parser.add_argument('--session_token', help='AWS Session Token', type=str, default=None) parser.add_argument('--region', - help='AWS Region', type=str, default=None) + help='AWS Regions (accepts single region, comma-separated list of regions or file containing regions)', type=str, default=None) parser.add_argument('--command', - help='Commands: list, create, delete, update', type=str, default=None) + help='Commands: list, list_all, create, delete, prune, update', type=str, default=None) parser.add_argument('--api_id', help='API ID', type=str, required=False) parser.add_argument('--url', @@ -383,33 +425,147 @@ def parse_arguments() -> Tuple[argparse.Namespace, str]: return parser.parse_args(), parser.format_help() +def parse_region(region: str | List, mode: str = "all")-> Union[str, List, None]: + """Parse 'region' and return the final region or set of regions according + to mode + """ + if region is None: + return None + if mode not in ['all', 'random']: + raise ValueError(f"mode should one of ['all', 'random']") + + elif isinstance(region, str): + # if region is a file containing regions, read from it + if os.path.isfile(region): + with open(region) as f: + regions = [reg.strip() for reg in f.readlines()] + if mode == "random": + return random.choice(regions) + elif mode == "all": + return regions + elif ',' in region: + regions = region.split(sep=',') + if mode == "random": + return random.choice(regions) + else: + return regions + else: + return region + + elif isinstance(region, list): + if mode == "all": + return region + elif mode == "random": + return random.choice(region) + + def main(): """Run the main program :return: """ args, help_text = parse_arguments() - fp = FireProx(args, help_text) - if args.command == 'list': - print(f'Listing API\'s...') - result = fp.list_api() - - elif args.command == 'create': - result = fp.create_api(fp.url) - - elif args.command == 'delete': - result = fp.delete_api(fp.api_id) - success = 'Success!' if result else 'Failed!' - print(f'Deleting {fp.api_id} => {success}') - - elif args.command == 'update': - print(f'Updating {fp.api_id} => {fp.url}...') - result = fp.update_api(fp.api_id, fp.url) - success = 'Success!' if result else 'Failed!' - print(f'API Update Complete: {success}') - - else: - print(f'[ERROR] Unsupported command: {args.command}\n') + + try: + if not args.command: + raise FireProxException('Please provide a valid command') + + if args.command == 'list': + region_parsed = parse_region(args.region) + if isinstance(region_parsed, list): + for region in region_parsed: + args.region = region + fp = FireProx(**vars(args)) + print(f'Listing API\'s from {fp.region}...') + results = fp.list_api(deleting=False) + for result in results: + print(result) + else: + args.region = region_parsed + fp = FireProx(**vars(args)) + print(f'Listing API\'s from {fp.region}...') + results = fp.list_api(deleting=False) + for result in results: + print(result) + + elif args.command == "list_all": + for region in AWS_DEFAULT_REGIONS: + args.region = region + fp = FireProx(**vars(args)) + print(f'Listing API\'s from {fp.region}...') + results = fp.list_api(deleting=False) + for result in results: + print(result) + + elif args.command == 'create': + if not args.url: + raise FireProxException('Please provide a valid URL end-point') + region_parsed = parse_region(args.region, mode="random") + args.region = region_parsed + print(f'Creating => {args.url}...') + fp = FireProx(**vars(args)) + _, result = fp.create_api(args.url) + print(result) + + elif args.command == 'delete': + if not args.api_id: + raise FireProxException('Please provide a valid API id') + region_parsed = parse_region(args.region) + if region_parsed is None or isinstance(region_parsed, str): + args.region = region_parsed + fp = FireProx(**vars(args)) + result, msg = fp.delete_api(args.api_id) + if result: + print(f'Deleting {args.api_id} => Success!') + else: + print(f'Deleting {args.api_id} => Failed! ({msg})') + else: + raise FireProxException(f'[ERROR] More than one region provided for command \'delete\'\n') + + elif args.command == 'prune': + region_parsed = parse_region(args.region) + if region_parsed is None: + region_parsed = AWS_DEFAULT_REGIONS + if isinstance(region_parsed, str): + region_parsed = [region_parsed] + while True: + choice = input(f"This will delete ALL APIs from region(s): {','.join(region_parsed)}. Proceed? [y/N] ") or 'N' + if choice.upper() in ['Y', 'N']: + break + if choice.upper() == 'Y': + for region in region_parsed: + args.region = region + fp = FireProx(**vars(args)) + print(f'Retrieving API\'s from {region}...') + current_apis = fp.list_api(deleting=True) + if len(current_apis) == 0: + print(f'No API found') + else: + for api in current_apis: + result, msg = fp.delete_api(api_id=api['id']) + if result: + print(f'Deleting {api["id"]} => Success!') + else: + print(f'Deleting {api["id"]} => Failed! ({msg})') + + elif args.command == 'update': + if not args.api_id: + raise FireProxException('Please provide a valid API id') + if not args.url: + raise FireProxException('Please provide a valid URL end-point') + region_parsed = parse_region(args.region) + if isinstance(region_parsed, list): + raise FireProxException(f'[ERROR] More than one region provided for command \'update\'\n') + fp = FireProx(**vars(args)) + print(f'Updating {args.api_id} => {args.url}...') + result = fp.update_api(args.api_id, args.url) + success = 'Success!' if result else 'Failed!' + print(f'API Update Complete: {success}') + + else: + raise FireProxException(f'[ERROR] Unsupported command: {args.command}\n') + + except FireProxException as ex: print(help_text) sys.exit(1)