Rating:

## Challenge description:

The site [log-me-in.web.ctfcompetition.com](https://log-me-in.web.ctfcompetition.com/) had a navbar with menu-items: home, about, profile, flag and login.

![image](https://user-images.githubusercontent.com/24471300/91640429-374e3b80-ea3d-11ea-9503-882102acdd56.png)

Clicking on the flag button sent us to `/flag` route which popped up an error `You must be logged in to access that`. The `/login` route had a simple login form:

![image](https://user-images.githubusercontent.com/24471300/91652179-47553200-eab4-11ea-979e-f9bfb7ae8a80.png)

We were also provided with the express-js source code for the app:

```javascript
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) =>a {
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>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"})
}
});
});
```

## Quest

The `const targetUser = "michelle"` in the source hinted we were to somehow bypass the login to be authenticated as `michelle`. My first guess was sql injection but there was a prepared query used:

```javascript
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>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"})
}
});
});
```

I decided to play around with the post parameters anyways. A normal request with `username=michelle&password=michelle&csrf=` simply gave an error `Invalid username or password`. Since this app was also written in express-js, I thought of trying out the trick from "pasteurize" challenge here as well. I repeated the request with `username=michelle&password=michelle&password=extra&csrf=` which gave back a new error `Unknown error: Error: ER_PARSE_ERROR: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ' 'extra' order by id' at line 1`. This confirmed similar behavior.

I then tried requesting with `username=michelle&password[0]=&csrf=` but it again gave the same old error `Invalid username or password` back. I repeated with `username=michelle&password[r]=&csrf=` and got back `Unknown error: Error: ER_BAD_FIELD_ERROR: Unknown column 'r' in 'where clause'`. Bingo! I was then able to login by simply replacing `r` with `id`(or any existing column). The password parameter `password[id]=` was being parsed as an object(something like `{id:null}`) which somehow bypassed the password check in the prepared query.

## Solution

I simply sent a post request via burp repeater to `/login` route with the body `username=michelle&password[id]=&csrf=`. This gave a 302/redirect response(unlike other requests which gave a 200/no-redirect). The response also (re)set the `session` cookie `Set-Cookie: session=eyJjc3JmIjoiYjU3OWM2MWYtNjhmMi00YTIzLWFkYzgtNzAxODE3YTU1YWIzIiwidXNlcm5hbWUiOiJtaWNoZWxsZSIsImZsYWciOiJDVEZ7YS1wcmVtaXVtLWVmZm9ydC1kZXNlcnZlcy1hLXByZW1pdW0tZmxhZ30ifQ==; path=/; httponly`. I was thus able to extract the flag from the base-64-decoded cookie: `CTF{a-premium-effort-deserves-a-premium-flag}`.

Original writeup (https://gist.github.com/0xffcourse/777b632be51998117e43eff71a5146f3#log-me-in).