Amazon Web Service (AWS) Lambda is a serverless event-driven compute service. It is a function as a service (FaaS) that allows users to deploy application functionality without the complexity of maintaining the underlying infrastructure. Lambda executions can be triggered by events from other AWS services or software-as-a-service (SaaS) applications.
Inside the Lambda execution environment is a set of AWS Security Token Service (AWS STS) temporary and limited-privilege credentials for AWS Identity and Access Management (IAM). An attacker may be able to steal these credentials via a user application vulnerability or resource misconfiguration. The attacker could then use these credentials to escalate privileges, maintain persistence, or move laterally through an organization’s AWS account or accounts.
Secureworks® Counter Threat Unit™ (CTU) researchers have developed a technique using AWS CloudTrail to detect the use of stolen credentials. Every time an AWS Lambda executes, it generates an AWS CloudTrail logging event that can be used to establish a baseline for normal operation. The use of stolen credentials can then be detected when a logging event deviates from the baseline. A similar approach could be applied to detect AWS credentials stolen from other services. Although Amazon GuardDuty detects EC2 instance credentials used from another AWS account, it does not apply to Lambda or any other services nor does it detect credentials being used within the same account.
AWS Lambda execution and event logging
Understanding the detection logic technique requires some knowledge of the AWS Lambda architecture, operation, and management API calls.
AWS CloudTrail monitors and records account activity and API usage across all AWS accounts. By default, CloudTrail records a 90-day history of management events and makes this data freely available to AWS customers. Customers may optionally enable data event logging that includes AWS Lambda execution and Invoke events, but this feature is an additional cost. Because not all AWS customers may enable this feature, the detections need to rely on the management events captured in all environments.
Lambda microVMs and Workers
According to AWS, “Lambda will create its execution environments on a fleet of Amazon EC2 instances called AWS Lambda Workers. Workers are […] launched and managed by Lambda in a separate isolated AWS account which is not visible to customers. Workers have one or more hardware-virtualized micro virtual machines [(microVMs)] created by Firecracker.” Figure 1 shows the isolation of two customers’ Lambda functions on shared infrastructure.
Figure 1. Isolation model for AWS Lambda Workers. (Source: AWS)
Lambda execution environment lifecycle
The standard lifecycle of the execution environment includes three primary phases (see Figure 2).
Figure 2. Lambda execution environment lifecycle. (Source: AWS)
- Init — Lambda creates or unfreezes an execution environment with the configured resources, downloads the code for the function and all layers, initializes extensions, initializes the runtime, and then runs the function’s initialization code (i.e., the code outside the main handler). The Init phase happens either during the first invocation or in advance of function invocations if provisioned concurrency is enabled. The Init phase is split into three sub-phases: Extension init, Runtime init, and Function init. These sub-phases ensure that all extensions and the runtime complete their setup tasks before the function code runs.
When a Lambda creates an execution environment during the Init phase, it is commonly referred to as the ‘cold start.’ When an already initialized environment is invoked again before shutdown is triggered, it is called a ‘warm start.’
- Invoke — Lambda invokes the function handler. After the function runs to completion, Lambda prepares to handle another function invocation.
- Shutdown — This phase is triggered if the Lambda function does not receive any invocations for a period of time. It is unclear what logic AWS uses to calculate this timeframe. In the Shutdown phase, Lambda shuts down the runtime, alerts the extensions to let them stop cleanly, and then removes the environment.
According to AWS, Workers have a maximum lease time of 14 hours. However, CTU researchers observed much shorter lifecycles. The AWS STS credentials used by the Worker have a default expiration of 12 hours, so it is probable that the credentials are still valid after a Worker is shut down.
AWS introduced Lambda SnapStart for Java 11 runtime in November 2022. Its lifecycle is slightly different from the standard Lambda lifecycle. However, the standard lifecycle is suitable for this analysis because CTU™ researchers observed no differences in the detection logic.
Lambda initialization and logging
Every Lambda cold start records two CloudTrail events: AssumeRole and CreateLogStream (see Figure 3).
Figure 3. API calls during Lambda initialization. (Source: Secureworks)
- The AssumeRole event is recorded when the customer-defined Lambda identity access management (IAM) execution role requests AWS STS credentials from the invoke service. The timing of this event in the execution lifecycle is unclear, but CTU research shows it occurs prior to initialization of the Lambda Worker.
- The Lambda Worker makes an API call and attempts to create a CloudWatch log stream. If the CloudWatch log group does not exist, then the first Lambda execution’s CreateLogStream API call fails with error code ResourceNotFoundException. The Lambda Worker then attempts to call CreateLogGroup, followed by a successful CreateLogStream call.
The detection outlined in this analysis relies on the following assumptions:
- The Lambda execution IAM role is configured with permissions to log to CloudWatch. The AWS managed policy that is typically used, AWSLambdaBasicExecutionRole, includes these permissions.
- If the CloudWatch log group exists but the Lambda execution role does not have logging permission, then CloudTrail will still log the failed events.
- If the log group does not exist and the execution role does not have logging permission, then CreateLogStream generates no CloudTrail events.
Because the Lambda Worker is the source of the CreateLogStream event, the event includes the AWS region and source IP address where the customer’s Lambda function executes. The source of the AssumeRole event is outside the Worker and therefore does not contain this information. Invoke events are data events and are labeled in Figure 3 for completeness, but they are excluded from the following detection logic because they may not exist in all environments.
Figure 4 lists an example AssumeRole event created during the Lambda Init phase.
Figure 4. AssumeRole CloudTrail event. (Source: Secureworks)
Figure 5 lists an example CreateLogStream event.
Figure 5. CreateLogStream CloudTrail event. (Source: Secureworks)
Sometimes multiple CreateLogStream events possess the same access key ID. These events possibly represent the Lambda Worker starting multiple processes with the same STS credentials. Figure 6 shows the diff command output where two event keys match but the IP addresses are different.
Figure 6. Output of diff command comparing CreateLogStream events. (Source: Secureworks)
Figure 7 is an example of a Lambda initialization with concurrency.
Figure 7. API calls during Lambda initialization with concurrent execution. (Source: Secureworks)
AWS STS key reuse
CTU researchers discovered that AWS STS access keys can be reused over time and associated with unrelated events. The two events in Figure 8 possess the same STS access key ID (accessKeyId), but the credentials were assumed by different user identities that had different IAM roles. The principalId and userName fields are different components of the IAM role’s Amazon resource names (ARNs). The time difference (‘creationDate’) is only 11 days. It is possible that the AWS account ID is used as input to the access key generation process.
Figure 8. Output of diff command comparing CloudTrail events for different roles with the same access key ID. (Source: Secureworks)
Amazon Virtual Private Cloud (VPC) access
A Lambda function runs inside a VPC owned by the Lambda service. Lambdas can use an elastic network interface (ENI) to connect from the AWS-managed Lambda VPC to private subnets in a customer-managed VPC. When a Lambda is configured to access resources in a customer VPC and security groups are configured to allow egress, the CreateLogStream event’s source IP address will not match the public source IP address of other events.
Figure 9 is an example of a complete Lambda configuration with VPC access. Each number corresponds to a different source IP address in the CloudTrail event logs. CreateNetworkInterface and AllocateAddress events are generated by the Lambda Worker when connecting to the customer VPC. Other source IP addresses may appear in CloudTrail when a Lambda function interacts with other AWS resources. The source IP address for a request to AWS S3 via a VPC gateway endpoint is the private IP address assigned to the ENI. The request to DynamoDB routes via the internet, and the source is the public IP address assigned to the NAT gateway.
Figure 9. API calls during Lambda initialization with VPC access. (Source: Secureworks)
The VPC access scenario uses AllocateAddress events instead of CreateLogStream to find the public IP address, specifically the responseElements.publicIp field (see Figure 10).
Figure 10. AllocateAddress CloudTrail event. (Source: Secureworks)
The proof-of-concept detection uses Amazon Athena to identify CloudTrail events associated with the use of stolen Lambda credentials. This detection is portable and can be used ad-hoc in any AWS account, such as during an incident response investigation.
The first step in building the detector is to extract metadata from the CreateLogStream events. These events are generated by the Workers during the Init phase of the Lambda execution lifecycle. The CloudTrail logs are filtered on the following fields:
- eventName — Filter for the ‘CreateLogStream’ value, which the Worker uses to set up CloudWatch logging
- userAgent — Filter for strings that contain the ‘awslambda-worker’ value (e.g., awslambda-worker/1.0 rusoto/0.48.0 rust/1.67.1 linux)
A table is created using these events to form a baseline of standard operation. The table is populated with the following metadata fields:
- eventTime — timestamp of the CreateLogStream event (This event occurs seconds after the related AssumeRole event and can be used to infer when the STS credentials were generated.)
- userIdentity.accessKeyId — AWS STS access key ID used by the Worker and Lambda function
- userIdentity.arn — ARN of the IAM role associated with the access credentials, used to deconflict when STS access key IDs are reused
- sourceIPAddress — source IP address of the event (When Lambda concurrency is enabled, multiple IP addresses associated with Lambda executions use the same credentials.)
- awsRegion — geographical location of the AWS region where the event originated (This location provides additional information to disambiguate events.)
A second table is created that contains all customer-allocated public IP addresses. These are IP addresses that have been associated with a resource in an AWS account, such as a NAT gateway or EC2 instance. CloudTrail logs are filtered for ‘AllocateAddress’ events, and the following fields are extracted:
- publicIp — IP address allocated to an AWS resource
- allocationId — unique identifier for the original AllocateAddress event
- ·networkBorderGroup — location where the IP address was allocated within an AWS region
To determine if credentials have been exfiltrated or used outside the standard Lambda execution, a query is run against all CloudTrail events. It looks for events where the access key ID exist in the baseline Lambda events table and where the following criteria is met:
- The sourceIPAddress is different from the IP addresses used by the Lambda Worker in the Init phase.
- The userIdentity.arn is the same as the ARN used by the Lambda.
- The eventName is not Decrypt. AWS Key Management Service (KMS) generates decrypt events if enabled. This detector ignores these events.
- The eventTime is more recent than the Lambda Init phase.
- The sourceIPAddress has not been observed in the account. False positives could occur if sourceIPAddresses are very old and are not within the log retention period. This condition should not be an issue with this detector because the address gets allocated to the ENI at approximately the same time as the CreateLogStream event.
Applying the detection using Amazon Athena
Before AWS CloudTrail logs can be queried with Amazon Athena, the following prerequisites are required:
- Configure a trail to write logs to an S3 bucket if one does not already exist. AWS recommends this process as best security practice to store and retain events longer than 90 days. For the detection to be accurate, the trail should be configured to collect logs from all enabled AWS regions.
- Create an Athena table for CloudTrail logs. The default table name is ‘cloudtrail_logs’. If a different table name is used, the FROM statements in the Athena queries needs to be updated with the revised name.
When the prerequisites are completed, Amazon Athena can search CloudTrail events for use of stolen credentials. The Appendix of this analysis includes these Athena queries in plain text for researchers who want to replicate the detection.
- Create a separate table containing the CreateLogStream events (see Figure 11). The query will retrieve the earliest eventTime value and combine the source IP addresses from concurrent execution environments into an array.
Figure 11. Athena query to create table for CreateLogStream events. (Source: Secureworks)
- Create another table containing all of the public IP addresses associated with customer resources (e.g., internet gateway) (see Figure 12).
Figure 12. Athena query to create table for customer-allocated IP addresses. (Source: Secureworks)
- Query for events using stolen Lambda credentials (see Figure 13).
Figure 13. Athena query to select events using stolen credentials. (Source: Secureworks)
Figure 14 shows an example GetCallerIdentity event found with this detection.
Figure 14. Event detected using stolen credentials. (Source: Secureworks)
The following values should immediately flag this event as suspicious:
- awsRegion “us-east-1” does not match “us-west-2”
- eventName “GetCallerIdentity” is unlikely to be run by legitimate Lambda
- sourceIPAddress “203 . 0 . 113 . 9” does not match “34 . 220 . 84 . 211”
- sourceIPAddress “203 . 0 . 113 . 9” is not within the AWS IP address space
Taking the detector further
AWS publishes its public IP address ranges using Classless Inter-Domain Routing (CIDR) notation in JSON format. This data could increase detection logic efficiency by first checking if an IP address exists outside AWS, and then using more computationally expensive logic to check other conditions.
Similar detection logic could be applied to other services within AWS. Primary candidates would be AWS Elastic Container Service (ECS) or Elastic Kubernetes Service (EKS). Both services can leverage the AWS Fargate serverless compute engine, which is built on the same Firecracker microVM as the Lambda Worker.
Lambda pricing is calculated per one million requests. Cold starts in environments with a high frequency of executions happen less often. However, real-time detection could generate a large quantity of detector database inserts and result in a large table size.
The detection logic described in this analysis relies on an undocumented feature. At any time, AWS could change how Lambda executes.
AWS CloudTrail is a rich source of management events. Network defenders can use specific events in the AWS Lambda operating environment to provide a baseline or context for other events. The proof-of-concept detector using Athena can effectively search events for malicious behavior.
Appendix — Athena queries
Text versions of the Athena queries used in the detection proof of concept are provided for convenience to other researchers who want to explore this functionality.
Create table for CreateLogStream events
CREATE TABLE "lambda_coldstart" AS SELECT useridentity.accessKeyId as accesskeyid, -- Source IP can be different for the same access key id due to Lambda concurrency array_agg(sourceipaddress) as sourceipaddresses, awsregion, useridentity.arn as arn, array_agg(useragent) as useragents, MIN(eventtime) as eventtime FROM cloudtrail_logs WHERE eventname = 'CreateLogStream' AND useragent LIKE 'awslambda-worker%' GROUP BY 1, 3, 4
Create table for customer-allocated IP addresses
CREATE TABLE allocated_addresses AS SELECT json_extract_scalar(responseelements, '$.publicIp') as publicip, json_extract_scalar(responseelements, '$.allocationId') as allocationid, -- this is region name json_extract_scalar(responseelements, '$.networkBorderGroup') as networkbordergroup FROM "cloudtrail_logs" WHERE eventname = 'AllocateAddress';
Select events using stolen credentials
SELECT lcs.accesskeyid, lcs.sourceipaddresses, lcs.awsregion, ct.useridentity.accessKeyId, ct.sourceipaddress, ct.awsregion, ct.eventname, ct.eventid FROM cloudtrail_logs ct, lambda_coldstart lcs WHERE lcs.accesskeyid = ct.useridentity.accessKeyId AND not contains(lcs.sourceipaddresses, ct.sourceipaddress) AND -- Exclude AWS managed services ct.sourceipaddress != 'AWS Internal' AND -- access keys can be reused to make sure it's the same ARN (which will differ) ct.useridentity.arn = lcs.arn AND -- Decrypt is noisy for the purposes of this detector ct.eventname != 'Decrypt' AND ct.eventtime > lcs.eventtime AND -- Lookup IP addresses that have been allocated to account and exclude if they match NOT EXISTS (SELECT 1 FROM allocated_addresses aa WHERE aa.publicip = ct.sourceipaddress);