Skip to content

Instantly share code, notes, and snippets.

@HackingLZ
Created January 13, 2026 21:00
Show Gist options
  • Select an option

  • Save HackingLZ/678afcc6770d3c0b46f3bd56f1c75e79 to your computer and use it in GitHub Desktop.

Select an option

Save HackingLZ/678afcc6770d3c0b46f3bd56f1c75e79 to your computer and use it in GitHub Desktop.
Standalone Azure Access Control Service (ACS) Domain Lookup
#!/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