For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

...
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

Content of edit.ejs

Content of utils.js

NGINX configuration

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]

  • 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.

The flag

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

References

  • http://nginx.org/en/docs/http/ngx_http_sub_module.html#sub_filter

Last updated