情人节引发的血案

首先, 如果你能看到这句话,那我就应该恭喜你,你已经被此文的标题所吸引。不过,千万不要想太多,此文不是什么《今日说法》,但也与法有那么一丁点的关系;此文也不是什么《我们约会吧》,约会自古与单身狗就无缘,何况此文的受众仅仅是大龄屌丝单身程序员。

等等等~~,先别着急关,既然来都来了,就别太在意自己是不是此文的受众了,相信我,看完此文你一定有一种想骂人的感觉,不过想骂的人应该不是我,至于是谁,我也不知道。只要不是我就行。

闲话少说,闲蛋少扯,现在跟着我的节奏进入正题吧,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没催我,时间上确实来不及实现那么多)。

 

 

好了,终于总结完了,人类的进步不就是从一次次的失败,一次次的不完美中总结出来的嘛,所以,活到老,学到老,善于总结,方能成事。

 

 

╮(╯▽╰)╭,先别着急关页面呀,看到下面的二维码了吧,关注不关注你看心情,反正我也不准备求你。

觉得本文可以吐槽的话,有本事就发到朋友圈,让全世界的朋友都来吐槽我吧。


 转载请注明出处哦。

posted @ 2017-02-16 15:05  billsking  阅读(1858)  评论(14编辑  收藏  举报