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