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

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 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 ниже, чем версии его зависимостей. Это показалось мне самым простым решением, не исключено, что есть и более элегантный способ.
Возможно, на тот момент, когда ты будешь читать эту статью, в репозиториях появятся пропатченные версии дистрибутива. Тогда ничего не останется, кроме как скомпилировать 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

Уязвимость
Первым делом скачаем эксплоит — это специально сформированная гифка, которая и вызывает проблемы у парсера.
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: }

Функция возвращает размер прочитанных данных в виде переменной 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
) тоже нетрудно обойти, ведь размер блока указывается в самом файле.

Самое время расчехлять отладчик и ставить брейк-пойнты на интересующие нас три функции.
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
разрядов.

В нашем случае 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