首先, 如果你能看到這句話,那我就應該恭喜你,你已經被此文的標題所吸引。不過,千萬不要想太多,此文不是什么《今日說法》,但也與法有那么一丁點的關系;此文也不是什么《我們約會吧》,約會自古與單身狗就無緣,何況此文的受眾僅僅是大齡屌絲單身程序員。
等等等~~,先別着急關,既然來都來了,就別太在意自己是不是此文的受眾了,相信我,看完此文你一定有一種想罵人的感覺,不過想罵的人應該不是我,至於是誰,我也不知道。只要不是我就行。
閑話少說,閑蛋少扯,現在跟着我的節奏進入正題吧,everybody,伸出你的雙手,讓我看到你~~,
先來說說此文產生的背景吧。本猿已經很久沒有更新文章了,已經忘卻了寫東西的感覺了,不過這次的經歷讓我覺得還是有必要記錄下來與大家分享的。一方面,作為***程序員(此處省略很多字),咱們的工作就是開發安全、穩定、高效的應用,服務於咱們的用戶。當然,作為用戶,不可否認的是,每個人都希望不需要動什么腦筋,就能玩轉任何的應用。所以,問題就來了,如果程序員覺得用戶都不太想動腦子,而自己開發的時候總是想當然的話,那么總有那么一次,你會為自己的想法買單。
2月14,本應該是個炮火連天的日子,而我也本應該浴血奮戰在槍林彈雨的第一線,可誰讓咱是個愛家,愛國,更愛工作的工作狂呢(boss,如果你看到了,記得偷偷給我加薪)。2月10號,好像腦子突然短路了,興沖沖找到boss。
我:boss,情人節咱們給咱們的公眾號粉絲發點福利吧。
Boss:好。
我:需要花點錢。
Boss:好。
我:額,你不問下需要花多少嗎?
Boss:好。
就怕空氣突然安靜……
我:那我直接找財務了。
Boss:好。
好吧,有個這樣壕的boss真不知道是幸福呢還是幸福呢。
好吧,突然發現到這里還是沒有進入正文
----------------------------我是華麗的分割線-------------------------------------------
用一句話總結:情人節前夕,我閑的dan疼,自報奮勇發紅包給粉絲,然后就做了個助力發狗糧(狗糧只是噱頭,其實是RMB啦)的活動,這里稍微做下解釋,大概的意思是,咱們給每個粉絲發個空碗(空紅包),然后粉絲拿着這個空碗去找朋友要狗糧(RMB),當然啦,這個狗糧肯定不是粉絲的朋友出,粉絲的朋友只需要幫他點個按鈕,系統就會自動增加隨機金額的狗糧了,條件是他的朋友必須先關注我們的公眾號,且滿一元才能兌換成RMB。由於從提案到上線只有三天,再去掉周末(不要問我周末為什么不加班,沒錢但任性,哼~~),也就只有一天了。13號開始做,加班到凌晨,終於開發完畢,不過也就是簡單的測試了下。第二天就上線了。
秀逗麻袋,好像忘記了什么。好吧,這里應該與上文呼應下(小學語文老師講過,好的文章要做到上下文呼應),在開頭的時候我講了,大概意思就是,程序員在開發的時候不能把用戶都當“傻子”,咱們要把用戶都當成無孔不入的黑客,做好防范,這樣才能保證活動的真實性與公平性。
2月14日上午十一點發布,截止到中午1點也就漲了區區200多粉絲,發出去不到100塊的紅包,哎,有點小失望,心想現在這種活動大家都不感冒了呀。
到了下午兩點,差點嚇得生活不能自理,當時粉絲量以每秒5-8的速度增加,趕緊查人均成本,發現與預期差不多,也就稍微放了點心。然后再查下總金額,還是嚇了一身冷汗,有人的紅包金額竟然高達100多,可我限制了最大紅包說只能是50呀。然后又查了下代碼,還是沒找到原因,百思不得其解。萬不得已,只能把那幾個人全部屏蔽,額外加了個強制條件,即在更新紅包金額前,先判斷金額是否大於50,如果大於50,則不加了。提現的時候同樣的處理。大於50就只給提現50。就這樣,過了半個小時,也沒發現什么大的紅包。直到下午5點的時候,有很多粉絲反饋提現失敗。遂進入商戶后台查看,發現余額已不足,趕緊找財務充錢,此時粉絲量已經增加了1w+了,然后又查了了數據,發現了好幾個50塊的紅包,然后又是各種屏蔽。但是當時已經快分不清哪些是真是的粉絲了。沒有辦法,最后在一個“業內人士”好心提醒下,不得已關閉了提現通道。那些惡意刷紅包的,看提現不了了,差不多心滿意足的走了。關閉提現通道后,粉絲依舊在增長,到晚上9點的時候凈增長了2.8w的粉絲。
好吧,至此,這場瘋狂的攻守之戰,以我的小勝而結束(但我們也算是損失慘重)。2.15人工審核了所有的紅包,將正常的粉絲的紅包一一的發了后,也就應該開始檢討下這次活動帶給我的經驗教訓,盡管最終增長的粉絲量以及所消耗的成本基本是可以接受的,但人均成本卻高了挺多,而且給忠實粉絲帶來了一些不便。發現的問題如下:
1、openid以明文保存在cookie中。
2、微信開發者模式沒有開啟加密模式。
3、沒有設置請求來源限制。
4、沒有限制必須真實的微信客戶端才能打開。
5、沒有使用https
6、客戶端提交信息沒有加密
7、時間問題。
大概也就上面這些了,下面再一一分析下,攻擊者是如何通過我的這些漏洞來攻擊我的系統的。
OpenId以明文保存在cookie
可能很多人看到這個會嘲笑說我活該,干嘛要把OpenId保存在cookie中,而且還明文。先別急,且聽我慢慢道來。
做過公眾號開發的同學應該都知道,訂閱號是沒有網頁授權的權限的,也沒有微信支付,更別提發微信紅包的接口權限了。而不巧的是,我們要吸引關注的是個訂閱號,又要實現授權、發紅包的功能。我的做法是,使用服務號的接口獲取粉絲對於服務號的OpenId,然后再通過服務號的接口發紅包。可還有一個問題就是,怎樣使用這個服務號關聯的粉絲信息判斷是否關注了我的訂閱號呢?
嗨,那個一臉問號的你,對,就是你,想到了沒?沒想到怎么解決吧。那我就告訴你們吧,記得待會兒給我發紅包。
UnionId,就是這個鬼。可能有些人做微信開發比較少,不是很理解。這里我跟大家簡單說說。首先呢,上文說的OpenId其實就是微信分配給用戶的一個唯一標識,但這個唯一標識並不是唯一的。是不是很拗口?哈哈,那就對了,其實這里說的唯一只是相對於某一個公眾號唯一,還是沒聽懂嗎?好吧,舉個例子說,我有兩個公眾號A和B,另外我有一個微信號,假如我現在分別發消息給公眾號A和B,雖然都是同一個微信號發的,但是收到的信息里的唯一標示確實不同的。因為唯一標示不一樣,所以根據OpenId來判斷多個公眾號里的粉絲是否是同一個是沒法實現的。再通俗點,我們可以把一個個公眾號想象成家與社會的關系。張三在家里的名字可能是‘小蘋果’、‘小櫻桃’之類的,因為家里人都是喊小名,張三在公司里上班的時候,同事可能就直接喊他‘張三’了,那怎么區分小蘋果與張三的關系呢?或許大家都聽說過身份證號這個東西(沒聽說過的自行百度哦)。終於講到重點了,UnionId就可以理解為是微信號的身份證號。但只有把公眾號綁定到開放平台才會有這個屬性,並且多個公眾號必須綁定在同一個開放平台,這個人的UnionId才是唯一的。這就好比在張三在中國我們可以根據他的身份證號來判斷“張三”與“小蘋果”的關系,但他出了國后,老外可就不懂這個了。
好累呀,說是簡單的說下,結果寫了這么一大段。
咱們繼續往后看,現在我的做法基本明了了,就是通過服務號獲取用戶的openid和unionId,我事先會將所有已關注訂閱號的粉絲信息導入到數據庫,后面只要有新的關注也添加到數據庫,有取消關注的則將關注狀態改為0。所以,判斷一個用戶的是否關注訂閱號,我只需要直接根據unionId從數據庫獲取關注狀態就行了。
再說說,我為什么把openid和unionId以明文的方式保存在cookie中,且以明文的形式。上面說了,我是通過服務號獲取openid后,然后與訂閱號共享這個用戶信息,因為服務號本身有一套單獨的程序,所以想讓兩套程序共享cookie,我能想到的就是將兩套程序部署在同一個域名下,iis完美解決了這個問題。至於為什么以明文的方式,我能說的是,我想當然了,一方面時間緊,另一方面我覺得加解密會影響效率,且我也想到別人拿到這openid也沒什么用,所以就…。至於這個問題的優化方式,現在我給出我的解決方式:
首先,如果你需要將一些信息保存在cookie中,又擔心安全的問題,那么只需要在cookie中額外添加一個簽名。當黑客模擬請求,並篡改了cookie的內容時,由於他們不知道咱們的加密方式,所以提交給我們服務器的cookie數據的簽名是有問題,我們只需要在服務器端驗證簽名即可。下面是我的簽名算法,僅供參考,請根據自己的實際需要進行修改:
public static string DictionaryToSign(Dictionary<string, string> dic) { if (dic.Count<=1) { new Exception("集合中項的數量必須大於1,如需要簽名的參數為1,可增加冗余隨機數"); } //第一步,將dic的鍵值通過=進行拼接,轉換成數組 var arr = dic.Select(d => d.Key + "=" + d.Value).ToArray(); //第二步,數組排序 Array.Sort(arr); //第三步,獲取數組的長度,並獲取中值 var length = arr.Length; var middleIndex = length%2 == 0 ? length/2 : (length/2) + 1; var middleValue = arr[middleIndex]; //第四步,將中值進行base64編碼 var base64key = GetCoding(middleValue); //第五步,以上一步生成的key分割,拼接數組為字符串,得到tempstr var tempstr = string.Join(base64key, arr); //第六步,將上一步得到的tempstr先base64編碼,再md5,最后轉換成小寫,得到最終的簽名 return MD5(GetCoding(tempstr)).ToLower(); }
使用的時候,只需要將cookie集合添加到集合中,生成簽名后,再額外將簽名添加到cookie中,最后,每次用戶的請求,都做下簽名驗證。
微信開發者模式沒有開啟加密模式
在開發公眾號時,做接入功能的時候,早期是沒有加密模式,唯一的安全點就是:token。因為微信接入時的算法大家都是知道的,有token之后,如果黑客不知道你的token,那么就算知道了你的url,在驗證消息真實性的時候對方還是不能得到正確的簽名,所以token必須復雜點。像筆者這么懶的人,也就吃一塹長一智吧。如下圖所示:
顯然,我沒有選擇明文模式,且token也是足夠簡單,黑客破解起來也是輕而易舉的。另外,有一點不明白的是,黑客是怎么知道我綁定的url的呢?這個抓包應該抓不了吧,所有的消息應該是通過微信服務器進行轉發的呀。費解,有知道的同學過來交流下。
沒有設置請求來源限制。
這個失誤也是大意了,我在跟別人講課的時候特別強調過要加上這個,相當於給用於接收微信推送消息的服務又加了吧鎖。結合安全模式一起,基本上不太可能會被攻破。下面詳細說下這個設置的詳細思路吧。
不知道大家有沒有注意過,在微信開發文檔有有個接口是獲取微信服務器IP地址。如下圖所示:
官方只是一句帶過,什么機遇安全等考慮呀。哎,這文檔寫的太敷衍了。
就是這么個鬼。在公眾號開啟了服務器配置后,消息的交互流程大概是這樣的:
微信客戶端→微信服務器→開發者服務器→微信服務器→微信客戶端。
看不懂的繼續往下面看:
首先,用戶在微信端給公眾號發消息,微信客戶端會將此消息推送給微信服務器,微信服務器處理后(加密)再對開發者服務器發送http請求,最后處理完成后,再按照來的路原路返回。所以,在微信用戶與公眾號交互時,直接跟開發者服務器交互的微信服務器,那么我們只需要在接收到請求時,判斷這個請求的來源ip,然后再通過獲取微信服務器ip接口,獲取微信服務器的ip,與這個來源ip進行匹配,匹配成功則表示是微信服務器請求的,則繼續處理,否則不處理請求。(目前,我還沒發現有什么技術可以仿造指定的ip進行請求)。
沒有限制必須真實的微信客戶端才能打開
這個請示微信授權鏈接是有限制的,但也只是最常規的限制,在網頁版微信中,還是可以走完授權的流程。有人說可以用UA限制,但UA也是可以仿冒的呀,感覺也是沒什么意義。
我們可以使用微信JSSDK來處理這個問題,JSSDK在配置成功后,有個ready接口,此接口是config信息驗證后會執行的ready方法,但這個ready目前僅能在微信客戶端和開發者工具中使用。所以可以在頁面加載后,寫個定時器,比如延遲2秒,判斷下ready接口是否執行了,如果沒執行,則表示用戶不是在微信端打開的,則跳轉到一個錯誤提示頁面。假如,你覺得這還不夠安全,你還可以通過UA來屏蔽用戶不准在pc客戶端的微信以及開發者工具中打開。具體怎么判斷,從下圖相信你能找到答案。
沒有使用https
這個我就不多說了,https相對於http還是比較安全的。相關的知識大家自行百度吧。至於怎么配置https,今天的篇幅有點長了,下一篇我會專門發個專門介紹https配置的文章,敬請期待。
客戶端提交信息沒有加密
這個其實還是比較重要的,之前做爬蟲的時候,發現百度和12306都是有相關的js加密的。原理就是在請求的參數中額外加個簽名的參數,這個簽名的參數是通過其他參數根據一定的算法生成的,這個算法無非就是md5,base64,sha1等多個算法的組合。然后當收到請求時,服務器端使用與js相同的算法生成一個簽名,與用戶發送過來的簽名進行比較,相同則表示請求合法。需要注意的時,我們在使用js寫算法生成簽名時,最好在發布前對代碼進行壓縮,如果可以的話,最后能稍微改下方法的命名,誠然,良好的命名習慣方便代碼的維護,但也方便黑客攻擊咱們的系統,所以,我建議,在發布的版本中,假如js有個md5運算,可能你的方法就類似於var md5=function(s){},可以試試把改成var sha1=function(s){}。方法體執行的代碼當然還是md5,這樣做的目的只是為了增加黑客的破解難度。
時間問題
當然了, 以上說的這些均是基於你有充足的條件。假如boss在后面催着上線,哪怕你想到了,也沒那么多精力來做這些事情。(我的boss沒催我,時間上確實來不及實現那么多)。
好了,終於總結完了,人類的進步不就是從一次次的失敗,一次次的不完美中總結出來的嘛,所以,活到老,學到老,善於總結,方能成事。
╮(╯▽╰)╭,先別着急關頁面呀,看到下面的二維碼了吧,關注不關注你看心情,反正我也不准備求你。
覺得本文可以吐槽的話,有本事就發到朋友圈,讓全世界的朋友都來吐槽我吧。
轉載請注明出處哦。