├── .github ├── FUNDING.yml └── banner.png ├── .gitignore ├── FindAzureDomainTenant.py ├── README.md └── requirements.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: p0dalirius 4 | patreon: Podalirius -------------------------------------------------------------------------------- /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/FindAzureDomainTenant/f951b9d37312d2fc97264478794548078e12f376/.github/banner.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /FindAzureDomainTenant.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : FindAzureDomainTenant.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 24 Apr 2023 6 | 7 | import argparse 8 | import datetime 9 | import json 10 | import os 11 | import traceback 12 | import requests 13 | import sqlite3 14 | import sys 15 | import threading 16 | import time 17 | import xlsxwriter 18 | from concurrent.futures import ThreadPoolExecutor 19 | 20 | 21 | VERSION = "1.1" 22 | 23 | 24 | def export_xlsx(data, path_to_file): 25 | print("[>] Writing '%s' ... " % path_to_file, end="") 26 | sys.stdout.flush() 27 | basepath = os.path.dirname(path_to_file) 28 | filename = os.path.basename(path_to_file) 29 | if basepath not in [".", ""]: 30 | if not os.path.exists(basepath): 31 | os.makedirs(basepath) 32 | path_to_file = basepath + os.path.sep + filename 33 | else: 34 | path_to_file = filename 35 | 36 | workbook = xlsxwriter.Workbook(path_to_file) 37 | worksheet = workbook.add_worksheet() 38 | 39 | header_format = workbook.add_format({'bold': 1}) 40 | header_fields = ["Tenant ID", "Domain", "Region"] 41 | for k in range(len(header_fields)): 42 | worksheet.set_column(k, k + 1, len(header_fields[k]) + 3) 43 | worksheet.set_row(0, 20, header_format) 44 | worksheet.write_row(0, 0, header_fields) 45 | 46 | row_id = 1 47 | for tenant_id in data.keys(): 48 | for domain in data[tenant_id]: 49 | worksheet.write_row(row_id, 0, [ 50 | tenant_id, 51 | domain["domain"], 52 | domain["tenant_region_scope"] 53 | ]) 54 | row_id += 1 55 | worksheet.autofilter(0, 0, row_id, len(header_fields) - 1) 56 | workbook.close() 57 | print("done.") 58 | 59 | 60 | def export_json(data, path_to_file): 61 | print("[>] Writing '%s' ... " % path_to_file, end="") 62 | sys.stdout.flush() 63 | basepath = os.path.dirname(path_to_file) 64 | filename = os.path.basename(path_to_file) 65 | if basepath not in [".", ""]: 66 | if not os.path.exists(basepath): 67 | os.makedirs(basepath) 68 | path_to_file = basepath + os.path.sep + filename 69 | else: 70 | path_to_file = filename 71 | f = open(path_to_file, 'w') 72 | f.write(json.dumps(data, indent=4)) 73 | f.close() 74 | print("done.") 75 | 76 | 77 | def export_sqlite(data, path_to_file): 78 | print("[>] Writing '%s' ... " % path_to_file, end="") 79 | sys.stdout.flush() 80 | basepath = os.path.dirname(path_to_file) 81 | filename = os.path.basename(path_to_file) 82 | if basepath not in [".", ""]: 83 | if not os.path.exists(basepath): 84 | os.makedirs(basepath) 85 | path_to_file = basepath + os.path.sep + filename 86 | else: 87 | path_to_file = filename 88 | 89 | conn = sqlite3.connect(path_to_file) 90 | cursor = conn.cursor() 91 | cursor.execute("CREATE TABLE IF NOT EXISTS results(tenant_id VARCHAR(255), domain VARCHAR(255), region VARCHAR(255));") 92 | for tenant_id in data.keys(): 93 | for domain in data[tenant_id]: 94 | cursor.execute("INSERT INTO results VALUES (?, ?, ?)", ( 95 | tenant_id, 96 | domain["domain"], 97 | domain["tenant_region_scope"] 98 | ) 99 | ) 100 | conn.commit() 101 | conn.close() 102 | print("done.") 103 | 104 | 105 | def monitor_thread(options, monitor_data, only_check_finished=False): 106 | time.sleep(1) 107 | last_check, monitoring = 0, True 108 | while monitoring: 109 | new_check = monitor_data["actions_performed"] 110 | rate = (new_check - last_check) 111 | monitor_data["lock"].acquire() 112 | if monitor_data["total"] == 0: 113 | print("\r[%s] Status (%d/%d) %5.2f %% | Rate %d tests/s " % ( 114 | datetime.datetime.now().strftime("%Y/%m/%d %Hh%Mm%Ss"), 115 | new_check, monitor_data["total"], 0, 116 | rate 117 | ), 118 | end="" 119 | ) 120 | else: 121 | print("\r[%s] Status (%d/%d) %5.2f %% | Rate %d tests/s " % ( 122 | datetime.datetime.now().strftime("%Y/%m/%d %Hh%Mm%Ss"), 123 | new_check, monitor_data["total"], (new_check / monitor_data["total"]) * 100, 124 | rate 125 | ), 126 | end="" 127 | ) 128 | last_check = new_check 129 | monitor_data["lock"].release() 130 | time.sleep(1) 131 | if only_check_finished: 132 | if monitor_data["finished"]: 133 | monitoring = False 134 | else: 135 | if rate == 0 and monitor_data["actions_performed"] == monitor_data["total"] or monitor_data["finished"]: 136 | monitoring = False 137 | print() 138 | 139 | 140 | def check_if_tenant_exists(domain, options, request_proxies, monitor_data): 141 | try: 142 | r = requests.get( 143 | f"https://login.microsoftonline.com/{domain}/v2.0/.well-known/openid-configuration", 144 | timeout=options.request_timeout, 145 | proxies=request_proxies 146 | ) 147 | data = r.json() 148 | if "error" in data.keys(): 149 | if data["error"] == "invalid_tenant": 150 | # https://learn.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes#aadsts-error-codes 151 | err_code = data["error_description"].split(':')[0].strip() 152 | if options.debug and False: 153 | print("[!] %s => %s" % (domain, data["error"])) 154 | elif "token_endpoint" in data.keys(): 155 | tenant_id = data["token_endpoint"].split("/")[3] 156 | if tenant_id not in monitor_data["tenants"].keys(): 157 | monitor_data["tenants"][tenant_id] = [] 158 | monitor_data["tenants"][tenant_id].append({ 159 | "id": tenant_id, 160 | "domain": domain, 161 | "tenant_region_scope": data["tenant_region_scope"], 162 | "data": data 163 | }) 164 | if options.no_colors: 165 | print("\r[+] tenant-id:%s domain:%s region:%s" % (tenant_id, domain, data["tenant_region_scope"])) 166 | else: 167 | print("\r[+] tenant-id:\x1b[1;92m%s\x1b[0m domain:\x1b[1;96m%s\x1b[0m region:\x1b[1;95m%s\x1b[0m" % (tenant_id, domain, data["tenant_region_scope"])) 168 | 169 | except Exception as e: 170 | traceback.print_exc() 171 | 172 | monitor_data["actions_performed"] += 1 173 | 174 | return None 175 | 176 | 177 | def parseArgs(): 178 | print("FindAzureDomainTenant.py v%s - by Remi GASCOU (Podalirius)\n" % VERSION) 179 | 180 | parser = argparse.ArgumentParser(description="") 181 | parser.add_argument("-v", "--verbose", default=False, action="store_true", help='Verbose mode. (default: False)') 182 | parser.add_argument("--debug", default=False, action="store_true", help="Debug mode, for huge verbosity. (default: False)") 183 | parser.add_argument("-T", "--threads", default=8, type=int, help="Number of threads (default: 8)") 184 | parser.add_argument("--no-colors", default=False, action="store_true", help="Disable colored output. (default: False)") 185 | 186 | group_configuration = parser.add_argument_group("Advanced configuration") 187 | group_configuration.add_argument("-PI", "--proxy-ip", default=None, type=str, help="Proxy IP.") 188 | group_configuration.add_argument("-PP", "--proxy-port", default=None, type=int, help="Proxy port.") 189 | group_configuration.add_argument("-rt", "--request-timeout", default=5, type=int, help="Set the timeout of HTTP requests.") 190 | 191 | group_export = parser.add_argument_group("Export results") 192 | group_export.add_argument("--export-xlsx", dest="export_xlsx", type=str, default=None, required=False, help="Output XLSX file to store the results in.") 193 | group_export.add_argument("--export-json", dest="export_json", type=str, default=None, required=False, help="Output JSON file to store the results in.") 194 | group_export.add_argument("--export-sqlite", dest="export_sqlite", type=str, default=None, required=False, help="Output SQLITE3 file to store the results in.") 195 | 196 | group_targets_source = parser.add_argument_group("Tenants") 197 | group_targets_source.add_argument("-tf", "--tenants-file", default=None, type=str, help="Path to file containing a line by line list of tenants names.") 198 | group_targets_source.add_argument("-tt", "--tenant", default=[], type=str, action='append', help="Tenant name.") 199 | group_targets_source.add_argument("--stdin", default=False, action="store_true", help="Read targets from stdin. (default: False)") 200 | 201 | options = parser.parse_args() 202 | 203 | if (options.tenants_file is None) and (options.stdin == False) and (len(options.tenant) == 0): 204 | parser.print_help() 205 | print("\n[!] No tenants specified.") 206 | sys.exit(0) 207 | 208 | return options 209 | 210 | 211 | if __name__ == '__main__': 212 | options = parseArgs() 213 | 214 | request_proxies = {} 215 | if options.proxy_ip is not None and options.proxy_port is not None: 216 | request_proxies = { 217 | "http": "http://%s:%d/" % (options.proxy_ip, options.proxy_port), 218 | "https": "https://%s:%d/" % (options.proxy_ip, options.proxy_port) 219 | } 220 | 221 | tenants = [] 222 | 223 | # Loading targets line by line from a tenants file 224 | if options.tenants_file is not None: 225 | if os.path.exists(options.tenants_file): 226 | if options.debug: 227 | print("[debug] Loading tenants line by line from targets file '%s'" % options.tenants_file) 228 | f = open(options.tenants_file, "r") 229 | for line in f.readlines(): 230 | tenants.append(line.strip()) 231 | f.close() 232 | else: 233 | print("[!] Could not open tenants file '%s'" % options.tenants_file) 234 | 235 | # Loading targets from a single --tenant option 236 | if len(options.tenant) != 0: 237 | if options.debug: 238 | print("[debug] Loading tenants from --target options") 239 | for tenant in options.tenant: 240 | tenants.append(tenant) 241 | 242 | if len(tenants) != 0: 243 | print("[>] Checking %d tenants if they exists" % len(tenants)) 244 | monitor_data = {"actions_performed": 0, "total": len(tenants), "tenants": {}, "lock": threading.Lock(), "finished": False} 245 | with ThreadPoolExecutor(max_workers=min(options.threads, (len(tenants)+1))) as tp: 246 | tp.submit(monitor_thread, options, monitor_data, False) 247 | for tenant in tenants: 248 | tp.submit(check_if_tenant_exists, tenant, options, request_proxies, monitor_data) 249 | 250 | if options.export_xlsx is not None: 251 | export_xlsx(monitor_data["tenants"], options.export_xlsx) 252 | 253 | if options.export_json is not None: 254 | export_json(monitor_data["tenants"], options.export_json) 255 | 256 | if options.export_sqlite is not None: 257 | export_sqlite(monitor_data["tenants"], options.export_sqlite) 258 | 259 | print("[>] All done!") 260 | 261 | elif options.stdin: 262 | print("[>] Checking tenants from stdin if they exists") 263 | monitor_data = {"actions_performed": 0, "total": 0, "tenants": {}, "lock": threading.Lock(), "finished": False} 264 | with ThreadPoolExecutor(max_workers=options.threads) as tp: 265 | tp.submit(monitor_thread, options, monitor_data, True) 266 | try: 267 | while True: 268 | tenant = input() 269 | monitor_data["total"] += 1 270 | tp.submit(check_if_tenant_exists, tenant, options, request_proxies, monitor_data) 271 | except EOFError as e: 272 | pass 273 | 274 | if options.export_xlsx is not None: 275 | export_xlsx(monitor_data["tenants"], options.export_xlsx) 276 | 277 | if options.export_json is not None: 278 | export_json(monitor_data["tenants"], options.export_json) 279 | 280 | if options.export_sqlite is not None: 281 | export_sqlite(monitor_data["tenants"], options.export_sqlite) 282 | 283 | print("[>] All done (%d tenants checked)!" % (monitor_data["actions_performed"])) 284 | 285 | else: 286 | print("[!] No tenants to find.") 287 | 288 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./.github/banner.png) 2 | 3 |

