Files
NetSPI-MicroBurst/Az/Invoke-AzAppServicesKuduDebug.ps1
Karl Fosaaen 7ace69a7b4 Update Invoke-AzAppServicesKuduDebug.ps1
Made corrections for the updates to scm hostnames.
Added support (-rbac) for using management tokens instead of publishing credentials.
2025-07-09 11:09:44 -07:00

264 lines
13 KiB
PowerShell

<#
File: Invoke-AzAppServicesKuduDebug.ps1
Author: Karl Fosaaen (@kfosaaen), NetSPI - 2024
Description: PowerShell function for running commands against a Windows Container App Services host, via the Kudu Debug Console (PowerShell or CMD) Shell
#>
Function Invoke-AzAppServicesKuduDebug {
<#
.SYNOPSIS
Runs a command against a Windows Container App Services host via the Kudu Debug Console (PowerShell or CMD Shell).
.DESCRIPTION
This function will run a command against a Windows Container App Services host. This can aid in enumerating environment variables for secrets, obtaining Managed Identities tokens at scale, or searching file systems for configuration files.
.PARAMETER command
The command to run against the host.
.PARAMETER appName
The name of the application to target.
.PARAMETER Username
The username for connecting to host. The script will attempt to fetch a username from the publishing profile if not provided.
.PARAMETER password
The Password for connecting to the host. The script will attempt to fetch a password from the publishing profile if not provided.
.EXAMPLE
To run against a single host with the Username and Password
PS C:\MicroBurst> Invoke-AzAppServicesKuduDebug -Verbose -command "dir D:\home" -AppName "Target-App" -Username "`$Target-App" -Password "[redacted]"
.EXAMPLE
To run as the authenticated Az PowerShell user and select multiple hosts to run commands against
PS C:\MicroBurst> Invoke-AzAppServicesKuduDebug -Verbose -command "dir D:\home"
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false,
HelpMessage="Subscription to use.")]
[string]$Subscription = "",
[Parameter(Mandatory=$true,
HelpMessage="The command to run.")]
[string]$Command = "",
[Parameter(Mandatory=$false,
HelpMessage="The name of the App to target.")]
[string]$AppName = "",
[Parameter(Mandatory=$false,
HelpMessage="The type of prompt to use.")]
[ValidateSet("powershell","CMD")]
[string]$PromptType = "powershell",
[Parameter(Mandatory=$false,
HelpMessage="The username for connecting to the App Service. The script will attempt to fetch a username from the publishing profile if not provided.")]
[string]$Username = "",
[Parameter(Mandatory=$false,
HelpMessage="The password for connecting to the App Service. The script will attempt to fetch a password from the publishing profile if not provided.")]
[string]$Password = "",
[Parameter(Mandatory=$false,
HelpMessage="The flag for using your existing user's RBAC role permissions to execute the command. Generates a management token to use against the Kudu APIs")]
[switch]$rbac
)
# If no User/Pass provided
if(($Username -eq "") -and ($Password -eq "")){
# 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) {
if ($rbac){Invoke-AzAppServicesKuduDebug -Subscription $sub -Command $Command -AppName $AppName -Username $Username -Password $Password -PromptType $PromptType -rbac}
else{Invoke-AzAppServicesKuduDebug -Subscription $sub -Command $Command -AppName $AppName -Username $Username -Password $Password -PromptType $PromptType}
}
break
}
Write-Verbose "Logged In as $accountName"
# List Apps in the subscription, pipe out to gridview selection
$ProgressPreference = "SilentlyContinue"
$subName = (Get-AzSubscription -SubscriptionId $Subscription).Name
Write-Verbose "Enumerating running App Services Applications with Windows Containers in the `"$subName`" Subscription"
# Filter on App Services Container type - This funciton only supports Windows images
$appsList = Get-AzWebApp | where state -EQ "Running" | where Kind -NotMatch "linux" | select Name,ResourceGroup,Kind | sort Name
$appChoice = $appsList | out-gridview -Title "Select One or More Applications" -PassThru
if($null -eq $appChoice){Write-Verbose "No Apps Selected"; break}
# For each app, grab the publish profile
foreach ($app in $appChoice){
$appObject = Get-AzWebApp -Name $app.Name
if(!$rbac){
Write-Verbose "`tAttempting to run the `"$Command`" command (via $PromptType) on the container for the `"$($app.Name)`" application using publishing credentials"
try{
[xml]$publishProfile = Get-AzWebAppPublishingProfile -Name $appObject.Name -ResourceGroupName $appObject.ResourceGroup
#They should all be the same so we can just grab the first
if($publishProfile){
$Username = $publishProfile.publishData.publishProfile[0].userName
$Password = $publishProfile.publishData.publishProfile[0].userPWD
}
}
catch{
Write-Error "$AppName - Either no publishing credentials were available or you have insufficient permissions"
break
}
Invoke-AzAppServKuduCMDExec -command $Command -username $Username -password $Password -appName $appObject.Name -PromptType $PromptType -AppHost $($appObject.EnabledHostNames | Where-Object {$_ -like "*.scm.*"})
}
else{
Write-Verbose "`tAttempting to run the `"$Command`" command (via $PromptType) on the container for the `"$($app.Name)`" application using RBAC permissions"
Invoke-AzAppServKuduCMDExec -command $Command -appName $appObject.Name -PromptType $PromptType -AppHost $($appObject.EnabledHostNames | Where-Object {$_ -like "*.scm.*"}) -rbac
}
}
Write-Verbose "App Services Command Execution Completed in the `"$subName`" Subscription"
}
elseif(($AppName -eq "") -or ($Username -eq "") -or ($Password -eq "")){
Write-Host "If publish profile username and password are in use, AppName is a required parameter. Check your parameters."
break
}
elseif($rbac){
# Run the command with RBAC
Write-Verbose "`tAttempting to run the `"$Command`" command (via $PromptType) on the container for the $AppName application using RBAC permissions"
Invoke-AzAppServKuduCMDExec -command $Command -username $Username -password $Password -appName $AppName -PromptType $PromptType -rbac
}
else{
# Run the command with User/Pass
Write-Verbose "`tAttempting to run the `"$Command`" command (via $PromptType) on the container for the $AppName application using publishing credentials"
Invoke-AzAppServKuduCMDExec -command $Command -username $Username -password $Password -appName $AppName -PromptType $PromptType
}
}
Function Invoke-AzAppServKuduCMDExec {
<#
Supporting Function
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true,
HelpMessage="The command to run.")]
[string]$command = "",
[Parameter(Mandatory=$false,
HelpMessage="The name of the App to target.")]
[string]$appName = "",
[Parameter(Mandatory=$false,
HelpMessage="The SCM hostname of the App to target.")]
[string]$AppHost = "",
[Parameter(Mandatory=$false,
HelpMessage="The type of prompt to use.")]
[string]$PromptType = "powershell",
[Parameter(Mandatory=$false,
HelpMessage="The username for connecting to the App Service. The script will attempt to fetch a username from the publishing profile if not provided.")]
[string]$username = "",
[Parameter(Mandatory=$false,
HelpMessage="The password for connecting to the App Service. The script will attempt to fetch a password from the publishing profile if not provided.")]
[string]$password = "",
[Parameter(Mandatory=$false,
HelpMessage="The flag for using your existing user's RBAC role permissions to execute the command. Generates a management token to use against the Kudu APIs")]
[switch]$rbac
)
if($rbac){
$mgmtAccessToken = Get-AzAccessToken -ResourceUrl "https://management.azure.com/"
if ($mgmtAccessToken.Token -is [System.Security.SecureString]) {
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($mgmtAccessToken.Token)
try {
$mgmtToken = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)
} finally {
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
}
} else {
$mgmtToken = $mgmtAccessToken.Token
}
$authHeader = @{Authorization="Bearer $mgmtToken"}
}
else{
# Convert these to a basic authentication header
$basicHeader = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes((-join($username,":",$password))))
$authHeader = @{Authorization="Basic $basicHeader"}
}
$tid = get-random -Minimum 0 -Maximum 10
$timeStamp = Get-Date -UFormat %s -Millisecond 0
# Send the Negotiate Request
$cmdReq = Invoke-WebRequest -Verbose:$false -Method Get -Uri (-join ("https://",$AppHost,"/api/commandstream/negotiate?clientProtocol=1.4&shell=$promptType&_=0")) -Headers $authHeader
$cmdResult = ($cmdReq.Content | ConvertFrom-Json)
Add-Type -AssemblyName System.Web
$connectionToken = ([System.Web.HttpUtility]::UrlPathEncode($cmdResult.ConnectionToken)).Replace('+',"%2b")
# Send the Message ID Request
$cmdLongPollReq = Invoke-WebRequest -Verbose:$false -Method Get -Uri (-join ("https://",$AppHost,"/api/commandstream/connect?transport=longPolling&clientProtocol=1.4&shell=$promptType&connectionToken=$connectionToken&tid=$tid&_=$timeStamp")) -Headers $authHeader -ContentType 'application/json; charset=UTF-8'
$messageId = ($cmdLongPollReq.Content |ConvertFrom-Json).C
# Start the command stream
$cmdSendReq = Invoke-WebRequest -Verbose:$false -Method Get -Uri (-join ("https://",$AppHost,"/api/commandstream/start?transport=longPolling&clientProtocol=1.4&shell=$promptType&connectionToken=$connectionToken")) -Headers $authHeader -ContentType 'application/json; charset=UTF-8'
# Send the Send Command Request
$postParams = @{data=$(-join($command,"`n"))}
$cmdSendReq = Invoke-WebRequest -Verbose:$false -Method Post -Uri (-join ("https://",$AppHost,"/api/commandstream/send?transport=longPolling&clientProtocol=1.4&shell=$promptType&connectionToken=$connectionToken")) -Body $postParams -Headers $authHeader -ContentType 'application/x-www-form-urlencoded; charset=UTF-8'
# Send the Poll Request
$mValueNext = ""
$messageIdCurrent = $messageId
$outputString = ""
while(1)
{
# Grab random TID and set epoch timestamp
$tid = get-random -Minimum 0 -Maximum 10
$timeStamp = Get-Date -UFormat %s -Millisecond 0
# Try the poll request, fail on timeout
try{
$cmdPollReq = Invoke-WebRequest -Verbose:$false -Method Get -Uri (-join ("https://",$AppHost,"/api/commandstream/poll?transport=longPolling&messageId=$messageIdCurrent&clientProtocol=1.4&shell=$promptType&connectionToken=$connectionToken&tid=$tid&_=$timeStamp")) -Headers $authHeader -TimeoutSec 2 -ErrorAction Continue
}
Catch{}
# Capture next message ID
$mValueNext = ($cmdPollReq.Content | ConvertFrom-Json).C
# If new message ID matches the current/old one, then break
if($mValueNext -eq $messageIdCurrent){
break
}
else{$messageIdCurrent = $mValueNext}
# Write output
$outputString += ($cmdPollReq.Content | ConvertFrom-Json).M.Output
}
# end the session on the container
$cmdAbortReq = Invoke-WebRequest -Verbose:$false -Method Post -Uri (-join ("https://",$AppHost,"/api/commandstream/abort?transport=longPolling&clientProtocol=1.4&shell=$promptType&connectionToken=$connectionToken")) -Headers $authHeader -ContentType 'application/json; charset=UTF-8'
Write-Host "`nOutput from the `"$appName`" Command Execution:"
$outputString
}