Try   HackMD

TSG CTF 2021 Beginner's Web 2021 Writeup

Challenge Summary

You are given a website.

It is doing some weird job. First, it constructs routes object based on the salt parameter and store it to the session.

const setRoutes = async (session, salt) => { const index = await fs.readFile('index.html'); session.routes = { flag: () => '*** CENSORED ***', index: () => index.toString(), scrypt: (input) => crypto.scryptSync(input, salt, 64).toString('hex'), base64: (input) => Buffer.from(input).toString('base64'), set_salt: async (salt) => { session.routes = await setRoutes(session, salt); session.salt = salt; return 'ok'; }, [salt]: () => salt, }; return session.routes; };

Then, it will render the page based on the routes object.

app.get('/', async (request, reply) => { // omitted const {action, data} = request.query || {}; let route; switch (action) { case 'Scrypt': route = 'scrypt'; break; case 'Base64': route = 'base64'; break; case 'SetSalt': route = 'set_salt'; break; case 'GetSalt': route = session.salt; break; default: route = 'index'; break; } reply.type('text/html') return session.routes[route](data); });

Solution

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

The flag is in the flag route, so you will want to set session.salt = 'flag', but by doing so, flag route will be overwritten by [salt] end point and you will lose access to it.

So, what we want to achieve is the following session state.

session = {
    routes: {
        flag: ...,
        index: ...,
        ...
        [salt]: ..., // salt is anything different from 'flag'
    },
    salt: 'flag',
}

1st request

First, you have to send GET /?action=SetSalt&data=flag to set salt = 'flag'. The session will be the following structure.

session = {
    routes: {
        flag: ..., // this route is overwritten and not accessible
        index: ...,
        ...
        flag: ...,
    },
    salt: 'flag',
}

2nd request

Second is the most important part. Now we want to recover session.routes so that we can access the flag route, but we don't want to update salt.

The key is set_salt function. Normally, it updates routes and salt together.

set_salt: async (salt) => { session.routes = await setRoutes(session, salt); session.salt = salt; return 'ok'; },

What if line 2 is executed but line 3 is NEVER executed? It is possible.

In line 2, we are await-ing the execution of setRoutes function. Do you know async/await in ECMAScript is just a syncax sugar of Promise? So, we can transform this function to the following equivalent code.

set_salt: (salt) => { return setRoutes(session, salt).then((result) => { session.routes = result; session.salt = salt; return 'ok'; }); },

The key is that the code is calling the chained method then() from the returned value of setRoutes function. What is the return value of this function?

const setRoutes = async (session, salt) => { const index = await fs.readFile('index.html'); session.routes = { // omitted [salt]: () => salt, }; return session.routes; };

It is returning the result of session.routes. This is unnecessary since the assignment to session.route is already done.

Okay, we can control this value by salt parameter. What if we set salt = 'then'? It will return the following object.

{
    // omitted
    then: () => salt,
}

As you can infer from the above code, this then method will be called with callback function as an argument. If the function is called, the returned value is considered to be resolve-ed and the process continues. But, this then() method is just ignoring the argument and the function is never called.

So, by setting salt = 'then', the assignment to session.routes happens inside setRoutes function, but setRoute function is not resolved and the assignment to session.salt never happens.

So, send GET /?action=SetSalt&data=then to server and this will result in the following session state.

session = {
    routes: {
        flag: ...,
        index: ...,
        ...
        then: ...,
    },
    salt: 'flag',
}

This is what we want to achieve!

3rd request

Now, just request the salt and it will print the flag!

GET /?action=GetSalt

return session.routes[route](data); // route = session.salt here

Appendix

In technical term, this is called Thenable Object.

Only 1 solve during contest? Why the fuck is this beginner???

Several reasons.

  • This challenge does not require experience in CTF. It consists of the combination of some logical errors in the code and knowledge of JavaScript itself.
  • A member of TSG, who is actually a beginner of CTF, could solve this challenge in just 3 hours.
  • I thought more people solve this challenge honestly