Server-Side Request Forgery (SSRF) means a server makes requests for us even though it shouldn’t. You can exploit SSRF in various ways. You can proxy requests through the vulnerable server. Or maybe requests from the server are “trusted” to access some sensitive resource, as bug bounty hunters love to do.
Or you try to access internal network resources from a local IP, thus bypassing the firewall. Which is what we’ll look at in this article.
Show me the code
If you’re a pentester, you likely prefer hands-on, DIY demonstration to theory. So let’s code up a simple app in Python that’s vulnerable to SSRF, then show how to exploit it.
Imagine we host a news site where users leave comments. When a comment includes a link, we generate a small text blurb “preview” of the page they link to. The preview text comes from the web site’s meta description. But if we can’t find a meta description, grab the first 100 chars of text on the page. First, we need a function to grab meta descriptions (and fallback on the page text if we don’t find any):
from html.parser import HTMLParser
import requests
# parses HTML to get us the meta description
class MetaDescriptionHTMLParser(HTMLParser):
def handle_starttag(self, tag, attrs):
name_attr = dict(attrs).get('name', None)
if tag == 'meta' and name_attr == 'description':
attributes = dict(attrs)
self.description = attributes['content']
def get_description(url):
r = requests.get(url)
parser = MetaDescriptionHTMLParser()
text = r.text
parser.feed(text)
desc = parser.description
# if no description, return the first 100 characters of the page
if not desc:
return text[:100]
return parser.description
So to make sure it really works, we test it on the Python REPL:
$ python3
>>> from meta_desc.meta_desc import get_description
>>> get_description('https://hackingloops.com/')
'Learn ethical hacking, penetration testing, cyber security, best security and web penetration testing techniques from best ethical hackers in security field.'
>>> # also test a site with no meta desc to get 100 chars
>>> get_description('https://icanhazip.com')
'190.14.134.77\n'
Great! Next, we’ll make this blurb generation feature an API endpoint using Flask. We don’t need anything fancy, since we’re just trying to show the concept.
from flask import Flask
from meta_desc.meta_desc import get_description
app = Flask(__name__)
@app.route("/blurb/<url>")
def blurb(url):
return get_description('https://' + url)
Since Flask is so simple, that’s all we need! Well, the app is now officially vulnerable – we can give this endpoint any URL and get (part of) the response. Because we don’t see the whole response, this would be classified as a semi-blind SSRF vulnerability.
Sadly there isn’t anything to exploit because we don’t actually have any sensitive services hiding behind a firewall in the local network. So let’s add another endpoint to our API that blocks out non-local traffic.
@app.route("/top_secret")
def secret():
if request.remote_addr != '127.0.0.1':
return 'BLOCKED, OUTSIDER!', 403
return 'the admin password is iL0v3_MONEEEEY'
It’s a bit reductive – in real life, there’d be a proper firewall for us to sneak under. But to make this simpler as a proof of concept, the above will have to suffice.
Now we can finally get to the fun part: creating an exploit to hack the app!
Exploiting our app’s SSRF
I’m running the blurb API on port 8000 and the top-secret internal-only API on port 5050, all on localhost. This is pointless if you run it all on your local machine, since all your requests are local anyway, and you can just access the internal-only API directly.
So I deployed all of the code to Heroku. You don’t have to do this, the exploit will still work if you do everything on your laptop. But it’s cooler feeling like I’m really hacking past a defense layer. As of the time of writing, the app can be found at ssrf-example.herokuapp.com.
To make sure everything’s working as expected, let’s try it out.
$ curl https://ssrf-example.herokuapp.com/blurb/hackingloops.com
Learn ethical hacking, penetration testing, cyber security, best security and web penetration testing techniques from best ethical hackers in security field.
Fantastic, we can confirm that it works to fetch blurbs, as well. Anyway, let’s craft a request to sneak past the code that blocks non-local IPs. This is it, if this works, it means our exploit is a success – we’ll have successfully executed an SSRF attack. (Against our own code, but still!)
$ curl https://ssrf-example.herokuapp.com/blurb/localhost/top_secret
the admin password is iL0v3_MONEEEEY
$ # It worked, we snagged the secret from the "forbidden" endpoint!
Beautiful! We’ve made used the blurb to make the request to the /top_secret
endpoint from a trusted, internal node.
Lessons for defenders
Firewalls are a great fallback for if your defenses fail. Which is good. No one is perfect, and your stack will inevitably have holes. A strong firewall can save you when attackers find those holes. You should not treat firewalls as a good-enough solution to protecting sensitive nodes in your network. Blessing internal traffic as inherently trustworthy just because you have a firewall is a dangerous mistake.
Why? As we’ve shown above, SSRF can easily crawl right under your firewall under the right circumstances. And it’s just one of a myriad of vulns that allow attackers to get through a firewall.
Thus, even the most well-hidden endpoints deserve authentication. You should authenticate, encrypt, prevent bots (using a secure Captcha, ideally), and apply all the other normal security measures to internal apps.
Even if your defense is perfect, it’s hard to predict something like a developer using ngrok to expose your internal network directly to the global internet. In a large org, anything can happen, so robust security is a necessity.
Security should come in layers – never assume that traffic is legitimate just because it comes from inside your network.
Preventing SSRF with code
One more note on defending against this – when developing a web app, make sure you disallow requests to private hosts. You can do this with the is_private
method of the ipaddress
module in Python, but any other popular language will have a similar library out there.
Even if you harden your internal infrastructure using authentication and so on, this will make you one level stronger by preventing SSRF requests from happening in the first place. The actual code looks like this:
import ipaddress
from socket import getaddrinfo
def is_ssrf(domain):
addrinfo, = getaddrinfo(domain, 80)
ip, _port = addrinfo[-1]
ipaddr = ipaddress.ip_address(ip)
return ipaddr.is_private
That’s to show you how the defense works, in real life you’d want to use a library to handle this for you. For example, Python’s antissrf package.
SSRF in the real world
Interesting stuff, but does anyone actually exploit this?
Absolutely, SSRF is a commonly exploited vulnerability, whether in bug bounties or real breaches. According to
One of the largest banks in the United States was just fined $80 million for a massive data breach – a 2019 incident which exposed personal data of 100 million Americans and six million Canadians.
https://cyberstartupobservatory.com/ssrf-attacks-and-data-breaches-what-you-need-to-know/
The breach was the result of an SSRF (Server Side Request Forgery) attack.
This attack has become more common over the last few years, as cloud architectures have created more opportunities for threat actors to use this technique.
And that’s just the beginning. SSRF is one of the most frequent security bugs disclosed via bug bounty programs on both BugCrownd and HackerOne. Companies are quickly learning that application security is every bit important as traditional infrastructure hardening.
Leave a Reply