维京勇士北风造
正文
2018年的10月,某个我吃完午饭在园区里面慢悠悠散步回公司的星期二中午,企业微信里面突然收到一条来自主管的消息。大意是在我们业务的后台用户中心看到我们的CTO一连注册了好几个用户,感觉到非常奇怪。让我回来看看线上环境是不是出什么代码BUG了,他则去联系CTO了解情况。回到电脑前,我看了下之前那几个非常诡异的账号,之所以被主观认为是CTO注册的,是因为这些账号的用户名都是用我们CTO的真名来注册的。而主管他也和CTO确认完毕,那些号并不是CTO自己注册的。“也许是有人恶作剧吧”,当时我们都是这么认为的,然而就在这时,这个后台的系统显示此时注册的数量逐渐变的越来越快,而且注册用的用户名都是用的我们的CTO的名字,而邮箱则是用了一串明显随机出来的字符串。看着日志看板里快速跳出的一行行注册日志,我意识到了刚刚诡异的账号注册只是一个试探,而我们现在则是遭受到了真正的恶意的API调用攻击。
此时遭受攻击的服务是我们业务中的账户服务,而我恰巧是这个服务的Owner。看着自己负责服务的注册接口被恶意刷接口调用,我心里贼气。然而这些恶意注册的账号都有一个非常明显的特点,就是不知道为什么他在填写用户名的时候都是用了我们CTO的真名,所以对于我们来说要鉴别出这些恶意请求非常容易,那就是将这些在注册请求中,所有用户名是那个名字的请求全部拒绝掉即可。想好对策后,我立即打开项目后写了相应的针对代码然后直接推送到线上。4分钟,从写代码到推送到代码仓库经历CICD到最后线上版本滚动更新的整个总共花了4分钟。线上服务版本更新后,这些恶意的请求果然都在日志中记录被拒绝了,我们暂时有了喘息的机会。
1 | { |
此时的我们面临了一个问题与一个难题。问题在于,作为一个在当时还处于创业期的业务,本身用户与平台侧并没有任何较高的价值,攻击者为什么要对我们进行大量的刷用户注册的攻击,他的意图到底在哪里?如果我们不能搞明白攻击者的真正目的,那么我们将永远处于被动的节奏。难题在于,我们之前上的新的线上版本只是一个专门针对刚刚攻击者的一个版本,等到那个恶意攻击者察觉到以后被针对以后,他只要随便更换一下规则我们就会又立即处于被攻击的状态,我们必须尽快的找到一个彻底的防御方法。就在我们思考与寻找上述的答案时,后台系统的用户增长数又开始了诡异的增长。我们知道,攻击者发现我们开始针对他了,在他简单将注册的脚本中用户名的值改为随机生成后,他的攻击对我们又开始生效。在我们彻底搞清这两个问题前,我们首先需要为自己争取时间,在不影响用户正常使用的情况下,我们必须要将那些恶意请求的特征找出来,然后拒绝掉。 在后台的数据系统内,我们看到这些恶意的注册请求,虽然在请求内容中有着随机的数据,但是请求IP却都是来自同一个IP,而这个IP是新加坡的某个云服务商的IP。很明显,这名恶意攻击者将自己的注册脚本放在了某个云服务器中来进行攻击。就在我想要在服务端BAN了这个IP时,我的主管突然拉住我说,“等等,别让他知道我们发现了”。 这一拉瞬间让我冷静了下来,当时作为一个刚刚毕业三个月的我来说,第一次遇到自己负责的服务在线上直面的恶意攻击,让我从发现开始到现在变得焦躁不安,而焦躁与愤怒正是在对抗时最致命的弱点。 当务之急是在保证网站、数据安全的情况下搞明白攻击者的动机与寻找到长期有效的防御手段,所以我们的防御手段不能让攻击者意识到我们又一次针对他了,从而使他麻痹,为我们争取到尽可能长的时间,即使用蜜罐手段。 我猜测攻击者之所以之前能如此快的意识到自己被针对了,肯定是对恶意请求的HTTP返回值进行了记录,当发现自己的所有请求都被拒绝时,从而更改了脚本。那么此时此刻我的蜜罐手段则非常简单,我在接口对这个注册IP进行特判,对于恶意注册者的IP,我并不会让代码走到数据库层面的逻辑,而只是在接口层稍微等待了1-2秒后,将伪造好的假数据返回给他,让他成功的进行一次HTTP请求。这里的精髓在于,我故意在这里加入了停顿1-2秒的逻辑,从而让这个请求看上去真的好像作为一个正常请求一样被处理了。我快速编写好这个蜜罐后,推送上了生产环境。一分钟、十分钟、一小时过去了,攻击者果然如预想中的一样并没有察觉到这个蜜罐的悄然上线。对于敌人的第二轮攻击,我们暂且挡住了。
1 | { |
当攻击暂且消停后,我们开始琢磨上文提到的一个问题和一个难题,对于敌人的动机我们都觉得非常奇怪,为什么要对一个还没有面向公众的业务进行攻击,无法理解能得到什么价值。所以我们决定先解决上文所提到的难题,即找到并立即上线一个长期有效的防御手段。由于之前敌人的攻击都是用同一个IP来进行攻击的,所以一个非常明显的手段就是进行IP限制,即对于同一个IP在同一时刻时能进行的连接进行绝对数量上的限定。由于我们在架构上使用Kubernetes配合Nginx作为流量的负载均衡,所以通过Ingress Anootation就能非常简单的做到在NGINX层进行IP层面的限制。
1 |
|
然后这只是在对于同一个IP而言,我们非常清楚当攻击者在使用IP池的情况下,我们的IP限制的策略同样也会被攻破。所以防御的关键手段在于如何在服务端甄别出这个请求究竟是正常人发的请求,还是某个API脚本所发的请求。这一点我相信大家都在日常的APP和网站的注册流程中都会遇到,即注册后会先进行一波手机号验证、邮件验证,或者是输入或滑动一些验证码,来达到人机甄别的目的。这个流程其实我在早期账户服务的构建中向我的主管提出过,然后我的主管否决了这个提案。原因在于对于一个处于创业期的项目而言,我们需要尽可能的降低获客成本,提升用户的注册率,当我们在注册过程中加入任何一个验证流程时,都会成为一个漏斗,从而将部分用户从漏斗中筛出去,降低了我们的注册率。所以对于一个早期业务而言,提高用户注册率的优先级高于各种复杂的验证流程去保证安全。对于这个说法我认为确实是有道理的,但也确实造成了我们目前遭受攻击的局面。万幸的是自从蜜罐上线后,攻击者也一直未能发现,从而给了我们喘息的机会。从中午受到攻击,到思考对策上线防御版本,一转眼的时间就到下班时间了,我和主管对了一下晚上盯防生产环境日志的安排,就先撤了。
敌在暗我在明,我们的服务放在公网上能被他攻击,但我并不知道他是谁、在哪。 敌在明我在暗,我能在后台看到他的每一个请求内容,从而抓出特征,但他并不能意识到我们的服务发生了哪些变化。
星期三的中午,就在我思考该如何彻底解决这个问题时,我的主管从会议室里出来急冲冲的问我,“我们的用户体系是否和国内主站是不是打通的?”“对呀,账号密码共通的” “坏了”,他说道:“那个黑客盯上的不是我们,而是国内业务。” 到这里我才如梦初醒,明白了那个攻击者的真正动机。十月初的时候,研发部决定用中台化的能力来对接所有前台业务,我们作为一个扩展海外的前台业务,则是接入了国内的用户中台,而这块的接入也正是我负责的。当时我想当然的将我们自己的国外业务的用户名密码原封不动的接入国内用户中台。虽然我们作为一个早期的创业业务并没有任何价值,但是我们的国内业务则是有着丰富的数据和方案,可以说是我们的核心资产与价值,而国内的业务在接口安全这块也做了非常多的防御与检测工作,可以说是严防死守。而由于我们业务在不做任何变动的情况下接入了用户中台,等同于在我们业务中注册的用户也能无缝访问登录国内业务。而由于国内的业务在防御这一侧做了相当多的工作,所以注册一个账号并进行爬虫的代价是极大的,所以至今为止击溃了无数的爬虫攻击者。而我们这里,则相当于无意中开了一个小口子,让攻击者能以极低的成本在我们这里注册账号,然后拿着这些账号凭证对我们的国内业务进行自杀式爬虫,这么做的结果必然会在拿到一点数据以后被我们国内防御策略封号,但是对于攻击者来说他并不怜惜这些账号,因为他可以通过我们取之不竭。而事实上,我们的国内业务也确实发现遭受了爬虫的攻击,而负责国内业务的大佬们从这些爬虫账号看出是在我们这里注册的账号,从而找到了我的主管。 一方面这个漏洞是因为我的疏忽而起,另一方面攻击者的隔山打牛确实很秀,让我有那么一瞬间感觉自己被晃得眼睛疼。
我快速更改了接入用户中台的方式,从而断开了国内外的用户体系,然后推送上线。知道了攻击者的意图后,我们开始联系国内主站一起进行对之前攻击者的僵尸账号进行封禁,从而让之前攻击者在我们这边注册的账号在主站那边也提前封禁掉,不让他用爬虫再继续爬我们的数据。在解决了第一个问题后,赶紧上线一个稳定的防御手段则是至关重要。这个时候主管找到了国外一个专门用来判断一个请求是真实用户还是机器行为的免费SAAS服务,简单的说就是你只要将请求中的用户数据比如邮箱啊、用户名啊这些内容以及请求的IP这些数据发送给他,他就会返回给你一个分数,分数越高则越接近人类,分数越低则越接近机器。于是我立马就调试了一波在注册流程中接入了这个SAAS服务,然后在内网中进行了测试,同时我也将这个版本推送到生产环境,去观察当这个SAAS服务面对生产者的恶意请求时所给出的分数。 令人沮丧的是,这个SAAS服务的表现并不令人满意,对于一些真正的人为操作请求,他给出了一个低分,而对于线上生产环境发生的真实恶意注册请求,他又给出了高分。很明显这个SAAS服务并不能帮助我们。雪上加霜的是,我们发现后台的数据系统上,注册数又开始出现诡异的增长了。很明显,黑客又一次发现了我们的伎俩,并更改了脚本。 “他这次是来真的了,”主管说到:“你看这些请求,用的全是真实手机号和163的邮箱,每个请求的IP都不一样了。” “也就是说他这是为了伪装成真实的用户,把自己的肉鸡邮箱和IP池给拿出来了?” “我想是的。” 情况对我们说一下子严峻了起来。
1 | { |
之前我们之所以能快速的在服务端给恶意请求下蜜罐,是因为他的请求中有一个或多个非常明显的特征将他与真实的用户请求区别开来。但是当他这次对于每个恶意请求,都进行精心的构造,伪装成一个真实的用户请求时,我们该在如何代码层面去彻底区分他与真实用户的区别呢?就在我们组在为这一点愁眉苦脸时,坐我旁边的前端开发凑了过来,看着这个恶意注册请求内容说,“这个locale属性有问题呀。” “啥问题?”,我问他,毕竟他的这个邮箱用的是网易的163邮箱,所以locale属性是zh_CN也很正常。”他这个locale属性太规范了,我的代码逻辑里面locale当时随手敲的,所以并没有遵守locale规范。” 听到这个,当时我们整个组都笑尿了,没想到黑客他把自己每个请求都精心伪装成真实用户,甚至连locale的值都遵守协议规范,结果我们自己的前端在开发的时候并没有care这个规范,导致了这个点致命的、难以察觉的特征。我迅速针对这个特征做了新版蜜罐推送到生产环境。再一次,我们为自己争取到了时间。而主管经过调研,发现Google的Recaptcha服务非常适合我们的场景。然而和我对接的前端开发明天才在公司,所以我们只能在明天才能将接入了recaptcha的版本推送到线上。不得不说,这个特征选取的真的非常刁钻。自从上线后,攻击者那边就一直处于未察觉的状态。星期三的晚上我们平安的度过去了。
周四中午,我看着生产环境的日志,发现攻击者的注册脚本停止了。“他在思考”,我想道,这个攻击者一定在思考为什么自己伪装的如此像的请求会被进入蜜罐处理,而他肯定也会在我们网站上正常注册,然后他就会发现正常注册的用户就能正常使用,而他的注册脚本则会进入蜜罐。而在这里我犯了一个错误,这是一场我和攻击者的博弈,但是这场博弈并未结束,攻击者正在整理他的牌思考如何破局,而我此时则对我下的这个蜜罐过于自信,而并没有紧急和前端开发对接调试recaptcha的上线。攻击者长时间的沉默让我们之前紧张的氛围变得稍微了轻松了起来,而我也从下午开始与前端开发不紧不慢的开发、调试、对接。非常不巧的是,今天的CICD机器出现了一点问题,导致每次应用打包时拉取maven倚赖变得异常缓慢,原本2-3分钟就能结束的CICD环节,则变成了20-30分钟。晚上7点的时候,就在我还在调试对接repcptcha的请求时,用户注册数又开始了诡异的增长。“坏了,他又发现了”,我心里一惊,那个黑客最终还是发现了locale的秘密,再一次的对我们展开了攻击。而此时我们这边rechpcha的对接工作并没有做完,在我和google服务器的通信中,总是会出现一个通信失败导致验证失败,外加每次CICD上线新版本服务又在今天出现异常的网络拉取问题,使得耗时非常长,让我变得非常急躁。 我在代码里增加了大量的log信息来帮助我查看究竟是哪里出了问题,但怎么都发现不了问题。我知道目前的状态非常糟糕,而且生产环境正在遭受攻击,而越是紧急的情况下我越是需要冷静。于是深吸一口气,从10倒数到1,平静了一下。这么一做确实很有效,我很快发现自己在于google通信时在构造url阶段时居然不小心错失了一个符号,从而导致通信失败。在我迅速解决这个问题后,立即将新版本推送到生产环境。晚上9点,新版本服务recptcha功能上线,而攻击者的请求也因为recptcha的验证被抵御住。至此,前文提到的两个问题终于彻底解决,我也如释重负的下班了。
周五中午,我们通过后台的数据系统将攻击者的恶意注册的僵尸账号整理起来,联合国内业务一起封禁了这些用户,让恶意攻击者之前的那些账户彻底报废。历经72小时,我们与这个攻击者的攻防拉锯也终于告一段落了。
总结
后来,我们在组里聊起这段往事时,感叹道这个黑客真的非常专业。首先他的攻击手段非常巧妙,他真正的攻击对象是我们国内业务的数据,但是国内业务严防死守,使得攻击的代价非常高。在国际业务对接用户中心以后,他敏锐的发现了国际业务与国内业务在账号体系上打通了,从而通过攻击国际业务的方式来攻击国内业务,实在是一招非常高明的隔山打牛。另一方面,他在伪造真实用户请求时,所用的手机号也都是真实用户的手机号,所以我相当怀疑他当时用的邮箱则是真实存在的邮箱,即他拥有了极大数量的邮箱池与IP池来躲过各种防御和限制。另一方面,在我用蜜罐与他的攻防拉锯中,他也一直步步紧逼,不断的见招拆招,逼迫我赶紧想出下一个对策。
对于这次攻击,我事后总结时,对自己的检讨在于对情绪的控制。在上文中提到,我的焦躁与轻敌让我没有在第一时刻给出正确、冷静的对策,从而减少对我们的损失。不过后来我和主管One on One聊到这件事的时候,他也安慰我说这种事情碰到过一次后才能成长,以后就能成熟应对了。后来为了应对以后更多的爬虫、攻击者时,我做了不少平台层基于K8S和ISTIO的监控、报警、防御、主动逐出的机制。这块以后有空整理吧。