Jeden z uczestników zawodów UIUCTF 2025 szczegółowo opisał, jak udało mu się wykonać zadanie, polegające na uruchomieniu swojego kodu na serwerze, mając jedynie możliwość zmiany zawartości tekstu komentarza w kodzie.
Uczestnicy mogli wysłać żądanie sieciowe do skryptu Python, który tworzył nowy skrypt Pythona z losową nazwą, dodawał dane przekazane przez użytkownika do tekstu komentarza, usuwając znaki „\n” i „\r”, a następnie uruchamiał ten skrypt poleceniem „python3 nazwa.py”. Kontrolując jedynie zawartość komentarza, uczestnik musiał wydobyć ciąg z pliku „/home/ctfuser/flag”. Skrypt był tworzony następującym kodem: comment = input(„> „).replace(„\n”, „”).replace(„\r”, „”) code = f„„„print(„hello world!”) # To jest komentarz. Oto kolejny: # {comment} print(„Thanks for playing!”)„„„
W miejsce „{comment}” wstawiane były dane przekazane przez uczestnika, a ostatecznie uruchamiany był następujący kod: print(„hello world!”) # To jest komentarz. Oto kolejny: # Dane przekazane przez uczestnika zawodów print(„Thanks for playing!”)
Zadanie zostało stworzone na podstawie luki w parserze CPython, która traktowała znak o zerowym kodzie jako koniec linii (lukę tę można było wykorzystać do ukrywania złośliwych działań w tekście komentarza). Problem został rozwiązany w wydaniach CPython 3.12.0 i 3.11.4. W używanym w konkursie parserze usuwane były jedynie znaki „\n” i „\r”, ale przy użyciu podatnej wersji CPython uczestnik mógł wykorzystać znak „\0” jako separator. Niemniej jednak ten trik nie zadziałał, ponieważ w konkursie używana była już poprawiona wersja CPython z założeniem, że w parserze mogą pozostać inne podobne błędy, a uczestnicy będą w stanie je wykryć.
Uczestnik, który pomyślnie poradził sobie z zadaniem, postanowił nie szukać nowych luk w parserze, które pozwoliłyby na podzielenie ciągu na części, lecz skorzystał z cechy wykonywania w Pythonie plików w zależności od ich zawartości. Na przykład zamiast oryginalnego kodu do pliku z rozszerzeniem „.py” można wstawić skompilowany bajtkod, który jest przechowywany w plikach z rozszerzeniem „.pyc”, i podobny plik zostanie wykonany. W rozważanym konkursie uczestnik mógł kontrolować jedynie zawartość w środku pliku, więc nie mógł dodać swojego nagłówka, aby zniekształcić typ MIME.
Zadanie udało się rozwiązać, korzystając z faktu, że Python od wersji 2.6 może wykonywać zawartość archiwów ZIP do dostarczania pakietów Python w skompresowanej formie. Tak jak w przypadku pamięci podręcznej bajtkodu, obecność archiwum zip określa się na podstawie zawartości, a nie rozszerzenia pliku, tzn. w "plik.py" można umieścić archiwum zip, a podczas uruchamiania poleceniem "python plik.py" zostanie on przetworzony jako skompresowany pakiet Python. Ponadto archiwa ZIP w Pythonie są indeksowane nie według nagłówka na początku pliku, lecz według sekcji EOCD (End of Central Directory Record) na końcu pliku. Jeśli w archiwum znajduje się plik "__main__.py", ten plik uruchamia się automatycznie podczas bezpośredniego uruchamiania archiwum poleceniem "python archiw".
Zadanie konkursowe zostało rozwiązane przez generowanie podobnego archiwum ZIP i wstawienie go w treść komentarza. Aby zachować poprawność struktury pliku w warunkach obecności na końcu oryginalnego pliku wywołania ‘print(„Dzięki za grę!”)’, wykorzystano obecność w sekcji EOCD obszaru komentarza, umieszczanego na samym końcu.

Źródło: opennet.ru
