Skip to content

BreizhCTF 2018 – Cuvée d’Exception

Cette année j’étais au BreizhCTF avec des amis dans la team OGC. Nous nous sommes fièrement battus pour arriver à la 10ème place.

 

Aussi, j’aimerais vous parler d’un challenge en particulier de la catégorie MISC : Cuvée d’Exception.

 

Explication de l’épreuve

Dans un premier temps, il faut se connecter via netcat à une machine qui nous affiche un petit message de bienvenue

Après quelques entrées au hasard on se rend vite compte de ce qu’il faut faire

Il faut en effet lever 35 exceptions.

 

La résolution

Maintenant que je connais le but, je me frotte à un problème… J’ai beau être habitué à python je ne connais pas 35 erreurs. Et même si python nous fourni une liste des exceptions, je ne vois pas forcement comment les lever.

J’ai donc fait ce que je sais faire de mieux : J’ai triché !

Ainsi, il a fallut revoir les objectifs. Je ne veut plus avoir 35 exceptions différentes mais obtenir un shell.

 

En théorie rien de plus simple, on import le module os, et on fait

os.system("/bin/bash")

Malheureusement, plusieurs mots clefs sont interdits et le __builtins__ est vidé. D’ailleurs on ne peut pas non plus passer par __import__

 

Donc, je me suis tout simplement aidé du Write-Up de Javex au PlaidCTF2013.

Dans un premier temps, je voulais obtenir la classe <class ‘warnings.catch_warnings’>.

Puis on monte jusqu’au module os.

().__class__.__base__.__subclasses__()[59].__init__.func_globals["linecache"].__dict__["os"]

Et là on a un problème : le mot os est interdit.

Faible problème en réalité, puisqu’il ne s’agit que d’une chaîne de caractères. On peut donc la décomposer.

().__class__.__base__.__subclasses__()[59].__init__.func_globals["linecache"].__dict__["o"+"s"]

 

Oui mais après, il faut appeler system() qui contient un mot interdit : “sys”.

 

En réalité, il suffit d’appliquer la même méthode qui nous sert à récupérer le module os. Il faut utiliser le dictionnaire du module, afin d’avoir la fonction system() via une chaîne de caractère.

Et on peut l’utiliser pour ouvrir un shell !

Avec ce shell j’ai pu lire le flag, et aussi récupérer le code source.

 

Comment le réussir sans “tricher” ?

Le fait de récupérer le code du chall m’a permis de comprendre un peu mieux son fonctionnement. Et c’est aussi ce qui m’a fait dire que je “trichais”.

Code source :

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

import exceptions, signal
from sys import stdout, exit

_raw_input = raw_input
_str = str
_type = type
_Exception = Exception
__builtins__.__dict__.clear()
__builtins = None

class Timeout(_Exception): pass

def clock_is_ticking(signum, frame):
	raise Timeout()

