diff --git a/AADInternals.psd1 b/AADInternals.psd1 index e4dafcc..1c311c3 100644 --- a/AADInternals.psd1 +++ b/AADInternals.psd1 @@ -181,6 +181,7 @@ DISCLAIMER: Functionality provided through this module are not supported by Micr "Get-OpenIDConfiguration" "Get-TenantId" "Get-TenantDomains" + "Get-TenantDomainsFromACS" "Get-Cache" "Clear-Cache" "Add-AccessTokenToCache" diff --git a/AccessToken_utils.ps1 b/AccessToken_utils.ps1 index 02e3d27..fd4770b 100644 --- a/AccessToken_utils.ps1 +++ b/AccessToken_utils.ps1 @@ -1290,6 +1290,116 @@ function Add-RefreshTokenToCache # Gets other domains of the given tenant # Jun 15th 2020 +# Gets domains from Access Control Service metadata +# Jan 10th 2026 +function Get-TenantDomainsFromACS +{ +<# + .SYNOPSIS + Gets domains from the tenant using Access Control Service metadata endpoint + + .DESCRIPTION + Uses the Azure Access Control Service (ACS) metadata endpoint to retrieve + registered email domains (allowedAudiences) for a tenant. This endpoint returns + domains that are registered with the tenant for email routing and federation purposes. + + Requires the tenant's initial domain name (*.onmicrosoft.com or *.onmicrosoft.us for gov clouds). + + .Parameter TenantName + The tenant's initial domain name (e.g., company.onmicrosoft.com, company.onmicrosoft.us) + + .Parameter SubScope + Optional tenant subscope/region identifier (DOD, DODCON, etc.) + + .Example + Get-AADIntTenantDomainsFromACS -TenantName company.onmicrosoft.com + + company.com + company.mail.onmicrosoft.com + company.onmicrosoft.com + + .Example + Get-AADIntTenantDomainsFromACS -TenantName company.onmicrosoft.us -SubScope DOD + + company.com + company.mail.onmicrosoft.us + company.onmicrosoft.us + +#> + [cmdletbinding()] + Param( + [Parameter(Mandatory=$True)] + [String]$TenantName, + [Parameter(Mandatory=$False)] + [String]$SubScope + ) + Process + { + try + { + # Build the ACS metadata endpoint URL based on cloud environment + # Reference: https://learn.microsoft.com/en-us/exchange/configure-oauth-authentication-between-exchange-and-exchange-online-organizations-exchange-2013-help + # Note: Government clouds use login.microsoftonline.us instead of accounts.accesscontrol + switch($SubScope) + { + "DOD" # DoD + { + $uri = "https://login.microsoftonline.us/$TenantName/metadata/json/1" + } + "DODCON" # GCC-High + { + $uri = "https://login.microsoftonline.us/$TenantName/metadata/json/1" + } + default # Commercial/GCC + { + $uri = "https://accounts.accesscontrol.windows.net/$TenantName/metadata/json/1" + } + } + + Write-Verbose "Querying ACS metadata endpoint: $uri" + + # Query the endpoint + $response = Invoke-RestMethod -Uri $uri -UseBasicParsing -ErrorAction Stop + + # Extract domains from allowedAudiences + # Format (Commercial): 00000001-0000-0000-c000-000000000000/accounts.accesscontrol.windows.net@ + # Format (Gov clouds): 00000001-0000-0000-c000-000000000000/login.microsoftonline.us@ + $domains = @() + if($response.allowedAudiences) + { + foreach($audience in $response.allowedAudiences) + { + if($audience -match '@(.+)$') + { + $domain = $matches[1] + # Filter out GUID entries (these represent tenant IDs, not domain names) + if($domain -notmatch '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') + { + $domains += $domain + } + } + } + } + + if($domains.Count -gt 0) + { + Write-Verbose "Found $($domains.Count) domains from ACS metadata" + return ($domains | Sort-Object -Unique) + } + else + { + Write-Verbose "No domains found in ACS metadata" + return @() + } + } + catch + { + Write-Verbose "Error querying ACS metadata: $($_.Exception.Message)" + return @() + } + } +} + function Get-TenantDomains { <# @@ -1297,8 +1407,10 @@ function Get-TenantDomains Gets other domains from the tenant of the given domain .DESCRIPTION - Uses Exchange Online autodiscover service to retrive other - domains from the tenant of the given domain. + Uses Exchange Online autodiscover service AND Access Control Service (ACS) + metadata endpoint to retrieve all domains from the tenant. Since Microsoft + has limited the autodiscover endpoint, this function now queries both sources + and merges the results for comprehensive domain enumeration. The given domain SHOULD be Managed, federated domains are not always found for some reason. If nothing is found, try to use .onmicrosoft.com @@ -1322,31 +1434,39 @@ function Get-TenantDomains ) Process { + # Collection for all discovered domains + $allDomains = @() + # Get Tenant Region subscope from Open ID configuration if not provided if([string]::IsNullOrEmpty($SubScope)) { $SubScope = Get-TenantSubscope -Domain $Domain } - # Use the correct url - switch($SubScope) + # Method 1: Try autodiscover (may return limited results due to Microsoft mitigation) + try { - "DOD" # DoD - { - $uri = "https://autodiscover-s-dod.office365.us/autodiscover/autodiscover.svc" - } - "DODCON" # GCC-High - { - $uri = "https://autodiscover-s.office365.us/autodiscover/autodiscover.svc" - } - default # Commercial/GCC + Write-Verbose "Querying autodiscover endpoint..." + + # Use the correct url + switch($SubScope) { - $uri = "https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc" + "DOD" # DoD + { + $uri = "https://autodiscover-s-dod.office365.us/autodiscover/autodiscover.svc" + } + "DODCON" # GCC-High + { + $uri = "https://autodiscover-s.office365.us/autodiscover/autodiscover.svc" + } + default # Commercial/GCC + { + $uri = "https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc" + } } - } - # Create the body - $body=@" + # Create the body + $body=@" @@ -1365,22 +1485,97 @@ function Get-TenantDomains "@ - # Create the headers - $headers=@{ - "Content-Type" = "text/xml; charset=utf-8" - "SOAPAction" = '"http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation"' - "User-Agent" = "AutodiscoverClient" + # Create the headers + $headers=@{ + "Content-Type" = "text/xml; charset=utf-8" + "SOAPAction" = '"http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation"' + "User-Agent" = "AutodiscoverClient" + } + + $response = Invoke-RestMethod -UseBasicParsing -Method Post -uri $uri -Body $body -Headers $headers -ErrorAction Stop + $autodiscoverDomains = @($response.Envelope.body.GetFederationInformationResponseMessage.response.Domains.Domain) + + if($autodiscoverDomains.Count -gt 0) + { + Write-Verbose "Autodiscover returned $($autodiscoverDomains.Count) domain(s)" + $allDomains += $autodiscoverDomains + } } - # Invoke - $response = Invoke-RestMethod -UseBasicParsing -Method Post -uri $uri -Body $body -Headers $headers - - # Return - $domains = @($response.Envelope.body.GetFederationInformationResponseMessage.response.Domains.Domain) - if($Domain -notin $domains) + catch + { + Write-Verbose "Autodiscover query failed: $($_.Exception.Message)" + } + + # Method 2: Try ACS metadata endpoint (provides comprehensive domain list) + try + { + Write-Verbose "Querying ACS metadata endpoint..." + + # Try to get tenant name (*.onmicrosoft.com) + $tenantName = $null + + # If the provided domain is already a tenant name, use it + if($Domain -match '^[^.]*\.onmicrosoft\.((com)|(us))$') + { + $tenantName = $Domain + } + else + { + # Try to get tenant ID and derive tenant name + try + { + $tenantId = Get-TenantID -Domain $Domain + if(![string]::IsNullOrEmpty($tenantId)) + { + Write-Verbose "Got tenant ID: $tenantId, retrieving tenant name..." + $tenantName = Get-TenantNameByTenantId -TenantId $tenantId -SubScope $SubScope -Domain $Domain + } + } + catch + { + Write-Verbose "Could not determine tenant name: $($_.Exception.Message)" + } + } + + # If we have a tenant name, query the ACS metadata endpoint + if(![string]::IsNullOrEmpty($tenantName)) + { + Write-Verbose "Using tenant name: $tenantName for ACS query" + $acsDomains = Get-TenantDomainsFromACS -TenantName $tenantName -SubScope $SubScope + + if($acsDomains.Count -gt 0) + { + Write-Verbose "ACS metadata returned $($acsDomains.Count) domain(s)" + $allDomains += $acsDomains + } + } + else + { + Write-Verbose "Could not determine tenant name, skipping ACS query" + } + } + catch + { + Write-Verbose "ACS metadata query failed: $($_.Exception.Message)" + } + + # Merge, deduplicate and ensure the queried domain is included + if($allDomains.Count -gt 0) + { + $uniqueDomains = $allDomains | Select-Object -Unique | Sort-Object + if($Domain -notin $uniqueDomains) + { + $uniqueDomains = @($uniqueDomains) + @($Domain) | Sort-Object + } + Write-Verbose "Total unique domains found: $($uniqueDomains.Count)" + return $uniqueDomains + } + else { - $domains += $Domain + # If both methods failed, return at least the queried domain + Write-Warning "Could not retrieve tenant domains. Returning only the queried domain." + return @($Domain) } - $domains | Sort-Object } } diff --git a/KillChain.ps1 b/KillChain.ps1 index 018ece8..a1fadb1 100644 --- a/KillChain.ps1 +++ b/KillChain.ps1 @@ -1,4 +1,4 @@ -# +# # This file contains functions for Azure AD / Office 365 kill chain # @@ -243,6 +243,19 @@ function Invoke-ReconAsOutsider } } + # Fallback: If tenant name was not found from autodiscover, try alternative method + if([string]::IsNullOrEmpty($tenantName)) + { + Write-Verbose "Tenant name not found from autodiscover, trying alternative method..." + $tenantName = Get-TenantNameByTenantId -TenantId $tenantId -SubScope $tenantSubscope -Domain $DomainName + + # If still not found, give user a hint + if([string]::IsNullOrEmpty($tenantName)) + { + Write-Warning "Tenant name lookup requires authentication. Run 'Get-AADIntAccessTokenForMSGraph -SaveToCache' first (can use any tenant)." + } + } + Write-Host "Tenant brand: $tenantBrand" Write-Host "Tenant name: $tenantName" Write-Host "Tenant id: $tenantId" @@ -269,7 +282,7 @@ function Invoke-ReconAsOutsider } # Cloud sync not definitive, may use different domain name - if(DoesUserExists -User "ADToAADSyncServiceAccount@$($tenantName)") + if(![string]::IsNullOrEmpty($tenantName) -and (DoesUserExists -User "ADToAADSyncServiceAccount@$($tenantName)")) { Write-Host "Uses cloud sync: $true" } @@ -280,7 +293,7 @@ function Invoke-ReconAsOutsider Write-Host "CBA enabled: $tenantCBA" } - return $domainInformation + return $domainInformation | Format-Table -AutoSize } } diff --git a/KillChain_utils.ps1 b/KillChain_utils.ps1 index 25bbab5..ca7eaa9 100644 --- a/KillChain_utils.ps1 +++ b/KillChain_utils.ps1 @@ -1,7 +1,8 @@ -# Checks whether the domain has MX records pointing to MS cloud +# Checks whether the domain has MX records pointing to MS cloud # Jun 16th 2020 # Aug 30th 2022: Fixed by maxgrim # Fe. 19th 2025: Add new mx.microsoft check by Michael Morten Sonne +# January 8th 2026: Added functionality to retrieve Tenant Name from Graph (authentication to dummy tenant required) and DKIM records (unauthenticated) function HasCloudMX { @@ -31,8 +32,13 @@ function HasCloudMX } } - $results = Resolve-DnsName -Name $Domain -Type MX -DnsOnly -NoHostsFile -NoIdn -ErrorAction SilentlyContinue | - Select-Object -ExpandProperty nameexchange + $dnsResults = Resolve-DnsName -Name $Domain -Type MX -DnsOnly -NoHostsFile -NoIdn -ErrorAction SilentlyContinue + + # Extract nameexchange property only if results exist and have the property + $results = @() + if($dnsResults) { + $results = $dnsResults | Where-Object { $_.NameExchange } | Select-Object -ExpandProperty NameExchange + } # Normalize $filter into an array if (-not ($filter -is [System.Collections.IEnumerable])) { @@ -41,8 +47,10 @@ function HasCloudMX # Check results $filteredResults = @() - foreach ($pat in $filter) { - $filteredResults += $results | Where-Object { $_ -like $pat } + if($results.Count -gt 0) { + foreach ($pat in $filter) { + $filteredResults += $results | Where-Object { $_ -like $pat } + } } return $filteredResults.Count -gt 0 @@ -440,3 +448,94 @@ function GetMDIInstance return $null } } + +# Gets the tenant's initial domain (*.onmicrosoft.com) by tenant ID +# Uses DKIM records (most reliable) or MS Graph API as fallback +# Jan 8th 2026 +function Get-TenantNameByTenantId +{ + [cmdletbinding()] + Param( + [Parameter(Mandatory=$True)] + [String]$TenantId, + [Parameter(Mandatory=$False)] + [String]$SubScope, + [Parameter(Mandatory=$False)] + [String]$Domain + ) + Process + { + # Determine the correct onmicrosoft suffix based on subscope + switch($SubScope) + { + "DOD" { $onmicrosoftSuffix = "\.onmicrosoft\.us$" } + "DODCON" { $onmicrosoftSuffix = "\.onmicrosoft\.us$" } + default { $onmicrosoftSuffix = "\.onmicrosoft\.com$" } + } + + # Method 1: Try DKIM records - most reliable unauthenticated method + # DKIM CNAME records point to selector1-._domainkey..onmicrosoft.com + if(-not [string]::IsNullOrEmpty($Domain)) + { + Write-Verbose "Trying to get tenant name from DKIM records for $Domain..." + $selectors = @("selector1", "selector2") + foreach($selector in $selectors) + { + try + { + $dkimRecord = Resolve-DnsName -Name "$selector._domainkey.$Domain" -Type CNAME -DnsOnly -NoHostsFile -NoIdn -ErrorAction SilentlyContinue | + Select-Object -ExpandProperty NameHost + + if($dkimRecord -match $onmicrosoftSuffix) + { + # Extract tenant name from: selector1-domain-com._domainkey.TENANTNAME.onmicrosoft.com + $tenantName = ($dkimRecord -split '\._domainkey\.')[1] + + # Verify this tenant name belongs to the correct tenant ID + $verifyTenantId = Get-TenantID -Domain $tenantName + if($verifyTenantId -eq $TenantId) + { + Write-Verbose "Found tenant name from DKIM: $tenantName (verified)" + return $tenantName + } + else + { + Write-Verbose "DKIM tenant name $tenantName belongs to different tenant: $verifyTenantId" + } + } + } + catch { } + } + } + + # Method 2: Try MS Graph API with cached access token + Write-Verbose "Trying to get tenant info via MS Graph API..." + try + { + $AccessToken = Get-AccessTokenFromCache -Resource "https://graph.microsoft.com" -ClientId "1b730954-1685-4b74-9bfd-dac224a7b894" + + if(-not [string]::IsNullOrEmpty($AccessToken)) + { + Write-Verbose "Found cached MS Graph access token, querying tenant info..." + $results = Call-MSGraphAPI -AccessToken $AccessToken -API "tenantRelationships/findTenantInformationByTenantId(tenantId='$TenantId')" + + if(-not [string]::IsNullOrEmpty($results.defaultDomainName)) + { + Write-Verbose "Got default domain from MS Graph: $($results.defaultDomainName)" + return $results.defaultDomainName + } + } + else + { + Write-Verbose "No cached MS Graph access token found." + } + } + catch + { + Write-Verbose "MS Graph API call failed: $_" + } + + Write-Verbose "Unable to determine tenant name." + return $null + } +}