Posts STF [Logged In]
Post
Cancel

STF [Logged In]

47 SOLVES

DESCRIPTION

It looks like COViD’s mobile application is connecting to this API! Fortunately, our agents stole part of the source code. Can you find a way to log in?

Resources

challenge6.zip

ZIP File Password: web-challenge-6

Initial

Looking through the provided source code, there are 3 endpoints available in this API application. Namely, /api, /api/login and /api/user/:userid.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var express = require('express');
var router = express.Router();
var { loginValidator, sendValidationErrors } = require('../middlewares/validators');
var { localAuthenticator } = require('../middlewares/authenticators');
var { User } = require('../models')
var encryptFlag = require('../helpers/encryptFlag');

router.get('/', function (req, res, next) {
  res.render('index', { title: 'Express' });
});

router.post('/login', loginValidator, sendValidationErrors, localAuthenticator, function (req, res) {
  res.json({ "flagOne": process.env.FLAG_ONE, "encryptedFlagTwo": encryptFlag(process.env.FLAG_TWO) })
});

router.get('/user/:userId', async function (req, res) {
  const user = await User.findByPk(req.params.userId, { "attributes": ["username"] });
  res.json(user)
});


module.exports = router;

Validation of inputs were done with express-validator module and authentication was done with passportjs.

The validation code attempts to check for username and password in the request and if the check fails, a 400 error will be shown.

The authentication code attempts to use passportjs’s local-strategy to authenticate user.

Unexpected findings

Looking at the code, I was initially not able to find any vulnerabilities or insecure coding. However, I assumed the login would trigger this part of the code:

1
2
3
4
5
6
7
8
9
10
11
12
passport.use(new LocalStrategy(
    async function (username, password, done) {
        const user = await User.findOne({ where: { username } });
        if (user !== null && bcrypt.compareSync(password, user.password)) {
            if (user.username === 'gru_felonius' && bcrypt.compareSync(password, user.password)) {
                return done(null, user);
            }
            return done(new Error('Only Gru is allowed!'));
        }
        return done(new Error('Invalid credentials'));
    }
));

As such, I used gru_felonius as the username and kept on trying different types of passwords. As I was feeling rather tired, I accidentally clicked on my mouse button which was coincidentally on the Send button with just the username entered in and password left empty and the flag was shown :O

After I solved it, I looked into how the check was bypassed with empty password and was surprised that User.findOne({ where: { username } }); will always return a Query object even if the value of where is undefined. As such, the check for user !== null will be bypassed as user will never be null. I was unable to understand how the other checks were bypassed.

Intended solution

The intended solution for the challenge was to have an empty request body while having username and password in the header of the request.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var { check, validationResult } = require('express-validator');

const loginValidator = [
  check('username').exists(),
  check('password').exists()
]

function sendValidationErrors(req, res, next) {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ error: `Invalid parameters: ${errors.array().map(error => error.param).join(', ')}` });
  }
  next()
}

module.exports = {
  loginValidator,
  sendValidationErrors,
}  

The check function checks the entire request for the field specified. As such, including it in the header will allow the check function to pass while other functions will only check in req.body. This will cause other functions to return null.

1
2
3
4
5
6
7
Creates a validation chain for one field. It may be located in any of the following request objects:

req.params
req.query
req.body
req.headers
req.cookies

The correct function to use in this legacy API is the checkBody function. The checkBody function, as the name implies, only checks req.body.

Post CTF

After the competition has ended, I had a discussion with @farisv from PDKT whose team achieved 4th on the open category leaderboard.

The reason why the check was bypassed with null username and password was because of an intended feature in passport which calls a callback function on error if a custom callback function was defined.

1
2
3
4
5
6
7
8
9
if (callback) {
  if (!multi) {
    return callback(null, false, failures[0].challenge, failures[0].status);
  } else {
    var challenges = failures.map(function(f) { return f.challenge; });
    var statuses = failures.map(function(f) { return f.status; });
    return callback(null, false, challenges, statuses);
  }
}

As mentioned by passportjs documentation If authentication failed, user will be set to false. As such, the correct function should include a check for if user is null.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function localAuthenticator(req, res, next) {
  passport.authenticate('local', { session: false }, function (err, user, info) {
      if (err) {
        return res.status(401).json({
            "error": err.message
        });
      }
      if (!user) { 
        return res.status(401).json({
          "error": err.message
        }); 
      }
      next();
  })(req, res, next)
}

Using either methods will cause the server to response with this result:

1
2
3
4
{
    "flagOne": "govtech-csg{m!sS1nG_cR3DeN+!@1s}",
    "encryptedFlagTwo": "717f4cda287d40c47e7b50cb772b4def5a415387257510d1"
}

Thoughts

Although I got the flag for the challenge, I still did not fully understand the source code. Maybe only part of the source code was given to the competitors. Anyways, I will add an additional section if I managed to figure it out.

More info

Ask me on Discord @Coldspot#7033

This post is licensed under CC BY 4.0 by the author.