Skip to content

Commit 9791b5b

Browse files
authored
Add files via upload
1 parent 162725a commit 9791b5b

1 file changed

Lines changed: 242 additions & 60 deletions

File tree

Lines changed: 242 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,242 @@
1-
# Replace with actual values
2-
# dsregcmd /status does the job
3-
#
4-
# Run as so: & "C:\Users\mcontestabile\foobar\get_az_token.ps1"
5-
# if running script from another folder.
6-
#
7-
# If any of the calls using the Resource Owner Password Credentials (ROPC) flow succeed and return an access token, that strongly suggests that:
8-
#
9-
# MFA is not enforced for that user or app.
10-
# The app registration is configured to allow public clients.
11-
# The user account is not blocked by Conditional Access policies that would prevent password-based login.
12-
#
13-
$tenantId = "???"
14-
$username = "???"
15-
$password = "???"
16-
$VerbosePreference = "Continue" # Enable verbose output for debugging
17-
$ErrorActionPreference = "Continue" #prevents non-fatal errors from halting the script.
18-
19-
# Define Token Endpoint
20-
$tokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
21-
22-
# Get all client IDs using Azure CLI
23-
$clientIds = az ad app list --all --query "[].appId" --output tsv
24-
25-
# Convert output into an array
26-
$clientIdList = $clientIds -split "`n"
27-
28-
29-
# Loop through each Client ID and get the access token
30-
foreach ($clientId in $clientIdList) {
31-
Write-Output "Fetching token for Client ID: $clientId"
32-
33-
# Create the body for the authentication request
34-
$body = @{
35-
client_id = $clientId
36-
scope = "https://management.azure.com/.default"
37-
grant_type = "password"
38-
username = $username
39-
password = $password
40-
}
41-
42-
# Send request to obtain access token
43-
try {
44-
Write-Host "Sending request to: $tokenUrl" -ForegroundColor Cyan
45-
Write-Host "Request Body: $($body | Out-String)" -ForegroundColor Magenta
46-
$response = Invoke-RestMethod -Method Post -Uri $tokenUrl -Body $body -ContentType "application/x-www-form-urlencoded"
47-
Write-Host "Raw API Response:" -ForegroundColor Blue
48-
Write-Host ($response | ConvertTo-Json -Depth 3) -ForegroundColor Blue
49-
$accessToken = $response.access_token
50-
Write-Host ("Access Token for Client ID: " + $accessToken) -ForegroundColor Red
51-
} catch {
52-
Write-Host "Failed to get token for Client ID: $clientId" -ForegroundColor White
53-
Write-Host "Error Details: $($_.Exception.Message)" -ForegroundColor White
54-
#Write-Host "Full Exception: $($_ | ConvertTo-Json -Depth 100)" -ForegroundColor White
55-
#Write-Output "Full Exception Details:"
56-
#$_.Exception | Format-List -Property *
57-
continue # Ensures the loop moves to the next iteration
58-
}
59-
60-
}
1+
#requires -Version 7.0
2+
<#
3+
.SYNOPSIS
4+
Performs a controlled Microsoft Entra ROPC authentication test against one
5+
explicitly authorized public-client application.
6+
7+
.DESCRIPTION
8+
Sends one Resource Owner Password Credentials (ROPC) token request for a
9+
designated nonprivileged test account, client application, and OAuth scope.
10+
The script does not enumerate tenant applications, does not store a password
11+
in the file, and does not write access or refresh tokens to disk.
12+
13+
ROPC is deprecated and incompatible with interactive MFA. Use this script
14+
only in an authorized test tenant or approved assessment scope.
15+
16+
.PARAMETER TenantId
17+
Microsoft Entra tenant GUID or verified tenant domain, for example:
18+
00000000-0000-0000-0000-000000000000 or contoso.onmicrosoft.com
19+
20+
.PARAMETER Username
21+
User principal name of a dedicated, nonprivileged test account.
22+
23+
.PARAMETER ClientId
24+
Application (client) ID of one explicitly authorized public-client app.
25+
26+
.PARAMETER Scope
27+
OAuth scope to request. The default requests Microsoft Graph delegated
28+
permissions already granted to the client by using .default.
29+
30+
.PARAMETER OutputPath
31+
Optional CSV path for the sanitized test result. Tokens and passwords are
32+
never included in the report.
33+
34+
.PARAMETER AcknowledgeAuthorizedTesting
35+
Required safety switch confirming that the test is authorized.
36+
37+
.EXAMPLE
38+
./Test-EntraRopcControl.ps1 `
39+
-TenantId '00000000-0000-0000-0000-000000000000' `
40+
-Username 'ropc-test@contoso.onmicrosoft.com' `
41+
-ClientId '11111111-1111-1111-1111-111111111111' `
42+
-AcknowledgeAuthorizedTesting
43+
44+
.NOTES
45+
Recommended test design:
46+
- Use a dedicated, nonprivileged test account.
47+
- Use one application that is owned and approved for testing.
48+
- Run during an approved assessment window.
49+
- Review the corresponding Microsoft Entra sign-in log.
50+
- Rotate the test password after the assessment.
51+
#>
52+
53+
[CmdletBinding()]
54+
param(
55+
[Parameter(Mandatory)]
56+
[ValidateNotNullOrEmpty()]
57+
[string]$TenantId,
58+
59+
[Parameter(Mandatory)]
60+
[ValidatePattern('^[^@\s]+@[^@\s]+\.[^@\s]+$')]
61+
[string]$Username,
62+
63+
[Parameter(Mandatory)]
64+
[ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')]
65+
[string]$ClientId,
66+
67+
[Parameter()]
68+
[ValidateNotNullOrEmpty()]
69+
[string]$Scope = 'https://graph.microsoft.com/.default',
70+
71+
[Parameter()]
72+
[string]$OutputPath = (Join-Path $PWD 'EntraRopcControlTest.csv'),
73+
74+
[Parameter(Mandatory)]
75+
[switch]$AcknowledgeAuthorizedTesting
76+
)
77+
78+
Set-StrictMode -Version Latest
79+
$ErrorActionPreference = 'Stop'
80+
81+
if (-not $AcknowledgeAuthorizedTesting) {
82+
throw 'Authorized-testing acknowledgement is required.'
83+
}
84+
85+
Write-Warning 'ROPC is deprecated and directly handles a password. Use only an approved nonprivileged test account and application.'
86+
87+
$securePassword = Read-Host -Prompt "Enter the password for authorized test account $Username" -AsSecureString
88+
$passwordPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword)
89+
$plainPassword = $null
90+
91+
$tokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
92+
$timestamp = Get-Date
93+
$correlationId = [guid]::NewGuid().Guid
94+
95+
try {
96+
$plainPassword = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($passwordPointer)
97+
98+
$body = @{
99+
client_id = $ClientId
100+
grant_type = 'password'
101+
username = $Username
102+
password = $plainPassword
103+
scope = $Scope
104+
}
105+
106+
$headers = @{
107+
'client-request-id' = $correlationId
108+
'return-client-request-id' = 'true'
109+
}
110+
111+
try {
112+
$response = Invoke-RestMethod `
113+
-Method Post `
114+
-Uri $tokenEndpoint `
115+
-ContentType 'application/x-www-form-urlencoded' `
116+
-Headers $headers `
117+
-Body $body `
118+
-ErrorAction Stop
119+
120+
$result = [pscustomobject]@{
121+
TimestampUtc = $timestamp.ToUniversalTime().ToString('o')
122+
Tenant = $TenantId
123+
Username = $Username
124+
ClientId = $ClientId
125+
Scope = $Scope
126+
Outcome = 'TokenIssued'
127+
HttpStatus = 200
128+
EntraError = $null
129+
EntraErrorCodes = $null
130+
Suberror = $null
131+
TraceId = $null
132+
CorrelationId = $correlationId
133+
MfaInterpretation = 'The tested request completed without an interactive MFA challenge. Review Conditional Access and sign-in logs before drawing broader conclusions.'
134+
TokenReturned = -not [string]::IsNullOrWhiteSpace([string]$response.access_token)
135+
}
136+
137+
Write-Host 'TOKEN ISSUED: The authorized ROPC test returned an access token.' -ForegroundColor Red
138+
Write-Host 'Review the corresponding Entra sign-in log and applicable Conditional Access policies.' -ForegroundColor Yellow
139+
}
140+
catch {
141+
$statusCode = $null
142+
$responseText = $null
143+
144+
if ($null -ne $_.Exception.Response) {
145+
try { $statusCode = [int]$_.Exception.Response.StatusCode } catch { }
146+
}
147+
148+
if ($null -ne $_.ErrorDetails -and -not [string]::IsNullOrWhiteSpace($_.ErrorDetails.Message)) {
149+
$responseText = $_.ErrorDetails.Message
150+
}
151+
152+
$errorPayload = $null
153+
if (-not [string]::IsNullOrWhiteSpace($responseText)) {
154+
try { $errorPayload = $responseText | ConvertFrom-Json -ErrorAction Stop } catch { }
155+
}
156+
157+
$entraError = if ($null -ne $errorPayload) { [string]$errorPayload.error } else { 'RequestFailed' }
158+
$description = if ($null -ne $errorPayload) { [string]$errorPayload.error_description } else { $_.Exception.Message }
159+
$errorCodes = if ($null -ne $errorPayload -and $null -ne $errorPayload.error_codes) {
160+
@($errorPayload.error_codes) -join ';'
161+
}
162+
else {
163+
$null
164+
}
165+
$suberrorProperty = if ($null -ne $errorPayload) {
166+
$errorPayload.PSObject.Properties['suberror']
167+
}
168+
else {
169+
$null
170+
}
171+
172+
$suberror = if ($null -ne $suberrorProperty) {
173+
[string]$suberrorProperty.Value
174+
}
175+
else {
176+
$null
177+
}
178+
$traceId = if ($null -ne $errorPayload -and $null -ne $errorPayload.trace_id) { [string]$errorPayload.trace_id } else { $null }
179+
$serverCorrelationId = if ($null -ne $errorPayload -and $null -ne $errorPayload.correlation_id) {
180+
[string]$errorPayload.correlation_id
181+
}
182+
else {
183+
$correlationId
184+
}
185+
186+
$mfaInterpretation = if ($errorCodes -split ';' -contains '50076') {
187+
'Expected secure outcome for this test: MFA was required and ROPC could not satisfy the challenge.'
188+
}
189+
elseif ($errorCodes -split ';' -contains '50079') {
190+
'MFA enrollment or stronger authentication was required; ROPC could not complete the interactive requirement.'
191+
}
192+
else {
193+
'The token request failed. Review the Entra error code and sign-in log; HTTP status alone is not sufficient for classification.'
194+
}
195+
196+
$result = [pscustomobject]@{
197+
TimestampUtc = $timestamp.ToUniversalTime().ToString('o')
198+
Tenant = $TenantId
199+
Username = $Username
200+
ClientId = $ClientId
201+
Scope = $Scope
202+
Outcome = 'TokenDenied'
203+
HttpStatus = $statusCode
204+
EntraError = $entraError
205+
EntraErrorCodes = $errorCodes
206+
Suberror = $suberror
207+
TraceId = $traceId
208+
CorrelationId = $serverCorrelationId
209+
MfaInterpretation = $mfaInterpretation
210+
TokenReturned = $false
211+
}
212+
213+
if ($errorCodes -split ';' -contains '50076') {
214+
Write-Host 'EXPECTED: MFA was required and the ROPC request was blocked (AADSTS50076).' -ForegroundColor Green
215+
}
216+
else {
217+
Write-Host "TOKEN DENIED: $entraError | Error code(s): $errorCodes" -ForegroundColor Yellow
218+
}
219+
Write-Host $description -ForegroundColor DarkGray
220+
}
221+
222+
$outputDirectory = Split-Path -Parent $OutputPath
223+
if (-not [string]::IsNullOrWhiteSpace($outputDirectory)) {
224+
New-Item -Path $outputDirectory -ItemType Directory -Force | Out-Null
225+
}
226+
227+
$result | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding utf8
228+
Write-Host "Sanitized result saved to: $OutputPath" -ForegroundColor Cyan
229+
}
230+
finally {
231+
if ($null -ne $plainPassword) {
232+
$plainPassword = $null
233+
}
234+
235+
if ($passwordPointer -ne [IntPtr]::Zero) {
236+
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordPointer)
237+
}
238+
239+
Remove-Variable securePassword -ErrorAction SilentlyContinue
240+
Remove-Variable body -ErrorAction SilentlyContinue
241+
[GC]::Collect()
242+
}

0 commit comments

Comments
 (0)