Fawazeer Cyber - L33T Challenge writeup

Part 1

Starting the challenge and reading the source code of server.js, you can observe that the application uses prepared queries ‘almost’ everywhere in the code.

For example :

Register endpoint

app.post('/api/register', async (req, res) => {
  try {
    const { username, password } = req.body;
    
    if (!username || !password) {
      return res.status(400).json({ error: 'Username and password are required' });
    }

    const hashedPassword = await bcrypt.hash(password, 10);
    
    db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', 
      [username, hashedPassword, 'user'], 
      function(err) {
        if (err) {
          if (err.message.includes('UNIQUE constraint failed')) {
            return res.status(400).json({ error: 'Username already exists' });
          }
          return res.status(500).json({ error: err.message });
        }

Tasks endpoint

However, that’s not the case for the Login endpoint.

When querying the last_login column from the database, the user input is directly embedded into the SQL string. That’s clearly vulnerable to SQL Injection.

Now, because the application is using db.exec, this is considered a Stacked Queries SQL Injection.

Which basically means you can terminate the current query and start another one by using ; :

But you can’t simply do that, because of the bcrypt.compare in the Login endpoint :

This will compare the hash of the user in the database with the hash of the user you entered, so you need to INSERT the hash of the password not the plain text password. Now I’m not going to give a cryptography lesson here, if you didn’t get it just google it.

We can observe that in the register endpoint, the salt used is 10 salt rounds :

Now let’s hash our password :

the output is : $2a$10$PJ1ERREt/nf8TbM63.98r.OL1zoRg.HaYDHlrPJ.Ti2qcgiosYRC6

Now we can inject our query again with the hashed password :

Logging in :

That’s it for this part.

Part 2

Since we have admin access, let's focus on the endpoints that require it, those are usually juicy, right?

I will assume that you’ve gone through all of them. So, what did you conclude?

Unexploitable?

Nope :

The GET tasks endpoint has an interesting function call tasks_viewer(tasks[0].description) .

Lets check it out, but first you need to npm install in the same directory of package.json .

Well, that’s perfect.

Your BASE64 input is being passed to a function and this function is a Self-Invoking Function.arrow-up-right

Which means it will call itself immediately, and the way it passes our input to it is more like an eval.

Lets pull this function to a separate file to test our payloads:

Now if you’ve tried something like this as input :

It won’t work, you will get a ReferenceError: require is not defined .

because the function operates in global scope while require is local to the module and not global.arrow-up-right

Basically this seems like a sandbox ( it’s not ) but it can be easily bypassed by googling JS sandbox bypassarrow-up-right , and soon you will land on this payload :

Note : we’re using webhook to exfiltrate the data, because the website doesn’t return the output.

You will see an error in the console, but ignore it, you got what you want ( check your webhook ).

Now that everything is working we just need to construct our request to /api/tasks :

Don’t forget to base64 your payload.

The task ID is 2, by going to /api/tasks/2 , the payload will be passed to the tasks_viewer function and we will be done.

Follow @CalledSTRIKER in Xarrow-up-right Follow @CalledSTRIKER on GitHubarrow-up-right

Last updated