0

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 \'.

Fabio R.
  • 393
  • 3
  • 15
  • Its sets the `req.data.username` to the user object when query is successful, right? I'm not sure what the goal of the CTF is, but it seems like you can do a wildcard attack to find an admin account. – S.Visser Oct 22 '21 at 11:21
  • @S.Visser The goal is to find the flag, no other description given. I just tried with `bob' OR WHERE username LIKE '%64_[^!_%65/%aa?F%64_D)_(F%64)_%36([)({}%33){()}£$&N%55_)$*£()$*R”_)][%55](%66[x])%ba][$*”£$-9]_%54'` and with `bob' OR WHERE username LIKE 'admin'` but to no avail, it just prints them out – Fabio R. Oct 22 '21 at 11:28
  • What happens if you enter a wrong username ? – S.Visser Oct 22 '21 at 11:45
  • @S.Visser A call is made to `/auth?error=Invalid%20username%20or%20password`, and the error parameter is just used to fill a message to the user. I'll edit the question to include the login function – Fabio R. Oct 22 '21 at 13:06
  • why don't you just fix the vulnerability? [SQL injection](https://owasp.org/www-community/attacks/SQL_Injection) is a well known vulnerability and there is no excuse to ever concatenate SQL like that, whether you can recreate the problem now or not. – Liam Oct 22 '21 at 13:14
  • the [Owasp page has numerous examples as to how to enact this exploit](https://owasp.org/www-community/attacks/SQL_Injection#examples) – Liam Oct 22 '21 at 13:16
  • @Liam the answer is in the first line of the question – Fabio R. Oct 22 '21 at 13:18

0 Answers0