def main():
	block_list = [
		'=',
		'read',
		'co',
		'os',
		'sys',
		'exec',
		'eval',
		'file',
		'reload',
		'__import__'
	]
	error_list = {}

	for k in exceptions.__dict__.keys():
		v = exceptions.__dict__[k]
		if _type(v) == _str or v == None:
			continue
		error_list[k] = { "t": exceptions.__dict__[k], "chk": 0 }

	signal.signal(signal.SIGALRM, clock_is_ticking)
	signal.alarm(60)

	welcome = '''

         _______ _    _ ______ _______ ______ _______ _____ _____  ______  _     _    _    
        (_______) \  / / _____|_______|_____ (_______|_____) ___ \|  ___ \| |   | |  | |   
         _____   \ \/ / /      _____   _____) )         _ | |   | | |   | | |   | |   \ \  
        |  ___)   )  (| |     |  ___) |  ____/ |       | || |   | | |   | | |   | |    \ \ 
        | |_____ / /\ \ \_____| |_____| |    | |_____ _| || |___| | |   | | |___| |_____) )
        |_______)_/  \_\______)_______)_|     \______|_____)_____/|_|   |_|\______(______/ 

                                Come back to the basics! 🙂
        
        Your mission if you accept it, understand how saxx's sandbox works and get the flag!
        
                                        PRESS <ENTER> TO BEGIN
        \n'''
	stdout.write(welcome)
	stdout.flush()

        STEPS = 35
	while 1:
		inp = _raw_input(">> ")

		sb = 0
		for bl in block_list:
			if bl.lower() in inp.lower():
                                stdout.write("[!!!] LoLiLoL... Good Try but Nope!\n\nRestricted keyword :/ ... \n\nBye")
				stdout.flush()
                                exit(0)
				sb = 1
				break
		if sb: continue

		try:
			exec(inp)
		except _Exception as e:
			err_t = _type(e)

			for k, v in error_list.iteritems():
				if v['t'] == err_t and v['chk'] == 0:
					v['chk'] = 1
					break
		finally:
			count = 0
			for k, v in error_list.iteritems():
				if v['chk'] == 1:
					stdout.write("{} : {}\n".format("[OK]", k))
					stdout.flush()
					count += 1
			stdout.write("{} / {}\n".format(count, STEPS))
			stdout.flush()

			if count >= STEPS:
                                stdout.write("I'm Impressed!!! Seems you deserve your flag :)\n")
                                with open("flag", "r") as fd:
					stdout.write( fd.read() )
					stdout.flush()
					return

if __name__ == "__main__":
	main()
exceptionus.py

En effet, on voit bien qu’il y a un véritable test en fonction des exceptions levée et qui lit le code.

Malgré tout, je n’ai pas trouvé comment avoir 35 exceptions (J’en avais au mieux une petite douzaine).

 

Mais si j’utilise la librairie exceptions qui est importée, je peux aisément lever toutes les exceptions que je veux de la manière suivante :

raise exception.ArithmeticError

Pour lever l’exception ArithmeticError.

J’ai alors créé un petit script pour lancer 35 exceptions, parce que 35 exceptions à taper c’est long et que l’on a que 60secondes pour le faire.

#!/usr/bin/python3

