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

Уязвимость получила идентификатор 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 в дистрибутиве 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;

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

1 комментарий

  1. Аватар

    IdFox

    30.01.2018 в 15:43

    Пошел узнавать как обновляться на Debian 🙂
    P.S Нуб

Оставить мнение