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:

Announcement

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 one should try is to insert a JavaScript-tag with an alert, to test for an XSS vulnerability: <script>alert(17);</script>. So, I just tried that:

First try to inject some malicious data

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".

Result of my first try

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

In a next step, I opened up Firefox' Development Tools and received a nice greeting from the JavaScript-Console:

Syntax error!

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.

Source code snippet

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 browser.

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&szlig;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 \'#--. In \\'#--, the first backslash escapes the second one, meaning that our apostrophe actually ends the string which is stored in result.innerHTML. Anything afterwards is treated as JavaScript-code. And since # is not a valid character for JavaScript source code we get the corresponding error message. Let's fix that!

Proof of concept

Knowing that ' is improperly escaped, we can abuse this for a nice XSS attack:

Exploit Payload

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 rendered.

XSS in action

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 &quot;, ' 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.

E-Mail

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.