mirror of
https://github.com/NetSPI/MicroBurst
synced 2026-06-08 12:01:29 +00:00
7ace69a7b4
Made corrections for the updates to scm hostnames. Added support (-rbac) for using management tokens instead of publishing credentials.
264 lines
13 KiB
PowerShell
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
|
|
}
|
|
|