Web Security

SSRF Exploitation: From URL Parameters to Internal Probing

Why SSRF is one of my favorite bugs to study

Server-side request forgery is interesting because the vulnerability is not always in the data that is returned. It is in the fact that the server can make requests to places I cannot reach directly. A small feature that lets a user submit a URL can turn into a path into the internal network, the cloud metadata service, or a service that was never meant to be public.

This post is my personal study note on SSRF. I am writing it because the bug is easy to overlook and hard to explain well. The first time I tested for SSRF, I sent a URL and received the content of that URL in the response. I thought the test was over. Later I learned that the most important part of SSRF testing is confirming the request with an out-of-band callback, because a response is not always visible.

Where SSRF hides

SSRF is usually hidden inside a feature that makes a request on behalf of the user. Image loading is the classic example. An application might let a user paste a URL for an avatar and then fetch that image on the server. PDF generation is another common example. The application receives a URL and includes the page in the PDF. Webhooks, URL scanners, RSS readers, link previews, and single sign-on features can also make server-side requests.

The feature does not have to look like a URL field. A parameter that controls a hostname, a file path, a redirect target, or a network address can be an SSRF entry point. I look for any place where the application asks for a destination and then connects to it. I also look at the JavaScript source for hidden URL parameters that are not shown in the UI.

The first step is recording every parameter that appears to control a server-side request. I keep a list with the parameter name, the HTTP method, the request body, and the normal response. This list is my test matrix. Without it, I tend to focus on the obvious URL field and miss the other parameters.

Confirming with an out-of-band callback

The most reliable confirmation is an out-of-band callback. I host a server that I control and listen for DNS and HTTP requests. When I submit my server's address as the URL, the target server should contact it. If I receive a callback, I know the application made a request. If I do not receive a callback, the URL may be blocked, validated, or never used.

I use a unique value in the callback path so that I can correlate the request with the exact parameter and the exact test. For example, I might request http://my-server.example/callback-123. If I see callback-123 in the access log, I know that this specific parameter caused the request.

The callback also tells me the source address. If the request comes from the application server, the request is a direct SSRF. If the request comes from a proxy or a cloud service, the path may include an intermediate system. The source address matters for the report, because it tells the developer where the request originated.

Reading the response when it is visible

Some SSRF endpoints return the response body. This makes testing much easier because I can see whether the request reached the destination. I submit a URL that returns known content, such as http://example.com/, and check whether the response contains that content. If it does, the application is returning the remote response.

The visible response is useful, but it also changes the risk. A blind SSRF is dangerous because it can still reach internal services, but a non-blind SSRF is easier to exploit because I can read the internal response. I test both cases and record whether the response is reflected in the output.

Even when the response is not visible, the request may still happen. I always use an out-of-band callback as the confirmation step. The visible response is a bonus, not a requirement.

Filters and how they fail

Many applications try to block SSRF by checking the submitted URL. The filter might reject private IP addresses, block certain protocols, or require the URL to match an allowlist. Filters are useful, but they fail in predictable ways.

The first failure is redirect following. The application checks the initial URL, allows it, and then follows a redirect to an internal address. The redirect target is not revalidated. I test this by hosting a URL that redirects to http://127.0.0.1/ and checking whether the server follows it.

The second failure is alternate host representations. An IP address can be written in decimal, octal, hex, or with trailing dots. A hostname can resolve to a private address after the filter has checked the string. I test these variations because the parser used by the filter may not be the same parser used by the HTTP client.

The third failure is DNS rebinding. The first DNS lookup returns a public address that passes the filter, and the second lookup returns a private address that the HTTP client actually connects to. This technique is more complex, but it is worth knowing about when the filter resolves hostnames.

The fourth failure is protocol confusion. A filter might allow HTTP and HTTPS but the underlying library also supports file, gopher, dict, ftp, or other schemes. I test whether the scheme is actually enforced by the library and not just by the filter.

Probing internal services

Once I have confirmed the SSRF, I can use it to probe the internal network. The probing must stay inside the authorized scope. I start with a small list of addresses that are commonly used for internal services, such as 127.0.0.1, 127.0.0.2, the local network range, and the metadata service addresses for the cloud provider.

Port scanning through SSRF is slow but possible. I send requests to the same host with different ports and compare the response time and the response body. A closed port often produces a connection error quickly, while an open port may produce a service banner, a timeout, or a different error message. The differences are noisy, so I repeat the test and look for patterns.