4 | A Python script to find tenant id and region from a list of domain names. 5 |
6 | GitHub release (latest by date) 7 | 8 | YouTube Channel Subscribers 9 |
10 |

11 | 12 | ## Features 13 | 14 | - [x] Find tenant id and region from a list of domain names. 15 | - [x] Export results in JSON with `--export-json `. 16 | - [x] Export results in XLSX with `--export-xlsx `. 17 | - [x] Export results in SQLITE3 with `--export-sqlite `. 18 | 19 | ## Usage 20 | 21 | ``` 22 | $ ./FindAzureDomainTenant.py -h 23 | FindAzureDomainTenant.py v1.1 - by Remi GASCOU (Podalirius) 24 | 25 | usage: FindAzureDomainTenant.py [-h] [-v] [--debug] [-T THREADS] [--no-colors] [-PI PROXY_IP] [-PP PROXY_PORT] [-rt REQUEST_TIMEOUT] [--export-xlsx EXPORT_XLSX] [--export-json EXPORT_JSON] [--export-sqlite EXPORT_SQLITE] 26 | [-tf TENANTS_FILE] [-tt TENANT] [--stdin] 27 | 28 | options: 29 | -h, --help show this help message and exit 30 | -v, --verbose Verbose mode. (default: False) 31 | --debug Debug mode, for huge verbosity. (default: False) 32 | -T THREADS, --threads THREADS 33 | Number of threads (default: 8) 34 | --no-colors Disable colored output. (default: False) 35 | 36 | Advanced configuration: 37 | -PI PROXY_IP, --proxy-ip PROXY_IP 38 | Proxy IP. 39 | -PP PROXY_PORT, --proxy-port PROXY_PORT 40 | Proxy port. 41 | -rt REQUEST_TIMEOUT, --request-timeout REQUEST_TIMEOUT 42 | Set the timeout of HTTP requests. 43 | 44 | Export results: 45 | --export-xlsx EXPORT_XLSX 46 | Output XLSX file to store the results in. 47 | --export-json EXPORT_JSON 48 | Output JSON file to store the results in. 49 | --export-sqlite EXPORT_SQLITE 50 | Output SQLITE3 file to store the results in. 51 | 52 | Tenants: 53 | -tf TENANTS_FILE, --tenants-file TENANTS_FILE 54 | Path to file containing a line by line list of tenants names. 55 | -tt TENANT, --tenant TENANT 56 | Tenant name. 57 | --stdin Read targets from stdin. (default: False) 58 | ``` 59 | 60 | ## Quick win commands 61 | 62 | + Find tenant ids from a list of domain read from a file: 63 | ``` 64 | ./FindAzureDomainTenant.py -tf domains.txt 65 | ``` 66 | 67 | + Find tenant ids from a list of domain read single options: 68 | ``` 69 | ./FindAzureDomainTenant.py -tt example.com -tt mail.example.com 70 | ``` 71 | 72 | + Find tenant ids read from stdin: 73 | ``` 74 | subfinder -silent -d example.com | ./FindAzureDomainTenant.py --stdin 75 | ``` 76 | 77 | ## Contributing 78 | 79 | Pull requests are welcome. Feel free to open an issue if you want to add other features. 80 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | xlsxwriter --------------------------------------------------------------------------------