{
"name": "breaking-grad",
"version": "1.0.0",
"description": "",
"main": "index.js",
"nodeVersion": "v12.18.1",
"scripts": {
"start": "node index.js",
"dev": "nodemon .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"authors": [
"makelaris",
"makelarisjr"
],
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"randomatic": "^3.1.1"
}
}
A file that particularly stands out is
const package = require('./package.json');
const nodeVersion = process.version;
if (package.nodeVersion == nodeVersion) {
console.log(`Everything is OK (${package.nodeVersion} == ${nodeVersion})`);
}
else{
console.log(`You are using a different version of nodejs (${package.nodeVersion} != ${nodeVersion})`);
}
This means that something very specific is happening in node v12.18.1
Another thing that stands out is that the word flag is nowhere to be found in the code, meaning that we must gain RCE to solve this challenge
The app has the below endpoints
router.get('/', (req, res) => {
return res.sendFile(path.resolve('views/index.html'));
});
router.get('/debug/:action', (req, res) => {
return DebugHelper.execute(res, req.params.action);
});
router.post('/api/calculate', (req, res) => {
let student = ObjectHelper.clone(req.body);
if (StudentHelper.isDumb(student.name) || !StudentHelper.hasBase(student.paper)) {
return res.send({
'pass': 'n' + randomize('?', 10, {chars: 'o0'}) + 'pe'
});
}
return res.send({
'pass': 'Passed'
});
});
But the cherry on top that gives it out is ObjectHelper.js
module.exports = {
isObject(obj) {
return typeof obj === 'function' || typeof obj === 'object';
},
isValidKey(key) {
return key !== '__proto__';
},
merge(target, source) {
for (let key in source) {
if (this.isValidKey(key)){
if (this.isObject(target[key]) && this.isObject(source[key])) {
this.merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
},
clone(target) {
return this.merge({}, target);
}
}
This is a classic prototype pollution vulnerability with the added caveat that the proto key itself is disabled making exploitation a bit harder
Javascript is a prototype based language, meaning every object inherits some base classes that are a static, global, object. This means that if we cam modify the prototype element, we can modify all other objects using that prototype.
let student = ObjectHelper.clone(req.body);
This creates a clone of the object we send in our request using ObjectHelper.js where the vulnerability lies
According to hacktricks ![[assets/image_1_33426e07.png]] The above means that if we manage to get a sucessful merge and set the constructor.prototype we can affect ALL js objects
Hmm what is our target then?
Well the debughelper is there waiting and asking to be abused, so lets have a look
const { execSync, fork } = require('child_process');
module.exports = {
execute(res, command) {
res.type('txt');
if (command == 'version') {
let proc = fork('VersionCheck.js', [], {
stdio: ['ignore', 'pipe', 'pipe', 'ipc']
});
proc.stderr.pipe(res);
proc.stdout.pipe(res);
return;
}
if (command =='ram') {
return res.send(execSync('free -m').toString());
}
return res.send('invalid command');
}
}
We cant do anything with execSync as there isn't really an option to change the command being executed, because even if we write the command attribute then it will simply be overwritten by free -m
at the time of execution
However for fork we can set execPath and execArgv to change the binaries being executed Meaning that we probably want execPath to be /bin/bash and execArgv to be a list of the rest of the arguments to read the flag
And then once we visit /debug/version
we receive the flag!