pax_global_header 0000666 0000000 0000000 00000000064 15203527414 0014515 g ustar 00root root 0000000 0000000 52 comment=4e0fa84939226c2204be09af4924edb91fbff33e
cloud_enum-0.8/ 0000775 0000000 0000000 00000000000 15203527414 0013516 5 ustar 00root root 0000000 0000000 cloud_enum-0.8/.github/ 0000775 0000000 0000000 00000000000 15203527414 0015056 5 ustar 00root root 0000000 0000000 cloud_enum-0.8/.github/workflows/ 0000775 0000000 0000000 00000000000 15203527414 0017113 5 ustar 00root root 0000000 0000000 cloud_enum-0.8/.github/workflows/python-app.yml 0000664 0000000 0000000 00000000652 15203527414 0021740 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000002260 15203527414 0021256 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000000424 15203527414 0015506 0 ustar 00root root 0000000 0000000 # 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/LICENSE 0000664 0000000 0000000 00000002053 15203527414 0014523 0 ustar 00root root 0000000 0000000 MIT 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.md 0000664 0000000 0000000 00000011446 15203527414 0015003 0 ustar 00root root 0000000 0000000 # 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.py 0000775 0000000 0000000 00000020640 15203527414 0016227 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 15203527414 0015702 5 ustar 00root root 0000000 0000000 cloud_enum-0.8/enum_tools/__init__.py 0000664 0000000 0000000 00000000000 15203527414 0020001 0 ustar 00root root 0000000 0000000 cloud_enum-0.8/enum_tools/aws_checks.py 0000664 0000000 0000000 00000010537 15203527414 0020374 0 ustar 00root root 0000000 0000000 """
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.py 0000664 0000000 0000000 00000043743 15203527414 0020735 0 ustar 00root root 0000000 0000000 """
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.py 0000664 0000000 0000000 00000002103 15203527414 0021124 0 ustar 00root root 0000000 0000000 """
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.txt 0000664 0000000 0000000 00000004243 15203527414 0017444 0 ustar 00root root 0000000 0000000 0
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.py 0000664 0000000 0000000 00000031457 15203527414 0020357 0 ustar 00root root 0000000 0000000 """
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.py 0000664 0000000 0000000 00000001567 15203527414 0020564 0 ustar 00root root 0000000 0000000 """
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.py 0000664 0000000 0000000 00000025400 15203527414 0017415 0 ustar 00root root 0000000 0000000 """
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)>(.*?)(?: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/ 0000775 0000000 0000000 00000000000 15203527414 0015126 5 ustar 00root root 0000000 0000000 cloud_enum-0.8/manpage/cloud_enum.1 0000664 0000000 0000000 00000004653 15203527414 0017352 0 ustar 00root root 0000000 0000000 .\" 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.txt 0000664 0000000 0000000 00000004305 15203527414 0020023 0 ustar 00root root 0000000 0000000 NAME
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.toml 0000664 0000000 0000000 00000001015 15203527414 0016427 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 15203527414 0014660 5 ustar 00root root 0000000 0000000 cloud_enum-0.8/tests/__init__.py 0000664 0000000 0000000 00000000000 15203527414 0016757 0 ustar 00root root 0000000 0000000 cloud_enum-0.8/tests/test_utils.py 0000664 0000000 0000000 00000000147 15203527414 0017433 0 ustar 00root root 0000000 0000000 # This test obviously does nothing, it is just setting up the framework
def test1():
assert 1 == 1