Trading-API

hacklu-ctf 2021

Bypass Authorization with path traversal

const got = require('got');
const jsonwebtoken = require('jsonwebtoken');
const uuid = require('uuid').v4;
const { getRequired, getOrDefault, getOrGenerate } = require('./config');

const AUTH_SERVICE = getOrDefault('AUTH_SERVICE', 'http://localhost:3001');
const AUTH_API_TOKEN = getRequired('AUTH_API_TOKEN');
const JWT_SECRET = getOrGenerate('JWT_SECRET', uuid);

async function login(req, res) {
    const { username, password } = req.body;
    if (!username || !password) {
        return res.status(400).send('missing username or password');
    }

    try {
        const r = await got.post(`${AUTH_SERVICE}/api/users/${encodeURI(username)}/auth`, {
            headers: { authorization: AUTH_API_TOKEN },
            json: { password },
        });
        if (r.statusCode !== 200) {
            return res.status(401).send('wrong');
        }

        const jwt = jsonwebtoken.sign({ username }, JWT_SECRET);
        return res.json({ token: jwt });
    } catch (error) {
        return res.status(503).end('error');
    }
}

function authn(req, res, next) {
    const authHeader = req.header('authorization');
    if (!authHeader) {
        return res.status(400).send('missing auth token');
    }
    try {
        req.user = jsonwebtoken.verify(authHeader, JWT_SECRET);
        next();
    } catch (error) {
        return res.status(401).send('invalid auth token');
    }
}

module.exports = {
    login,
    authn,
};

To succesfully bypass the login mechanism, we need a status code 200 from the auth component.

Looking at how the request is being made, we see the use of encodeURI which does not sanitize the input properly.

Therfore we can append ../ sequences to another api call present called /health, terminating it with a # character, which will allow us to bypass the login altogether.

Bypassing authz with path normalization

We see the use of a middleware authz which is used to determine access to the /api/priv/* calls

Looking at authz, we see that it is validating req.url against the regex.

When applying a middleware, req.url strips the front part of the url, taking only the path.

For example, if a user requests http://example.com/users/profile?id=123, then req.url would be /users/profile?id=1.

But regex.test is checking for whether req.url starts with /api/priv so if we send a url like http://x/api/priv..., it will resolve to /api/priv... thus failing the regex check, allowing us to bypass the hasPermission call.

SQLi with prototype pollution

Looking at the privileged path /api/priv/assets/:asset/:action, we see that we are able to specify the asset variable which leads to transactions[asset][txId] but txId seems to be randomly generated.

If we perform prototype pollution, we will have something like transactions['__proto__'][randomId] = "Whatever we want"

Looking at the way our statement is prepared, we see a permissive sqlEscape function which allows ASCII printable characters. It then set the escaped values to the keys with replaceAll.

If we overwrite the property of txID and set a placeholder in the query, replaceAll will substitute the value of "Whatever we want" in the placeholder, allowing us to perform SQLi.

Let's see that in action.

We see that it is replacing each of the set keys (E.g :txId, :asset etc) in the for .. in loop, and by setting the placeholder ::txId in our username, we are able to set the placeholder :108446613024694 whose value has been set earlier here transactions['__proto__'][randomId] = action

Piecing together a solve script, we get the flag.

Last updated