Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 47 additions & 76 deletions bbot/modules/azure_tenant.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import regex as re
from contextlib import suppress

from bbot.modules.base import BaseModule


Expand All @@ -9,116 +6,90 @@ class azure_tenant(BaseModule):
produced_events = ["DNS_NAME"]
flags = ["affiliates", "subdomain-enum", "cloud-enum", "passive", "safe"]
meta = {
"description": "Query Azure for tenant sister domains",
"description": "Query Azure via azmap.dev for tenant sister domains",
"created_date": "2024-07-04",
"author": "@TheTechromancer",
}

base_url = "https://autodiscover-s.outlook.com"
base_url = "https://azmap.dev/api/tenant"
in_scope_only = True
per_domain_only = True

async def setup(self):
self.processed = set()
self.d_xml_regex = re.compile(r"<Domain>([^<>/]*)</Domain>", re.I)
return True

async def handle_event(self, event):
_, query = self.helpers.split_domain(event.data)
domains, openid_config = await self.query(query)

tenant_id = None
authorization_endpoint = openid_config.get("authorization_endpoint", "")
matches = await self.helpers.re.findall(self.helpers.regexes.uuid_regex, authorization_endpoint)
if matches:
tenant_id = matches[0]

tenant_names = set()
if domains:
self.verbose(f'Found {len(domains):,} domains under tenant for "{query}": {", ".join(sorted(domains))}')
for domain in domains:
tenant_data = await self.query(query)

if not tenant_data:
return

tenant_id = tenant_data.get("tenant_id")
tenant_name = tenant_data.get("tenant_name")
email_domains = tenant_data.get("email_domains", [])

if email_domains:
self.verbose(
f'Found {len(email_domains):,} domains under tenant for "{query}": {", ".join(sorted(email_domains))}'
)
for domain in email_domains:
if domain != query:
await self.emit_event(
domain,
"DNS_NAME",
parent=event,
tags=["affiliate", "azure-tenant"],
context=f'{{module}} queried Outlook autodiscover for "{query}" and found {{event.type}}: {{event.data}}',
context=f'{{module}} queried azmap.dev for "{query}" and found {{event.type}}: {{event.data}}',
)
# tenant names
if domain.lower().endswith(".onmicrosoft.com"):
tenantname = domain.split(".")[0].lower()
if tenantname:
tenant_names.add(tenantname)

tenant_names = sorted(tenant_names)
event_data = {"tenant-names": tenant_names, "domains": sorted(domains)}

# Build tenant names list (include the tenant name from the API)
tenant_names = []
if tenant_name:
tenant_names.append(tenant_name)

# Also extract tenant names from .onmicrosoft.com domains
for domain in email_domains:
if domain.lower().endswith(".onmicrosoft.com"):
tenantname = domain.split(".")[0].lower()
if tenantname and tenantname not in tenant_names:
tenant_names.append(tenantname)

event_data = {"tenant-names": tenant_names, "domains": sorted(email_domains)}
tenant_names_str = ",".join(tenant_names)
if tenant_id is not None:
if tenant_id:
event_data["tenant-id"] = tenant_id
await self.emit_event(
event_data,
"AZURE_TENANT",
parent=event,
context=f'{{module}} queried Outlook autodiscover for "{query}" and found {{event.type}}: {tenant_names_str}',
context=f'{{module}} queried azmap.dev for "{query}" and found {{event.type}}: {tenant_names_str}',
)

async def query(self, domain):
url = f"{self.base_url}/autodiscover/autodiscover.svc"
data = f"""<?xml version="1.0" encoding="utf-8"?>
<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">
<soap:Header>
<a:Action soap:mustUnderstand="1">http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation</a:Action>
<a:To soap:mustUnderstand="1">https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc</a:To>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
</soap:Header>
<soap:Body>
<GetFederationInformationRequestMessage xmlns="http://schemas.microsoft.com/exchange/2010/Autodiscover">
<Request>
<Domain>{domain}</Domain>
</Request>
</GetFederationInformationRequestMessage>
</soap:Body>
</soap:Envelope>"""

headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": '"http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation"',
"User-Agent": "AutodiscoverClient",
"Accept-Encoding": "identity",
}
url = f"{self.base_url}?domain={domain}&extract=true"

self.debug(f"Retrieving tenant domains at {url}")

autodiscover_task = self.helpers.create_task(
self.helpers.request(url, method="POST", headers=headers, content=data)
)
openid_url = f"https://login.windows.net/{domain}/.well-known/openid-configuration"
openid_task = self.helpers.create_task(self.helpers.request(openid_url))

r = await autodiscover_task
r = await self.helpers.request(url)
status_code = getattr(r, "status_code", 0)
if status_code not in (200, 421):
if status_code != 200:
self.verbose(f'Error retrieving azure_tenant domains for "{domain}" (status code: {status_code})')
return set(), {}
found_domains = list(set(await self.helpers.re.findall(self.d_xml_regex, r.text)))
domains = set()
return {}

for d in found_domains:
# make sure we don't make any unnecessary api calls
try:
tenant_data = r.json()
except Exception as e:
self.warning(f'Error parsing JSON response for "{domain}": {e}')
return {}

