Overview
This tutorial shows you how to hunt for IAM privilege escalation activity in an AWS environment using Claude Code and the Elastic MCP server. You query Elasticsearch indexes that contain CloudTrail logs, analyze suspicious IAM API calls, and investigate privilege escalation chains, all through conversational Claude Code sessions.
By the end of this tutorial, you can:
Configure Claude Code with the Elastic MCP server to query Elasticsearch directly
Write Elasticsearch Query DSL expressions targeting high-fidelity IAM privilege escalation patterns
Identify common privilege escalation techniques mapped to MITRE ATT&CK T1098 and T1548
Pivot from an initial indicator to a full principal activity timeline
Encode validated hunting queries as persistent Kibana detection rules
Prerequisites
Before you begin, make sure you have the following:
Software
Claude Code 1.0 or later installed and licensed
Node.js 18 or later (required by the Elastic MCP server)
npm9 or laterElasticsearch 8.x cluster with the Elastic AWS integration configured
AWS configuration
AWS CloudTrail enabled and delivering logs to your Elasticsearch cluster
An organization-level trail covers all regions; account-level trails are region-specific
CloudTrail logs typically land in the index pattern
logs-aws.cloudtrail-*when ingested by the Elastic AWS integration
Read access to the CloudTrail indexes in Elasticsearch (the
monitorandreadcluster privileges, plusreadon the target index pattern)
Knowledge
Basic understanding of AWS IAM concepts: users, roles, policies, and the AWS Security Token Service (STS)
Understanding IAM privilege escalation
IAM privilege escalation occurs when a principal with limited permissions manipulates IAM resources to gain elevated access (often administrative). The table below summarizes the most common techniques, the IAM actions they abuse, and their MITRE ATT&CK mappings.
Technique | Abused IAM actions | MITRE ATT&CK |
|---|---|---|
Policy manipulation |
| T1098 |
Access key creation for another user |
| T1098.001 |
Role assumption chaining |
| T1550.001 |
Login profile takeover |
| T1098.001 |
PassRole to compute service |
| T1548 |
Admin user creation |
| T1136.003 |
These techniques share a pattern: a low-privilege principal calls one or more IAM or STS API operations that grant a principal, itself or another, the permissions it was not supposed to have. CloudTrail records every IAM and STS API call, making it your primary data source for hunting these patterns.
Anomalies to look for:
IAM write operations (
Create,Attach,Put,Update) from principals that do not normally call themIAM write operations targeting a different principal than the caller
AssumeRolecalls where the source identity is itself an assumed-role sessionA burst of
AccessDeniederrors on IAM actions immediately before a successful privilege escalation
Setting up the Elastic MCP server with Claude Code
Installing the MCP server
Install the Elastic MCP server package globally:
npm install -g @elastic/mcp-server-elasticsearchVerify the installation:
elastic-mcp-server --versionExpected output:
elastic-mcp-server 1.2.0Configuring Claude Code
Add the Elastic MCP server to your Claude Code configuration file at ~/.claude/claude.json:
{
"mcpServers": {
"elasticsearch": {
"command": "elastic-mcp-server",
"args": [],
"env": {
"ES_URL": "https://your-cluster:9200",
"ES_API_KEY": "your-api-key-here",
"ES_CA_CERT": "/path/to/ca.crt"
}
}
}
}Replace the following placeholder values:
your-cluster:9200: your Elasticsearch cluster endpointyour-api-key-here: your Elasticsearch API key (base64-encodedid:keystring)/path/to/ca.crt: path to your CA certificate; omit this field entirely if your cluster uses a public certificate authority
Verifying the connection
Start a Claude Code session:
claudeCheck that the Elasticsearch MCP server is connected:
/mcpExpected output includes elasticsearch in the list of connected servers:
Connected MCP servers:
elasticsearch (6 tools available)If the server doesn't appear, check ~/.claude/mcp.log for connection errors. The most common causes are a malformed claude.json file, an unreachable cluster endpoint, or an API key with insufficient permissions.
Tip: To confirm your API key has the right permissions, run:
curl -H "Authorization: ApiKey your-api-key-here" \
https://your-cluster:9200/_security/authenticateThe response shows the roles and privileges associated with the key.
Establishing baseline IAM activity
Before hunting anomalies, establish what normal IAM API activity looks like in your environment. Run the following aggregation in your Claude Code session:
Using the Elasticsearch MCP, query logs-aws.cloudtrail-* for the past
30 days and summarize IAM API activity. I need:
1. The top 25 IAM API calls by volume; flag any associated with privilege
escalation (CreatePolicyVersion, AttachUserPolicy, PutUserPolicy,
CreateAccessKey, UpdateLoginProfile, AssumeRole).
2. The top 10 principals by IAM call volume.
3. A callout for any IAM write operations (Create*, Put*, Attach*, Update*)
in the top 25, with a breakdown of which principals are making them.Example Claude Code output:
✦ Queried logs-aws.cloudtrail-* · 47,832 IAM events · 2026-09-13 to 2026-10-13
TOP IAM API CALLS
──────────────────────────────────────────────────────────
# Event name Count Share
──────────────────────────────────────────────────────────
1 ListRoles 21,450 44.8%
2 GetRole 12,210 25.5%
3 ListPolicies 5,640 11.8%
4 AttachRolePolicy 1,820 3.8% ← WRITE
5 ListAttachedRolePolicies 1,340 2.8%
...
18 CreatePolicyVersion 48 0.1% ← WRITE
22 PutRolePolicy 31 0.1% ← WRITE
TOP CALLERS
──────────────────────────────────────────────────────────
TerraformProvisioner (role) 31,200 65.2%
ci-pipeline (user) 9,440 19.7%
john.doe (user) 4,100 8.6%
svc-monitoring (role) 1,890 3.9%
⚠ WRITE OPERATIONS IN TOP 25
AttachRolePolicy (#4): 1,820 calls, 98% from TerraformProvisioner
CreatePolicyVersion (#18): 48 calls: john.doe (44), ci-pipeline (4)
PutRolePolicy (#22): 31 calls, all from TerraformProvisioner
Observation: john.doe made 44 of 48 CreatePolicyVersion calls. This is
unusual; CreatePolicyVersion is an administrator-level action. Recommend
reviewing whether these calls set permissive policy versions (setAsDefault:
true). Continue with the detection queries below.Claude Code returns a summary of IAM call volumes over time, top callers, and flags any write operations present. Record the principals that normally generate IAM write calls. These are your expected actors. Any other principal performing IAM writes is an immediate focus for the hunting queries that follow.
Detecting policy manipulation
Policy manipulation is the highest-impact privilege escalation path: a single AttachUserPolicy call granting AdministratorAccess can give an attacker full control of an AWS account. Run this query in your Claude Code session:
Using the Elasticsearch MCP, search logs-aws.cloudtrail-* for the past
7 days for any of these IAM policy manipulation events:
CreatePolicyVersion, SetDefaultPolicyVersion, AttachUserPolicy,
AttachGroupPolicy, AttachRolePolicy, PutUserPolicy, PutGroupPolicy,
PutRolePolicy. Return up to 100 results sorted newest first.
For each result:
1. Identify the caller (with ARN) and target principal.
2. Note whether the attached or modified policy grants admin-level access
(AdministratorAccess, Action: "*", or iam:*).
3. Flag any caller that is not in the expected list:
TerraformProvisioner, ci-pipeline.
Group by caller and highlight outliers.
Example Claude Code output:
✦ 23 policy manipulation events · 3 unique callers · 2026-10-06 to 2026-10-13
EXPECTED ACTIVITY
──────────────────────────────────────────────────────────────────
TerraformProvisioner: 18× AttachRolePolicy on service roles
Policies: AmazonS3ReadOnlyAccess, AmazonDynamoDBReadOnlyAccess
→ No admin-level grants. Consistent with provisioning.
ci-pipeline: 3× AttachRolePolicy on CIDeployRole
Policy: AWSCodeDeployDeployerAccess
→ No admin-level grants. Expected CI behavior.
⚠ SUSPICIOUS ACTIVITY: CALLER NOT IN EXPECTED LIST
──────────────────────────────────────────────────────────────────
Caller: arn:aws:iam::123456789012:user/dev-temp-user
Source IP: 198.51.100.47 (not a known corporate IP range)
2026-10-11 03:14:02 UTC · CreatePolicyVersion
Policy: arn:aws:iam::123456789012:policy/DevAccess
Version: v3 (setAsDefault: true)
Change: Added "Action": "*", "Resource": "*" ← grants full admin ⚠
2026-10-11 03:14:09 UTC · AttachUserPolicy
Caller: dev-temp-user
Target: dev-temp-user (self-escalation)
Policy: arn:aws:iam::aws:policy/AdministratorAccess ⚠
dev-temp-user self-escalated to AdministratorAccess within 7 seconds.
Recommend immediate investigation → proceed to the investigation section.
Any result where the caller is not in your expected list, or where the attached policy grants Action: "*" or iam:*, needs immediate investigation. Proceed to the Investigating a suspected privilege escalation section for those principals.
High-confidence indicator: A CreatePolicyVersion call with setAsDefault: true in requestParameters means the caller is replacing an existing policy with a new, potentially more permissive version and activated it immediately, with no delay between the change and its effect.
Detecting access key creation abuse
An attacker with iam:CreateAccessKey can create programmatic credentials for any IAM user, including administrators, establishing persistent backdoor access. The signal is straightforward: the caller and target user differ.
Run this query in your Claude Code session:
Using the Elasticsearch MCP, search logs-aws.cloudtrail-* for the past
7 days for CreateAccessKey events where the caller and target user are
different principals. For each result:
- Show the caller identity (with ARN), target username, and source IP.
- Check whether the newly created access key ID subsequently appears in
any CloudTrail events as the caller's access key.
- If the key was used, list those follow-on API calls with timestamps.Example Claude Code output:
✦ 1 cross-principal CreateAccessKey event · 2026-10-06 to 2026-10-13
⚠ HIGH CONFIDENCE INDICATOR
──────────────────────────────────────────────────────────────────
Timestamp: 2026-10-10 22:47:31 UTC
Caller: arn:aws:iam::123456789012:user/dev-temp-user
Target user: admin-backup
New key ID: AKIAIOSFODNN7EXAMPLE
Source IP: 198.51.100.47
Subsequent key usage (3 calls found using AKIAIOSFODNN7EXAMPLE):
22:58:44 UTC s3:ListBuckets 198.51.100.47
23:01:12 UTC s3:GetObject ×14 198.51.100.47
→ s3:::finance-reports/Q3_2026.xlsx
23:04:55 UTC secretsmanager:GetSecretValue 198.51.100.47
→ prod/db/credentials
The backdoor key was used 11 minutes after creation to access financial
reports and production database credentials. This is an active incident.
Recommend immediate revocation of AKIAIOSFODNN7EXAMPLE and escalation.Every result where userIdentity.userName differs from requestParameters.userName is a direct indicator of one identity creating credentials for another. If the newly created key ID subsequently appears in CloudTrail userIdentity.accessKeyId, the attacker actively used the backdoor key; escalate immediately.
Hunting for role assumption chains
Privilege escalation via role chaining occurs when an attacker assumes a role, then uses that role's credentials to assume a second, more privileged, role. CloudTrail records each AssumeRole call, including the caller identity, so you can reconstruct multi-hop chains.
Run this query to surface AssumeRole calls made from within an existing assumed-role session:
Using the Elasticsearch MCP, find all AssumeRole events in
logs-aws.cloudtrail-* in the past 24 hours where the caller is itself
an assumed-role session. Group results by origin session to reconstruct
role assumption chains in chronological order. For each chain:
1. Show the full hop sequence (origin identity → each assumed role ARN).
2. Note the elapsed time between first and last hop.
3. Flag any chain that terminates at a role containing "Admin",
"PowerUser", "FullAccess", or "OrganizationAccountAccessRole".Example Claude Code output:
✦ 3 role assumption chains from assumed-role sessions · past 24 hours
CHAIN 1: Expected
TerraformProvisioner → DeployRole (12:01 UTC)
Permissions: AWSCloudFormationFullAccess, EC2:Describe*
→ Low-privilege deployment role. No escalation.
CHAIN 2: Expected
svc-monitoring → ReadOnlyAccessRole (14:00 UTC)
Cross-account to 987654321098 (security-audit account)
→ Routine scheduled monitoring.
⚠ CHAIN 3: Unexpected privilege escalation
Source IP for all hops: 198.51.100.47
────────────────────────────────────────────────────────
14:22:03 UTC dev-temp-user
→ assumed DataAnalystRole
arn:aws:iam::123456789012:role/DataAnalystRole
Expected scope: Athena + Glue read access
14:31:47 UTC DataAnalystRole
→ assumed OrganizationAccountAccessRole ⚠
arn:aws:iam::123456789012:role/OrganizationAccountAccessRole
Grants: AdministratorAccess in management account
────────────────────────────────────────────────────────
DataAnalystRole should NOT have sts:AssumeRole permission on
OrganizationAccountAccessRole. Chain elapsed: 9 min 44 sec.
Recommend reviewing DataAnalystRole's trust policy immediately.Claude Code groups events by origin session and reconstructs each chain. A sequence starting from a low-privilege service role and ending at an OrganizationAccountAccessRole or similar cross-account admin role is a critical finding.
Tip: Extend the look-back window to 7 days and widen the scope to all AssumeRole calls (remove the AssumedRole filter) to map all role relationships in your environment. This gives you a baseline assumption graph that makes future chains immediately recognizable as anomalies.
Detecting UpdateLoginProfile abuse
A principal with iam:UpdateLoginProfile can reset the console password of any IAM user, including administrators, to hijack their account. This technique does not require the attacker to know the target's current password and leaves no failed authentication trail.
Run this query in your Claude Code session:
Using the Elasticsearch MCP, find all UpdateLoginProfile events in
logs-aws.cloudtrail-* in the past 7 days where the caller and target
user are different. For each result:
1. Show the caller, target user, and source IP.
2. Check whether the target user is a member of any admin-level group
(look for subsequent GetGroup or ListAttachedGroupPolicies calls
that mention AdministratorAccess or PowerUserAccess).
3. Check for a ConsoleLogin event from the target user within 30 minutes
of the password reset, and note whether the login IP differs from the
reset IP.Example Claude Code output:
✦ 1 cross-principal UpdateLoginProfile event · 2026-10-06 to 2026-10-13
⚠ ACCOUNT TAKEOVER CONFIRMED
──────────────────────────────────────────────────────────────────
Timestamp: 2026-10-10 23:19:08 UTC
Caller: arn:aws:iam::123456789012:user/dev-temp-user
Target user: john.doe
Source IP: 198.51.100.47
john.doe group membership: AdminGroup
→ Attached policy: AdministratorAccess ⚠
ConsoleLogin check (±30 min window):
2026-10-10 23:23:41 UTC ConsoleLogin for john.doe ✓ found
Login IP: 203.0.113.12 (different from reset IP; attacker switched)
MFA used: false
Elapsed: 4 min 33 sec after password reset
The attacker reset john.doe's password and signed in as that user 4 minutes
later from a different source IP. john.doe carries AdministratorAccess via
AdminGroup. Full console control is now in attacker hands.A ConsoleLogin event from the target user shortly after their UpdateLoginProfile call confirms that the attacker successfully used the reset password to sign in. The source IP for the ConsoleLogin will often differ from the IP that made the UpdateLoginProfile call, since the attacker switches to the target account.
Investigating a suspected privilege escalation
When a hunting query surfaces a suspicious principal, use Claude Code to pivot from the initial indicator to a full activity timeline.
Building a principal timeline
Replace <PRINCIPAL_ARN> with the ARN of the suspicious identity:
Using the Elasticsearch MCP, build a complete activity timeline for
<PRINCIPAL_ARN> in logs-aws.cloudtrail-* over the past 48 hours.
Include all events where this ARN appears as the direct caller or as
the session issuer for an assumed-role session. Then:
1. Identify the first IAM or STS action and what it changed.
2. Map each relevant action to MITRE ATT&CK (T1098, T1548, T1550, T1530).
3. List all AWS services accessed after the first privilege escalation event.
4. Flag data exfiltration indicators: bulk S3 GetObject, Secrets Manager
GetSecretValue, SSM GetParameter on SecureString, or KMS Decrypt on
keys this principal did not previously use.
5. Summarize the full attack chain in one paragraph.Example Claude Code output:
✦ 127 events · arn:aws:iam::123456789012:user/dev-temp-user
2026-10-09 18:00 UTC to 2026-10-11 18:00 UTC
TIMELINE (privilege-relevant events)
──────────────────────────────────────────────────────────────────
2026-10-09 18:12 iam:ListPolicies (reconnaissance)
2026-10-09 18:13 iam:ListAttachedUserPolicies (reconnaissance)
2026-10-09 18:14 iam:GetPolicy (reconnaissance)
2026-10-10 22:47 iam:CreateAccessKey T1098.001 ⚠
→ Key created for admin-backup (cross-user)
2026-10-10 22:58 [admin-backup key] s3:ListBuckets
2026-10-10 23:01 [admin-backup key] s3:GetObject ×14 T1530 ⚠
→ s3:::finance-reports/Q3_2026.xlsx
2026-10-10 23:04 [admin-backup key] secretsmanager:GetSecretValue T1552 ⚠
→ prod/db/credentials
2026-10-11 03:14 iam:CreatePolicyVersion T1098 ⚠
→ DevAccess v3: Action:"*", Resource:"*"
2026-10-11 03:14 iam:AttachUserPolicy T1098 ⚠
→ AdministratorAccess attached to self
2026-10-11 03:19 iam:UpdateLoginProfile T1098.001 ⚠
→ Password reset for john.doe (AdminGroup)
POST-ESCALATION SERVICES: s3 · secretsmanager · iam · sts · ec2 · ssm
MITRE ATT&CK
T1078.004 Valid Cloud Accounts (initial access)
T1098.001 Account Manipulation: Additional Cloud Credentials
T1098 Account Manipulation: Policy modification
T1530 Data from Cloud Storage
T1552 Unsecured Credentials (Secrets Manager)
ATTACK SUMMARY
dev-temp-user performed IAM reconnaissance on Oct 9, then on Oct 10 at
22:47 created a backdoor access key for admin-backup and used it to
exfiltrate financial data and production credentials. On Oct 11 at 03:14
it self-escalated to AdministratorAccess by modifying its own policy,
then took over john.doe's console account (AdminGroup) 5 minutes later.
Total time from first IAM write to full admin control: ~4.5 hours.Claude Code returns a narrative timeline with TTP labels and an impact summary, ready to paste into an incident report.
Correlating with failed attempts
Attackers often probe permissions before finding a working path. The failed attempts leave an AccessDenied trail immediately before the successful escalation. Run:
Using the Elasticsearch MCP, find all AccessDenied and
Client.UnauthorizedOperation errors for <PRINCIPAL_ARN> on IAM and
STS APIs in logs-aws.cloudtrail-* in the 6 hours before
<FIRST_SUSPICIOUS_TIMESTAMP>.
Summarize:
1. Which IAM and STS actions were denied, and in what order.
2. What the denials suggest about the attacker's initial privilege
boundaries (for example, blocked by SCPs, resource-based conditions,
or missing identity-based permissions).
3. What escalation path the attacker ultimately found to bypass those
denials; connect the denied attempts to the successful actions that
followed.Example Claude Code output:
✦ 12 denied IAM/STS calls · 2026-10-10 21:14 to 2026-10-11 03:14 UTC
DENIED ACTIONS (chronological)
──────────────────────────────────────────────────────────────────
21:14 UTC iam:AttachUserPolicy AccessDenied (SCP: deny IAM write)
21:14 UTC iam:PutUserPolicy AccessDenied (SCP: deny IAM write)
21:15 UTC iam:CreatePolicyVersion AccessDenied (target: AWS-managed policy)
21:15 UTC iam:SetDefaultPolicyVersion AccessDenied (target: AWS-managed policy)
22:31 UTC iam:CreateUser AccessDenied (SCP: deny IAM write)
22:44 UTC iam:CreateAccessKey AccessDenied (target: admin-user)
22:46 UTC iam:CreateAccessKey AccessDenied (target: devops-lead)
22:47 UTC iam:CreateAccessKey SUCCEEDED (target: admin-backup) ✓
BOUNDARY ANALYSIS
The SCP blocks direct IAM write actions (AttachUserPolicy, PutUserPolicy,
CreateUser) and attempts to modify AWS-managed policies are rejected by the
API. CreateAccessKey is not in the SCP deny list; only individual user-level
conditions governed it.
ESCALATION PATH RECONSTRUCTED
1. Direct policy writes blocked by SCP (21:14) → pivoted to alternative paths.
2. AWS-managed policy modification rejected (21:15) → shifted to customer-managed.
3. New user creation blocked by SCP (22:31) → tried credential theft instead.
4. CreateAccessKey probed on admin-user and devops-lead (22:44–22:46); both
had IAM conditions restricting key creation to specific IPs or roles.
5. admin-backup lacked those conditions → key creation succeeded (22:47).
6. Returned at 03:14 to exploit CreatePolicyVersion on the customer-managed
DevAccess policy, which the SCP did not cover.
Recommend: Add an explicit SCP deny on iam:CreatePolicyVersion and
iam:SetDefaultPolicyVersion for all non-admin principals.A dense cluster of AccessDenied errors on IAM write actions followed by a successful escalation is strong evidence of intentional permission probing rather than a misconfiguration or accidental call.
Running autonomous hunts with /goal

For IAM privilege escalation hunting, interactive prompts are better for investigation and triage where you need judgment calls as findings emerge. The pattern-scanning phase (running all four detection queries and writing results to files) is mechanical and exactly what /goal handles.
When to use /goal vs interactive prompts
Scenario | Better choice |
|---|---|
Scan all four escalation patterns and log to files |
|
Pivot on a specific principal after a hit | Interactive prompts |
Nightly sweep of the past 24 hours |
|
Decide whether a finding is a false positive | Interactive prompts |
Writing a /goal for IAM hunting
A well-formed security goal has three parts: a role, a task with explicit constraints, and a verifiable success condition. Claude Code reads the goal, takes an action, checks whether the success condition is met, and loops until it is.
/goal Role: Threat hunter checking for IAM privilege escalation in AWS.
Task: Using the Elasticsearch MCP, run these four queries against
logs-aws.cloudtrail-* for the past 7 days:
1. Policy manipulation (CreatePolicyVersion, SetDefaultPolicyVersion,
AttachUserPolicy, AttachGroupPolicy, AttachRolePolicy, PutUserPolicy,
PutGroupPolicy, PutRolePolicy). Flag callers not in expected list:
TerraformProvisioner, ci-pipeline.
2. Cross-user CreateAccessKey events (caller differs from target user).
3. AssumeRole chains where the caller is an AssumedRole session and the
destination role ARN contains "Admin", "PowerUser", or "FullAccess".
4. Cross-user UpdateLoginProfile followed by a ConsoleLogin from the
target user within 30 minutes.
For each query:
- Write results to hunt/findings/<query_name>.md
- Include caller ARN, target, source IP, and timestamp per finding
- Write "No findings." if the query returns zero results
Constraints:
- Read-only queries; do not modify any Elasticsearch index
- Write only to hunt/findings/
Success condition: hunt/findings/ contains four .md files. Each contains
structured results or "No findings." Any confirmed escalation pattern
carries a recommended next step.Claude Code works through each query, writes the four files, and stops when all success conditions are met. Open the files and triage any findings manually.
Setting standing rules with CLAUDE.md
For recurring hunts, put a CLAUDE.md in your project directory. Claude Code reads it before every session in that directory, so you do not need to repeat constraints in each /goal invocation:
# IAM Threat Hunt Rules
## Target
Index pattern: logs-aws.cloudtrail-*
Expected IAM admins: TerraformProvisioner, ci-pipeline
## Constraints
- Read-only Elasticsearch queries
- Write findings to hunt/findings/ only
- Do not take direct action on any AWS resource
## Stop conditions
- Stop and flag if any query returns more than 50 results
(likely a noisy rule that needs human review before continuing)
- Stop if Elasticsearch returns connection errors on three consecutive queriesFor more /goal patterns, including multi-phase vulnerability discovery and penetration testing loops, see the SecEngAI guide on autonomous security engineering workflows.
What's next
You can extend this threat hunting workflow in several directions:
Expand coverage to compute-based escalation paths. Add hunting queries for
iam:PassRolecombined withec2:RunInstancesorlambda:CreateFunction.Add cross-account coverage. If your AWS organization uses AWS Organizations, add queries for
AssumeRolecalls targeting roles in other accounts (the destinationroleArncontains a different account ID than the caller's account).
Related resources:
