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!!}