# Absorb domains into word cloud
email_domains = tenant_data.get("email_domains", [])
for d in email_domains:
d = str(d).lower()
_, query = self.helpers.split_domain(d)
self.processed.add(hash(query))
domains.add(d)
# absorb into word cloud
self.scan.word_cloud.absorb_word(d)

r = await openid_task
openid_config = {}
with suppress(Exception):
openid_config = r.json()

domains = sorted(domains)
return domains, openid_config
return tenant_data
93 changes: 11 additions & 82 deletions bbot/test/test_step_2/module_tests/test_module_azure_tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,93 +2,22 @@


class TestAzure_Tenant(ModuleTestBase):
tenant_response = """
<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformationResponse</a:Action>
<h:ServerVersionInfo xmlns:h="http://schemas.microsoft.com/exchange/2010/Autodiscover" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<h:MajorVersion>15</h:MajorVersion>
<h:MinorVersion>20</h:MinorVersion>
<h:MajorBuildNumber>6411</h:MajorBuildNumber>
<h:MinorBuildNumber>14</h:MinorBuildNumber>
<h:Version>Exchange2015</h:Version>
</h:ServerVersionInfo>
</s:Header>
<s:Body>
<GetFederationInformationResponseMessage xmlns="http://schemas.microsoft.com/exchange/2010/Autodiscover">
<Response xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<ErrorCode>NoError</ErrorCode>
<ErrorMessage/>
<ApplicationUri>outlook.com</ApplicationUri>
<Domains>
<Domain>blacklanternsecurity.onmicrosoft.com</Domain>
</Domains>
<TokenIssuers>
<TokenIssuer>
<Endpoint>https://login.microsoftonline.com/extSTS.srf</Endpoint>
<Uri>urn:federation:MicrosoftOnline</Uri>
</TokenIssuer>
</TokenIssuers>
</Response>
</GetFederationInformationResponseMessage>
</s:Body>
</s:Envelope>"""

openid_config_azure = {
"token_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/oauth2/token",
"token_endpoint_auth_methods_supported": ["client_secret_post", "private_key_jwt", "client_secret_basic"],
"jwks_uri": "https://login.windows.net/common/discovery/keys",
"response_modes_supported": ["query", "fragment", "form_post"],
"subject_types_supported": ["pairwise"],
"id_token_signing_alg_values_supported": ["RS256"],
"response_types_supported": ["code", "id_token", "code id_token", "token id_token", "token"],
"scopes_supported": ["openid"],
"issuer": "https://sts.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/",
"microsoft_multi_refresh_token": True,
"authorization_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/oauth2/authorize",
"device_authorization_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/oauth2/devicecode",
"http_logout_supported": True,
"frontchannel_logout_supported": True,
"end_session_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/oauth2/logout",
"claims_supported": [
"sub",
"iss",
"cloud_instance_name",
"cloud_instance_host_name",
"cloud_graph_host_name",
"msgraph_host",
"aud",
"exp",
"iat",
"auth_time",
"acr",
"amr",
"nonce",
"email",
"given_name",
"family_name",
"nickname",
tenant_response = {
"tenant_id": "cc74fc12-4142-400e-a653-f98bdeadbeef",
"tenant_name": "blacklanternsecurity",
"domain": "blacklanternsecurity.com",
"email_domains": [
"blacklanternsecurity.com",
"blacklanternsecurity.onmicrosoft.com",
"blsgvt.com",
"o365.blacklanternsecurity.com",
],
"check_session_iframe": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/oauth2/checksession",
"userinfo_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/openid/userinfo",
"kerberos_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/kerberos",
"tenant_region_scope": "NA",
"cloud_instance_name": "microsoftonline.com",
"cloud_graph_host_name": "graph.windows.net",
"msgraph_host": "graph.microsoft.com",
"rbac_url": "https://pas.windows.net",
}

async def setup_after_prep(self, module_test):
module_test.httpx_mock.add_response(
method="POST",
url="https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc",
text=self.tenant_response,
)
module_test.httpx_mock.add_response(
url="https://login.windows.net/blacklanternsecurity.com/.well-known/openid-configuration",
json=self.openid_config_azure,
url="https://azmap.dev/api/tenant?domain=blacklanternsecurity.com&extract=true",
json=self.tenant_response,
)

def check(self, module_test, events):
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "bbot"
version = "2.8.0"
version = "2.8.1"
description = "OSINT automation for hackers."
authors = [
"TheTechromancer",
Expand Down Expand Up @@ -121,7 +121,7 @@ lint.ignore = ["E402", "E711", "E713", "E721", "E741", "F403", "F405", "E501"]
[tool.poetry-dynamic-versioning]
enable = true
metadata = false
format-jinja = 'v2.8.0{% if branch == "dev" %}.{{ distance }}rc{% endif %}'
format-jinja = 'v2.8.1{% if branch == "dev" %}.{{ distance }}rc{% endif %}'

[tool.poetry-dynamic-versioning.substitution]
files = ["*/__init__.py"]
Loading