baby breaking grad
4 minutes to read
We are provided with this webpage:
Let’s click in the button:
Source code analysis
Alright, since we are provided with source code, let’s take a look. It is a Node.js project using Express JS. This is index.js
:
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const routes = require('./routes');
const path = require('path');
app.use(bodyParser.json());
app.set('views','./views');
app.use('/static', express.static(path.resolve('static')));
app.use(routes);
app.all('*', (req, res) => {
return res.status(404).send('404 page not found');
});
app.listen(1337, () => console.log('Listening on port 1337'));
This file is quite standard. Let’s take a look at the routes (routes/index.js
):
const randomize = require('randomatic');
const path = require('path');
const express = require('express');
const router = express.Router();
const StudentHelper = require('../helpers/StudentHelper');
router.get('/', (req, res) => {
return res.sendFile(path.resolve('views/index.html'));
});
router.post('/api/calculate', (req, res) => {
let student = req.body;
if (student.name === undefined) {
return res.send({
error: 'Specify student name'
})
}
let formula = student.formula || '[0.20 * assignment + 0.25 * exam + 0.25 * paper]';
if (StudentHelper.isDumb(student.name) || !StudentHelper.hasPassed(student, formula)) {
return res.send({
'pass': 'n' + randomize('?', 10, {chars: 'o0'}) + 'pe'
});
}
return res.send({
'pass': 'Passed'
});
});
module.exports = router;
Here we see that /api/calculate
is the unique endpoint we have. We are able to pass name
and formula
in the request body, and they will be processed by StudentHelper.isDumb
and StudentHelper.hasPassed
:
const evaluate = require('static-eval');
const parse = require('esprima').parse;
module.exports = {
isDumb(name){
return (name.includes('Baker') || name.includes('Purvis'));
},
hasPassed({ exam, paper, assignment }, formula) {
let ast = parse(formula).body[0].expression;
let weight = evaluate(ast, { exam, paper, assignment });
return parseFloat(weight) >= parseFloat(10.5);
}
};
The above functions allow us to calculate some grade. For the first one, we must enter a name that does not contain Baker
or Purvis
. Then, for Student.hasPassed
, the object student
must contain attributes exam
, paper
and assignment
. After that, the formula
is parsed and executed on the previous attributes.
Notice that there’s a default formula
:
let formula = student.formula || '[0.20 * assignment + 0.25 * exam + 0.25 * paper]';
However, the key here is that we can define a new formula that will be processed by evaluate
(static-eval
). This is a problem because we have the chance to execute JavaScript code.
Let’s start by passing the subject (grade greater than 10.5
):
$ curl 134.122.103.40:30580/api/calculate -d '{"formula":"[11]","name":"x","exam":"","paper":"","assignment":""}' -H 'Content-Type: application/json'
{"pass":"Passed"}
Finding a working exploit
There are no vulnerabilities that apply to static-eval
version 2.0.2 in snyk.io:
{
"name": "breaking-grad",
"version": "1.0.0",
"description": "",
"main": "index.js",
"nodeVersion": "v12.18.1",
"scripts": {
"start": "node index.js",
"dev": "nodemon .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"authors": [
"makelaris",
"makelarisjr"
],
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"randomatic": "^3.1.1",
"static-eval": "2.0.2"
}
}
However, we can find some articles showing how to bypass a restrictive sandbox and get Remote Code Execution (RCE) in static-eval
via Prototype Pollution. For example, this blogpost from Faraday. Nevertheless, the key is to look at the static-eval
GitHub repository, because we can find a commit for version 2.0.3 that corrected a security issue in version 2.0.2:
In fact, the pull request adds a new test case that looks like an exploit:
Let’s use Burp Suite (Repeater) to test a bit:
Now, let’s try the exploit:
It seems to work, since there are no errors shown. Hence, we might be able to get RCE since we can execute arbitrary JavaScript code. For instance, let’s use child_process
to execute sleep 5
:
Alright, so we have blind RCE. We know this because we won’t see the output of our commands:
Well, not all the output… Here we have to consider two things. First, the flag filename is randomized in the entrypoint.sh
:
#!/bin/bash
# Secure entrypoint
chmod 600 /entrypoint.sh
# Generate random flag filename
FLAG=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1)
mv /app/flag /app/flag$FLAG
exec "$@"
Which can be bypassed using a wildcard /app/flag*
:
And the second thing is that errors are shown. Therefore, instead of printing the flag, let’s try to execute the flag as a command in a subshell (so that the shell errors out):
Flag
And there is the flag (notice that '
is a single quote in HTML encoding):
HTB{f33l1ng_4_l1ttl3_blu3_0r_m4yb3_p1nk?...you_n33d_to_b3h4v'eval!!}