Skip to content
Merged
Show file tree
Hide file tree
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,198 @@
[[prebuilt-rule-8-19-37-aws-bedrock-model-invocation-from-high-number-of-countries]]
=== AWS Bedrock Model Invocation From High Number of Countries

Detects a single AWS principal invoking Amazon Bedrock models from three or more countries and three or more source IPs within the lookback window. LLMjacking operators often distribute stolen AWS credentials across proxies or resell them to multiple buyers, which produces inference traffic for one identity from many unrelated locations.

*Rule type*: esql

*Rule indices*: None

*Severity*: critical

*Risk score*: 99

*Runs every*: 15m

*Searches indices from*: now-4h ({ref}/common-options.html#date-math[Date Math format], see also <<rule-schedule, `Additional look-back time`>>)

*Maximum alerts per execution*: 100

*References*:

* https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html
* https://www.elastic.co/docs/reference/integrations/aws/cloudtrail

*Tags*:

* Domain: Cloud
* Domain: GenAI
* Data Source: AWS
* Data Source: Amazon Web Services
* Data Source: AWS CloudTrail
* Platform: AWS
* Service: AWS Bedrock
* Use Case: Threat Detection
* Tactic: Credential Access
* Tactic: Initial Access
* Threat: Impossible Travel
* Threat: LLMjacking
* Threat: Unauthorized AI Usage
* Mitre Atlas: AML.T0012
* Mitre Atlas: AML.T0091
* Mitre Atlas: AML.T0091.000
* Resources: Investigation Guide
* Rule Type: ES|QL

*Version*: 1

*Rule authors*:

* Elastic

*Rule license*: Elastic License v2


==== Investigation guide



*Triage and analysis*



*Investigating AWS Bedrock Model Invocation From High Number of Countries*


One AWS principal (role issuer ARN when present, otherwise `aws.cloudtrail.user_identity.arn`) invoked Amazon Bedrock
models from at least three countries and three source IPs inside the lookback window. Assumed-role sessions are grouped
by the issuing role so re-assumes of the same role still correlate. This pattern fits credentials that have been shared,
resold, or proxied for LLMjacking.


*Possible investigation steps*


- Review `Esql.source_geo_country_name_values`, `Esql.source_ip_values`, and
`Esql.source_as_organization_name_values`. Residential proxies and hosting ASNs across several countries are more
suspicious than one corporate VPN provider.
- Check `Esql.aws_cloudtrail_user_identity_access_key_id_values` and `Esql.aws_cloudtrail_user_identity_type_values`.
A single long-term `AKIA` key seen in many countries is a strong indicator of credential theft.
- Compare `Esql.user_agent_original_values`. Several distinct clients for one identity often means several operators.
- Search CloudTrail for the principal's other activity in the window, especially model access changes, guardrail or
logging changes, and IAM changes.
- Review Bedrock cost and usage for the account over the same period.


*False positive analysis*


- Shared identities for global workloads. Move these to region-scoped roles or add an exception for the principal.
- VPN and privacy relay users. Confirm with the account owner before rotating credentials.


*Response and remediation*


- If the activity is not explained, deactivate the access key or revoke the role session, rotate credentials, and
restrict `bedrock:InvokeModel*` permissions.
- Set AWS Budgets or service quotas to limit the cost of further abuse.


==== Setup



*Setup*


This rule requires AWS CloudTrail management events ingested via the AWS integration (`aws.cloudtrail` data stream)
with `source.geo.country_name` populated by the integration's GeoIP enrichment. Bedrock runtime operations are logged
as management events by default; no Bedrock model invocation logging is required.


==== Rule query


[source, js]
----------------------------------
from logs-aws.cloudtrail-*
| where
data_stream.dataset == "aws.cloudtrail" and
event.provider == "bedrock.amazonaws.com" and
event.action in ("InvokeModel", "InvokeModelWithResponseStream", "Converse", "ConverseStream") and
event.outcome == "success" and
aws.cloudtrail.user_identity.arn is not null and
source.ip is not null and
source.geo.country_name is not null
| eval
Esql.identity = coalesce(
aws.cloudtrail.user_identity.session_context.session_issuer.arn,
aws.cloudtrail.user_identity.arn
)
| stats
Esql.country_count = count_distinct(source.geo.country_name),
Esql.source_ip_count = count_distinct(source.ip),
Esql.user_agent_count = count_distinct(user_agent.original),
Esql.event_count = count(*),
Esql.event_action_values = values(event.action),
Esql.source_ip_values = values(source.ip),
Esql.source_geo_country_name_values = values(source.geo.country_name),
Esql.source_geo_city_name_values = values(source.geo.city_name),
Esql.source_as_organization_name_values = values(source.as.organization.name),
Esql.user_agent_original_values = values(user_agent.original),
Esql.aws_cloudtrail_user_identity_arn_values = values(aws.cloudtrail.user_identity.arn),
Esql.aws_cloudtrail_user_identity_type_values = values(aws.cloudtrail.user_identity.type),
Esql.aws_cloudtrail_user_identity_access_key_id_values = values(aws.cloudtrail.user_identity.access_key_id),
Esql.cloud_region_values = values(cloud.region),
Esql.timestamp_first_seen = min(@timestamp),
Esql.timestamp_last_seen = max(@timestamp)
by
Esql.identity,
cloud.account.id
| where Esql.country_count >= 3 and Esql.source_ip_count >= 3
| eval source.ip = mv_first(Esql.source_ip_values)
| keep
Esql.identity,
cloud.account.id,
source.ip,
Esql.country_count,
Esql.source_ip_count,
Esql.user_agent_count,
Esql.event_count,
Esql.event_action_values,
Esql.source_ip_values,
Esql.source_geo_country_name_values,
Esql.source_geo_city_name_values,
Esql.source_as_organization_name_values,
Esql.user_agent_original_values,
Esql.aws_cloudtrail_user_identity_arn_values,
Esql.aws_cloudtrail_user_identity_type_values,
Esql.aws_cloudtrail_user_identity_access_key_id_values,
Esql.cloud_region_values,
Esql.timestamp_first_seen,
Esql.timestamp_last_seen

----------------------------------

*Framework*: MITRE ATT&CK^TM^

* Tactic:
** Name: Credential Access
** ID: TA0006
** Reference URL: https://attack.mitre.org/tactics/TA0006/
* Technique:
** Name: Steal Application Access Token
** ID: T1528
** Reference URL: https://attack.mitre.org/techniques/T1528/
* Tactic:
** Name: Initial Access
** ID: TA0001
** Reference URL: https://attack.mitre.org/tactics/TA0001/
* Technique:
** Name: Valid Accounts
** ID: T1078
** Reference URL: https://attack.mitre.org/techniques/T1078/
* Sub-technique:
** Name: Cloud Accounts
** ID: T1078.004
** Reference URL: https://attack.mitre.org/techniques/T1078/004/
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
[[prebuilt-rule-8-19-37-aws-ec2-bulk-user-data-enumeration]]
=== AWS EC2 Bulk User Data Enumeration

Identifies when the same AWS principal, from the same source IP, successfully calls EC2 "DescribeInstanceAttribute" for the "userData" attribute against more than 20 distinct instances within a 10-minute lookback. Instance user data often contains bootstrap scripts and hardcoded credentials, so retrieving it across many instances in a short window can indicate reconnaissance or credential harvesting. Known CSPM roles, AWS service invocations, and Terraform calls from major cloud and carrier networks are excluded.

*Rule type*: esql

*Rule indices*: None

*Severity*: high

*Risk score*: 73

*Runs every*: 5m

*Searches indices from*: now-10m ({ref}/common-options.html#date-math[Date Math format], see also <<rule-schedule, `Additional look-back time`>>)

*Maximum alerts per execution*: 100

*References*:

* https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstanceAttribute.html
* https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
* https://hackingthe.cloud/aws/exploitation/local_ec2_priv_esc_through_user_data

*Tags*:

* Domain: Cloud
* Data Source: AWS
* Data Source: Amazon Web Services
* Data Source: AWS CloudTrail
* Platform: AWS
* Service: AWS EC2
* Use Case: Threat Detection
* Tactic: Discovery
* Tactic: Credential Access
* Resources: Investigation Guide
* Rule Type: ES|QL

*Version*: 1

*Rule authors*:

* Elastic

*Rule license*: Elastic License v2


==== Investigation guide



*Triage and analysis*



*Investigating AWS EC2 Bulk User Data Enumeration*


EC2 `userData` is returned by `DescribeInstanceAttribute` when `attribute` is `userData`. It is frequently used for
bootstrap scripts and sometimes contains credentials or tokens. This rule alerts when one principal
(`aws.cloudtrail.user_identity.arn`), identity type, source IP, and source ASN reads that attribute for more than 20
distinct `aws.cloudtrail.request_parameters` values in the 10-minute lookback. For this API those values correspond to
distinct instance requests.


*Possible investigation steps*


- Review `aws.cloudtrail.user_identity.arn` and `aws.cloudtrail.user_identity.type`. Confirm whether this identity is
expected to read instance user data at fleet scale.
- Review `Esql.count_distinct_instance_id` and `Esql.aws_request_parameters_values`. Extract the `instanceId` values and
check whether the instances belong to sensitive workloads.
- Review `source.ip`, `source.as.organization.name`, and `Esql.user_agent_values`. Unexpected ASNs or clients such as
the AWS CLI or an SDK from an unfamiliar network are higher priority than a known automation host.
- Search CloudTrail for the same principal and source IP in the surrounding hour for `ModifyInstanceAttribute`,
`RunInstances`, `GetPasswordData`, `AssumeRole`, and IAM changes.
- If the instances are still available, inspect their current user data for secrets. Prefer AWS Secrets Manager or
Parameter Store over credentials stored in user data.


*False positive analysis*


- Approved CSPM and inventory tools. This rule already excludes principal names matching CrowdStrike CSPM, Orca
Security, Aikido Security, Stream Security (`streamsec-*`), and `AWSReservedSSO_Poweruser`.
- Calls invoked by AWS Internal, CloudFormation, AI DevOps, EMR, or AIOps.
- Terraform user agents whose source ASN is Amazon, Google, Microsoft, Verizon, or AT&T.
- Add further exceptions by principal ARN or user agent when the behavior is documented and recurring.


*Response and remediation*


- If the activity is not authorized, revoke the role session or disable the access key and restrict the source IP.
- Rotate any credentials found in the retrieved user data and remove secrets from instance user data.
- Limit `ec2:DescribeInstanceAttribute` to principals that need it.


==== Setup



*Setup*


The AWS integration must collect CloudTrail management events into `logs-aws.cloudtrail-*`.
`DescribeInstanceAttribute` is a management event logged by default. `source.ip` and
`source.as.organization.name` must be populated, because events missing either field are excluded.


==== Rule query


[source, js]
----------------------------------
from logs-aws.cloudtrail-*
| where KQL("""event.provider: "ec2.amazonaws.com" and event.action: "DescribeInstanceAttribute" and event.outcome: "success" and aws.cloudtrail.flattened.request_parameters.attribute: "userData" and source.ip: * and source.as.organization.name: * and not user.name: (*CrowdStrikeCSPMReader* or *OrcaSecurityRole* or *AikidoSecurityRole* or *streamsec-*) and not aws.cloudtrail.user_identity.invoked_by: ("AWS Internal" or "cloudformation.amazonaws.com" or "aidevops.amazonaws.com" or "elasticmapreduce.amazonaws.com" or "aiops.amazonaws.com")""")
and not (
user_agent.original like "*Terraform*"
and source.as.organization.name in (
"Amazon.com, Inc.",
"Google LLC",
"Microsoft Corporation",
"Verizon Business",
"AT&T Enterprises, LLC"
)
)
| grok aws.cloudtrail.request_parameters """instanceId=(?<Esql.instance_id>[^,}\]]+)"""
| where Esql.instance_id is not null
| stats
Esql.count_distinct_instance_id = count_distinct(Esql.instance_id),
Esql.aws_request_parameters_values = values(aws.cloudtrail.request_parameters),
Esql.user_agent_values = values(user_agent.original)
by
data_stream.namespace,
aws.cloudtrail.user_identity.arn,
aws.cloudtrail.user_identity.type,
source.as.organization.name,
source.ip,
source.`as`.number
| where Esql.count_distinct_instance_id > 20
| keep
Esql.count_distinct_instance_id,
data_stream.namespace,
aws.cloudtrail.user_identity.arn,
aws.cloudtrail.user_identity.type,
source.ip,
source.`as`.number,
source.as.organization.name,
Esql.aws_request_parameters_values,
Esql.user_agent_values

----------------------------------

*Framework*: MITRE ATT&CK^TM^

* Tactic:
** Name: Discovery
** ID: TA0007
** Reference URL: https://attack.mitre.org/tactics/TA0007/
* Technique:
** Name: Cloud Infrastructure Discovery
** ID: T1580
** Reference URL: https://attack.mitre.org/techniques/T1580/
* Tactic:
** Name: Credential Access
** ID: TA0006
** Reference URL: https://attack.mitre.org/tactics/TA0006/
* Technique:
** Name: Unsecured Credentials
** ID: T1552
** Reference URL: https://attack.mitre.org/techniques/T1552/
* Sub-technique:
** Name: Cloud Instance Metadata API
** ID: T1552.005
** Reference URL: https://attack.mitre.org/techniques/T1552/005/
Loading
Loading