x=['ArithmeticError', 'AssertionError', 'AttributeError',  'BufferError', 'BytesWarning', 'DeprecationWarning', 'EOFError', 'EnvironmentError', 'Exception', 'FloatingPointError', 'FutureWarning',  'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'KeyError', 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OverflowError', 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'TabError', 'TypeError', 'UnboundLocalError', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError']

for i in x:
    print("raise exceptions."+i)
resolve.py

Comme on peut le voir, je n’ai pas toutes les exceptions dans ma liste. En effet, certaines exceptions fermaient le programme.

Mais… Le flag ne s’affichait pas.

Eh oui, dans le script les builtins sont vidés. Donc la fonction open, qui se trouve dans les builtins, n’était plus définie.

Erreur ou vraie volonté ? Je n’en sais rien.

 

Comment corriger la sandbox ?

On va partir du principe que le but était d’utiliser la librairie exceptions pour toutes les lever.

Dans un premier temps, le principe de “créer une sandbox” est de ne pas exécuter le code donné dans le même environnement que notre programme.

Pour cela en python, il faut créer un dictionnaire étant notre “faux builtins” et les donner en paramètre à la fonction exec.

Exemple :

#!/usr/bin/python2
trusted_builtins="False True exit print".split()

orig=__builtins__.__dict__
fakeBuiltins={}
for i in trusted_builtins:
    fakeBuiltins[i] = orig[i]

c=raw_input("Votre code : ")
exec(c,fakeBuiltins)

Si l’on veut pouvoir résoudre ce chall grâce au module exceptions, il faut l’ajouter

fakeBuiltins["exceptions"]=exceptions

Maintenant, question de pratique… Si on veut pas que ce challenge soit du guessing (toujours en partant du principe que exceptions est obligatoire pour l’utilisateur), il faut autoriser la fonction dir() pour afficher entre-autre le module.

Puis, vu qu’on ne veut pas que l’utilisateur puisse remonter petit à petit on va changer la liste des “mots” interdits.

block_list=['__', 'import', 'globals', 'locals', 'exec', 'eval', 'join', 'format', 'replace', 'translate', 'try', 'except', 'with', 'content', 'frame', 'back']

Ainsi, si on garde le programme en python2 on obtient le code suivant :

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

import exceptions, signal
from sys import stdout, exit

_raw_input = raw_input
_str = str
_type = type
_Exception = Exception


#Création du fake builtins
trusted_builtins="False True exit print dir".split()

orig=__builtins__.__dict__
fakeBuiltins={}
for i in trusted_builtins:
    fakeBuiltins[i] = orig[i]

fakeBuiltins["exceptions"]=exceptions



class Timeout(_Exception): pass

def clock_is_ticking(signum, frame):
	raise Timeout()

def main():
        block_list=['__', 
                'import', 
                'globals', 
                'locals', 
                'exec', 
                'eval', 
                'join', 
                'format', 
                'replace', 
                'translate', 
                'try', 
                'except', 
                'with', 
                'content', 
                'frame', 
                'back'
                ]

	error_list = {}

	for k in exceptions.__dict__.keys():
		v = exceptions.__dict__[k]
		if _type(v) == _str or v == None:
			continue
		error_list[k] = { "t": exceptions.__dict__[k], "chk": 0 }

	signal.signal(signal.SIGALRM, clock_is_ticking)
	signal.alarm(60)

	welcome = '''

         _______ _    _ ______ _______ ______ _______ _____ _____  ______  _     _    _    
        (_______) \  / / _____|_______|_____ (_______|_____) ___ \|  ___ \| |   | |  | |   
         _____   \ \/ / /      _____   _____) )         _ | |   | | |   | | |   | |   \ \  
        |  ___)   )  (| |     |  ___) |  ____/ |       | || |   | | |   | | |   | |    \ \ 
        | |_____ / /\ \ \_____| |_____| |    | |_____ _| || |___| | |   | | |___| |_____) )
        |_______)_/  \_\______)_______)_|     \______|_____)_____/|_|   |_|\______(______/ 

                                Come back to the basics! 🙂
        
        Your mission if you accept it, understand how saxx's sandbox works and get the flag!
                                                      ^^^^
                                                and Driikolu 😉
        
                                        PRESS <ENTER> TO BEGIN
        \n'''
	stdout.write(welcome)
	stdout.flush()

        STEPS = 35
	while 1:
		inp = _raw_input(">> ")

		sb = 0
		for bl in block_list:
			if bl.lower() in inp.lower():
                                stdout.write("[!!!] LoLiLoL... Good Try but Nope!\n\nRestricted keyword :/ ... \n\nBye")
				stdout.flush()
                                exit(0)
				sb = 1
				break
		if sb: continue

		try:
			exec(inp,fakeBuiltins)
		except _Exception as e:
			err_t = _type(e)

			for k, v in error_list.iteritems():
				if v['t'] == err_t and v['chk'] == 0:
					v['chk'] = 1
					break
		finally:
			count = 0
			for k, v in error_list.iteritems():
				if v['chk'] == 1:
					stdout.write("{} : {}\n".format("[OK]", k))
					stdout.flush()
					count += 1
			stdout.write("{} / {}\n".format(count, STEPS))
			stdout.flush()

			if count >= STEPS:
                                stdout.write("I'm Impressed!!! Seems you deserve your flag :)\n")
                                with open("flag", "r") as fd:
					stdout.write( fd.read() )
					stdout.flush()
					return

if __name__ == "__main__":
	main()
new_exceptionus.py

Bien sûr on peut encore l’améliorer.

 

Aussi, j’invite volontiers Saxx à se manifester s’il ne fallait pas utiliser le module exceptions.

Published inWrite-Up

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *