Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
[metadata]
Comment thread
bryans3c marked this conversation as resolved.
creation_date = "2026/06/18"
integration = ["aws"]
maturity = "production"
updated_date = "2026/06/18"

[rule]
author = ["Elastic"]
description = """
Identifies a single AWS Lambda execution-role credential (a temporary assumed-role access key) that is used both from
inside the Lambda execution environment and from outside it within the same window. When a function runs, its temporary
credentials are presented with the Lambda runtime user agent (containing "exec-env/AWS_Lambda") or with a request
invoked by "lambda.amazonaws.com". An adversary who exfiltrates those credentials, typically through a server-side
request forgery or remote code execution flaw in the function, can replay them from their own host, where the same
access key appears without the Lambda runtime markers and from an unrelated source. Observing the same temporary key
both inside and outside the runtime is a strong indicator of stolen Lambda credentials being abused.
"""
false_positives = [
"""
Some architectures proxy or forward Lambda activity in ways that can strip the runtime user agent, and certain AWS
service integrations may act on a function's behalf. Validate the access key, the principal in
`aws.cloudtrail.user_identity.arn`, the external source IPs, and the user agents observed outside the runtime before
treating the activity as malicious. Known proxies or service integrations can be tuned out after validation.
""",
]
from = "now-61m"
interval = "60m"
language = "esql"
license = "Elastic License v2"
name = "AWS Lambda Execution Role Credentials Used Outside Function"
note = """## Triage and analysis

### Investigating AWS Lambda Execution Role Credentials Used Outside Function

AWS Lambda functions receive temporary credentials for their execution role and use them from within the managed runtime. CloudTrail records those calls with a user agent containing `exec-env/AWS_Lambda` or with `aws.cloudtrail.user_identity.invoked_by` set to `lambda.amazonaws.com`. If the same temporary access key is also seen making calls without those runtime markers, the credentials were almost certainly extracted from the function (for example via SSRF or RCE against the function code) and replayed from an attacker-controlled location.

This rule aggregates CloudTrail by the temporary access key and flags keys that were used both inside and outside the Lambda runtime in the same window.

### Possible investigation steps

- Review `Esql.source_ips` and `Esql.user_agents` for the external (non-runtime) activity and determine whether the source IPs and clients are expected for the workload.
- Map `aws.cloudtrail.user_identity.arn` to the Lambda function and its execution role, and review the function's code, dependencies, and recent changes for an SSRF/RCE or credential-exposure path.
- Examine `Esql.actions` for the operations performed with the credential outside the runtime, especially data access, IAM, or STS calls that exceed the function's normal behavior.
- Determine the blast radius of the execution role's permissions and whether any sensitive actions succeeded.
- Pivot on the external source IPs across other identities and accounts for signs of broader credential abuse.

### False positive analysis

- Proxies, log-forwarding layers, or service integrations that act on a function's behalf may occasionally present a Lambda credential without the runtime user agent. Validate the external source and client, and tune out known benign patterns on `aws.cloudtrail.user_identity.arn` or the observed user agents.

### Response and remediation

- If the credential abuse is confirmed, terminate active sessions and rotate or revoke the execution role's permissions; the temporary key cannot be individually revoked, so remove or restrict the role's trust and permissions.
- Patch the function's credential-exposure path (for example the SSRF/RCE vulnerability) and redeploy from a known-good source.
- Review and reduce the execution role's permissions to least privilege, and assess any actions performed with the stolen credential for data exposure or further compromise.

### Additional information

- [Using AWS Lambda environment variables (runtime identifiers)](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html)
"""
references = [
"https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html",
"https://docs.aws.amazon.com/lambda/latest/dg/logging-using-cloudtrail.html",
"https://hackingthe.cloud/aws/exploitation/lambda-steal-iam-credentials/",
]
risk_score = 73

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested on Zocalo and it looks too common, you will need to baseline this behavior before assigning high severity

rule_id = "0ae9b6c5-1deb-4ed4-9337-659dd701dfbc"
severity = "high"
tags = [
"Domain: Cloud",
"Data Source: AWS",
"Data Source: Amazon Web Services",
"Data Source: AWS Lambda",
"Use Case: Threat Detection",
"Tactic: Defense Evasion",
"Resources: Investigation Guide",
]
timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-aws.cloudtrail-*

| where
aws.cloudtrail.user_identity.type == "AssumedRole"
and aws.cloudtrail.user_identity.access_key_id IS NOT NULL

| eval is_lambda_call = case(
user_agent.original LIKE "*exec-env/AWS_Lambda*" or user_agent.original LIKE "*awslambda-worker/*"
or aws.cloudtrail.user_identity.invoked_by == "lambda.amazonaws.com",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only time I see this invoked_by value is when aws.cloudtrail.user_identity.type == AWSService, how are you comparing these AssumedRole lambda calls to the original service Lambda calls?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch Isai, you're right. I tested the rule with a detonation script and confirmed your observation in the alert data: invoked_by is null for all the AssumedRole events, so that side of the OR never fires. The user-agent check is carrying the whole detection. Digging into the alert payload also surfaced a related FP I want to flag: the user_agent_values on my detonation alert included awslambda-worker/1.0 rusoto/0.48.0 rust/1.94.0 linux events in the external bucket. That's the Lambda worker service creating CloudWatch log groups using the function's execution role on cold-start. With the rule as written, those land in external_call_count because they don't match exec-env/AWS_Lambda and invoked_by is null, which means a normal Lambda first-invocation could trigger this rule on its own with no attacker involvement.

Proposed fix:

  • Drop the dead invoked_by clause
  • Add awslambda-worker* to the Lambda runtime UA allow-list

@imays11 imays11 Jun 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes to both suggestions here

1, 0)

| stats
Esql.lambda_runtime_call_sum = sum(is_lambda_call),
Esql.total_call_count = count(),
Esql.source_ip_values = values(source.ip),
Esql.user_agent_values = values(user_agent.original),
Esql.event_action_values = values(event.action)
Comment on lines +94 to +98

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also consider adding some @timestamp fields to support triage later

by
aws.cloudtrail.user_identity.access_key_id,
aws.cloudtrail.user_identity.arn,
cloud.account.id

| eval Esql.external_call_count = Esql.total_call_count - Esql.lambda_runtime_call_sum

| where Esql.lambda_runtime_call_sum > 0 and Esql.external_call_count > 0

| keep
aws.cloudtrail.user_identity.access_key_id,
aws.cloudtrail.user_identity.arn,
cloud.account.id,
Esql.lambda_runtime_call_sum,
Esql.external_call_count,
Esql.source_ip_values,
Esql.user_agent_values,
Esql.event_action_values

| sort Esql.external_call_count desc
'''


[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1078"
name = "Valid Accounts"
reference = "https://attack.mitre.org/techniques/T1078/"
[[rule.threat.technique.subtechnique]]
id = "T1078.004"
name = "Cloud Accounts"
reference = "https://attack.mitre.org/techniques/T1078/004/"



[rule.threat.tactic]
id = "TA0005"
name = "Defense Evasion"
reference = "https://attack.mitre.org/tactics/TA0005/"

[rule.investigation_fields]
field_names = [
"aws.cloudtrail.user_identity.access_key_id",
"aws.cloudtrail.user_identity.arn",
"cloud.account.id",
"Esql.lambda_runtime_call_sum",
"Esql.external_call_count",
"Esql.source_ip_values",
"Esql.user_agent_values",
"Esql.event_action_values",
]

Loading