I'm trying to beat this CTF: a website under construction that has a simple login page where you can login or register a new user.
It uses node express
and a SQLite
DB.
Analyzing the source code I found this query:
getUser(username){
return new Promise((res, rej) => {
db.get(`SELECT * FROM users WHERE username = '${username}'`, (err, data) => {
if (err) return rej(err);
res(data);
});
});
},
This function gets called after a Middleware checked session cookies with jsonwebtoken
to retrieve the username when you GET '/'
.
This is where it's called:
router.get('/', AuthMiddleware, async (req, res, next) => {
try{
let user = await DBHelper.getUser(req.data.username);
if (user === undefined) {
return res.send(`user ${req.data.username} doesn't exist in our database.`);
}
return res.render('index.html', { user });
}catch (err){
return next(err);
}
});
AuthMiddleware:
module.exports = async (req, res, next) => {
try{
if (req.cookies.session === undefined) return res.redirect('/auth');
let data = await JWTHelper.decode(req.cookies.session);
req.data = {
username: data.username
}
next();
} catch(e) {
console.log(e);
return res.status(500).send('Internal server error');
}
}
Since that appears to be the only query formatted that way (the others all use ?
) and in general the only evident vulnerability, I suspect the flag is stored somewhere in the database.
As the only way to get that function called is to have an active session i registered a user with 'malicious' sql code as the username. At first I tried to close the quote and attach an OR WHERE
to get all the users:
SELECT * FROM users WHERE username = '${username}'
+
bob' OR WHERE username IS NOT NULL
=
SELECT * FROM users WHERE username = 'bob' OR WHERE username IS NOT NULL
This should at least throw an error as WHERE username = 'bob' OR WHERE username IS NOT NULL
should return a collection with all the users in the database while it's rendered on the webpage as
Welcome {{ user.username }}
This site is under development.
Please come back later.
I was expecting at least "no username property on array" or something like that. Instead it always return the full username I gave him
Welcome bob' OR WHERE username IS NOT NULL
This site is under development.
Please come back later.
Am I missing something? Is there a way to escape eventual quotes that might be added during the cookie reading?
EDIT:
Here is the function that gets called when you attempt a login /auth route:
router.post('/auth', async (req, res) => {
const { username, password } = req.body;
if((username !== undefined && username.trim().length === 0)
|| (password !== undefined && password.trim().length === 0)){
return res.redirect('/auth');
}
if(req.body.register !== undefined){
let canRegister = await DBHelper.checkUser(username);
if(!canRegister){
return res.redirect('/auth?error=Username already exists');
}
DBHelper.createUser(username, password);
return res.redirect('/auth?error=Registered successfully&type=success');
}
// login user
let canLogin = await DBHelper.attemptLogin(username, password);
if(!canLogin){
return res.redirect('/auth?error=Invalid username or password');
}
let token = await JWTHelper.sign({ // Maybe something can be done with this function?
username: username.replace(/'/g, "\'\'").replace(/"/g, "\"\"")
})
res.cookie('session', token, { maxAge: 900000 });
return res.redirect('/');
});
attemptLogin():
attemptLogin(username, password){
return new Promise((res, rej) => {
db.get(`SELECT * FROM users WHERE username = ? AND password = ?`, username, password, (err, data) => {
if (err) return rej();
res(data !== undefined);
});
});
}
EDIT 2.0:
I just noticed the part where it stores the session cookie:
let token = await JWTHelper.sign({
username: username.replace(/'/g, "\'\'").replace(/"/g, "\"\"")
})
It apparently replaces all '
with \'\'
. I can solve half of that by escaping the quote so that it becomes \\'\'
. This allows me to close the username=''
statement, but I still need to find a way to invalidate the second \'
.