Совсем недавно известный эксперт по безопасности Стефан Эссер (Stefan Esser) опубликовал в своем блоге статью, посвященную новой уязвимости, связанной с особенностями сравнения строк и автоматического усечения данных в MySQL. Именно Стефан обнаружил знаменитую дыру zend_hash_del_key_or_index, после чего в Сети появилось множество эксплоитов для популярных веб-приложений. Похоже в этот раз дело грозит обернуться теми же последствиями.
Как известно, любой столбец в таблице имеет свою максимальную длину. Вся суть уязвимости заключается в том, что при превышении этого значения MySQL отсекает оставшуюся часть данных, при этом не вызывая никаких предупреждений. Как отмечает Стефан, никаких дополнительных действий по настройке MySQL совершать не нужно, т.е. данные будут обрезаться по умолчанию, однако при испытании на своем веб-сервере с MySQL 5.0.20-nt, ожидаемого результата я не получил, хотя перед этим системные переменные я не переназначал. Действительно, запуск MySQL с системной переменной sql_mode, выставленной в STRICT_ALL_TABLES или STRICT_TRANS_TABLES, не позволит беспрепятственно записать данные в таблицу, если они превышают максимально допустимую длину, – в этом случае возникнет ошибка. Как выяснилось позже, мой локальный сервер MySQL был скофигурирован именно с STRICT_TRANS_TABLES, что поначалу вводило меня в заблуждение. Поэтому, если вы будете проводить испытания на своем компьютере, убедитесь, что MySQL запущен в нормальном режиме.
Чтобы понять каким образом можно воспользоваться данной особенностью MySQL, рассмотрим следующий пример:
<?php include 'connect.php'; if(!isset($_POST['submit'])) { ?> <h1>Registration</h1> <form method="POST"> Username: <input type="text" name="username" /> Password: <input type="password" name="password" /> <input type="submit" name="submit" /> </form> <?php } else { $username = mysql_escape_string($_POST['username']); $password = mysql_escape_string($_POST['password']); $r = mysql_query("SELECT * FROM users WHERE username='$username'"); if(mysql_num_rows($r)) die("User already registered!"); else { if(mysql_query("INSERT INTO users (username,passhash,status) VALUES ('$username','".md5($password)."','user')")) { echo "Successfully registered!"; } else echo mysql_error(); } } ?>
Как несложно догадаться, – это скрипт регистрации пользователей. Предположим, что admin – это логин существующего пользователя со статусом администратора. Максимальная длина данных для столбца username – 16 байт. При регистрации пользователя с логином “admin ” (с пробелом) запрос, проверяющий логин на существование, вернет данные уже существующего в базе данных пользователя, так как для MySQL “admin” и “admin ” – это одно и то же строковое значение. Однако, если передать значение содержащее “admin” (5 байт) + 11 пробелов (уже 16 байт) + любое количество других символов (например просто “x” – итого 17 байт), то первый запрос вернет false, так как такого пользователя не существует, а вот второй успешно выполнится, при этом “x” будет отсечен, и в таблице users появится новая запись c логином admin и одинадцатью пробелами.
Дальнейшая реализация уязвимости зависит от способа осуществления авторизации пользователей. Если сначала отдельным запросом проверяется соответствие пароля, а затем уже вторым запросом извлекаются данные пользователя и впоследствии назначается сессия этого юзера, то уязвимость имеет место. Т.е. код примерно такой:
<?php $userdata = null; $r = mysql_query("SELECT username FROM users WHERE username = '$username' AND passhash = MD5($password)"); if (mysql_num_rows($r)) { $r = mysql_query("SELECT * FROM users WHERE username = '$username'"); $userdata = mysql_fetch_array($r); /* ... */ } ?>
Таким образом, проверка на соответствие пароля будет проведена только для нашего пользователя, а вот второй запрос вернет уже две записи, причем первым элементом в массиве будет запись настоящего администратора, что приведет к повышению прав.
Честно говоря, такую модель можно встретить довольно редко, однако уязвимость может возникнуть в самых неожиданных местах, так как веб-разработчики обычно мало знакомы с особенностями языков C и C++, на которых написаны интерпретатор PHP (вспомните про NULL-байт) и MySQL (тема этого поста)
Главной мерой, направленной на предотвращение подобного рода уязвимостей – это проверка данных на длину перед осуществлением запроса. Кроме этого, неуязвимы перед данной атакой поля с индексами. И не забывайте про STRICT_ALL_TABLES, хотя в некоторых случаях это может вызвать некорректную работу ваших веб-приложений.
Добавлено 08.09.2008
Как заметил Elekt, уникальные поля (UNIQUE KEY) неуязвимы перед SQL Column Truncation также как и поля с индексами.
Leave a Reply