pax_global_header00006660000000000000000000000064152035274140014515gustar00rootroot0000000000000052 comment=4e0fa84939226c2204be09af4924edb91fbff33e cloud_enum-0.8/000077500000000000000000000000001520352741400135165ustar00rootroot00000000000000cloud_enum-0.8/.github/000077500000000000000000000000001520352741400150565ustar00rootroot00000000000000cloud_enum-0.8/.github/workflows/000077500000000000000000000000001520352741400171135ustar00rootroot00000000000000cloud_enum-0.8/.github/workflows/python-app.yml000066400000000000000000000006521520352741400217400ustar00rootroot00000000000000name: Python application on: push: branches: [ master ] pull_request: branches: [ master ] permissions: contents: read jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v6 with: enable-cache: true - name: Install dependencies run: uv sync - name: Test with pytest run: uv run pytest cloud_enum-0.8/.github/workflows/release.yml000066400000000000000000000022601520352741400212560ustar00rootroot00000000000000name: Release on: push: branches: [ master ] permissions: contents: write jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v6 with: enable-cache: true - name: Read version from pyproject.toml id: version run: | VERSION=$(grep '^version' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/') echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Check if tag already exists id: tag_check run: | if git rev-parse "refs/tags/${{ steps.version.outputs.version }}" >/dev/null 2>&1; then echo "exists=true" >> "$GITHUB_OUTPUT" else echo "exists=false" >> "$GITHUB_OUTPUT" fi - name: Create tag and release if: steps.tag_check.outputs.exists == 'false' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION="${{ steps.version.outputs.version }}" git tag "$VERSION" git push origin "$VERSION" gh release create "$VERSION" \ --title "Release $VERSION" \ --generate-notes cloud_enum-0.8/.gitignore000066400000000000000000000004241520352741400155060ustar00rootroot00000000000000# custom cloud-enum-output # MacOS .DS_Store # Python __pycache__/ *.py[cod] *$py.class *.so .Python build/ dist/ *.egg-info/ .pytest_cache/ .coverage .coverage.* coverage.xml htmlcov/ .tox/ .nox/ .hypothesis/ # uv .venv/ uv.lock # vim swap files *.swp # vscode .vscode/ cloud_enum-0.8/LICENSE000066400000000000000000000020531520352741400145230ustar00rootroot00000000000000MIT License Copyright (c) 2022 initstring Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cloud_enum-0.8/README.md000066400000000000000000000114461520352741400150030ustar00rootroot00000000000000# cloud_enum ## Future of cloud_enum I built this tool in 2019 for a pentest involving Azure, as no other enumeration tools supported it at the time. It grew from there, and I learned a lot while adding features. Building tools is fun, but maintaining tools is hard. I haven't actively used this tool myself in a while, but I've done my best to fix bugs and review pull requests. Moving forward, it makes sense to consolidate this functionality into a well-maintained project that handles the essentials (web/dns requests, threading, I/O, logging, etc.). [Nuclei](https://github.com/projectdiscovery/nuclei) is really well suited for this. You can see my first PR to migrate cloud_enum functionality to Nuclei [here](https://github.com/projectdiscovery/nuclei-templates/pull/6865). I encourage others to contribute templates to Nuclei, allowing us to focus on detecting cloud resources while leaving the groundwork to Nuclei. I'll still try to review PRs here to address bugs as time permits, but likely won't have time for major changes. Thanks to all the great contributors. Good luck with your recon! ## Overview Multi-cloud OSINT tool. Enumerate public resources in AWS, Azure, and Google Cloud. Currently enumerates the following: **Amazon Web Services**: - Open / Protected S3 Buckets - awsapps (WorkMail, WorkDocs, Connect, etc.) **Microsoft Azure**: - Storage Accounts - Open Blob Storage Containers - Hosted Databases - Virtual Machines - Web Apps **Google Cloud Platform** - Open / Protected GCP Buckets - Open / Protected Firebase Realtime Databases - Google App Engine sites - Cloud Functions (enumerates project/regions with existing functions, then brute forces actual function names) - Open Firebase Apps See it in action in [Codingo](https://github.com/codingo)'s video demo [here](https://www.youtube.com/embed/pTUDJhWJ1m0). ## Usage ### Setup This project uses [uv](https://github.com/astral-sh/uv) for dependency management. Install uv, then run: ```sh uv sync ``` ### Running The only required argument is at least one keyword. You can use the built-in fuzzing strings, but you will get better results if you supply your own with `-m` and/or `-b`. You can provide multiple keywords by specifying the `-k` argument multiple times. Keywords are mutated automatically using strings from `enum_tools/fuzz.txt` or a file you provide with the `-m` flag. Services that require a second-level of brute forcing (Azure Containers and GCP Functions) will also use `fuzz.txt` by default or a file you provide with the `-b` flag. Let's say you were researching "somecompany" whose website is "somecompany.io" that makes a product called "blockchaindoohickey". You could run the tool like this: ```sh uv run cloud_enum -k somecompany -k somecompany.io -k blockchaindoohickey ``` HTTP scraping and DNS lookups use 5 threads each by default. You can try increasing this, but eventually the cloud providers will rate limit you. Here is an example to increase to 10. ```sh uv run cloud_enum -k keyword -t 10 ``` **IMPORTANT**: Some resources (Azure Containers, GCP Functions) are discovered per-region. To save time scanning, there is a "REGIONS" variable defined in `cloudenum/azure_regions.py and cloudenum/gcp_regions.py` that is set by default to use only 1 region. You may want to look at these files and edit them to be relevant to your own work. **Complete Usage Details** ``` usage: cloud_enum.py [-h] -k KEYWORD [-m MUTATIONS] [-b BRUTE] Multi-cloud enumeration utility. All hail OSINT! optional arguments: -h, --help show this help message and exit -k KEYWORD, --keyword KEYWORD Keyword. Can use argument multiple times. -kf KEYFILE, --keyfile KEYFILE Input file with a single keyword per line. -m MUTATIONS, --mutations MUTATIONS Mutations. Default: enum_tools/fuzz.txt -b BRUTE, --brute BRUTE List to brute-force Azure container names. Default: enum_tools/fuzz.txt -t THREADS, --threads THREADS Threads for HTTP brute-force. Default = 5 -ns NAMESERVER, --nameserver NAMESERVER DNS server to use in brute-force. -l LOGFILE, --logfile LOGFILE Will APPEND found items to specified file. -f FORMAT, --format FORMAT Format for log file (text,json,csv - defaults to text) --disable-aws Disable Amazon checks. --disable-azure Disable Azure checks. --disable-gcp Disable Google checks. -qs, --quickscan Disable all mutations and second-level scans ``` ## Thanks So far, I have borrowed from: - Some of the permutations from [GCPBucketBrute](https://github.com/RhinoSecurityLabs/GCPBucketBrute/blob/master/permutations.txt) cloud_enum-0.8/cloud_enum.py000077500000000000000000000206401520352741400162270ustar00rootroot00000000000000#!/usr/bin/env python3 """ cloud_enum by initstring (github.com/initstring) Multi-cloud OSINT tool designed to enumerate storage and services in AWS, Azure, and GCP. Enjoy! """ import os import sys import argparse import re from enum_tools import aws_checks from enum_tools import azure_checks from enum_tools import gcp_checks from enum_tools import utils BANNER = ''' ########################## cloud_enum github.com/initstring ########################## ''' def parse_arguments(): """ Handles user-passed parameters """ desc = "Multi-cloud enumeration utility. All hail OSINT!" parser = argparse.ArgumentParser(description=desc) # Grab the current dir of the script, for setting some defaults below script_path = os.path.split(os.path.abspath(sys.argv[0]))[0] kw_group = parser.add_mutually_exclusive_group(required=True) # Keyword can given multiple times kw_group.add_argument('-k', '--keyword', type=str, action='append', help='Keyword. Can use argument multiple times.') # OR, a keyword file can be used kw_group.add_argument('-kf', '--keyfile', type=str, action='store', help='Input file with a single keyword per line.') # Use included mutations file by default, or let the user provide one parser.add_argument('-m', '--mutations', type=str, action='store', default=script_path + '/enum_tools/fuzz.txt', help='Mutations. Default: enum_tools/fuzz.txt') # Use include container brute-force or let the user provide one parser.add_argument('-b', '--brute', type=str, action='store', default=script_path + '/enum_tools/fuzz.txt', help='List to brute-force Azure container names.' ' Default: enum_tools/fuzz.txt') parser.add_argument('-t', '--threads', type=int, action='store', default=5, help='Threads for HTTP brute-force.' ' Default = 5') parser.add_argument('-ns', '--nameserver', type=str, action='store', default='1.1.1.1', help='DNS server to use in brute-force.') parser.add_argument('-nsf', '--nameserverfile', type=str, help='Path to the file containing nameserver IPs') parser.add_argument('-l', '--logfile', type=str, action='store', help='Appends found items to specified file.') parser.add_argument('-f', '--format', type=str, action='store', default='text', help='Format for log file (text,json,csv)' ' - default: text') parser.add_argument('--disable-aws', action='store_true', help='Disable Amazon checks.') parser.add_argument('--disable-azure', action='store_true', help='Disable Azure checks.') parser.add_argument('--disable-gcp', action='store_true', help='Disable Google checks.') parser.add_argument('-qs', '--quickscan', action='store_true', help='Disable all mutations and second-level scans') args = parser.parse_args() # Ensure mutations file is readable if not os.access(args.mutations, os.R_OK): print(f"[!] Cannot access mutations file: {args.mutations}") sys.exit() # Ensure brute file is readable if not os.access(args.brute, os.R_OK): print("[!] Cannot access brute-force file, exiting") sys.exit() # Ensure keywords file is readable if args.keyfile: if not os.access(args.keyfile, os.R_OK): print("[!] Cannot access keyword file, exiting") sys.exit() # Parse keywords from input file with open(args.keyfile, encoding='utf-8') as infile: args.keyword = [keyword.strip() for keyword in infile] # Ensure log file is writeable if args.logfile: if os.path.isdir(args.logfile): print("[!] Can't specify a directory as the logfile, exiting.") sys.exit() if os.path.isfile(args.logfile): target = args.logfile else: target = os.path.dirname(args.logfile) if target == '': target = '.' if not os.access(target, os.W_OK): print("[!] Cannot write to log file, exiting") sys.exit() # Set up logging format if args.format not in ('text', 'json', 'csv'): print("[!] Sorry! Allowed log formats: 'text', 'json', or 'csv'") sys.exit() # Set the global in the utils file, where logging needs to happen utils.init_logfile(args.logfile, args.format) return args def print_status(args): """ Print a short pre-run status message """ print(f"Keywords: {', '.join(args.keyword)}") if args.quickscan: print("Mutations: NONE! (Using quickscan)") else: print(f"Mutations: {args.mutations}") print(f"Brute-list: {args.brute}") print("") def check_windows(): """ Fixes pretty color printing for Windows users. Keeping out of requirements.txt to avoid the library requirement for most users. """ if os.name == 'nt': try: import colorama colorama.init() except ModuleNotFoundError: print("[!] Yo, Windows user - if you want pretty colors, you can" " install the colorama python package.") def read_mutations(mutations_file): """ Read mutations file into memory for processing. """ with open(mutations_file, encoding="utf8", errors="ignore") as infile: mutations = infile.read().splitlines() print(f"[+] Mutations list imported: {len(mutations)} items") return mutations def clean_text(text): """ Clean text to be RFC compliant for hostnames / DNS """ banned_chars = re.compile('[^a-z0-9.-]') text_lower = text.lower() text_clean = banned_chars.sub('', text_lower) return text_clean def append_name(name, names_list): """ Ensure strings stick to DNS label limit of 63 characters """ if len(name) <= 63: names_list.append(name) def build_names(base_list, mutations): """ Combine base and mutations for processing by individual modules. """ names = [] for base in base_list: # Clean base base = clean_text(base) # First, include with no mutations append_name(base, names) for mutation in mutations: # Clean mutation mutation = clean_text(mutation) # Then, do appends append_name(f"{base}{mutation}", names) append_name(f"{base}.{mutation}", names) append_name(f"{base}-{mutation}", names) # Then, do prepends append_name(f"{mutation}{base}", names) append_name(f"{mutation}.{base}", names) append_name(f"{mutation}-{base}", names) print(f"[+] Mutated results: {len(names)} items") return names def read_nameservers(file_path): try: with open(file_path, 'r') as file: nameservers = [line.strip() for line in file if line.strip()] if not nameservers: raise ValueError("Nameserver file is empty") return nameservers except FileNotFoundError: print(f"Error: File '{file_path}' not found.") exit(1) except ValueError as e: print(e) exit(1) def main(): """ Main program function. """ args = parse_arguments() print(BANNER) # Generate a basic status on targets and parameters print_status(args) # Give our Windows friends a chance at pretty colors check_windows() # First, build a sorted base list of target names if args.quickscan: mutations = [] else: mutations = read_mutations(args.mutations) names = build_names(args.keyword, mutations) # All the work is done in the individual modules try: if not args.disable_aws: aws_checks.run_all(names, args) if not args.disable_azure: azure_checks.run_all(names, args) if not args.disable_gcp: gcp_checks.run_all(names, args) except KeyboardInterrupt: print("Thanks for playing!") sys.exit() # Best of luck to you! print("\n[+] All done, happy hacking!\n") sys.exit() if __name__ == '__main__': main() cloud_enum-0.8/enum_tools/000077500000000000000000000000001520352741400157025ustar00rootroot00000000000000cloud_enum-0.8/enum_tools/__init__.py000066400000000000000000000000001520352741400200010ustar00rootroot00000000000000cloud_enum-0.8/enum_tools/aws_checks.py000066400000000000000000000105371520352741400203740ustar00rootroot00000000000000""" AWS-specific checks. Part of the cloud_enum package available at github.com/initstring/cloud_enum """ from enum_tools import utils BANNER = ''' ++++++++++++++++++++++++++ amazon checks ++++++++++++++++++++++++++ ''' # Known S3 domain names S3_URL = 's3.amazonaws.com' APPS_URL = 'awsapps.com' # Known AWS region names. This global will be used unless the user passes # in a specific region name. (NOT YET IMPLEMENTED) AWS_REGIONS = ['amazonaws.com', 'ap-east-1.amazonaws.com', 'us-east-2.amazonaws.com', 'us-west-1.amazonaws.com', 'us-west-2.amazonaws.com', 'ap-south-1.amazonaws.com', 'ap-northeast-1.amazonaws.com', 'ap-northeast-2.amazonaws.com', 'ap-northeast-3.amazonaws.com', 'ap-southeast-1.amazonaws.com', 'ap-southeast-2.amazonaws.com', 'ca-central-1.amazonaws.com', 'cn-north-1.amazonaws.com.cn', 'cn-northwest-1.amazonaws.com.cn', 'eu-central-1.amazonaws.com', 'eu-west-1.amazonaws.com', 'eu-west-2.amazonaws.com', 'eu-west-3.amazonaws.com', 'eu-north-1.amazonaws.com', 'sa-east-1.amazonaws.com'] def print_s3_response(reply): """ Parses the HTTP reply of a brute-force attempt This function is passed into the class object so we can view results in real-time. """ data = {'platform': 'aws', 'msg': '', 'target': '', 'access': ''} if reply.status_code == 404: pass elif 'Bad Request' in reply.reason: pass elif reply.status_code == 200: data['msg'] = 'OPEN S3 BUCKET' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) utils.list_bucket_contents(reply.url) elif reply.status_code == 403: data['msg'] = 'Protected S3 Bucket' data['target'] = reply.url data['access'] = 'protected' utils.fmt_output(data) elif 'Slow Down' in reply.reason: print("[!] You've been rate limited, skipping rest of check...") return 'breakout' else: print(f" Unknown status codes being received from {reply.url}:\n" " {reply.status_code}: {reply.reason}") return None def check_s3_buckets(names, threads): """ Checks for open and restricted Amazon S3 buckets """ print("[+] Checking for S3 buckets") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of correctly formatted urls candidates = [] # Take each mutated keyword craft a url with the correct format for name in names: candidates.append(f'{name}.{S3_URL}') # Send the valid names to the batch HTTP processor utils.get_url_batch(candidates, use_ssl=False, callback=print_s3_response, threads=threads) # Stop the time utils.stop_timer(start_time) def check_awsapps(names, threads, nameserver, nameserverfile=False): """ Checks for existence of AWS Apps (ie. WorkDocs, WorkMail, Connect, etc.) """ data = {'platform': 'aws', 'msg': 'AWS App Found:', 'target': '', 'access': ''} print("[+] Checking for AWS Apps") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of domain names to look up candidates = [] # Initialize the list of valid hostnames valid_names = [] # Take each mutated keyword craft a domain name to lookup. for name in names: candidates.append(f'{name}.{APPS_URL}') # AWS Apps use DNS sub-domains. First, see which are valid. valid_names = utils.fast_dns_lookup(candidates, nameserver, nameserverfile, threads=threads) for name in valid_names: data['target'] = f'https://{name}' data['access'] = 'protected' utils.fmt_output(data) # Stop the timer utils.stop_timer(start_time) def run_all(names, args): """ Function is called by main program """ print(BANNER) # Use user-supplied AWS region if provided # if not regions: # regions = AWS_REGIONS check_s3_buckets(names, args.threads) check_awsapps(names, args.threads, args.nameserver, args.nameserverfile) cloud_enum-0.8/enum_tools/azure_checks.py000066400000000000000000000437431520352741400207350ustar00rootroot00000000000000""" Azure-specific checks. Part of the cloud_enum package available at github.com/initstring/cloud_enum """ import re import requests from enum_tools import utils from enum_tools import azure_regions BANNER = ''' ++++++++++++++++++++++++++ azure checks ++++++++++++++++++++++++++ ''' # Known Azure domain names BLOB_URL = 'blob.core.windows.net' FILE_URL= 'file.core.windows.net' QUEUE_URL = 'queue.core.windows.net' TABLE_URL = 'table.core.windows.net' MGMT_URL = 'scm.azurewebsites.net' VAULT_URL = 'vault.azure.net' WEBAPP_URL = 'azurewebsites.net' DATABASE_URL = 'database.windows.net' # Virtual machine DNS names are actually: # {whatever}.{region}.cloudapp.azure.com VM_URL = 'cloudapp.azure.com' def print_account_response(reply): """ Parses the HTTP reply of a brute-force attempt This function is passed into the class object so we can view results in real-time. """ data = {'platform': 'azure', 'msg': '', 'target': '', 'access': ''} if reply.status_code == 404 or 'The requested URI does not represent' in reply.reason: pass elif 'Server failed to authenticate the request' in reply.reason: data['msg'] = 'Auth-Only Account' data['target'] = reply.url data['access'] = 'protected' utils.fmt_output(data) elif 'The specified account is disabled' in reply.reason: data['msg'] = 'Disabled Account' data['target'] = reply.url data['access'] = 'disabled' utils.fmt_output(data) elif 'Value for one of the query' in reply.reason: data['msg'] = 'HTTP-OK Account' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) elif 'The account being accessed' in reply.reason: data['msg'] = 'HTTPS-Only Account' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) elif 'Unauthorized' in reply.reason: data['msg'] = 'Unathorized Account' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) else: print(" Unknown status codes being received from " + reply.url +":\n" " "+ str(reply.status_code)+" : "+ reply.reason) def check_storage_accounts(names, threads, nameserver, nameserverfile=False): """ Checks storage account names """ print("[+] Checking for Azure Storage Accounts") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of domain names to look up candidates = [] # Initialize the list of valid hostnames valid_names = [] # Take each mutated keyword craft a domain name to lookup. # As Azure Storage Accounts can contain only letters and numbers, # discard those not matching to save time on the DNS lookups. regex = re.compile('[^a-zA-Z0-9]') for name in names: if not re.search(regex, name): candidates.append(f'{name}.{BLOB_URL}') # Azure Storage Accounts use DNS sub-domains. First, see which are valid. valid_names = utils.fast_dns_lookup(candidates, nameserver, nameserverfile, threads=threads) # Send the valid names to the batch HTTP processor utils.get_url_batch(valid_names, use_ssl=False, callback=print_account_response, threads=threads) # Stop the timer utils.stop_timer(start_time) # de-dupe the results and return return list(set(valid_names)) def check_file_accounts(names, threads, nameserver, nameserverfile=False): """ Checks File account names """ print("[+] Checking for Azure File Accounts") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of domain names to look up candidates = [] # Initialize the list of valid hostnames valid_names = [] # Take each mutated keyword craft a domain name to lookup. # As Azure Storage Accounts can contain only letters and numbers, # discard those not matching to save time on the DNS lookups. regex = re.compile('[^a-zA-Z0-9]') for name in names: if not re.search(regex, name): candidates.append(f'{name}.{FILE_URL}') # Azure Storage Accounts use DNS sub-domains. First, see which are valid. valid_names = utils.fast_dns_lookup(candidates, nameserver, nameserverfile, threads=threads) # Send the valid names to the batch HTTP processor utils.get_url_batch(valid_names, use_ssl=False, callback=print_account_response, threads=threads) # Stop the timer utils.stop_timer(start_time) # de-dupe the results and return return list(set(valid_names)) def check_queue_accounts(names, threads, nameserver, nameserverfile=False): """ Checks Queue account names """ print("[+] Checking for Azure Queue Accounts") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of domain names to look up candidates = [] # Initialize the list of valid hostnames valid_names = [] # Take each mutated keyword craft a domain name to lookup. # As Azure Storage Accounts can contain only letters and numbers, # discard those not matching to save time on the DNS lookups. regex = re.compile('[^a-zA-Z0-9]') for name in names: if not re.search(regex, name): candidates.append(f'{name}.{QUEUE_URL}') # Azure Storage Accounts use DNS sub-domains. First, see which are valid. valid_names = utils.fast_dns_lookup(candidates, nameserver, nameserverfile, threads=threads) # Send the valid names to the batch HTTP processor utils.get_url_batch(valid_names, use_ssl=False, callback=print_account_response, threads=threads) # Stop the timer utils.stop_timer(start_time) # de-dupe the results and return return list(set(valid_names)) def check_table_accounts(names, threads, nameserver, nameserverfile=False): """ Checks Table account names """ print("[+] Checking for Azure Table Accounts") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of domain names to look up candidates = [] # Initialize the list of valid hostnames valid_names = [] # Take each mutated keyword craft a domain name to lookup. # As Azure Storage Accounts can contain only letters and numbers, # discard those not matching to save time on the DNS lookups. regex = re.compile('[^a-zA-Z0-9]') for name in names: if not re.search(regex, name): candidates.append(f'{name}.{TABLE_URL}') # Azure Storage Accounts use DNS sub-domains. First, see which are valid. valid_names = utils.fast_dns_lookup(candidates, nameserver, nameserverfile, threads=threads) # Send the valid names to the batch HTTP processor utils.get_url_batch(valid_names, use_ssl=False, callback=print_account_response, threads=threads) # Stop the timer utils.stop_timer(start_time) # de-dupe the results and return return list(set(valid_names)) def check_mgmt_accounts(names, threads, nameserver, nameserverfile=False): """ Checks App Management account names """ print("[+] Checking for Azure App Management Accounts") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of domain names to look up candidates = [] # Initialize the list of valid hostnames valid_names = [] # Take each mutated keyword craft a domain name to lookup. # As Azure Storage Accounts can contain only letters and numbers, # discard those not matching to save time on the DNS lookups. regex = re.compile('[^a-zA-Z0-9]') for name in names: if not re.search(regex, name): candidates.append(f'{name}.{MGMT_URL}') # Azure Storage Accounts use DNS sub-domains. First, see which are valid. valid_names = utils.fast_dns_lookup(candidates, nameserver, nameserverfile, threads=threads) # Send the valid names to the batch HTTP processor utils.get_url_batch(valid_names, use_ssl=False, callback=print_account_response, threads=threads) # Stop the timer utils.stop_timer(start_time) # de-dupe the results and return return list(set(valid_names)) def check_vault_accounts(names, threads, nameserver, nameserverfile=False): """ Checks Key Vault account names """ print("[+] Checking for Azure Key Vault Accounts") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of domain names to look up candidates = [] # Initialize the list of valid hostnames valid_names = [] # Take each mutated keyword craft a domain name to lookup. # As Azure Storage Accounts can contain only letters and numbers, # discard those not matching to save time on the DNS lookups. regex = re.compile('[^a-zA-Z0-9]') for name in names: if not re.search(regex, name): candidates.append(f'{name}.{VAULT_URL}') # Azure Storage Accounts use DNS sub-domains. First, see which are valid. valid_names = utils.fast_dns_lookup(candidates, nameserver, nameserverfile, threads=threads) # Send the valid names to the batch HTTP processor utils.get_url_batch(valid_names, use_ssl=False, callback=print_account_response, threads=threads) # Stop the timer utils.stop_timer(start_time) # de-dupe the results and return return list(set(valid_names)) def print_container_response(reply): """ Parses the HTTP reply of a brute-force attempt This function is passed into the class object so we can view results in real-time. """ data = {'platform': 'azure', 'msg': '', 'target': '', 'access': ''} # Stop brute forcing disabled accounts if 'The specified account is disabled' in reply.reason: print(" [!] Breaking out early, account disabled.") return 'breakout' # Stop brute forcing accounts without permission if ('not authorized to perform this operation' in reply.reason or 'not have sufficient permissions' in reply.reason or 'Public access is not permitted' in reply.reason or 'Server failed to authenticate the request' in reply.reason): print(" [!] Breaking out early, auth required.") return 'breakout' # Stop brute forcing unsupported accounts if 'Blob API is not yet supported' in reply.reason: print(" [!] Breaking out early, Hierarchical namespace account") return 'breakout' # Handle other responses if reply.status_code == 404: pass elif reply.status_code == 200: data['msg'] = 'OPEN AZURE CONTAINER' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) utils.list_bucket_contents(reply.url) elif 'One of the request inputs is out of range' in reply.reason: pass elif 'The request URI is invalid' in reply.reason: pass else: print(f" Unknown status codes being received from {reply.url}:\n" " {reply.status_code}: {reply.reason}") return None def brute_force_containers(storage_accounts, brute_list, threads): """ Attempts to find public Blob Containers in valid Storage Accounts Here is the URL format to list Azure Blog Container contents: .blob.core.windows.net//?restype=container&comp=list """ # We have a list of valid DNS names that might not be worth scraping, # such as disabled accounts or authentication required. Let's quickly # weed those out. print(f"[*] Checking {len(storage_accounts)} accounts for status before brute-forcing") valid_accounts = [] for account in storage_accounts: try: reply = requests.get(f'https://{account}/') if 'Server failed to authenticate the request' in reply.reason: storage_accounts.remove(account) elif 'The specified account is disabled' in reply.reason: storage_accounts.remove(account) else: valid_accounts.append(account) except requests.exceptions.ConnectionError as error_msg: print(f" [!] Connection error on https://{account}:") print(error_msg) # Read the brute force file into memory clean_names = utils.get_brute(brute_list, mini=3) # Start a counter to report on elapsed time start_time = utils.start_timer() print(f"[*] Brute-forcing container names in {len(valid_accounts)} storage accounts") for account in valid_accounts: print(f"[*] Brute-forcing {len(clean_names)} container names in {account}") # Initialize the list of correctly formatted urls candidates = [] # Take each mutated keyword and craft a url with correct format for name in clean_names: candidates.append(f'{account}/{name}/?restype=container&comp=list') # Send the valid names to the batch HTTP processor utils.get_url_batch(candidates, use_ssl=True, callback=print_container_response, threads=threads) # Stop the timer utils.stop_timer(start_time) def print_website_response(hostname): """ This function is passed into the DNS brute force as a callback, so we can get real-time results. """ data = {'platform': 'azure', 'msg': '', 'target': '', 'access': ''} data['msg'] = 'Registered Azure Website DNS Name' data['target'] = hostname data['access'] = 'public' utils.fmt_output(data) def check_azure_websites(names, nameserver, threads, nameserverfile=False): """ Checks for Azure Websites (PaaS) """ print("[+] Checking for Azure Websites") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of domain names to look up candidates = [name + '.' + WEBAPP_URL for name in names] # Azure Websites use DNS sub-domains. If it resolves, it is registered. utils.fast_dns_lookup(candidates, nameserver, nameserverfile, callback=print_website_response, threads=threads) # Stop the timer utils.stop_timer(start_time) def print_database_response(hostname): """ This function is passed into the DNS brute force as a callback, so we can get real-time results. """ data = {'platform': 'azure', 'msg': '', 'target': '', 'access': ''} data['msg'] = 'Registered Azure Database DNS Name' data['target'] = hostname data['access'] = 'public' utils.fmt_output(data) def check_azure_databases(names, nameserver, threads, nameserverfile=False): """ Checks for Azure Databases """ print("[+] Checking for Azure Databases") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of domain names to look up candidates = [name + '.' + DATABASE_URL for name in names] # Azure databases use DNS sub-domains. If it resolves, it is registered. utils.fast_dns_lookup(candidates, nameserver, nameserverfile, callback=print_database_response, threads=threads) # Stop the timer utils.stop_timer(start_time) def print_vm_response(hostname): """ This function is passed into the DNS brute force as a callback, so we can get real-time results. """ data = {'platform': 'azure', 'msg': '', 'target': '', 'access': ''} data['msg'] = 'Registered Azure Virtual Machine DNS Name' data['target'] = hostname data['access'] = 'public' utils.fmt_output(data) def check_azure_vms(names, nameserver, threads, nameserverfile=False): """ Checks for Azure Virtual Machines """ print("[+] Checking for Azure Virtual Machines") # Start a counter to report on elapsed time start_time = utils.start_timer() # Pull the regions from a config file regions = azure_regions.REGIONS print(f"[*] Testing across {len(regions)} regions defined in the config file") for region in regions: # Initialize the list of domain names to look up candidates = [name + '.' + region + '.' + VM_URL for name in names] # Azure VMs use DNS sub-domains. If it resolves, it is registered. utils.fast_dns_lookup(candidates, nameserver, nameserverfile, callback=print_vm_response, threads=threads) # Stop the timer utils.stop_timer(start_time) def run_all(names, args): """ Function is called by main program """ print(BANNER) valid_accounts = check_storage_accounts(names, args.threads, args.nameserver, args.nameserverfile) if valid_accounts and not args.quickscan: brute_force_containers(valid_accounts, args.brute, args.threads) check_file_accounts(names, args.threads, args.nameserver, args.nameserverfile) check_queue_accounts(names, args.threads, args.nameserver, args.nameserverfile) check_table_accounts(names, args.threads, args.nameserver, args.nameserverfile) check_mgmt_accounts(names, args.threads, args.nameserver, args.nameserverfile) check_vault_accounts(names, args.threads, args.nameserver, args.nameserverfile) check_azure_websites(names, args.nameserver, args.threads, args.nameserverfile) check_azure_databases(names, args.nameserver, args.threads, args.nameserverfile) check_azure_vms(names, args.nameserver, args.threads, args.nameserverfile) cloud_enum-0.8/enum_tools/azure_regions.py000066400000000000000000000021031520352741400211240ustar00rootroot00000000000000""" File used to track the DNS regions for Azure resources. """ # Some enumeration tasks will need to go through the complete list of # possible DNS names for each region. You may want to modify this file to # use the regions meaningful to you. # # Whatever is listed in the last instance of 'REGIONS' below is what the tool # will use. # Here is the list I get when running `az account list-locations` in Azure # Powershell: REGIONS = ['eastasia', 'southeastasia', 'centralus', 'eastus', 'eastus2', 'westus', 'northcentralus', 'southcentralus', 'northeurope', 'westeurope', 'japanwest', 'japaneast', 'brazilsouth', 'australiaeast', 'australiasoutheast', 'southindia', 'centralindia', 'westindia', 'canadacentral', 'canadaeast', 'uksouth', 'ukwest', 'westcentralus', 'westus2', 'koreacentral', 'koreasouth', 'francecentral', 'francesouth', 'australiacentral', 'australiacentral2', 'southafricanorth', 'southafricawest'] # And here I am limiting the search by overwriting this variable: REGIONS = ['eastus', ] cloud_enum-0.8/enum_tools/fuzz.txt000066400000000000000000000042431520352741400174440ustar00rootroot000000000000000 001 002 003 01 02 03 1 2 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 3 4 5 6 7 8 9 access-logs access.logs accounting admin administrator ae alpha amazon analytics android api app appengine appspot appspot.com archive artifacts assets attachments audit audit-logs aws aws-billing aws-logs aws.billing aws.logs azure azure-logs backup backups bak bamboo beta betas bigquery bigtable billing blob blog bucket build builds cache cdn ce central centralus cf chef client cloud cloudfunction club cluster com com.au common composer compute computeengine conf confidential config configuration consultants contact container content core corp corporate customer data data-private data-public data.private data.public database dataflow dataproc datastore db debug demo dev developer developers development devops directory discount dist dl dns docker docs download downloads dr ec2 elastic emails endpoints es events exe export files fileshare filestorage filestore finance firebase firestore functions gateway gcp gcp-logs gcplogs git github gitlab gke graphite graphql gs gw help hidden hr hub iaas iam images img infra internal internal-dist internal-repo internal-tools internal.dist internal.repo ios iot it jenkins jira js k8s key keys kube kubeengine kubernetes kubernetesengine landing ldap loadbalancer logs logstash mail main manuals mattermost media memorystore mercurial ml mobile monitoring my mysql net northcentralus ops oracle org paas packages panel passwords photos pics pictures postgres pre-prod preprod presentations preview private pro processed prod product productcontent production products project projects psql public pubsub qa repo reports resources root rtdb s3 saas screenshots scripts sec secret secrets secure security service services share shared shop site sitemaps slack snapshots source source-code spanner splunk sql sql-logs src ssh stackdriver stage staging static stats storage storageaccount store subversion support svc svn syslog tasks teamcity temp templates terraform test themes tmp tmp-logs tmp.logs trace traffic training travis troposphere uploads useast useast2 userfiles userpictures users ux videos vm web website westcentralus westus westus2 wp www cloud_enum-0.8/enum_tools/gcp_checks.py000066400000000000000000000314571520352741400203570ustar00rootroot00000000000000""" Google-specific checks. Part of the cloud_enum package available at github.com/initstring/cloud_enum """ from enum_tools import utils from enum_tools import gcp_regions BANNER = ''' ++++++++++++++++++++++++++ google checks ++++++++++++++++++++++++++ ''' # Known GCP domain names GCP_URL = 'storage.googleapis.com' FBRTDB_URL = 'firebaseio.com' APPSPOT_URL = 'appspot.com' FUNC_URL = 'cloudfunctions.net' FBAPP_URL = 'firebaseapp.com' # Hacky, I know. Used to store project/region combos that report at least # one cloud function, to brute force later on HAS_FUNCS = [] def print_bucket_response(reply): """ Parses the HTTP reply of a brute-force attempt This function is passed into the class object so we can view results in real-time. """ data = {'platform': 'gcp', 'msg': '', 'target': '', 'access': ''} if reply.status_code == 404: pass elif reply.status_code == 200: data['msg'] = 'OPEN GOOGLE BUCKET' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) utils.list_bucket_contents(reply.url + '/') elif reply.status_code == 403: data['msg'] = 'Protected Google Bucket' data['target'] = reply.url data['access'] = 'protected' utils.fmt_output(data) else: print(f" Unknown status codes being received from {reply.url}:\n" " {reply.status_code}: {reply.reason}") def check_gcp_buckets(names, threads): """ Checks for open and restricted Google Cloud buckets """ print("[+] Checking for Google buckets") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of correctly formatted urls candidates = [] # Take each mutated keyword craft a url with the correct format for name in names: candidates.append(f'{GCP_URL}/{name}') # Send the valid names to the batch HTTP processor utils.get_url_batch(candidates, use_ssl=False, callback=print_bucket_response, threads=threads) # Stop the time utils.stop_timer(start_time) def print_fbrtdb_response(reply): """ Parses the HTTP reply of a brute-force attempt This function is passed into the class object so we can view results in real-time. """ data = {'platform': 'gcp', 'msg': '', 'target': '', 'access': ''} if reply.status_code == 404: pass elif reply.status_code == 200: data['msg'] = 'OPEN GOOGLE FIREBASE RTDB' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) elif reply.status_code == 401: data['msg'] = 'Protected Google Firebase RTDB' data['target'] = reply.url data['access'] = 'protected' utils.fmt_output(data) elif reply.status_code == 402: data['msg'] = 'Payment required on Google Firebase RTDB' data['target'] = reply.url data['access'] = 'disabled' utils.fmt_output(data) elif reply.status_code == 423: data['msg'] = 'The Firebase database has been deactivated.' data['target'] = reply.url data['access'] = 'disabled' utils.fmt_output(data) else: print(f" Unknown status codes being received from {reply.url}:\n" " {reply.status_code}: {reply.reason}") def check_fbrtdb(names, threads): """ Checks for Google Firebase RTDB """ print("[+] Checking for Google Firebase Realtime Databases") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of correctly formatted urls candidates = [] # Take each mutated keyword craft a url with the correct format for name in names: # Firebase RTDB names cannot include a period. We'll exlcude # those from the global candidates list if '.' not in name: candidates.append(f'{name}.{FBRTDB_URL}/.json') # Send the valid names to the batch HTTP processor utils.get_url_batch(candidates, use_ssl=True, callback=print_fbrtdb_response, threads=threads, redir=False) # Stop the time utils.stop_timer(start_time) def print_fbapp_response(reply): """ Parses the HTTP reply of a brute-force attempt This function is passed into the class object so we can view results in real-time. """ data = {'platform': 'gcp', 'msg': '', 'target': '', 'access': ''} if reply.status_code == 404: pass elif reply.status_code == 200: data['msg'] = 'OPEN GOOGLE FIREBASE APP' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) else: print(f" Unknown status codes being received from {reply.url}:\n" " {reply.status_code}: {reply.reason}") def check_fbapp(names, threads): """ Checks for Google Firebase Applications """ print("[+] Checking for Google Firebase Applications") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of correctly formatted urls candidates = [] # Take each mutated keyword craft a url with the correct format for name in names: # Firebase App names cannot include a period. We'll exlcude # those from the global candidates list if '.' not in name: candidates.append(f'{name}.{FBAPP_URL}') # Send the valid names to the batch HTTP processor utils.get_url_batch(candidates, use_ssl=True, callback=print_fbapp_response, threads=threads, redir=False) # Stop the time utils.stop_timer(start_time) def print_appspot_response(reply): """ Parses the HTTP reply of a brute-force attempt This function is passed into the class object so we can view results in real-time. """ data = {'platform': 'gcp', 'msg': '', 'target': '', 'access': ''} if reply.status_code == 404: pass elif str(reply.status_code)[0] == 5: data['msg'] = 'Google App Engine app with a 50x error' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) elif reply.status_code in (200, 302, 404): if 'accounts.google.com' in reply.url: data['msg'] = 'Protected Google App Engine app' data['target'] = reply.history[0].url data['access'] = 'protected' utils.fmt_output(data) else: data['msg'] = 'Open Google App Engine app' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) else: print(f" Unknown status codes being received from {reply.url}:\n" " {reply.status_code}: {reply.reason}") def check_appspot(names, threads): """ Checks for Google App Engine sites running on appspot.com """ print("[+] Checking for Google App Engine apps") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of correctly formatted urls candidates = [] # Take each mutated keyword craft a url with the correct format for name in names: # App Engine project names cannot include a period. We'll exlcude # those from the global candidates list if '.' not in name: candidates.append(f'{name}.{APPSPOT_URL}') # Send the valid names to the batch HTTP processor utils.get_url_batch(candidates, use_ssl=False, callback=print_appspot_response, threads=threads) # Stop the time utils.stop_timer(start_time) def print_functions_response1(reply): """ Parses the HTTP reply the initial Cloud Functions check This function is passed into the class object so we can view results in real-time. """ data = {'platform': 'gcp', 'msg': '', 'target': '', 'access': ''} if reply.status_code == 404: pass elif reply.status_code == 302: data['msg'] = 'Contains at least 1 Cloud Function' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) HAS_FUNCS.append(reply.url) else: print(f" Unknown status codes being received from {reply.url}:\n" " {reply.status_code}: {reply.reason}") def print_functions_response2(reply): """ Parses the HTTP reply from the secondary, brute-force Cloud Functions check This function is passed into the class object so we can view results in real-time. """ data = {'platform': 'gcp', 'msg': '', 'target': '', 'access': ''} if 'accounts.google.com/ServiceLogin' in reply.url: pass elif reply.status_code in (403, 401): data['msg'] = 'Auth required Cloud Function' data['target'] = reply.url data['access'] = 'protected' utils.fmt_output(data) elif reply.status_code == 405: data['msg'] = 'UNAUTHENTICATED Cloud Function (POST-Only)' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) elif reply.status_code in (200, 404): data['msg'] = 'UNAUTHENTICATED Cloud Function (GET-OK)' data['target'] = reply.url data['access'] = 'public' utils.fmt_output(data) else: print(f" Unknown status codes being received from {reply.url}:\n" " {reply.status_code}: {reply.reason}") def check_functions(names, brute_list, quickscan, threads): """ Checks for Google Cloud Functions running on cloudfunctions.net This is a two-part process. First, we want to find region/project combos that have existing Cloud Functions. The URL for a function looks like this: https://[ZONE]-[PROJECT-ID].cloudfunctions.net/[FUNCTION-NAME] We look for a 302 in [ZONE]-[PROJECT-ID].cloudfunctions.net. That means there are some functions defined in that region. Then, we brute force a list of possible function names there. See gcp_regions.py to define which regions to check. The tool currently defaults to only 1 region, so you should really modify it for best results. """ print("[+] Checking for project/zones with Google Cloud Functions.") # Start a counter to report on elapsed time start_time = utils.start_timer() # Initialize the list of correctly formatted urls candidates = [] # Pull the regions from a config file regions = gcp_regions.REGIONS print(f"[*] Testing across {len(regions)} regions defined in the config file") # Take each mutated keyword craft a url with the correct format for region in regions: candidates += [region + '-' + name + '.' + FUNC_URL for name in names] # Send the valid names to the batch HTTP processor utils.get_url_batch(candidates, use_ssl=False, callback=print_functions_response1, threads=threads, redir=False) # Retun from function if we have not found any valid combos if not HAS_FUNCS: utils.stop_timer(start_time) return # Also bail out if doing a quick scan if quickscan: return # If we did find something, we'll use the brute list. This will allow people # to provide a separate fuzzing list if they choose. print(f"[*] Brute-forcing function names in {len(HAS_FUNCS)} project/region combos") # Load brute list in memory, based on allowed chars/etc brute_strings = utils.get_brute(brute_list) # The global was built in a previous function. We only want to brute force # project/region combos that we know have existing functions defined for func in HAS_FUNCS: print(f"[*] Brute-forcing {len(brute_strings)} function names in {func}") # Initialize the list of initial URLs to check. Strip out the HTTP # protocol first, as that is handled in the utility func = func.replace("http://", "") # Noticed weird behaviour with functions when a slash is not appended. # Works for some, but not others. However, appending a slash seems to # get consistent results. Might need further validation. candidates = [func + brute + '/' for brute in brute_strings] # Send the valid names to the batch HTTP processor utils.get_url_batch(candidates, use_ssl=False, callback=print_functions_response2, threads=threads) # Stop the time utils.stop_timer(start_time) def run_all(names, args): """ Function is called by main program """ print(BANNER) check_gcp_buckets(names, args.threads) check_fbrtdb(names, args.threads) check_appspot(names, args.threads) check_functions(names, args.brute, args.quickscan, args.threads) cloud_enum-0.8/enum_tools/gcp_regions.py000066400000000000000000000015671520352741400205640ustar00rootroot00000000000000""" File used to track the DNS regions for GCP resources. """ # Some enumeration tasks will need to go through the complete list of # possible DNS names for each region. You may want to modify this file to # use the regions meaningful to you. # # Whatever is listed in the last instance of 'REGIONS' below is what the tool # will use. # Here is the list I get when running `gcloud functions regions list` REGIONS = ['us-central1', 'us-east1', 'us-east4', 'us-west2', 'us-west3', 'us-west4', 'europe-west1', 'europe-west2', 'europe-west3', 'europe-west6', 'asia-east2', 'asia-northeast1', 'asia-northeast2', 'asia-northeast3', 'asia-south1', 'asia-southeast2', 'northamerica-northeast1', 'southamerica-east1', 'australia-southeast1'] # And here I am limiting the search by overwriting this variable: REGIONS = ['us-central1', ] cloud_enum-0.8/enum_tools/utils.py000066400000000000000000000254001520352741400174150ustar00rootroot00000000000000""" Helper functions for network requests, etc """ import time import sys import datetime import re import csv import json import ipaddress from multiprocessing.dummy import Pool as ThreadPool from functools import partial from urllib.parse import urlparse try: import requests import dns import dns.resolver from concurrent.futures import ThreadPoolExecutor from requests_futures.sessions import FuturesSession from concurrent.futures._base import TimeoutError except ImportError: print("[!] Please pip install requirements.txt.") sys.exit() LOGFILE = False LOGFILE_FMT = '' def init_logfile(logfile, fmt): """ Initialize the global logfile if specified as a user-supplied argument """ if logfile: global LOGFILE LOGFILE = logfile global LOGFILE_FMT LOGFILE_FMT = fmt now = datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S") with open(logfile, 'a', encoding='utf-8') as log_writer: log_writer.write(f"\n\n#### CLOUD_ENUM {now} ####\n") def is_valid_domain(domain): """ Checks if the domain has a valid format and length """ # Check for domain total length if len(domain) > 253: # According to DNS specifications return False # Check each label in the domain for label in domain.split('.'): # Each label should be between 1 and 63 characters long if not (1 <= len(label) <= 63): return False return True def get_url_batch(url_list, use_ssl=False, callback='', threads=5, redir=True): """ Processes a list of URLs, sending the results back to the calling function in real-time via the `callback` parameter """ # Start a counter for a status message tick = {} tick['total'] = len(url_list) tick['current'] = 0 # Filter out invalid URLs url_list = [url for url in url_list if is_valid_domain(url)] # Break the url list into smaller lists based on thread size queue = [url_list[x:x+threads] for x in range(0, len(url_list), threads)] # Define the protocol if use_ssl: proto = 'https://' else: proto = 'http://' # Using the async requests-futures module, work in batches based on # the 'queue' list created above. Call each URL, sending the results # back to the callback function. for batch in queue: # I used to initialize the session object outside of this loop, BUT # there were a lot of errors that looked related to pool cleanup not # happening. Putting it in here fixes the issue. # There is an unresolved discussion here: # https://github.com/ross/requests-futures/issues/20 session = FuturesSession(executor=ThreadPoolExecutor(max_workers=threads+5)) batch_pending = {} batch_results = {} # First, grab the pending async request and store it in a dict for url in batch: batch_pending[url] = session.get(proto + url, allow_redirects=redir) # Then, grab all the results from the queue. # This is where we need to catch exceptions that occur with large # fuzz lists and dodgy connections. for url in batch_pending: try: # Timeout is set due to observation of some large jobs simply # hanging forever with no exception raised. batch_results[url] = batch_pending[url].result(timeout=30) except requests.exceptions.ConnectionError as error_msg: print(f" [!] Connection error on {url}:") print(error_msg) except TimeoutError: print(f" [!] Timeout on {url}. Investigate if there are" " many of these") # Now, send all the results to the callback function for analysis # We need a way to stop processing unnecessary brute-forces, so the # callback may tell us to bail out. for url in batch_results: check = callback(batch_results[url]) if check == 'breakout': return # Refresh a status message tick['current'] += threads sys.stdout.flush() sys.stdout.write(f" {tick['current']}/{tick['total']} complete...") sys.stdout.write('\r') # Clear the status message sys.stdout.write(' \r') def read_nameservers(file_path): """ Reads nameservers from a given file. Each line in the file should contain one nameserver IP address. Lines starting with '#' will be ignored as comments. """ try: with open(file_path, 'r') as file: nameservers = [line.strip() for line in file if line.strip() and not line.startswith('#')] if not nameservers: raise ValueError("Nameserver file is empty or only contains comments") return nameservers except FileNotFoundError: print(f"Error: File '{file_path}' not found.") exit(1) except ValueError as e: print(e) exit(1) def is_valid_ip(address): try: ipaddress.ip_address(address) return True except ValueError: return False def dns_lookup(nameserver, name): """ This function performs the actual DNS lookup when called in a threadpool by the fast_dns_lookup function. """ nameserverfile = False if not is_valid_ip(nameserver): nameserverfile = nameserver res = dns.resolver.Resolver() res.timeout = 3 if nameserverfile: nameservers = read_nameservers(nameserverfile) res.nameservers = nameservers else: res.nameservers = [nameserver] tries = 0 while tries < 3: try: res.query(name) # If no exception is thrown, return the valid name return name except dns.resolver.NXDOMAIN: return '' except dns.resolver.NoNameservers as exc_text: print(" [!] Error querying nameservers! This could be a problem.") print(" [!] If you're using a VPN, try setting --ns to your VPN's nameserver.") print(" [!] Bailing because you need to fix this") print(" [!] More Info:") print(exc_text) return '-#BREAKOUT_DNS_ERROR#-' except dns.exception.Timeout: tries += 1 print(f" [!] DNS lookup for {name} timed out after 3 tries. Investigate if there are many of these.") return '' def fast_dns_lookup(names, nameserver, nameserverfile, callback='', threads=5): """ Helper function to resolve DNS names. Uses multithreading. """ total = len(names) current = 0 valid_names = [] print(f"[*] Brute-forcing a list of {total} possible DNS names") # Filter out invalid domains names = [name for name in names if is_valid_domain(name)] # Break the url list into smaller lists based on thread size queue = [names[x:x+threads] for x in range(0, len(names), threads)] for batch in queue: pool = ThreadPool(threads) # Because pool.map takes only a single function arg, we need to # define this partial so that each iteration uses the same ns if nameserverfile: dns_lookup_params = partial(dns_lookup, nameserverfile) else: dns_lookup_params = partial(dns_lookup, nameserver) results = pool.map(dns_lookup_params, batch) # We should now have the batch of results back, process them. for name in results: if name: if name == '-#BREAKOUT_DNS_ERROR#-': sys.exit() if callback: callback(name) valid_names.append(name) current += threads # Update the status message sys.stdout.flush() sys.stdout.write(f" {current}/{total} complete...") sys.stdout.write('\r') pool.close() # Clear the status message sys.stdout.write(' \r') return valid_names def list_bucket_contents(bucket): """ Provides a list of full URLs to each open bucket """ key_regex = re.compile(r'<(?:Key|Name)>(.*?)') reply = requests.get(bucket) # Make a list of all the relative-path key name keys = re.findall(key_regex, reply.text) # Need to remove URL parameters before appending file names # from Azure buckets sub_regex = re.compile(r'(\?.*)') bucket = sub_regex.sub('', bucket) # Format them to full URLs and print to console if keys: print(" FILES:") for key in keys: url = bucket + key print(f" ->{url}") else: print(" ...empty bucket, so sad. :(") def fmt_output(data): """ Handles the output - printing and logging based on a specified format """ # ANSI escape sequences are set based on accessibility of target # (basically, how public it is)) bold = '\033[1m' end = '\033[0m' if data['access'] == 'public': ansi = bold + '\033[92m' # green if data['access'] == 'protected': ansi = bold + '\033[33m' # orange if data['access'] == 'disabled': ansi = bold + '\033[31m' # red sys.stdout.write(' ' + ansi + data['msg'] + ': ' + data['target'] + end + '\n') if LOGFILE: with open(LOGFILE, 'a', encoding='utf-8') as log_writer: if LOGFILE_FMT == 'text': log_writer.write(f'{data["msg"]}: {data["target"]}\n') if LOGFILE_FMT == 'csv': writer = csv.DictWriter(log_writer, data.keys()) writer.writerow(data) if LOGFILE_FMT == 'json': log_writer.write(json.dumps(data) + '\n') def get_brute(brute_file, mini=1, maxi=63, banned='[^a-z0-9_-]'): """ Generates a list of brute-force words based on length and allowed chars """ # Read the brute force file into memory with open(brute_file, encoding="utf8", errors="ignore") as infile: names = infile.read().splitlines() # Clean up the names to usable for containers banned_chars = re.compile(banned) clean_names = [] for name in names: name = name.lower() name = banned_chars.sub('', name) if maxi >= len(name) >= mini: if name not in clean_names: clean_names.append(name) return clean_names def start_timer(): """ Starts a timer for functions in main module """ # Start a counter to report on elapsed time start_time = time.time() return start_time def stop_timer(start_time): """ Stops timer and prints a status """ # Stop the timer elapsed_time = time.time() - start_time formatted_time = time.strftime("%H:%M:%S", time.gmtime(elapsed_time)) # Print some statistics print("") print(f" Elapsed time: {formatted_time}") print("") cloud_enum-0.8/manpage/000077500000000000000000000000001520352741400151265ustar00rootroot00000000000000cloud_enum-0.8/manpage/cloud_enum.1000066400000000000000000000046531520352741400173520ustar00rootroot00000000000000.\" Text automatically generated by txt2man .TH cloud_enum 1 "01 Apr 2022" "cloud_enum-0.7" "Multi-cloud open source intelligence tool" .SH NAME \fBcloud_enum \fP- enumerates public resources matching user requested keyword \fB .SH SYNOPSIS .nf .fam C cloud_enum [OPTIONS] [ARGS] \.\.\. .fam T .fi .fam T .fi .SH DESCRIPTION Multi-cloud OSINT tool. Enumerate public resources in AWS, Azure, and Google Cloud. Currently enumerates the following: .PP .nf .fam C Amazon Web Services: Open / Protected S3 Buckets awsapps (WorkMail, WorkDocs, Connect, etc.) Microsoft Azure: Storage Accounts Open Blob Storage Containers Hosted Databases Virtual Machines Web Apps Google Cloud Platform Open / Protected GCP Buckets Open / Protected Firebase Realtime Databases Google App Engine sites Cloud Functions (enumerates project/regions with existing functions, then brute forces actual function names) .fam T .fi .SH OPTIONS .TP .B \fB-h\fP, \fB--help\fP Show this help message and exit. .TP .B \fB-k\fP KEYWORD, \fB--keyword\fP KEYWORD Keyword. Can use argument multiple times. .TP .B \fB-kf\fP KEYFILE, \fB--keyfile\fP KEYFILE Input file with a single keyword per line. .TP .B \fB-m\fP MUTATIONS, \fB--mutations\fP MUTATIONS Mutations. Default: /usr/lib/cloud-enum/enum_tools/fuzz.txt. .TP .B \fB-b\fP BRUTE, \fB--brute\fP BRUTE List to brute-force Azure container names. Default: /usr/lib/cloud-enum/enum_tools/fuzz.txt. .TP .B \fB-t\fP THREADS, \fB--threads\fP THREADS Threads for HTTP brute-force. Default = 5. .TP .B \fB-ns\fP NAMESERVER, \fB--nameserver\fP NAMESERVER DNS server to use in brute-force. .TP .B \fB-l\fP LOGFILE, \fB--logfile\fP LOGFILE Will APPEND found items to specified file. .TP .B \fB-f\fP FORMAT, \fB--format\fP Format Format for log file (text,json,csv - defaults to text) .TP .B \fB--disable-aws\fP Disable Amazon checks. .TP .B \fB--disable-azure\fP Disable Azure checks. .TP .B \fB--disable-gcp\fP Disable Google checks. .TP .B \fB-qs\fP, \fB--quickscan\fP Disable all mutations and second-level scan. .SH EXAMPLES cloud_enum \fB-k\fP keyword .PP cloud_enum \fB-k\fP keyword \fB-t\fP 10 .PP cloud_enum \fB-k\fP somecompany \fB-k\fP somecompany.io \fB-k\fP blockchaindoohickey .SH AUTHOR Written by initstring .PP This manual page was written by Guilherme de Paula Xavier Segundo for the Debian project (but may be used by others). cloud_enum-0.8/manpage/cloud_enum.txt000066400000000000000000000043051520352741400200230ustar00rootroot00000000000000NAME cloud_enum - enumerates public resources matching user requested keyword SYNOPSIS cloud_enum [OPTIONS] [ARGS] ... DESCRIPTION Multi-cloud OSINT tool. Enumerate public resources in AWS, Azure, and Google Cloud. Currently enumerates the following: Amazon Web Services: Open / Protected S3 Buckets awsapps (WorkMail, WorkDocs, Connect, etc.) Microsoft Azure: Storage Accounts Open Blob Storage Containers Hosted Databases Virtual Machines Web Apps Google Cloud Platform Open / Protected GCP Buckets Open / Protected Firebase Realtime Databases Google App Engine sites Cloud Functions (enumerates project/regions with existing functions, then brute forces actual function names) OPTIONS -h, --help Show this help message and exit. -k KEYWORD, --keyword KEYWORD Keyword. Can use argument multiple times. -kf KEYFILE, --keyfile KEYFILE Input file with a single keyword per line. -m MUTATIONS, --mutations MUTATIONS Mutations. Default: /usr/lib/cloud-enum/enum_tools/fuzz.txt. -b BRUTE, --brute BRUTE List to brute-force Azure container names. Default: /usr/lib/cloud-enum/enum_tools/fuzz.txt. -t THREADS, --threads THREADS Threads for HTTP brute-force. Default = 5. -ns NAMESERVER, --nameserver NAMESERVER DNS server to use in brute-force. -l LOGFILE, --logfile LOGFILE Will APPEND found items to specified file. -f FORMAT, --format Format Format for log file (text,json,csv - defaults to text) --disable-aws Disable Amazon checks. --disable-azure Disable Azure checks. --disable-gcp Disable Google checks. -qs, --quickscan Disable all mutations and second-level scan. EXAMPLES cloud_enum -k keyword cloud_enum -k keyword -t 10 cloud_enum -k somecompany -k somecompany.io -k blockchaindoohickey AUTHOR Written by initstring This manual page was written by Guilherme de Paula Xavier Segundo for the Debian project (but may be used by others). cloud_enum-0.8/pyproject.toml000066400000000000000000000010151520352741400164270ustar00rootroot00000000000000[project] name = "cloud_enum" version = "0.8" description = "Multi-cloud OSINT tool. Enumerate public resources in AWS, Azure, and Google Cloud." requires-python = ">=3.10" dependencies = [ "dnspython>=2.8.0", "requests>=2.34.2", "requests-futures>=1.0.2", ] [project.scripts] cloud_enum = "cloud_enum:main" [dependency-groups] dev = [ "pytest", ] [tool.setuptools] py-modules = ["cloud_enum"] packages = ["enum_tools"] [build-system] requires = ["setuptools>=61"] build-backend = "setuptools.build_meta" cloud_enum-0.8/tests/000077500000000000000000000000001520352741400146605ustar00rootroot00000000000000cloud_enum-0.8/tests/__init__.py000066400000000000000000000000001520352741400167570ustar00rootroot00000000000000cloud_enum-0.8/tests/test_utils.py000066400000000000000000000001471520352741400174330ustar00rootroot00000000000000# This test obviously does nothing, it is just setting up the framework def test1(): assert 1 == 1