BBN challenge resolutions: "A properly secured parameter" and "Exploiting a static page"

Posted

BugBountyNotes is quickly becoming a great resource for security researches. Their challenges in particular are a fun way of learning ways to exploit vulnerable code. So a month ago I decided to contribute and created two challenges: A properly secured parameter (easy) and Exploiting a static page (medium). Unlike most other challenges, these don’t really have any hidden parts. Pretty much everything going on there is visible, yet exploiting the vulnerabilities still requires some thinking. So if you haven’t looked at these challenges, feel free to stop reading at this point and go try it out. You won’t be able to submit your answer any more, but as both are about exploiting XSS vulnerabilities you will know yourself when you are there. Of course, you can also participate in any of the ongoing challenges as well.

Still here? Ok, I’m going to explain these challenges then.

What’s up with that parameter?

We’ll start with the easier challenge first, dedicated to all the custom URL parsers that developers seem to be very fond of for some reason. The client-side code makes it very obvious that the “message” parameter is vulnerable. With the parameter value being passed to innerHTML, we would want to pass something like <img src=dummy onerror=alert("xss")> here (note that innerHTML won’t execute <script> tags).

But there is a catch of course. Supposedly, the owners of that page discovered the issue. But instead of putting resources into fixing it, they preferred a quick band-aid and configured a Web Application Firewall to stop attacks. That’s the PHP code emulating the firewall here:

 if (preg_match('/[^\\w\\s-.,&=]/', urldecode($_SERVER['QUERY_STRING'])))
    exit("Invalid parameter value");

The allowed character set here is the bare minimum to allow the “functionality” to work, and I feel really sorry for anybody who tried to solve the challenge by attacking this “firewall.” The only way around this filter is to avoid going through it in the first place.

It might not be immediately obvious but the URL parser used by the challenge is flawed:

      function getParam(name)
      {
        var query = location.href.split("?")[1];
        if (!query)
          return null;

        var params = query.split("&");
        for (var i = 0; i < params.length; i++)
        {
          var parts = params[i].split("=");
          if (parts[0] == name)
            return decodeURIComponent(parts[1]);
        }
        return null;
      }

Do you see the issue? Yes, it assumes that anything following the question mark is the query string. What it forgets about is the fragment part of the URL, the one following the # symbol. Any parameters in the fragment will be parsed as well. This wouldn’t normally be a big deal, but the fragment isn’t sent to the server! This means that no server-side firewall can see it, so it cannot stop attacks coming from this direction.

So here are some URLs that will trigger the XSS vulnerability here:

  • https://www.bugbountytraining.com/challenges/challenge-10.php#?message=%3Cimg%20src%3Ddumm%20onerror%3Dalert(%22xss%22)%3E
  • https://www.bugbountytraining.com/challenges/challenge-10.php?message=#%3Cimg%20src%3Ddumm%20onerror%3Dalert(%22xss%22)%3E

Of course, answers submitted by BBN users contained quite a few more variations. But what really surprised me was just how many people managed to solve this challenge without understanding how their solution worked. It seems that they attacked the Web Application Firewall blindly and just assumed that the firewall treated the # character specially for some reason.

Let’s close with an advise for all developers out there: don’t write your own URL parser. Even though URL parsing appears simple, there are many fall traps. If you need to do it, use the URL object. If you need to parse query parameters, use the URLSearchParams object. Even in non-browser environments, there are always well-tested URL parsers already available.

The long route to exploiting a message handler

The other challenge has no server side whatsoever, it’s merely a static web page. And the issue with that page should also be fairly obvious: it listens to message events. When browsers added window.postMessage() API as a means of cross-domain communication, the idea was that any recipient would always check event.origin and reject unknown sources. But of course, many websites fail to validate the message sender at all or go for broken validation schemes. It is no different for this challenge.

Instead of validating the sender, this page validates the recipient: the recipient stated in the message has to match the page’s window name. Now the window name can be easily set by the attacker, e.g. by setting a name for the frame that this page is loaded into. The difficulty here is that the page will only consider certain recipients as “valid,” namely those where its own Buzhash variant results in 0x70617373 (or as a string: “pass”).

And that hash function is mean: no matter the input, the two bytes in the middle will always be NUL bytes! At least that’s the case as long as you constrain yourself to the ASCII character set. Once you start playing around with Unicode, the desired answer actually becomes possible. A bit of experimentation gives me "\x70\x61\u6161\0\0\0\0\u7373" as a valid recipient. But because NUL bytes in the <iframe name> attribute won’t work, I had to experiment a bit more to find a somewhat less obvious solution: "\x70\x10\x10\x10\x10\u6161\u6100\u7373". Some BBN users solved this issue more elegantly: while NUL bytes in attributes don’t work, using them when setting iframe.name property works just fine. One submission also used Microsoft Z3 theorem prover instead of mere experimentation to find a valid recipient.

Once we managed to get the page to accept our message, what can we do then? Not a lot, we can make the page create a custom event for us. But there are no matching event listeners! That is, until you realize that jQuery’s ajaxSuccess callback is actually a regular event handler. So we can trigger that callback.

But the callback merely sets element text, it doesn’t use innerHTML or its jQuery equivalent. So not vulnerable? No, setting text is unproblematic. But this code selecting the element is:

$(data.selector)

The jQuery constructor is typically called with a selector. However, it supports a large number of different calling conventions. In particular, it (and many other jQuery methods) can be called with HTML code as parameter. This can lead to very non-obvious security issues as I pointed out a few years ago. Here, passing some HTML code as “selector” will allow the attacker to run JavaScript code.

Here is my complete solution:

<script>
  window.onload = function()
  {
    var frame = document.getElementById("frame");
    frame.contentWindow.postMessage({
      type: "forward",
      event: "ajaxSuccess",
      selector: "<img src=x onerror=alert(document.domain)>",
      recipient: "\x70\x10\x10\x10\x10\u6161\u6100\u7373"
    }, "*");
  };
</script>
<iframe id="frame" src="https://www.bugbountytraining.com/challenges/challenge-8.html" name="&#x70;&#x10;&#x10;&#x10;&#x10;&#x6161;&#x6100;&#x7373;"></iframe>

This is only one way of demonstrating the issue of course, and some of the submissions from BBN users were more elegant than what I came up with myself.

Categories: ,

Comments

There are currently no comments on this article.

Comment

Enter your comment below. Fields marked * are required. You must preview your comment before submitting it.





← Older Newer →