npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
NIST CSF 2.0
Overview
Adversaries increasingly bypass MFA not by defeating it but by stealing the artifacts issued after a successful authentication — session cookies, OAuth access/refresh tokens, and Primary Refresh Tokens (PRTs). With a stolen token an attacker replays the existing session ("pass-the-cookie" / token replay), inheriting the victim's authenticated state across federated SaaS without ever prompting for credentials or MFA. Mandiant's M-Trends reporting and Microsoft/Okta incident data both highlight token theft as a dominant cloud lateral-movement technique, mapped to MITRE ATT&CK T1550.001 Use Alternate Authentication Material: Application Access Token.
Detection relies on correlating identity telemetry rather than watching for failed logins. In Microsoft Entra ID the key tables are SigninLogs (interactive), AADNonInteractiveUserSignInLogs (where replayed cookies/refresh tokens commonly surface), and AADServicePrincipalSignInLogs. Entra now exposes linkable identifiers — SessionId and UniqueTokenIdentifier — that let a hunter stitch every artifact derived from one root authentication event together and spot a single session being used from multiple IPs, ASNs, or device fingerprints. In Okta the System Log carries authentication.sso, policy.evaluate_sign_on, and user.session.start events with a deviceToken/session context; the same session token appearing from divergent IPs/user-agents is the tell. Okta Identity Threat Protection (ITP) can natively flag "suspected session hijacking."
This skill provides a hypothesis-driven hunt: baseline normal session behavior, then look for impossible travel within a single session, refresh-token reuse, token use from anomalous infrastructure (hosting/VPS ASNs), and SaaS access patterns inconsistent with the user's device. Source: MITRE ATT&CK T1550.001; Microsoft Entra ID sign-in log documentation; Okta System Log reference; Mandiant M-Trends.
When to Use
- Threat hunting for MFA-bypass via stolen tokens/cookies across Entra ID and SaaS
- Investigating an alert for impossible travel, anomalous OAuth grant, or token reuse
- Validating detection coverage for T1550.001 after a phishing/AiTM incident
- Building Sentinel/Splunk/Okta detections for session-token replay
- Post-incident hunting to scope SaaS lateral movement from a compromised identity
Prerequisites
- Entra ID sign-in logs flowing to a queryable store (Microsoft Sentinel / Log Analytics):
# Confirm the diagnostic settings export SigninLogs + non-interactive logs to a workspace az monitor diagnostic-settings list --resource \ /providers/Microsoft.aadiam/diagnosticSettings -o table - Okta System Log access via API or SIEM ingestion:
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \ "https://<org>.okta.com/api/v1/logs?filter=eventType eq \"user.session.start\"&since=2026-06-01T00:00:00Z" - An IP enrichment source (GeoIP + ASN/hosting-provider classification)
- Read access to the SIEM (KQL for Sentinel, SPL for Splunk)
- Python 3.9+ for the helper script (
requestsfor the Okta API)
Objectives
- Baseline normal per-user session behavior (IPs, ASNs, devices, SaaS apps)
- Correlate Entra sign-in artifacts by
SessionId/UniqueTokenIdentifier - Detect a single session used from multiple IPs/ASNs (token replay)
- Detect impossible travel within one authenticated session
- Detect refresh-token reuse and anomalous OAuth grants
- Hunt Okta System Log for reused session tokens across contexts
- Produce findings and feed confirmed patterns into standing detections
MITRE ATT&CK Mapping
| ID | Name | Use in this skill |
|---|---|---|
| T1550.001 | Use Alternate Authentication Material: Application Access Token | Core technique — replaying stolen OAuth tokens/cookies |
| T1539 | Steal Web Session Cookie | The cookie theft that precedes pass-the-cookie replay |
| T1528 | Steal Application Access Token | Acquisition of OAuth tokens via phishing/illicit consent |
| T1078.004 | Valid Accounts: Cloud Accounts | Replayed tokens grant valid-account access to SaaS |
| T1098.001 | Account Manipulation: Additional Cloud Credentials | Follow-on persistence after token abuse |
Workflow
1. Correlate Entra sign-in artifacts by session
Stitch interactive, non-interactive, and SP sign-ins for one session to see the full chain.
union SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(7d)
| where isnotempty(SessionId)
| summarize IPs=make_set(IPAddress), Apps=make_set(AppDisplayName),
Locations=make_set(tostring(LocationDetails.countryOrRegion)),
Count=count() by SessionId, UserPrincipalName
| where array_length(IPs) > 12. Detect a single session used from multiple ASNs (token replay)
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(24h)
| extend ASN = tostring(parse_json(tostring(NetworkLocationDetails))[0].networkType)
| summarize distinctIPs = dcount(IPAddress),
ipset = make_set(IPAddress) by SessionId, UserPrincipalName
| where distinctIPs >= 23. Detect impossible travel within one authenticated session
SigninLogs
| where TimeGenerated > ago(7d)
| project TimeGenerated, UserPrincipalName, IPAddress,
City=tostring(LocationDetails.city),
Country=tostring(LocationDetails.countryOrRegion), SessionId
| order by UserPrincipalName, TimeGenerated asc
| serialize
| extend prevCountry = prev(Country), prevTime = prev(TimeGenerated),
prevUser = prev(UserPrincipalName)
| where UserPrincipalName == prevUser and Country != prevCountry
and datetime_diff('minute', TimeGenerated, prevTime) < 604. Detect token use from hosting/VPS infrastructure
Replayed tokens are frequently used from datacenter ASNs, unlike the user's residential/corporate ranges.
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| extend asnOrg = tostring(parse_json(tostring(AutonomousSystemNumber)))
| where IPAddress in (toscalar(externaldata(ip:string)["<hosting-asn-iplist>"]))
| project TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress5. Hunt anomalous OAuth grants / illicit consent (token theft precursor)
AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName in ("Consent to application", "Add OAuth2PermissionGrant",
"Add delegated permission grant")
| extend app = tostring(TargetResources[0].displayName)
| project TimeGenerated, InitiatedBy, app, Result6. Hunt the Okta System Log for reused session tokens
A single Okta session (deviceToken) used from divergent IPs/clients indicates hijack.
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
"https://<org>.okta.com/api/v1/logs?filter=eventType eq \"policy.evaluate_sign_on\"&since=2026-06-15T00:00:00Z" \
| jq -r '.[] | [.authenticationContext.externalSessionId, .client.ipAddress, .client.userAgent.rawUserAgent] | @tsv' \
| sort | uniq -c | sort -rn7. Splunk equivalent for Okta session reuse
index=okta eventType="policy.evaluate_sign_on"
| stats dc(client.ipAddress) as ip_count
values(client.ipAddress) as ips
values(client.userAgent.rawUserAgent) as agents
by authenticationContext.externalSessionId actor.alternateId
| where ip_count > 18. Triage and respond
For confirmed token abuse, revoke sessions and rotate, then promote the hunt to a rule.
# Revoke all refresh tokens / sessions for the user in Entra
az rest --method POST \
--url "https://graph.microsoft.com/v1.0/users/<userId>/revokeSignInSessions"See scripts/agent.py to pull Okta logs and flag reused session tokens automatically.
Tools and Resources
| Resource | Purpose | Link |
|---|---|---|
| MITRE ATT&CK T1550.001 | Technique reference | https://attack.mitre.org/techniques/T1550/001/ |
| Entra sign-in logs schema | KQL hunting field reference | https://learn.microsoft.com/en-us/entra/identity/monitoring-health/reference-azure-monitor-sign-ins-log-schema |
| Azure-Sentinel hunting repo | Community KQL detections | https://github.com/Azure/Azure-Sentinel |
| Okta System Log API | Event hunting source | https://developer.okta.com/docs/reference/api/system-log/ |
| Mandiant M-Trends | Token-theft threat landscape | https://www.mandiant.com/m-trends |
| AzureAD-Attack-Defense | PRT/token replay detection guidance | https://github.com/Cloud-Architekt/AzureAD-Attack-Defense |
Preventive Controls to Recommend
Detection should pair with controls that make stolen tokens far less useful:
- Entra Conditional Access "token protection" binds the sign-in session to the device, so an exfiltrated cookie/PRT cannot be replayed off-device.
- Continuous Access Evaluation (CAE) revokes access in near-real-time on risk events instead of waiting for token expiry.
- Phishing-resistant MFA (FIDO2/passkeys) blocks the AiTM proxy phishing that harvests tokens in the first place.
- Short token lifetimes + refresh-token rotation shrink the replay window and turn refresh-token reuse into an unambiguous compromise signal.
- Okta Identity Threat Protection (ITP) flags suspected session hijacking natively.
False-Positive Tuning
| Benign cause | Tuning |
|---|---|
| Corporate VPN/proxy egress (many users, few IPs) | Allowlist known egress IPs/ASNs |
| Mobile carrier IP rotation | Widen impossible-travel time/distance thresholds |
| Legitimate multi-device users | Correlate device IDs, not just IPs |
| Backend/API calls within one session | Exclude expected service principals |
Key Indicators
| Indicator | Signal |
|---|---|
One SessionId across multiple IPs/ASNs |
Token/cookie replay |
| Non-interactive sign-in from new datacenter IP | Replayed refresh token |
| Impossible travel within < 1h | Concurrent session use |
| Refresh-token reuse after rotation | Strong compromise signal |
| New OAuth consent to unfamiliar app | Illicit-consent token theft |
| Okta session token from divergent user-agents | Session hijack |
Validation Criteria
- Entra
SigninLogsandAADNonInteractiveUserSignInLogsqueryable - Okta System Log accessible via API or SIEM
- Per-session correlation by
SessionIdproduces results - Multi-IP / multi-ASN single-session query implemented
- Impossible-travel-within-session query implemented
- Anomalous OAuth consent hunt implemented
- Okta reused-session-token hunt implemented
- Confirmed findings triaged and sessions revoked
- Effective queries promoted to standing detection rules
- False-positive baseline (VPN/proxy egress) documented
References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 2
api-reference.md2.2 KB
SSO Token-Abuse Hunting — Data Source Reference
Microsoft Entra ID (Sentinel / Log Analytics) Tables
| Table | Description |
|---|---|
SigninLogs |
Interactive user sign-ins |
AADNonInteractiveUserSignInLogs |
Non-interactive sign-ins; common surface for replayed cookies/refresh tokens |
AADServicePrincipalSignInLogs |
Service-principal sign-ins |
AuditLogs |
Directory changes incl. OAuth consent / permission grants |
AADManagedIdentitySignInLogs |
Managed-identity authentications |
Key correlation fields
| Field | Meaning |
|---|---|
SessionId |
Linkable identifier joining all artifacts from one root auth event |
UniqueTokenIdentifier |
Per-token identifier; correlate issuance to usage |
IPAddress / LocationDetails |
Source IP and GeoIP for impossible-travel/ASN checks |
AppDisplayName / ResourceDisplayName |
Which SaaS app/resource the token accessed |
ConditionalAccessStatus |
Whether CA applied (notApplied can indicate replay) |
ResultType |
0 = success |
Okta System Log API
GET https://<org>.okta.com/api/v1/logs with header Authorization: SSWS <token>.
| Parameter | Description |
|---|---|
filter |
SCIM filter, e.g. eventType eq "policy.evaluate_sign_on" |
since / until |
ISO-8601 time bounds |
q |
Free-text search |
limit |
Page size |
Key event types and fields
| Item | Meaning |
|---|---|
user.session.start |
New session created |
policy.evaluate_sign_on |
Sign-on policy evaluation (per-access) |
authentication.sso |
SSO into a downstream app |
authenticationContext.externalSessionId |
Session identifier for reuse detection |
client.ipAddress / client.userAgent.rawUserAgent |
Source context for divergence checks |
actor.alternateId |
The user |
Response / Remediation
| Action | Command/API |
|---|---|
| Revoke Entra sessions | POST https://graph.microsoft.com/v1.0/users/{id}/revokeSignInSessions |
| Clear Okta user sessions | DELETE https://<org>.okta.com/api/v1/users/{id}/sessions |
| Enforce token protection | Entra Conditional Access "Require token protection for sign-in sessions" |
standards.md1.4 KB
Standards and Framework Mapping
NIST Cybersecurity Framework 2.0
| ID | Name | Rationale |
|---|---|---|
| DE.CM-01 | Networks and network services are monitored to find potentially adverse events | Hunting SSO/OAuth token replay across identity logs is continuous monitoring of authentication services for adverse events. |
MITRE ATT&CK (Enterprise / Cloud)
| ID | Name | Rationale |
|---|---|---|
| T1550.001 | Use Alternate Authentication Material: Application Access Token | Core hunted technique — replaying stolen OAuth tokens/cookies. |
| T1539 | Steal Web Session Cookie | The cookie theft that enables pass-the-cookie. |
| T1528 | Steal Application Access Token | Token acquisition via phishing/illicit consent. |
| T1078.004 | Valid Accounts: Cloud Accounts | Replayed tokens provide valid-account SaaS access. |
| T1098.001 | Account Manipulation: Additional Cloud Credentials | Follow-on persistence after token abuse. |
Supporting References
- MITRE ATT&CK T1550.001 — https://attack.mitre.org/techniques/T1550/001/
- Microsoft Entra sign-in log schema — https://learn.microsoft.com/en-us/entra/identity/monitoring-health/reference-azure-monitor-sign-ins-log-schema
- Okta System Log API — https://developer.okta.com/docs/reference/api/system-log/
- Mandiant M-Trends — https://www.mandiant.com/m-trends
- NIST CSF 2.0 — https://www.nist.gov/cyberframework
Scripts 1
agent.py5.2 KB
#!/usr/bin/env python3
"""
SaaS SSO token-abuse hunter (Okta System Log).
Pulls Okta System Log events over the API and flags session tokens
(externalSessionId) that are observed from multiple source IPs or user-agents
within the window — a strong indicator of session-cookie/token replay
(MITRE ATT&CK T1550.001 / pass-the-cookie).
Defensive tool. Requires a read-only Okta API token.
Examples:
export OKTA_ORG=example.okta.com
export OKTA_API_TOKEN=00xxxx
python agent.py --since 2026-06-15T00:00:00Z --min-ips 2
python agent.py --since 2026-06-15T00:00:00Z --event-type user.session.start --json out.json
"""
import argparse
import json
import os
import sys
from collections import defaultdict
try:
import requests
except ImportError:
sys.exit("error: 'requests' required. Install with: pip install requests")
def fetch_logs(org, token, since, event_type, page_limit):
url = f"https://{org}/api/v1/logs"
headers = {"Authorization": f"SSWS {token}", "Accept": "application/json"}
params = {"since": since, "limit": 1000}
if event_type:
params["filter"] = f'eventType eq "{event_type}"'
events = []
pages = 0
while url and pages < page_limit:
try:
resp = requests.get(url, headers=headers, params=params, timeout=60)
except requests.RequestException as exc:
sys.exit(f"error: request failed: {exc}")
if resp.status_code == 401:
sys.exit("error: 401 Unauthorized — check OKTA_API_TOKEN")
if resp.status_code == 429:
sys.exit("error: 429 rate limited — retry later")
if resp.status_code != 200:
sys.exit(f"error: Okta API returned {resp.status_code}: {resp.text[:200]}")
batch = resp.json()
if not batch:
break
events.extend(batch)
params = None # subsequent pages use the full 'next' link
url = None
link = resp.headers.get("link", "")
for part in link.split(","):
if 'rel="next"' in part:
url = part[part.find("<") + 1:part.find(">")]
pages += 1
return events
def analyze(events, min_ips):
sessions = defaultdict(lambda: {"ips": set(), "agents": set(), "users": set(), "count": 0})
for ev in events:
ctx = ev.get("authenticationContext") or {}
sid = ctx.get("externalSessionId")
if not sid or sid == "unknown":
continue
client = ev.get("client") or {}
ua = (client.get("userAgent") or {}).get("rawUserAgent")
actor = ev.get("actor") or {}
rec = sessions[sid]
if client.get("ipAddress"):
rec["ips"].add(client["ipAddress"])
if ua:
rec["agents"].add(ua)
if actor.get("alternateId"):
rec["users"].add(actor["alternateId"])
rec["count"] += 1
findings = []
for sid, rec in sessions.items():
if len(rec["ips"]) >= min_ips or len(rec["agents"]) >= 2:
findings.append({
"session_id": sid,
"users": sorted(rec["users"]),
"distinct_ips": sorted(rec["ips"]),
"distinct_user_agents": sorted(rec["agents"]),
"event_count": rec["count"],
})
findings.sort(key=lambda f: len(f["distinct_ips"]), reverse=True)
return findings
def main():
p = argparse.ArgumentParser(description="Okta SSO token-abuse hunter")
p.add_argument("--org", default=os.environ.get("OKTA_ORG"),
help="Okta org domain, e.g. example.okta.com (or OKTA_ORG)")
p.add_argument("--token", default=os.environ.get("OKTA_API_TOKEN"),
help="Okta API token (or OKTA_API_TOKEN)")
p.add_argument("--since", required=True, help="ISO-8601 start time")
p.add_argument("--event-type", default="policy.evaluate_sign_on",
help="Okta eventType to query")
p.add_argument("--min-ips", type=int, default=2,
help="flag sessions seen from >= this many IPs")
p.add_argument("--max-pages", type=int, default=20, help="max API pages to pull")
p.add_argument("--json", metavar="FILE", help="write findings JSON to file")
args = p.parse_args()
if not args.org or not args.token:
sys.exit("error: provide --org/--token or set OKTA_ORG/OKTA_API_TOKEN")
print(f"[*] fetching Okta '{args.event_type}' events since {args.since} ...")
events = fetch_logs(args.org, args.token, args.since, args.event_type, args.max_pages)
print(f"[+] retrieved {len(events)} events")
findings = analyze(events, args.min_ips)
print(f"[+] {len(findings)} suspicious session(s) (multi-IP / multi-UA)\n")
for f in findings:
print(f" session {f['session_id']} user={','.join(f['users'])}")
print(f" IPs ({len(f['distinct_ips'])}): {', '.join(f['distinct_ips'])}")
print(f" UAs ({len(f['distinct_user_agents'])}): {len(f['distinct_user_agents'])} distinct")
print(f" events: {f['event_count']}")
if args.json:
with open(args.json, "w", encoding="utf-8") as fh:
json.dump(findings, fh, indent=2)
print(f"\n[+] wrote {args.json}")
sys.exit(1 if findings else 0)
if __name__ == "__main__":
main()