In this post I'd like to describe the process of how I found a stored XSS vulnerability in my University's Web-Portal and it's implications on the systems' users.
How it all started
It was a Friday morning. I got up early because I wanted to get stuff done before our Campus' CTF team meeting later that day. However, when I logged into the student's web portal for downloading some course materials, I saw this announcement from the day before:
Summary for the non-german readers: The portal now allows anyone to add custom appointments to their personal time table.
While most user's first thought was probably something along the lines "Wow, thats nice!", mine was actually more like "Uh nice, let's see where they screwed up!". So this was about the point in time where I decided to postpone my planned work and check some things.
Finding a potential attack vector
So, let's first see how you can add a personal appointment. In the menu on the right, there's a new button "Privaten Termin hinzufügen" ("Add private appointment"). When clicking on it, a popup opens where one can enter a title, a description and can set start and end time(s).
When looking for vulnerabilities in a web application, one of the first things
<script>alert(17);</script>. So, I just tried that:
Unfortunately, that didn't quite work, as all special characters were properly escaped at all occurances. Even encoding the special characters when entering the strings into the formular didn't result in anything "helpful".
So, I decided to try something else. I created another appointment with the
following string as title and description:
'#--. Skilled readers may recognize
this as a very very basic attempt to check for an
SQL injection. Anyways, I tried
my luck. Aaaand ... something unexpected happened indeed: When clicking on the
appointment, no yellow speech bubble appeared. While this could just be a minor
bug, I still decided to investigate this. I tested various different strings
and realized that the yellow speech bubble didn't appear whenever the title
contained an apostrophe (
Investigating the issue
SyntaxError: ilegal character This clearly looks like something went wrong
somewhere. As the error message also told me that the error occured in
remotecall.js:88, I started reading the source code there.
Interested readers can check out the source code themselves, others can find an explanation below.
On first sight, I was shocked. There were evil
eval()s all over the place.
eval() is a function that let's you pass it a string and it will execute it,
as if the string were source code.
Its use inside any codebase is frowned upon, as it can
negatively affect performance and security of the application.
However, I won't lose any more words about the badness of eval here.
So, what does this code actually do? If you click on an appointment, the browser
sends a request to the web-server which responds with something (more on that
later). When the response is completely received, it is being run through
eval() ... twice.
So, whatever was being inserted into one of the
eval()s, really upset the
Checking the response
So, let's see what the server sends back to us:
var result = new ResultObject('BubbleDialog_Content','BubbleDialogShow_Callback', 'true');result.innerHTML='<div id="infobubble"><table cellpadding="0" cellspacing="0" style="width:100%;height:100%"><tr><td valign="top"><table cellpadding="0" cellspacing="0" style="width:100%"><tr><td style="padding-bottom:5px;border-bottom:1px solid #000000"><table cellpadding="0" cellspacing="0"><tr><td><img src="../images/bubble/cal0.gif" border="0" width="10" height="13"/></td><td class="bd_datum" style="padding-left:3px">Fr., 24. März 2017</td><td style="padding-left:15px;padding-right:3px"><img src="../images/bubble/clock0.gif" border="0" width="14" height="13"/></td><td class="bd_datum">11:30-12:00</td></tr></table></td><td class="bd_datum" style="padding-bottom:5px;border-bottom:1px solid #000000" align="right"><img title="Schließen" src="../images/bubble/close0.gif" border="0" width="14" height="14"/ style="cursor:pointer;" onclick="BubbleDialog.hide();"></td></tr><tr><td colspan="2"><table cellpadding="0" cellspacing="0" style="width:100%"><tr><td valign="top"><div class="bd_titel">\\'#--</div><div class="bd_text" style="margin-top:10px">\'#--</div></td></tr></table></td></tr></table></td></tr></table></div>';RemoteCallBubble.returnResult(result);
If syntax highlighting of the above code snippet works correctly, you may
already see what's causing the error. Apparently, the server sends back the
complete HTML-code responsible for displaying the popup. The
eval()s are then
responsible for rendering and displaying the popup.
So, let's come back to the
illegal syntax error message. For that, let's
reduce the code-snippet a bit:
var result = new ResultObject('BubbleDialog_Content','BubbleDialogShow_Callback', 'true'); result.innerHTML='<div class="bd_titel">\\'#--</div><div class="bd_text" style="margin-top:10px">\'#--</div>'; RemoteCallBubble.returnResult(result);
Remember, that our malicious string we inserted was the following:
For the title this was escaped from the web server to
\\'#--, while for the
description it was escaped (to the correct!) sequence
first backslash escapes the second one, meaning that our apostrophe actually
ends the string which is stored in
result.innerHTML. Anything afterwards is
# is not a valid character for
Proof of concept
' is improperly escaped, we can abuse this for a nice XSS attack:
The first semicolon is necessary to separate the
result.innerHTML = '..'
statement from our code. The double slash at the end (a comment-delimiter) is
nesecarry to tell the
eval() to "ignore the chunk afterwards". As we're
already in control of execution it doesn't matter if and how the HTML could be
Limitations of the exploit
Despite being a stored XSS, the vulnerability has some drawbacks:
- The entire payload is limited to 100 characters, as this is the maximimum
allowed length of the title. This also includes the bare minimum prefix (
';') and suffix (
;//) characters, leaving only 95 characters for exploit code. And yes, the length limit is checked server-side, so no way to circumvent that.
- We cannot really use strings.
"are escaped to
\\', which means that we cannot use either of them.
- This only leaves one option to generate strings:
String.fromCharcode(int, ...)which is very verbose and does not play nice with the character limit.
- The exploit is only triggered when a user actively clicks on the malformed appointment.
- The user who creates the malicious appointment is also the only user who will ever see it (as it is a private appointment after all). The same exploit would also have been possible for any regular time-table entry (a dev confirmed this), however only a limited number of people can actually create those entries. Additionally these are checked more thoroughly.
- Therefore, no user was at risk at any time.
Shoutouts to Gregor Fleissner
So, at this point in such a disclosure usually there's some information about when the developers where initially informed about the vulnerability, when they fixed it, and so on. Well, actually it was a bit different in this case.
As I already said at the beginning, I had not much time because there was a meeting later that day where the Campus Security team participated in a CTF. Therefore, I had to go offline before I could summarize my findings and inform the development team of this vulnerability.
However, on my way to University, I received a mail from the department which is responsible for the web portal.
They identified my tampering with the system and fixed the vulnerability even
before I could tell them about it. I'm honestly impressed. So shoutouts to
Gregor and his crew for creating and providing a good platform. Despite the
heavy use of
eval()s, the site is robust and well engineered.