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

Was this helpful?