The service banner can reveal the protocol. If I request port 6379 and receive a Redis error, I know Redis is running. If I request port 3306 and receive a MySQL error, I know the database is running. I use this information to decide which protocol to try next.

The gopher protocol is especially interesting because it lets me send a complete protocol conversation in a single request. This is how I can interact with Redis, Memcached, and other services that do not use HTTP. The technique is powerful, but it requires precise formatting and a clear understanding of the service protocol.

Cloud metadata and the danger of credentials

The most serious SSRF targets are cloud metadata services. On many cloud platforms, an instance can query http://169.254.169.254/ to retrieve information about itself. The metadata service can return instance identity documents, network configuration, user data, and temporary role credentials. If the application server runs in the cloud and the SSRF can reach the metadata address, the attacker can steal those credentials.

I treat metadata testing carefully. The credentials returned by the metadata service may grant access to the entire cloud account. I do not want to use them for destructive actions or to access resources outside the authorized scope. I test whether the metadata endpoint is reachable and, if it is, I document the finding without causing unnecessary damage.

The response from the metadata service varies by provider and by metadata version. Some providers require a header, some use a different address, and some have changed the default settings to block access. I check the provider documentation and adapt the test.

Building a clean proof of concept

A good SSRF proof of concept starts with the out-of-band callback. I include the unique callback URL, the request that triggered it, and the callback log showing the connection. If the response body is visible, I include the response as well. If I probed an internal service, I include the service banner or the timing difference.

The proof should be reproducible. I write down the exact request, the parameters, the method, and the headers. I also write down the environment, because the result may depend on the cloud provider, the network configuration, or the application version. A proof that cannot be reproduced is not useful to the developer.

I do not include internal data that is not relevant to the finding. If I found a database banner, I do not need to dump the database. If I found the metadata service, I do not need to extract every credential. The goal is to demonstrate the risk with the least possible impact.

Preventing SSRF

The best prevention is an allowlist of destinations and protocols. If the application only needs to fetch images from a specific CDN, it should only allow URLs from that CDN. The allowlist should be enforced after DNS resolution, not just on the string, because the string can be misleading.

The application should also reject private, loopback, link-local, and metadata addresses. The rejection should happen after resolution and again before the connection is made. Redirects should be revalidated through the same policy, or disabled entirely.

Response handling is another control. The application should not return the raw response body for internal requests. If the feature needs to process the response, it should validate the content and return a generic result. This limits the attacker's ability to read internal data.

Practical notes on the callback server

The callback server needs to be reliable. I run it on a host that I control and that can accept DNS and HTTP traffic. The host should have a stable domain name, because the SSRF payload may use the domain in a URL.

I use a unique path and a unique subdomain for every test. The uniqueness makes the callback log easy to correlate. If I reuse the same callback for two tests, I cannot tell which request triggered the connection.

The callback server should also log the full request. The log includes the source IP, the user agent, the path, and the headers. The source IP tells me whether the request came from the target or from a proxy.

The callback can also be used to test DNS rebinding. I configure the domain to return a public IP on the first lookup and a private IP on the second. The target's DNS resolver may use the second value and connect to the internal service.

Mistakes I make

My biggest mistake is confirming SSRF with a visible response and stopping there. The visible response is convenient, but it is not proof that the request came from the server. I now use out-of-band callbacks for every confirmation. My second mistake is testing only HTTP. The other protocols are where the interesting services live. My third mistake is forgetting to check the redirect behavior. A filter can look perfect and still be bypassed by a redirect.

The fourth mistake is probing too aggressively. SSRF is powerful, but it can also touch services that are outside the scope or that are sensitive. I keep the probing minimal and document every step.

My quick checklist

  1. List every parameter that controls a server-side request.
  2. Confirm SSRF with an out-of-band callback and a unique token.
  3. Test redirect following and alternate host representations.
  4. Test protocols other than HTTP.
  5. Probe internal addresses only within the authorized scope.
  6. Check cloud metadata addresses carefully.
  7. Record the exact request, response, and callback log.
  8. Report the finding with a clean, reproducible proof.

What I would do next time

Next time I want to build the internal address map before I start probing. The map helps me decide which addresses are worth testing and which are outside the scope. I also want to spend more time on the redirect behavior, because that is where filters fail most often. SSRF is not just a URL field. It is a server-side network capability that deserves the same care as any other privilege.