原文链接:https://www.inforsec.org/wp/?p=1759
在线支付已经走进每个人的生活。抢红包、网上购物、生活缴费等服务中处处都有在线支付的身影。但是在线支付体系暴露过许多次安全问题,黑客利用在线支付的漏洞可以悄无声息的免费清空购物车等,造成商户和支付平台的损失。由于种种原因,支付平台的安全问题很少被细致公开的讨论过。
本文由2015年支付宝特别奖得主丁羽和黎桐辛,以及韦韬共同撰写。其中丁羽和韦韬来自百度X-Lab,黎桐辛来自北京大学。文章对在线支付体系的原理进行了介绍,并分享了几个在线支付过程中曾出现的严重漏洞以及利用过程。希望这些经验能引起相关厂家和商户的重视,使我们的在线支付更加安全。
电子商务已经成为当今互联网中重要的组成部分。同时“钱包”类服务成为了电子商务的关键组件。越来越多的电商服务通过“钱包”服务来进行支付。“钱包”提供的接口简单易用,任何一个开发者都可以快速的将“钱包”服务供应商提供的SDK整合进自己的App中,提供App内的快速支付手段。目前国内最大的“钱包”类服务包括:支付宝钱包、微信钱包、百度钱包等,各有长处。
因此,支付过程的安全问题也成为了关键。如果钱包服务出现了安全漏洞,那么很可能会影响到成千上万的商家,数十亿的现金流,后果往往非常严重。对于支付平台的安全研究自从其诞生之日起就开始了。经过数次血的教训,几大支付平台均修正了数个大大小小的漏洞,反复改进设计和实现。现今的支付平台已经相当安全可靠。
本文对借助支付平台进行的支付流程进行分析,对支付平台的安全进行讨论。同时我们展示了商户端和支付平台出现过的几个严重安全漏洞。攻击者通过这几个安全漏洞可以达到修改金额、任意购买等效果,使得支付平台和商户的利益收到巨大损失。本文中涉及的安全漏洞均已得到修正。
目前市场上的大大小小的第三方支付平台有许多家,规模有大有小但是从整个支付流程上看这些支付平台的同质化程度很高。只有少数很大的支付平台做出了比较大的改动,进一步增强了安全性。这一节我们对支付平台普遍采用的支付协议做下介绍。
在支付流程中主要包括四个实体:商户前端、商家服务器、钱包模块、以及钱包服务器。商户前端是用户直接交互的部分,用户通过操作商户前端来购买商品等。这里商户前端不仅限于移动设备上安装的App,也可以是商家提供的网站。商家服务器是商户的后端,提供相应的服务。钱包模块是进行支付的“中介”,如支付宝、微信钱包、百度钱包等都有对应的支付模块。通常支付模块可以是用户App中的一个SDK,与商户App一起安装在用户的手机上。支付模块也可以以web的形式提供服务。钱包服务器负责处理支付请求,并通知商家服务器支付结果。钱包服务需要与钱包模块交互以获得订单信息,同时需要与相关的机构(例如银行)进行扣款处理,最后与商家服务和钱包模块进行通讯以通知支付结果。因此整个支付过程是一个”四方通讯”的过程。一笔成功交易的后面通常包含四方之间数十次的复杂交互,任何一个环节的安全隐患都会扩大整个支付过程的攻击面。多个安全隐患的叠加可能使得攻击者可以进行订单篡改等攻击,使用户、商户、钱包服务的利益收到损失。
一个完整的经由支付平台的支付过程通常可以划分为以下几步:
我们用图1来介绍支付的四方通讯的详细过程。
在整个支付过程中,各个消息的完整性是最为关键的。如果消息完整性保护存在漏洞,攻击者即可发起修改金额、修改订单号、构造虚假订单等攻击。作为消息完整性保护的关键——签名机制,是支付协议的核心之一。目前应用在第三方支付平台中的签名机制可以分成两种:基于非对称密码体制的签名,和基于散列函数的签名。基于非对称密码的签名机制只在极少数支付平台上得到实现。而基于散列函数的签名则基本被所有平台应用或曾经应用过。
在基于非对称密码体制的签名机制中,每个商户和支付平台都生成自己的一套公钥-私钥对,并互相告知对方自己的公钥。在进行支付时,发送消息方使用自己的私钥对消息(或消息的散列值)进行签名,接受消息方使用对方(发送消息方)的公钥进行验签。这个方法的安全性来自于非对称密码体制的安全性,例如RSA的大质数分解难度,或计算椭圆曲线离散对数的难度。在这个机制中,最关键的是支付平台的私钥。攻击者一旦获得支付平台私钥,就可以对任意消息进行签名,从而欺骗商户。其次是商户的私钥。攻击者获得商户私钥后,结合商户App的其他漏洞,就可以进行各类攻击。
基于散列函数的签名机制安全性来自于哈希函数的不可逆性。在支付平台中此类签名机制几乎得到所有平台的使用。每个商户与支付平台预先共享一个密钥,以及协商一个hash函数(例如MD5或者SHA1)。在发送消息时,每个商户活支付平台在消息中附加上与对方共享的这个密钥,再对整体进行hash运算,得到签名值,与原始消息合并作为最后的消息发送给对方。在接受消息时则将签名值剥离,将剩下的部分与密钥组合后进行hash运算,检验生成的散列值是否与发来的签名值相符。在这个体制中,最关键的无疑就是商户和支付平台预先共享的这个密钥了。一旦这个密钥泄露,攻击者既可以模仿商户给支付平台发信息,又可以模仿支付平台给商户发信息,以进行各类欺骗和攻击,危害无穷。在目前支付平台所采用的签名机制中,以基于MD5的签名最为常见。
此外,以上提到的私钥泄露,其实等同于令商户/支付平台对指定字符串进行签名的能力。如果攻击者可以在很少代价的情况下对指定的“畸形”/“恶意”串进行签名的话,也相当于获得了任意签名的能力,从而以很小的代价发起攻击。
值得注意的是,基于MD5的签名机制并不仅限于”支付协议”中使用,在相当多类型的通信中均得到大量应用。而MD5如果不严格限定输入并使用正确的模式将会是相当脆弱的,我们可以构造通用的签名碰撞攻击,将在后继文章中进行介绍。
此外,支付结果的同步、异步通知则是最容易受到攻击的点。支付结果的同步通知可以在端上被攻击者篡改(例如使用代理或者Xposed)。对于异步通知,由于异步通知经常缺乏可靠的对发送者的身份鉴定,因此攻击者可以自行构造异步通知来通知商户服务已支付成功,从而完成攻击。此外,异步通知的地址往往是可变的,以参数的形式传递给支付平台。攻击者一旦获得了修改异步通知地址的能力,也会对支付过程的安全性造成威胁。
在目前国内大部分支付平台以及诸如anySDK平台等平台的接口中均使用或曾使用基于MD5的消息完整性签名机制。该机制主要用户保证图2中四方通讯时消息传递的完整性。
基于MD5的消息完整性签名机制如图2所示。该方法的关键在于:商家和钱包服务之间共享一个签名密钥。该签名密钥参与到每个签名生成以及签名验证过程中。该密钥不能泄露,一旦泄露则会造成极大安全隐患。攻击者可以借助泄露的密钥来伪造消息,修改订单,发送支付成功消息等。
在签名过程中,签名方将待签名的原请求中的key-value对按照key的字母序进行排序,然后连接在一起。这个连接可以使用‘&’组合,也可以不使用‘&’。再将签名密钥附带在组合的结尾,生成“待签字符串”。有的签名方案使用‘&key=’来连接key,有的则直接附加key在末尾,区别不大。然后使用MD5算法生成待签字符串的散列值作为签名。最后将该散列值作为一个域附加在原请求中,得到最终的请求。
验签过程和签名过程是基本相同的。首先从最终请求中分离出签名域,再将需要验签的部分按照key的顺序排列并重新组合,附加上签名密钥,生成签名过程中的“待签字符串”,最后计算其MD5值,判断其与最终请求中所带的散列值是否相符。
应用这一类签名机制的平台较少,其支付过程可参见图3。
以RSA为例。商户生成一对RSA公私钥对,钱包服务生成一对RSA公私钥对。双方把各自的公钥(金色钥匙)发给对方。
对消息进行签名的过程和基于MD5的过程类似,也是首先将请求按key-value对排序,再使用RSA-SHA1算法(先SHA1再变形再RSA)和对方的RSA公钥生成签名,最后将签名附在原请求中形成完整的请求。 验签过程使用自己的RSA私钥进行验签,具体过程不表。
以上两类签名机制均依赖于“待签字符串”的生成。在待签字符串的生成过程中有以下三个主要问题:
待签字符串的种种性质导致了其“二义性”的出现。在某些情况下,同一个待签字符串可以等价于两个不同请求。例如这个待签字符串
a=A&b=B&c=C&d=D
可以由一个包含四个key-value对的原请求
{“a”:”A”, “b”:”B”, “c”:”C”, “d”:”D”}
生成。在某些情况下,还可以由以下包含五个key-value对的原请求生成
{“a”:”A”, “b”:”B”, “c”:”C”, “d”:”D”, “junk”:”JUNK”}
或者,在另一些情况下,可以由以下只包含三个key-value对的原请求生成
{“a”:”A&b=B”, “c”:”C”, “d”:”D”}
这些变形依赖于“待签字符串”的生成方法。攻击者可以通过构造畸形请求来生成具有相同“待签字符串”的请求,从而绕过签名验证限制。
由以上分析,第三方支付过程的安全严重依赖于以下三点:
然而在生产环境中每个环节都有可能出错,引起严重的安全隐患。
这是最常见的一种安全漏洞。密钥泄漏并不是一种罕见的情况,不少app在开发时,将密钥硬编码在app代码中(用于本地实现签名计算)。消息的完整性依赖于签名的计算,密钥泄漏后消息将无法保证未被篡改或伪造。但对称密钥和不对称密钥泄漏后的利用和危害有所区别。
图4展示了最常见的情况。MD5签名密钥编码在用户App中造成密钥泄露。在对相当多的app进行逆向工程后我们发现,有部分app直接照搬一些样例代码,导致key被直接明文编码到程序中,非常容易提取。还有一部分app作者使用了一些变形手段,例如将key拆成奇数位、偶数位分别存储,或使用特定常数进行异或存储。这些简单变形在熟练的攻击者面前是徒劳的。
由于商户和支付平台共享密钥,密钥泄漏后,攻击者既可以冒充商户向支付平台发送订单消息,又可以冒充支付平台向商户发送支付结果。当然,后者更加直接(如图4)。
例如,若攻击者准备购买一件商品,其订单消息为
notify_url=http://seller.com/notify&out_trade_no=12345&seller=alice&total_fee=100&sign=XXX,
攻击者可以首先通过修改notify_url到攻击者掌控的地址,如http://attacker.com/,提交请求:
notify_url=http://attacker.com/notify&out_trade_no=12345&seller=alice&total_fee=100&sign=XXX
来获得notify_url的结构。再伪造以下消息签名后发送给商户,伪造异步通知,实现免费购物。
target_url: http://seller.com/notify
post_data: put_trade_no=12345&seller=alice&total_fee=100&trade_status=SUCCESS&sign=XXX.
商户收到消息后验证签名正确,所有参数均正确,将完成攻击者的订单。而事实上,攻击者并未进行过任何支付。
另一方面,基于非对称密码体制的签名方案中,私钥泄露后攻击者也可以进行攻击。但是仍依赖于其他的逻辑漏洞。攻击者只能获取商户的私钥,而支付平台的私钥往往被妥善保护无法获得。因此,攻击者无法冒充支付平台向商户发送支付成功的消息,而只能冒充商户向支付平台伪造订单或者篡改订单,修改支付金额。如图5所示,若攻击者准备购买一件商品,其订单消息为
notify_url=http://seller.com/notify&out_trade_no=12345&seller=alice&total_fee=100&sign=XXX
攻击者修改金额,使用私钥重新签名,并提交支付订单
notify_url=http://seller.com/notify&out_trade_no=12345&seller=alice&total_fee=1&sign=XXX
成功支付1元后,商户会收到支付结果消息
out_trade_no=12345&seller=alice&total_fee=1&trade_status=SUCCESS&sign=XXX
商户进行消息的验证,会发现签名正确,商户号正确,订单12345支付成功。若商户没有验证支付金额与订单是否匹配,将完成攻击者的订单。从而攻击者以1元购买了100元的商品。在许多App中,曾出现过只验证签名和订单id的情况,没有验证实付金额,因此可以通过这种金额篡改进行攻击。
为了防御这样的攻击,商家一定要修改app和服务端的设计,使得签名全部在服务端进行。网上充斥着大量可以直接照搬的富含漏洞的样例代码,一定不要简单修改这些代码就直接接入支付平台。此外,每笔交易均要进行查账,验证钱真的得到了支付,才可以标记订单为成功支付。
一旦出现这样的秘钥泄漏商家将面临严峻的安全风险,支付平台也将面临严重的连带品牌危机。发生这样的危机时,如果简单的替换秘钥将会直接导致老App客户端无法进行交易,如果不替换则将面临严峻的支付风险。目前临时缓解方案是依靠商家服务后台与钱包服务后台的增强校验和风控来探测和抵抗攻击。
签名泄露只影响个别自己实现错误的商家,而支付平台的漏洞则会影响千万商家。在这一节我们讨论我们提交给两个国内著名支付平台的平台漏洞,他们均已得到修复。
案例一
第三方支付平台A采用了对称密钥的设计,并提供了服务端SDK供商家集成。服务端SDK提供了API验证签名是否正确。
在PHP和C#的SDK实现中,当签名字段不存在时,SDK会直接返回签名正确,这就导致了攻击者可以直接冒充支付平台向商户发送伪造的支付结果消息并通过签名认证。
以PHP版代码为例
public function CheckSign()
{
if(!$this->IsSignSet()){
return true;
}
$sign = $this->MakeSign();
if($this->GetSign() == $sign){
return true;
}
throw new Exception(“签名错误!”);
}
public function IsSignSet()
{
return array_key_exists(‘sign’, $this->values);
}
检查签名时,首先会利用函数 IsSignSet判断签名是否存在。若签名不存在,直接认为签名正确。
由于该支付平台要求商户服务器将订单(包括通知URL)发送给支付服务器获取一个ID,随后商户应用将ID传递给支付客户端调起支付界面,在实际攻击中, 攻击者还需要从其他渠道获取通知URL(例如路径猜测、URL硬编码或存在网络请求中等)才可伪造支付结果。
案例二
支付平台B采用了非对称密钥的设计,每个商户有自己的一套公钥私钥,但支付平台的公钥私钥仅一套,即所有商户使用同一个公钥验证来自支付平台的消息。
除此以外,订单消息和支付结果消息中包含字段body描述商品信息,且订单消息和对应支付结果消息中的body一致。
如果攻击者希望免费(低价)购物,应该如何进行呢?回顾上文中关于待签字符串二义性的讨论,待签字符串为形如:
key1=value1&key2=value2&key3=value3
的格式。&和=作为了连接符号。
如果某一个参数值(value)中包含&和=符号,待签字符串和原始的参数集合就可能不再是一对一,即存在多组参数集合对应同一组待签字符串。
例如:参数集合
{“key1“:”value1“, “key2“:”value2&key3=fake_value&zend_key=a“, “key3“:”value3“}
的待签字符串为
key1=value1&key2=value2&key3=fake_value&zend_key=a&key3=value3
考虑另一个参数集合
{“key1“:”value1“, “key2“:”value2“, “key3“:”fake_value“, “zend_key“:”a&key3=value3“}
的待签字符串同为
key1=value1&key2=value2&key3=fake_value&zend_key=a&key3=value3
两组集合的待签字符串一样,但key3的值不同。攻击者若知道其中一组的签名,便知道另一组参数在相同密钥下的签名了。
有了这个发现,如何在实际中利用并实现免费(低价)购物呢? 攻击者需要“骗一个畸形订单支付成功的签名”!
首先,攻击者需要拥有一个商户evil的私钥(可自行注册或利用已泄漏密钥)
现在攻击者准备向商户alice购买一件商品,正常的订单消息参数为
{“body”:”商品A”,”notify_url”:”http://seller.com/notify”, “out_trade_no”:”12345″, “seller”:”alice”, “total_fee”:”100″, “sign”:”XXX”}
若订单支付成功,支付结果的参数应为
{“body”:”商品A”, “out_trade_no”:”12345″, “seller”:”alice”, “total_fee”:”100″, “trade_status”:”SUCCESS”, “sign”:”YYY”},
因此,攻击者的目标是向http://seller.com/notify发送一个支付完成的消息,并且包含
{“body“:”商品A“, “out_trade_no“:”12345“, “seller“:”alice“, “total_fee“:”100“, “trade_status“:”SUCCESS“}
这些参数和正确的签名。
这样一条消息会经过支付平台的签名,攻击者无法直接伪造。但是前面提到,所有商户使用同一个公钥验证来自支付平台的消息,也就是说支付平台发给商户evil的消息若转发给商户alice,签名可以通过验证,仅仅是商户号等参数值不正确。因此,攻击者可以考虑利用evil的支付结果来伪造给alice的支付结果。
攻击者首先发给支付平台一个属于商户Evil的订单消息
{“body”:“商品A&out_trade_no=12345&seller=alice&total_fee=100&trade_status=SUCCESS&z=”,“notify_url”:“http://evil.com/notify”, “out_trade_no”:”12345”, “seller”:“evil”, “total_fee”:“1”, “sign”:“XXXX”},
支付1元后,http://evil.com/notify会收到支付结果,并且经过了支付平台签名。
{“body”:“商品A&out_trade_no=12345&seller=alice&total_fee=100&trade_status=SUCCESS&z=”, “out_trade_no”:”12345”, “seller”:“evil”, “total_fee”:“1”, “trade_status”:“SUCCESS”, “sign”:“signed by payment platform”},
攻击者可以将其变换为
{“body”:“商品A”,“out_trade_no”:“12345”,“seller”:“alice”, “total_fee”:“100”, “trade_status”:“SUCCESS”,“z”:“&out_trade_no=12345&seller=evil&total_fee=1&trade_status=SUCCESS”, “sign”:“signed by payment platform”}
然后发送给商户Alice的URL http://seller.com/notify。这些数据具有正确的签名和期望的商家号、订单号、支付金额,将会通过alice验证,从而alice会通过攻击者的订单。
攻击者因此实现了免费(低价)购物。
尽管支付平台提供了服务端SDK,商户后端在实现逻辑时可能并未使用SDK。那么,商户应正确实现签名的验证逻辑,避免相关逻辑错误,如签名不存在时通过签名验证。
商户在收到支付结果通知,验证完签名后,还应正确处理支付结果中的相关参数。支付金额、商户号都应该被验证。
当支付金额未被验证, 攻击者可以支付较低的费用实现购物。支付金额不一致可能是订单消息在签名之前金额被篡改或是攻击者伪造了订单消息(参考不对称密钥泄漏)。
若商户号未被验证,攻击者可以考虑复用另一商户的支付结果,对当前商户进行攻击。攻击发生的条件在于发给不同商户的支付结果使用了同样的私钥签名,同时攻击者注册了自己商户。攻击者生成一个订单,支付给自己的商户后,将支付结果复用。
若商户号和总金额均未被验证,攻击者还可以再次考虑复用另一商户的支付结果对当前商户进行攻击。虽然复用支付结果仍要求发给不同商户的支付结果使用了同样的私钥签名,但攻击者不再需要注册自己的商户,而是利用已泄漏的商户密钥。实现攻击时,攻击者生成一个费用较低的订单,完成支付后复用支付结果。
在用户完成支付后,支付平台往往既会异步通知商户服务器支付结果,又会同步通知商户客户端支付结果以向用户展示。
然而,支付结果的验证应在服务端完成。仅在客户端验证将容易受到攻击。攻击者可以直接修改客户端逻辑实现免费购物。
本文总结了常见支付平台的支付过程的机制,探讨了其中存在和可能存在的问题,并通过实例展现了商户端和支付平台出现的安全问题。除了本文中探讨的问题,还有许许多多安全隐患在近年得到不断修正。总的来说现在的网上支付还是比较可靠的。在下篇中我们继续对基于MD5的消息签名机制进行讨论,发掘更多的安全问题。
参考文献