1- import regex as re
2- from contextlib import suppress
3-
41from bbot .modules .base import BaseModule
52
63
@@ -9,116 +6,90 @@ class azure_tenant(BaseModule):
96 produced_events = ["DNS_NAME" ]
107 flags = ["affiliates" , "subdomain-enum" , "cloud-enum" , "passive" , "safe" ]
118 meta = {
12- "description" : "Query Azure for tenant sister domains" ,
9+ "description" : "Query Azure via azmap.dev for tenant sister domains" ,
1310 "created_date" : "2024-07-04" ,
1411 "author" : "@TheTechromancer" ,
1512 }
1613
17- base_url = "https://autodiscover-s.outlook.com "
14+ base_url = "https://azmap.dev/api/tenant "
1815 in_scope_only = True
1916 per_domain_only = True
2017
2118 async def setup (self ):
2219 self .processed = set ()
23- self .d_xml_regex = re .compile (r"<Domain>([^<>/]*)</Domain>" , re .I )
2420 return True
2521
2622 async def handle_event (self , event ):
2723 _ , query = self .helpers .split_domain (event .data )
28- domains , openid_config = await self .query (query )
29-
30- tenant_id = None
31- authorization_endpoint = openid_config .get ("authorization_endpoint" , "" )
32- matches = await self .helpers .re .findall (self .helpers .regexes .uuid_regex , authorization_endpoint )
33- if matches :
34- tenant_id = matches [0 ]
35-
36- tenant_names = set ()
37- if domains :
38- self .verbose (f'Found { len (domains ):,} domains under tenant for "{ query } ": { ", " .join (sorted (domains ))} ' )
39- for domain in domains :
24+ tenant_data = await self .query (query )
25+
26+ if not tenant_data :
27+ return
28+
29+ tenant_id = tenant_data .get ("tenant_id" )
30+ tenant_name = tenant_data .get ("tenant_name" )
31+ email_domains = tenant_data .get ("email_domains" , [])
32+
33+ if email_domains :
34+ self .verbose (
35+ f'Found { len (email_domains ):,} domains under tenant for "{ query } ": { ", " .join (sorted (email_domains ))} '
36+ )
37+ for domain in email_domains :
4038 if domain != query :
4139 await self .emit_event (
4240 domain ,
4341 "DNS_NAME" ,
4442 parent = event ,
4543 tags = ["affiliate" , "azure-tenant" ],
46- context = f'{{module}} queried Outlook autodiscover for "{ query } " and found {{event.type}}: {{event.data}}' ,
44+ context = f'{{module}} queried azmap.dev for "{ query } " and found {{event.type}}: {{event.data}}' ,
4745 )
48- # tenant names
49- if domain .lower ().endswith (".onmicrosoft.com" ):
50- tenantname = domain .split ("." )[0 ].lower ()
51- if tenantname :
52- tenant_names .add (tenantname )
53-
54- tenant_names = sorted (tenant_names )
55- event_data = {"tenant-names" : tenant_names , "domains" : sorted (domains )}
46+
47+ # Build tenant names list (include the tenant name from the API)
48+ tenant_names = []
49+ if tenant_name :
50+ tenant_names .append (tenant_name )
51+
52+ # Also extract tenant names from .onmicrosoft.com domains
53+ for domain in email_domains :
54+ if domain .lower ().endswith (".onmicrosoft.com" ):
55+ tenantname = domain .split ("." )[0 ].lower ()
56+ if tenantname and tenantname not in tenant_names :
57+ tenant_names .append (tenantname )
58+
59+ event_data = {"tenant-names" : tenant_names , "domains" : sorted (email_domains )}
5660 tenant_names_str = "," .join (tenant_names )
57- if tenant_id is not None :
61+ if tenant_id :
5862 event_data ["tenant-id" ] = tenant_id
5963 await self .emit_event (
6064 event_data ,
6165 "AZURE_TENANT" ,
6266 parent = event ,
63- context = f'{{module}} queried Outlook autodiscover for "{ query } " and found {{event.type}}: { tenant_names_str } ' ,
67+ context = f'{{module}} queried azmap.dev for "{ query } " and found {{event.type}}: { tenant_names_str } ' ,
6468 )
6569
6670 async def query (self , domain ):
67- url = f"{ self .base_url } /autodiscover/autodiscover.svc"
68- data = f"""<?xml version="1.0" encoding="utf-8"?>
69- <soap:Envelope xmlns:exm="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:ext="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
70- <soap:Header>
71- <a:Action soap:mustUnderstand="1">http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation</a:Action>
72- <a:To soap:mustUnderstand="1">https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc</a:To>
73- <a:ReplyTo>
74- <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
75- </a:ReplyTo>
76- </soap:Header>
77- <soap:Body>
78- <GetFederationInformationRequestMessage xmlns="http://schemas.microsoft.com/exchange/2010/Autodiscover">
79- <Request>
80- <Domain>{ domain } </Domain>
81- </Request>
82- </GetFederationInformationRequestMessage>
83- </soap:Body>
84- </soap:Envelope>"""
85-
86- headers = {
87- "Content-Type" : "text/xml; charset=utf-8" ,
88- "SOAPAction" : '"http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation"' ,
89- "User-Agent" : "AutodiscoverClient" ,
90- "Accept-Encoding" : "identity" ,
91- }
71+ url = f"{ self .base_url } ?domain={ domain } &extract=true"
9272
9373 self .debug (f"Retrieving tenant domains at { url } " )
9474
95- autodiscover_task = self .helpers .create_task (
96- self .helpers .request (url , method = "POST" , headers = headers , content = data )
97- )
98- openid_url = f"https://login.windows.net/{ domain } /.well-known/openid-configuration"
99- openid_task = self .helpers .create_task (self .helpers .request (openid_url ))
100-
101- r = await autodiscover_task
75+ r = await self .helpers .request (url )
10276 status_code = getattr (r , "status_code" , 0 )
103- if status_code not in ( 200 , 421 ) :
77+ if status_code != 200 :
10478 self .verbose (f'Error retrieving azure_tenant domains for "{ domain } " (status code: { status_code } )' )
105- return set (), {}
106- found_domains = list (set (await self .helpers .re .findall (self .d_xml_regex , r .text )))
107- domains = set ()
79+ return {}
10880
109- for d in found_domains :
110- # make sure we don't make any unnecessary api calls
81+ try :
82+ tenant_data = r .json ()
83+ except Exception as e :
84+ self .warning (f'Error parsing JSON response for "{ domain } ": { e } ' )
85+ return {}
86+
87+ # Absorb domains into word cloud
88+ email_domains = tenant_data .get ("email_domains" , [])
89+ for d in email_domains :
11190 d = str (d ).lower ()
11291 _ , query = self .helpers .split_domain (d )
11392 self .processed .add (hash (query ))
114- domains .add (d )
115- # absorb into word cloud
11693 self .scan .word_cloud .absorb_word (d )
11794
118- r = await openid_task
119- openid_config = {}
120- with suppress (Exception ):
121- openid_config = r .json ()
122-
123- domains = sorted (domains )
124- return domains , openid_config
95+ return tenant_data
0 commit comments