From be193e05b4767215f7c5cd7d87552e6873d7ac6d Mon Sep 17 00:00:00 2001 From: Sid Wadikar Date: Mon, 29 Jul 2024 11:13:55 -0700 Subject: [PATCH] Create lambda function Summary: Opensourcing new TTP for Cloud. Differential Revision: D60400010 --- .../lambda/create-lambda-function/README.md | 89 ++++++++++ .../create-lambda-function.yaml | 161 ++++++++++++++++++ .../create-lambda-function/lambda-function.py | 25 +++ .../create-lambda-function/trust-policy.json | 13 ++ 4 files changed, 288 insertions(+) create mode 100644 ttps/cloud/aws/lambda/create-lambda-function/README.md create mode 100644 ttps/cloud/aws/lambda/create-lambda-function/create-lambda-function.yaml create mode 100644 ttps/cloud/aws/lambda/create-lambda-function/lambda-function.py create mode 100644 ttps/cloud/aws/lambda/create-lambda-function/trust-policy.json diff --git a/ttps/cloud/aws/lambda/create-lambda-function/README.md b/ttps/cloud/aws/lambda/create-lambda-function/README.md new file mode 100644 index 0000000..72df6b2 --- /dev/null +++ b/ttps/cloud/aws/lambda/create-lambda-function/README.md @@ -0,0 +1,89 @@ +# Create AWS Lambda Function + +![Meta TTP](https://img.shields.io/badge/Meta_TTP-blue) + +This TTP is used to create a new Lambda function in AWS. It uses the AWS CLI to create a new function with the specified name. + A temprory IAM role, S3 bucket are created to hold the lambda function code. + If a function with given name exists, no new function is created. + If a function does not exist a new function is created. It is also deleted during cleanup. + A public API gateway is created to also invoke this function. This enables use to invoke the function from the internet. + `--no-cleanup` options should be explicitly specified if we do not want the new function created to be deleted. + + +## Arguments + +- **region**: Name of the AWS region to use with TTPForge. +- **lambda_function_name**: The name of the new Lambda function to be created. + +## Steps + +1. Set up necessary cloud environment variables. +2. Create a temprory IAM role to be assumed by the lambda function. +3. Create a S3 bucket with a random string name +4. Zip and upload lambad code from local device to the newly created S3 bucket. +5. Create a new Lambda function if no existing function is found with given function name. +6. Create a API which is publicly accessible and integrate the newly created lambda function with it. +6. Invoke the newly created Lambda function by accessing the API URL +7. By default during the cleanup, delete the recently created Lambda function, S3 bucket, IAM role and the API gateway. + +## Manual Reproduction Steps + +``` +# Set necessary env variables + +# Create new temp IAM role +aws iam create-role --role-name "ROLE-NAME" --assume-role-policy-document file://trust-policy.json + +# Create a new S3 bucket to host lambda function +aws s3 mb s3://BUCKET_NAME + +# Upload lambda function zip file to S3 bucket +aws s3 cp lambda-function.zip s3://BUCKET_NAME + +# Create a new function +aws lambda create-function --function-name "FUNCTION_NAME" --runtime "RUNTIME" --handler "HANDLER" --role "ROLE_ARN" --code "S3Bucket=$BUCKET_NAME,S3Key=lambda-function.zip" + +# Create a new REST API +rest_api_id=$(aws apigateway create-rest-api --name "FUNCTION_NAME"-api --query 'id' --output text) + +# Get the root resource id +root_resource_id=$(aws apigateway get-resources --rest-api-id $rest_api_id --query 'items[?path==`/`].id' --output text) + +# Create a GET method for the new resource +aws apigateway put-method --rest-api-id $rest_api_id --resource-id $root_resource_id --http-method GET --authorization-type "NONE" > /dev/null 2>&1 + +# Add necessary permissions to the new resource method +lambda_func_arn=$(aws lambda get-function --function-name "FUNCTION_NAME" --query 'Configuration.FunctionArn' --output text) +account_id=$(aws sts get-caller-identity --query Account --output text) +account_region=$(aws configure get region) +source_arn="arn:aws:execute-api:$account_region:$account_id:$rest_api_id/*/*/" + +aws lambda add-permission --function-name $lambda_func_arn --source-arn $source_arn --principal apigateway.amazonaws.com --statement-id apigateway-test1-"FUNCTION_NAME" --action lambda:InvokeFunction > /dev/null 2>&1 + + +# Connect the GET method to the Lambda function (define integration) +lambda_invocation_uri="arn:aws:apigateway:$account_region:lambda:path/2015-03-31/functions/$lambda_func_arn/invocations" +aws apigateway put-integration --rest-api-id $rest_api_id --resource-id $root_resource_id --http-method GET --type AWS --integration-http-method POST --uri $lambda_invocation_uri > /dev/null 2>&1 + + +# Specify an INTEGRATION RESPONSE for a method response code of 200 and allow Passthrough content handling +aws apigateway put-integration-response --rest-api-id $rest_api_id --resource-id $root_resource_id --http-method GET --status-code 200 > /dev/null 2>&1 + +# Specify a METHOD response for HTTP status code 200 +aws apigateway put-method-response --rest-api-id $rest_api_id --resource-id $root_resource_id --http-method GET --status-code 200 > /dev/null 2>&1 + +# Deploy the API +aws apigateway create-deployment --rest-api-id $rest_api_id --stage-name prod > /dev/null 2>&1 + +# Get the invoke URL and curl it +invoke_url="https://$rest_api_id.execute-api.$account_region.amazonaws.com/prod" +curl $invoke_url + +``` + +## MITRE ATT&CK Mapping + +- **Tactics**: + - TA0003 Persistence +- **Techniques**: + - T1098 Account Manipulation diff --git a/ttps/cloud/aws/lambda/create-lambda-function/create-lambda-function.yaml b/ttps/cloud/aws/lambda/create-lambda-function/create-lambda-function.yaml new file mode 100644 index 0000000..7cffea3 --- /dev/null +++ b/ttps/cloud/aws/lambda/create-lambda-function/create-lambda-function.yaml @@ -0,0 +1,161 @@ +--- +api_version: 2.0 +uuid: 4b783ca9-7cf7-49c4-a74f-26c753a4b1c2 +name: create_lambda_function +description: | + This TTP is used to create a new Lambda function in AWS. It uses the AWS CLI to create a new function with the specified name and runtime. + If a function with given name exists, nothing is done and the TTP is closed. + If a function does not exist a new function is created. It is also deleted during cleanup. + A public API gateway is also created to invoke this function. This enables use to invoke the function from the internet. + `--no-cleanup` options should be explicitly specified if we do not want the new function created to be deleted. + +args: + - name: lambda_function_name + description: The name of the new Lambda function to be created. + default: ttpforge-lambda-function + - name: region + description: Name of the AWS region to use with TTPForge + default: us-east-1 + +mitre: + tactics: + - TA0003 Persistence + techniques: + - T1098 Account Manipulation + +steps: + - name: aws-connector + description: This step invokes the verifies aws creds are present and aws cli is available. + ttp: //helpers/cloud/aws/validate-aws-env-configured.yaml + args: + region: "{{ .Args.region }}" + + - name: create_temp_iam_role + description: Create a temp IAM role that will be used by the lambda function. + inline: | + echo "Creating temp IAM role..." + + role_name="{{.Args.lambda_function_name}}-role" + aws iam create-role --role-name $role_name --assume-role-policy-document file://trust-policy.json + echo "Temp IAM role created: $role_name" + + cleanup: + inline: | + echo "Deleting temp IAM role..." + + role_name="{{.Args.lambda_function_name}}-role" + aws iam delete-role --role-name $role_name + + - name: create_s3_bucket + description: Create a S3 bucket with a random string name. + inline: | + echo "Creating S3 bucket..." + + bucket_suffix=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 10 | head -n 1) + bucket_name="{{.Args.lambda_function_name}}-${bucket_suffix}" + + aws s3 mb s3://$bucket_name + echo "S3 bucket created: $bucket_name" + cleanup: + inline: | + echo "Deleting S3 bucket..." + bucket_name=$(aws s3 ls | grep {{.Args.lambda_function_name}} | awk '{print $3}') + echo $bucket_name + + aws s3 rb s3://$bucket_name --force + + - name: upload_lambda_code + description: Upload the lambda code to the newly created S3 bucket. + inline: | + echo "Uploading lambda code to S3 bucket..." + + bucket_name=$(aws s3 ls | grep {{.Args.lambda_function_name}} | awk '{print $3}') + echo $bucket_name + + zip -r lambda-function.zip lambda-function.py + aws s3 cp lambda-function.zip s3://$bucket_name + + echo "Lambda code uploaded to S3 bucket: $bucket_name" + + cleanup: + inline: | + echo "Deleting created lambda function zip file..." + rm -rf lambda-function.zip + + - name: create_lambda_function + description: Check if the specified Lambda function exists. If not create a new one. + inline: | + echo "Checking if function {{.Args.lambda_function_name}} exists..." + role_name="{{.Args.lambda_function_name}}-role" + role_arn=$(aws iam get-role --role-name $role_name --query 'Role.Arn' --output text) + bucket_name=$(aws s3 ls | grep {{.Args.lambda_function_name}} | awk '{print $3}') + + function_exists=$(aws lambda list-functions --query 'Functions[?FunctionName==`{{.Args.lambda_function_name}}`]' 2>&1) + if [ -n "$function_exists" ] && [ "$function_exists" = "[]" ]; then + echo - "Function does not exist. Creating a new function..." + aws lambda create-function --function-name {{.Args.lambda_function_name}} --runtime "python3.9" --handler lambda-function.lambda_handler --role $role_arn --code S3Bucket=$bucket_name,S3Key=lambda-function.zip + echo "New Lambda function created: {{.Args.lambda_function_name}}" + sleep 5 + else + echo "Function {{.Args.lambda_function_name}} already exists." + fi + cleanup: + inline: | + echo "Deleting Lambda function..." + aws lambda delete-function --function-name {{.Args.lambda_function_name}} + echo "Lambda function deleted: {{.Args.lambda_function_name}}" + + - name: create_api_and_invoke + description: Invoke the newly created Lambda function and display the output. This is a public API, that means anyone on the internet can invoke this lambda function + inline: | + echo "Creating REST API Gateway..." + + # Create a new REST API + rest_api_id=$(aws apigateway create-rest-api --name {{.Args.lambda_function_name}}-api --query 'id' --output text) + + # Get the root resource id + root_resource_id=$(aws apigateway get-resources --rest-api-id $rest_api_id --query 'items[?path==`/`].id' --output text) + + # Create a GET method for the new resource + aws apigateway put-method --rest-api-id $rest_api_id --resource-id $root_resource_id --http-method GET --authorization-type "NONE" > /dev/null 2>&1 + + # Add necessary permissions to the new resource method + lambda_func_arn=$(aws lambda get-function --function-name {{.Args.lambda_function_name}} --query 'Configuration.FunctionArn' --output text) + account_id=$(aws sts get-caller-identity --query Account --output text) + account_region=$(aws configure get region) + statement_id=apigateway-$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 10 | head -n 1)-{{.Args.lambda_function_name}} + source_arn="arn:aws:execute-api:$account_region:$account_id:$rest_api_id/*/GET/" + + aws lambda add-permission --function-name $lambda_func_arn --source-arn $source_arn \ + --principal apigateway.amazonaws.com --statement-id $statement_id --action lambda:InvokeFunction > /dev/null 2>&1 + + + # Connect the GET method to the Lambda function (define integration) + lambda_invocation_uri="arn:aws:apigateway:$account_region:lambda:path/2015-03-31/functions/$lambda_func_arn/invocations" + aws apigateway put-integration --rest-api-id $rest_api_id --resource-id $root_resource_id \ + --http-method GET --type AWS --integration-http-method POST --uri $lambda_invocation_uri > /dev/null 2>&1 + + + # Specify an INTEGRATION RESPONSE for a method response code of 200 and allow Passthrough content handling + aws apigateway put-integration-response --rest-api-id $rest_api_id --resource-id $root_resource_id \ + --http-method GET --status-code 200 --response-templates '{"application/json":"$input.path('\'$.body\'')"}' > /dev/null 2>&1 + + # Specify a METHOD response for HTTP status code 200 + aws apigateway put-method-response --rest-api-id $rest_api_id --resource-id $root_resource_id \ + --http-method GET --status-code 200 > /dev/null 2>&1 + + # Deploy the API + aws apigateway create-deployment --rest-api-id $rest_api_id --stage-name prod > /dev/null 2>&1 + + # Get the invoke URL and curl it + invoke_url="https://$rest_api_id.execute-api.$account_region.amazonaws.com/prod" + echo "Invoke URL: $invoke_url" + sleep 10 + curl $invoke_url + + cleanup: + inline: | + echo "Deleting REST API Gateway..." + rest_api_id=$(aws apigateway get-rest-apis --query 'items[?name==`{{.Args.lambda_function_name}}-api`].id' --output text) + aws apigateway delete-rest-api --rest-api-id $rest_api_id + echo "REST API Gateway deleted: $rest_api_id" diff --git a/ttps/cloud/aws/lambda/create-lambda-function/lambda-function.py b/ttps/cloud/aws/lambda/create-lambda-function/lambda-function.py new file mode 100644 index 0000000..09f7ee6 --- /dev/null +++ b/ttps/cloud/aws/lambda/create-lambda-function/lambda-function.py @@ -0,0 +1,25 @@ +import json +import logging + +import boto3 + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +def lambda_handler(event, context): + sts = boto3.client("sts") + sts_response = sts.get_caller_identity() + print("STS Response: ", sts_response) + + logger.info(f"CloudWatch logs group: {context.log_group_name}") + + formatted_sts_response = json.dumps(sts_response, indent=4) + return { + "statusCode": 200, + "body": formatted_sts_response, + "headers": { + "Content-Type": "text/plain", + }, + "isBase64Encoded": False, + } diff --git a/ttps/cloud/aws/lambda/create-lambda-function/trust-policy.json b/ttps/cloud/aws/lambda/create-lambda-function/trust-policy.json new file mode 100644 index 0000000..381f2d3 --- /dev/null +++ b/ttps/cloud/aws/lambda/create-lambda-function/trust-policy.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Effect": "Allow", + "Sid": "" + } + ] + }