Created
January 13, 2026 21:00
-
-
Save HackingLZ/678afcc6770d3c0b46f3bd56f1c75e79 to your computer and use it in GitHub Desktop.
Standalone Azure Access Control Service (ACS) Domain Lookup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Standalone Azure Access Control Service (ACS) Domain Lookup | |
| Queries the ACS metadata endpoint to extract domains associated with a tenant. | |
| Accepts either a domain name or tenant GUID as input. | |
| Usage: | |
| python3 acs_lookup.py contoso.com | |
| python3 acs_lookup.py ff13934a-ea67-4ad5-9552-dd16aad35221 | |
| python3 acs_lookup.py contoso.com --json | |
| """ | |
| import argparse | |
| import json | |
| import re | |
| import sys | |
| import requests | |
| def get_tenant_guid(domain: str) -> str | None: | |
| """ | |
| Get tenant GUID from OIDC metadata endpoint | |
| """ | |
| url = f"https://login.microsoftonline.com/{domain}/v2.0/.well-known/openid-configuration" | |
| try: | |
| response = requests.get(url, timeout=10) | |
| response.raise_for_status() | |
| metadata = response.json() | |
| issuer = metadata.get('issuer', '') | |
| match = re.search(r'/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/', issuer) | |
| return match.group(1) if match else None | |
| except: | |
| return None | |
| def get_acs_domains(tenant_identifier: str) -> dict: | |
| """ | |
| Query ACS metadata endpoint and extract domains from allowedAudiences | |
| Args: | |
| tenant_identifier: Domain name or tenant GUID | |
| Returns: | |
| Dict with status, domains found, and raw allowedAudiences | |
| """ | |
| guid_pattern = re.compile( | |
| r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', | |
| re.IGNORECASE | |
| ) | |
| uri = f"https://accounts.accesscontrol.windows.net/{tenant_identifier}/metadata/json/1" | |
| result = { | |
| 'query': tenant_identifier, | |
| 'url': uri, | |
| 'success': False, | |
| 'domains': [], | |
| 'onmicrosoft_domains': [], | |
| 'custom_domains': [], | |
| 'raw_audiences': [], | |
| 'error': None | |
| } | |
| try: | |
| response = requests.get(uri, timeout=10) | |
| response.raise_for_status() | |
| data = response.json() | |
| result['success'] = True | |
| result['raw_audiences'] = data.get('allowedAudiences', []) | |
| for audience in result['raw_audiences']: | |
| # Extract domain after @ symbol | |
| # Format: 00000001-0000-0000-c000-000000000000/accounts.accesscontrol.windows.net@<domain> | |
| match = re.search(r'@(.+)$', audience) | |
| if match: | |
| domain = match.group(1) | |
| # Filter out GUID entries | |
| if not guid_pattern.match(domain): | |
| result['domains'].append(domain) | |
| if domain.endswith('.onmicrosoft.com'): | |
| result['onmicrosoft_domains'].append(domain) | |
| else: | |
| result['custom_domains'].append(domain) | |
| # Dedupe and sort | |
| result['domains'] = sorted(set(result['domains'])) | |
| result['onmicrosoft_domains'] = sorted(set(result['onmicrosoft_domains'])) | |
| result['custom_domains'] = sorted(set(result['custom_domains'])) | |
| except requests.exceptions.HTTPError as e: | |
| result['error'] = f"HTTP {e.response.status_code}: {e.response.reason}" | |
| except requests.exceptions.ConnectionError: | |
| result['error'] = "Connection failed" | |
| except requests.exceptions.Timeout: | |
| result['error'] = "Request timed out" | |
| except json.JSONDecodeError: | |
| result['error'] = "Invalid JSON response" | |
| except Exception as e: | |
| result['error'] = str(e) | |
| return result | |
| def is_guid(value: str) -> bool: | |
| """Check if value is a GUID format""" | |
| pattern = re.compile( | |
| r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', | |
| re.IGNORECASE | |
| ) | |
| return bool(pattern.match(value)) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='Query Azure ACS metadata for tenant domains', | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| python3 acs_lookup.py contoso.com | |
| python3 acs_lookup.py ff13934a-ea67-4ad5-9552-dd16aad35221 | |
| python3 acs_lookup.py contoso.com --json | |
| python3 acs_lookup.py contoso.com --resolve-guid | |
| """ | |
| ) | |
| parser.add_argument('identifier', help='Domain name or tenant GUID') | |
| parser.add_argument('--json', '-j', action='store_true', help='Output as JSON') | |
| parser.add_argument('--resolve-guid', '-r', action='store_true', | |
| help='If domain provided, first resolve to GUID then query') | |
| parser.add_argument('--raw', action='store_true', help='Show raw allowedAudiences') | |
| args = parser.parse_args() | |
| identifier = args.identifier | |
| resolved_guid = None | |
| # Resolve domain to GUID if requested | |
| if args.resolve_guid and not is_guid(identifier): | |
| print(f"[*] Resolving GUID for: {identifier}") | |
| resolved_guid = get_tenant_guid(identifier) | |
| if resolved_guid: | |
| print(f"[+] Tenant GUID: {resolved_guid}") | |
| identifier = resolved_guid | |
| else: | |
| print(f"[!] Could not resolve GUID, using domain directly") | |
| # Query ACS | |
| if not args.json: | |
| print(f"[*] Querying ACS: {identifier}") | |
| result = get_acs_domains(identifier) | |
| if resolved_guid: | |
| result['resolved_guid'] = resolved_guid | |
| # Output | |
| if args.json: | |
| print(json.dumps(result, indent=2)) | |
| else: | |
| if result['success']: | |
| print(f"\n[+] Found {len(result['domains'])} domain(s)") | |
| if result['onmicrosoft_domains']: | |
| print(f"\n OnMicrosoft Domains ({len(result['onmicrosoft_domains'])}):") | |
| for domain in result['onmicrosoft_domains']: | |
| print(f" - {domain}") | |
| if result['custom_domains']: | |
| print(f"\n Custom Domains ({len(result['custom_domains'])}):") | |
| for domain in result['custom_domains']: | |
| print(f" - {domain}") | |
| if not result['domains']: | |
| print(" (No domains found in allowedAudiences)") | |
| if args.raw: | |
| print(f"\n Raw allowedAudiences ({len(result['raw_audiences'])}):") | |
| for audience in result['raw_audiences']: | |
| print(f" - {audience}") | |
| else: | |
| print(f"\n[!] Error: {result['error']}") | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment