Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .build/cspell-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ contoso
CTMM
Datacenter
dcom
devicecode
DMARC
Dsamain
DTLS
Expand Down Expand Up @@ -171,4 +172,5 @@ Webex
Weve
wevtutil
windir
wids
Xlsb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@
If you provide the thumbprint, the script searches and exports the certificate with the thumbprint provided from the local machines certificate
store. If you provide the file path, the script uploads the certificate, which was specified.
This parameter allows you to run granular configurations. Note that some of the tasks depend on others and can't be run alone.
.PARAMETER UseDeviceCodeFlow
Use this switch parameter to force the Graph API access token to be acquired via the OAuth 2.0 device code flow
instead of the default authorization code flow with PKCE. Windows Server Core is detected automatically and uses
the device code flow without this switch, so you only need it to force the device code flow on other browser-less hosts.
You will be asked to open a verification URL on another device and enter a user code to complete the sign-in.
Note that the device code flow may be blocked by Conditional Access policies in your tenant.
.PARAMETER ScriptUpdateOnly
This optional parameter allows you to only update the script without performing any other actions.
.PARAMETER SkipVersionCheck
Expand Down Expand Up @@ -227,6 +233,13 @@ param(
[Parameter(Mandatory = $false, ParameterSetName = "Create")]
[string]$CertificateInformation,

[Parameter(Mandatory = $false, ParameterSetName = "FullyConfigureExchangeHybridApplication")]
[Parameter(Mandatory = $false, ParameterSetName = "FirstPartyKeyCredentialsCleanup")]
[Parameter(Mandatory = $false, ParameterSetName = "Create")]
[Parameter(Mandatory = $false, ParameterSetName = "Delete")]
[Parameter(Mandatory = $false, ParameterSetName = "RemovePermissions")]
[switch]$UseDeviceCodeFlow,

[Parameter(Mandatory = $true, ParameterSetName = "ScriptUpdateOnly")]
[switch]$ScriptUpdateOnly,

Expand All @@ -249,6 +262,7 @@ begin {
. $PSScriptRoot\..\..\Shared\Get-PSSessionDetails.ps1
. $PSScriptRoot\..\..\Shared\Get-ProtocolEndpointViaAutoDv2.ps1
. $PSScriptRoot\..\..\Shared\Show-Disclaimer.ps1
. $PSScriptRoot\..\..\Shared\Test-IsServerCoreOperatingSystem.ps1
. $PSScriptRoot\..\..\Shared\ActiveDirectoryFunctions\Get-ExchangeOrganizationGuid.ps1
. $PSScriptRoot\..\..\Shared\AzureFunctions\Get-Consent.ps1
. $PSScriptRoot\..\..\Shared\AzureFunctions\Get-CloudServiceEndpoint.ps1
Expand Down Expand Up @@ -622,6 +636,14 @@ begin {
$getGraphAccessTokenParams.Add("ClientId", $Script:CustomClientId)
}

# Use the device code flow when it was explicitly requested via -UseDeviceCodeFlow, or when we detect a
# Windows Server Core installation, where no browser is available for the default authorization code flow.
if ($UseDeviceCodeFlow -or
(Test-IsServerCoreOperatingSystem)) {
Write-Verbose "The device code flow will be used to acquire the access token"
$getGraphAccessTokenParams.Add("UseDeviceCodeFlow", $true)
}

$graphAccessToken = Get-GraphAccessToken @getGraphAccessTokenParams

if ($null -eq $graphAccessToken) {
Expand Down Expand Up @@ -1384,7 +1406,8 @@ begin {
$graphApiFeatureEnabledCount = 0

foreach ($o in $exchangeOnpremAsThirdPartyAppIdSettingOverrides) {
$match = [regex]::Match($o.Parameters, $settingOverridesEnabledRegex, "IgnoreCase")
$enabledParameter = @($o.Parameters) | Where-Object { $_ -match "^\s*Enabled\s*=" } | Select-Object -First 1
$match = [regex]::Match([string]$enabledParameter, $settingOverridesEnabledRegex, "IgnoreCase")
$featureIsEnabled = ($match.Success -and $match.Groups[1].Value -eq "true")
$featureSettingOverrideValue = if (-not $match.Success) { "Unknown" } else { $match.Groups[1].Value }

Expand All @@ -1398,7 +1421,8 @@ begin {
}

foreach ($o in $routeThroughMSGraphSettingOverrides) {
$match = [regex]::Match($o.Parameters, $settingOverridesEnabledRegex, "IgnoreCase")
$enabledParameter = @($o.Parameters) | Where-Object { $_ -match "^\s*Enabled\s*=" } | Select-Object -First 1
$match = [regex]::Match([string]$enabledParameter, $settingOverridesEnabledRegex, "IgnoreCase")
$featureIsEnabled = ($match.Success -and $match.Groups[1].Value -eq "true")
$featureSettingOverrideValue = if (-not $match.Success) { "Unknown" } else { $match.Groups[1].Value }

Expand Down Expand Up @@ -1525,7 +1549,7 @@ begin {
Name = "EnableRouteThroughMSGraphFeature"
Component = "SettingOverride"
Section = "RouteThroughMSGraph"
Parameters = @("Enabled=true")
Parameters = @("Enabled=true", "EnabledForMailTips=true", "EnabledForAutomaticReplies=false")
Reason = "Created by $($script:MyInvocation.MyCommand.Name) on $(Get-Date)"
}
# Execute the commands to create the new setting override and to refresh the variant configuration
Expand Down
25 changes: 21 additions & 4 deletions Security/src/CVE-2023-23397/CVE-2023-23397.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
Skip the first pass of -UseSearchFolders and just check the existing search folders for results.
.PARAMETER TimeoutSeconds
This optional parameter lets you specify the timeout value for the ExchangeService object. Defaults to 5 minutes.
.PARAMETER UseDeviceCodeFlow
This optional switch parameter forces the script to acquire the Graph API access token via the OAuth 2.0 device code flow
instead of the default authorization code flow with PKCE. Windows Server Core is detected automatically and uses the
device code flow without this switch, so you only need it to force the device code flow on other browser-less hosts.
You will be asked to open a verification URL on another device and enter a user code to complete the sign-in.
Note that the device code flow may be blocked by Conditional Access policies in your tenant.
.EXAMPLE
PS C:\> .\CVE-2023-23397.ps1 -CreateAzureApplication
This will run the tool to create a new Azure application with required permissions
Expand Down Expand Up @@ -183,7 +189,13 @@ param(
[Parameter(Mandatory = $false, ParameterSetName = "Audit")]
[Parameter(Mandatory = $false, ParameterSetName = "Cleanup")]
[ValidateRange(1, 2147483)]
[int]$TimeoutSeconds = 300
[int]$TimeoutSeconds = 300,

[Parameter(Mandatory = $false, ParameterSetName = "CreateAzureApplication")]
[Parameter(Mandatory = $false, ParameterSetName = "DeleteAzureApplication")]
[Parameter(Mandatory = $false, ParameterSetName = "Audit")]
[Parameter(Mandatory = $false, ParameterSetName = "Cleanup")]
[switch]$UseDeviceCodeFlow
)

dynamicparam {
Expand Down Expand Up @@ -235,6 +247,7 @@ begin {
. $PSScriptRoot\..\..\..\Shared\Get-NuGetPackage.ps1
. $PSScriptRoot\..\..\..\Shared\Invoke-ExtractArchive.ps1
. $PSScriptRoot\..\..\..\Shared\LoggerFunctions.ps1
. $PSScriptRoot\..\..\..\Shared\Test-IsServerCoreOperatingSystem.ps1
. $PSScriptRoot\..\..\..\Shared\ActiveDirectoryFunctions\Test-ADCredentials.ps1
. $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-GraphAccessToken.ps1
. $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-CloudServiceEndpoint.ps1
Expand Down Expand Up @@ -627,8 +640,12 @@ begin {
Enable-TrustAnyCertificateCallback
}

# Use the device code flow when it was explicitly requested via -UseDeviceCodeFlow, or when we detect a
# Windows Server Core installation, where no browser is available for the default authorization code flow.
$useDeviceCodeFlow = $UseDeviceCodeFlow -or (Test-IsServerCoreOperatingSystem)

if ($CreateAzureApplication) {
$graphAccessToken = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint
$graphAccessToken = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint -UseDeviceCodeFlow:$useDeviceCodeFlow

if ($null -eq $graphAccessToken) {
Write-Host "Failed to acquire an access token - the script cannot continue" -ForegroundColor Red
Expand All @@ -653,7 +670,7 @@ begin {
}

if ($DeleteAzureApplication) {
$graphAccessToken = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint
$graphAccessToken = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint -UseDeviceCodeFlow:$useDeviceCodeFlow

if ($null -eq $graphAccessToken) {
Write-Host "Failed to acquire an access token - the script cannot continue" -ForegroundColor Red
Expand Down Expand Up @@ -726,7 +743,7 @@ begin {
([System.String]::IsNullOrEmpty($Organization)) -or
([System.String]::IsNullOrEmpty($CertificateThumbprint))) {
# We need to query the Azure application information from the Azure AD if not explicitly provided via parameter
$azAccountsObject = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint
$azAccountsObject = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint -UseDeviceCodeFlow:$useDeviceCodeFlow

if ($null -eq $azAccountsObject) {
Write-Host "Failed to acquire an access token - the script cannot continue" -ForegroundColor Red
Expand Down
145 changes: 140 additions & 5 deletions Shared/AzureFunctions/Get-GraphAccessToken.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@
. $PSScriptRoot\..\ScriptUpdateFunctions\Invoke-WebRequestWithProxyDetection.ps1

<#
This function is used to get an access token for the Azure Graph API by using the OAuth 2.0 authorization code flow
with PKCE (Proof Key for Code Exchange). The OAuth 2.0 authorization code grant type, or auth code flow,
enables a client application to obtain authorized access to protected resources like web APIs.
This function is used to get an access token for the Azure Graph API. By default it uses the OAuth 2.0
authorization code flow with PKCE (Proof Key for Code Exchange). The OAuth 2.0 authorization code grant type,
or auth code flow, enables a client application to obtain authorized access to protected resources like web APIs.
The auth code flow requires a user-agent that supports redirection from the authorization server
(the Microsoft identity platform) back to your application.

More information about the auth code flow with PKCE can be found here:
https://learn.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow#protocol-details

For hosts without a browser (for example, Windows Server Core), the -UseDeviceCodeFlow switch can be used to
acquire the token via the OAuth 2.0 device authorization grant (device code flow) instead. In that flow the user
is asked to open a verification URL on another device and enter a user code to complete the sign-in, so no local
browser or redirect listener is required. Note that the device code flow is less phishing resistant than the auth
code flow and may be blocked by Conditional Access policies, so it should only be used when no browser is available.

More information about the device code flow can be found here:
https://learn.microsoft.com/azure/active-directory/develop/v2-oauth2-device-code
#>
function Get-GraphAccessToken {
[CmdletBinding()]
Expand All @@ -26,10 +35,18 @@ function Get-GraphAccessToken {
[string]$GraphApiUrl = "https://graph.microsoft.com",

[Parameter(Mandatory = $false)]
[string]$ClientId = "1950a258-227b-4e31-a9cf-717495945fc2", # Well-known Microsoft Azure PowerShell application ID
[string]$ClientId = "4d2f5175-f06b-49e2-9f4a-8e614a8abc03", # Microsoft Exchange Hybrid Wizard application ID

# The OpenID Connect scopes "openid" and "profile" must be requested explicitly so that an id_token is
# returned alongside the access_token. The id_token is required for the nonce replay check and to read the
# tenant id. ".default" requests all permissions that have already been consented for the app (required for
# a first party application, which cannot rely on dynamic consent / pre-authorization for a first party
# resource).
[Parameter(Mandatory = $false)]
[string]$Scope = "$($GraphApiUrl)/.default openid profile",

[Parameter(Mandatory = $false)]
[string]$Scope = "$($GraphApiUrl)//AuditLog.Read.All Directory.AccessAsUser.All email openid profile"
[switch]$UseDeviceCodeFlow
)

begin {
Expand Down Expand Up @@ -112,6 +129,115 @@ function Get-GraphAccessToken {
$connectionSuccessful = $false
}
process {
if ($UseDeviceCodeFlow) {
Write-Verbose "Device code flow was selected to acquire the access token"
Write-Host "Device code flow is intended for hosts without a browser (for example, Windows Server Core)." -ForegroundColor Yellow
Write-Host "Note: This flow may be blocked by Conditional Access policies in your tenant." -ForegroundColor Yellow

# Request a device code from the Microsoft Azure Active Directory endpoint
$deviceCodeRequestParams = @{
Uri = "$AzureADEndpoint/organizations/oauth2/v2.0/devicecode"
Method = "POST"
ContentType = "application/x-www-form-urlencoded"
Body = @{
client_id = $ClientId
scope = $Scope
}
UseBasicParsing = $true
}
$deviceCodeResponse = Invoke-WebRequestWithProxyDetection -ParametersObject $deviceCodeRequestParams

if (($null -eq $deviceCodeResponse) -or
($deviceCodeResponse.StatusCode -ne 200)) {
Write-Host "Unable to acquire a device code from the Microsoft Azure Active Directory endpoint." -ForegroundColor Red

return
}

$deviceCode = $deviceCodeResponse.Content | ConvertFrom-Json

# Display the user instructions returned by the endpoint (verification URL and user code)
Write-Host $deviceCode.message -ForegroundColor Cyan

$pollingInterval = [int]$deviceCode.interval
$pollingStopwatch = New-Object System.Diagnostics.Stopwatch
$pollingStopwatch.Start()

do {
Start-Sleep -Seconds $pollingInterval

# Poll the token endpoint to check whether the user completed the sign-in. We use
# -ReturnErrorResponse so that the helper surfaces the HTTP 400 error body instead of swallowing
# it - the device code flow relies on the authorization_pending / slow_down error codes to drive
# the polling loop. Proxy detection is handled centrally by Invoke-WebRequestWithProxyDetection.
$redeemDeviceCodeParams = @{
Uri = "$AzureADEndpoint/organizations/oauth2/v2.0/token"
Method = "POST"
ContentType = "application/x-www-form-urlencoded"
Body = @{
client_id = $ClientId
grant_type = "urn:ietf:params:oauth:grant-type:device_code"
device_code = $deviceCode.device_code
}
UseBasicParsing = $true
}
$redeemDeviceCodeResponse = Invoke-WebRequestWithProxyDetection -ParametersObject $redeemDeviceCodeParams -ReturnErrorResponse

if (($null -ne $redeemDeviceCodeResponse) -and
($redeemDeviceCodeResponse.StatusCode -eq 200)) {
$tokens = $redeemDeviceCodeResponse.Content | ConvertFrom-Json
$idTokenPayload = (Convert-JsonWebTokenToObject $tokens.id_token).Payload
$connectionSuccessful = $true
Comment on lines +186 to +190

break
}

$errorResponse = $null

if (($null -ne $redeemDeviceCodeResponse) -and
(-not [System.String]::IsNullOrEmpty($redeemDeviceCodeResponse.Content))) {
$errorResponse = $redeemDeviceCodeResponse.Content | ConvertFrom-Json
}

switch ($errorResponse.error) {
"authorization_pending" {
Write-Verbose "Authorization is pending - the user has not completed the sign-in yet"
}
"slow_down" {
Write-Verbose "Polling too fast - increasing the polling interval by 5 seconds"
$pollingInterval += 5
}
"authorization_declined" {
Write-Host "The user declined the sign-in request." -ForegroundColor Red

return
}
"expired_token" {
Write-Host "The device code has expired before the sign-in was completed." -ForegroundColor Red

return
}
"access_denied" {
Write-Host "The sign-in request was denied." -ForegroundColor Red

return
}
default {
Write-Host "Unable to redeem the device code for an access token." -ForegroundColor Red
Write-Verbose "Unexpected error: $($errorResponse.error) - $($errorResponse.error_description)"

return
}
}
} while ($pollingStopwatch.Elapsed.TotalSeconds -lt [int]$deviceCode.expires_in)

if (-not $connectionSuccessful) {
Write-Host "Timed out waiting for the device code sign-in to complete." -ForegroundColor Red
}

return
}
Comment on lines +234 to +239

$codeChallenge = $codeChallengeVerifier.CodeChallenge
$codeVerifier = $codeChallengeVerifier.Verifier

Expand Down Expand Up @@ -171,6 +297,15 @@ function Get-GraphAccessToken {

if ($redeemAuthCodeResponse.StatusCode -eq 200) {
$tokens = $redeemAuthCodeResponse.Content | ConvertFrom-Json

# An id_token is only returned when the "openid" scope was requested. It is required to perform
# the nonce replay check and to read the tenant id, so fail clearly if it is missing.
if ([System.String]::IsNullOrEmpty($tokens.id_token)) {
Write-Host "No id_token was returned - make sure the 'openid' scope is part of the requested scope" -ForegroundColor Red

return
}

$idTokenPayload = (Convert-JsonWebTokenToObject $tokens.id_token).Payload

Write-Verbose "Script nonce: '$nonce' - Returned nonce: '$($idTokenPayload.nonce)'"
Expand Down
Loading