基于JS-Injection的反爬虫分析、绕过和加固建议
【前言】
最近华为云web应用防火墙(英文简称WAF)将要上线一个反爬虫的功能特性,于是华为云WAF的安全专家在此对反爬虫技术做了简单的分析与分享。反爬虫技术的核心其实是需要判断请求是由真实的浏览器发起,还是由脚本或者bot发起(这里把所有非真实浏览器发起的请求都视为爬虫行为)。
目前爬虫的检测方法,大概有三类:
基于User-Agent:最简单的检测爬虫方法是检测请求HEADER中的User-Agent,防御方需要收集所有的爬虫,脚本语言,扫描器对应的User-Agent指纹。然而,这种方法非常容易(不费吹灰之力)被绕过(User-Agent是攻击者可控的)。
基于JS-Injection:稍微进阶的爬虫检测方法,就是注入js,这个方法也是目前业界广泛使用的,在第一次访问请求时,返回注入的js,js执行一些计算逻辑,并将其放入cookie,再次发起请求验证,验证成功后,server向client发放一个token放在cookie里,以后的所有请求都需要携带该cookie标识已认证为非bot身份;阿里的则是偏向于人机交互方面,比如滑块验证。这种检测方法,实际上基于大部分非浏览器爬虫无法执行js脚本这个前提。其实也可以被绕过,但是需要具体分析注入的js的执行逻辑,然后用脚本去实现该逻辑运算即可。另外基于JS-Injection的检测方法,逻辑上篡改了防护网站的页面。
基于Machine-Learning:通过机器学习,对访问请求进行建模,根据访问频率,访问路径,判断是否是爬虫行为. 这种检测方法,较为滞后,无法及时去主动防御。因为该检测方法需要达到一定的请求量才能判定是否为爬虫行为。
上面文字写的太多,接下来选取某安全公司的基于JS-Injection的反爬虫实例进行分析:
【0x01 基于JS-Injection的反爬虫实例分析】
反爬虫方案流程
上图为某安全公司基于JS-Injection的反爬虫的整个方案的请求-响应流
请求1:client向其反爬虫防御的站点某个页面发送一个GET请求。流量经过其反爬虫引擎时,会检查在请求cookie里是否携带一个ccpassport,实际上该字段是在其注入js验证成功后生成的token(对应图中的步骤4),如果木有ccpassport或者ccpassport验证失败(恶意伪造或者cookie失效),则会直接返回其替身页面,其中包含注入的JS。如果验证成功,则直接将请求upstream到后端server
响应2:即请求1中验证失败,该反爬虫引擎则会将返回替身页面,并且设置两个cookie:wzwsconfirm和wzwsvtim,如下图分别显示响应的header和替身页面:
请求3:如果client为真实浏览器并且allow js执行,则会将执行响应2中的js,并再次发起进行“反爬虫验证”,该请求将携带计算的结果。具体的js的执行逻辑将在[注入JS解析]中详细分析。
响应4:该反爬虫引擎收到请求3,验证cookie中携带的js计算结果,如果通过,则生成一个具有时效性的token:ccpassport,将其响应给client,并返回响应码302,让浏览器重新跳转到[请求1]。如果验证失败,则到[响应2],返回替身页面,并设置两个cookie:wzwsconfirm和wzwsvtime。
响应5:如果client的请求携带ccpassport并验证通过,则该请求才能得的真正的server响应页面
注入JS解析
根据上图,[响应2]的图片可知,替身页面中直接调用eval()去执行js代码,下面就来详细的看下js代码做了哪些操作
首先,一般这种js代码都会做点混淆压缩,所以需要解压缩或反混淆,网上也有很多在线js反混淆网站。这里推荐开源的[jsbeautifier],一个比较好的js反混淆模块,并且支持js和python(后面我们的bypass脚本也会用到[jsbeautifier]的python模块)。
反混淆后的js代码如下:(这里将代码中的dynamicurl的真实值屏蔽了)
对上面js代码的执行逻辑进行分析,即执行HXXTTKKLLPPP5()函数。
首先执行findDimensions()函数,如果该函数返回true,则神马都不做。其实不用怎么关心这个函数具体逻辑。猜想:应该是判断浏览器相关东西的,比如浏览器的一些信息,cookie是否开启。但是这里还是稍微分析下(因为我发现这个函数并不是如猜想所说的,略矬)。findDimensions()函数体里调用了window.innerWidth,window.screenX...等API,逻辑上,首先获取窗口的宽和高,如果宽和高的乘积小雨等于12000,则返回true(好奇为啥要用这种逻辑判断,难道是判断该网页是否会被别的网站嵌入盗链?)。也就是说如果你把浏览器窗口缩到足够小,就被认为是爬虫了。。。如下图:
后面的逻辑是取当前窗口相对于屏幕的x,y坐标。(反正我想说的是:这种判断逻辑很矬)
如果findDimensions()函数返回false,说明它预判认为你是正常的浏览器了,然后执行else里面的代码块:
根据template变量计算生成第一个cookie:wzwstempalte;
根据wzwschallenge和wzwschallengex两个变量,计算生成confirm的值
根据上一步计算生成的confirm变量,计算生成第二个cookie:wzwschallenge
重定向到dynamicurl指向的URL,该URL实际上是防护的URI的base64编码
js完成上述计算,设置cookie后,发起验证请求(即上述的[请求3]),下图为请求信息:
访问的URL即dynamicurl,请求携带了之前[响应2]返回的两个cookie:wzwsconfirm和wzwsvtime,以及js代码执行后计算生成的两个cookie:wzwstemplate和wzwschanllenge。该反爬虫引擎收到请求后,进行验证,验证成功后则会返回一个token:ccpassport,然后重定向到之前的访问页面。响应信息如下图:
通过多次测试,发现,该反爬虫引擎每次注入的JS代码,除了wzwschallenge,wzwschallengex,以及template三个变量值动态随机变化,其他代码都不变(也就是,js代码中的算法函数不变)。因此根据wzwschallenge,wzwschallengex,以及template三个变量值计算得到的两个cookie也是动态变化的。但变量dynamicurl并不是随机的,其值为所防护的URL的base64编码,比如,如果你访问的URL为 :http://www.test.com/index.html,那么这个验证的URL应为:http://www.test.com/base64.encode("index.html")。
js代码中的其他一些计算函数:QWERTASDFGXYSF(), KTKY2RBD9NHPBCIHV9ZMEQQDARSLVFDU(),只是单纯的做些运算,无须深入分析
【0x02 利用python脚本绕过此反爬】
通过上述分析,发现一个此反爬虫注入的JS,版本唯一,只是其中三个变量动态变化。另外,其执行的算法逻辑简单,从算法的API使用上来将,不依赖js脚本语言,不依赖浏览器环境,因此,我们在分析完该反爬虫的整个流程后,完全可以使用任何一种脚本语言,实现其注入的js代码逻辑,计算出cookie,以绕过其反爬虫方案
最简单的bypass方法
发送非GET请求:经过测试,发现该反爬虫方案仅针对GET请求有效,因此,最简单的绕过方法就是发送POST,HEAD,PATCH....等请求方法
绕过思路
首先发送正常页面请求,获取响应的替身页面,以及响应的两个cookie:wzwsconfirm和wzwsvtime
解析js,获取其中的wzwschallenge,wzwschallengex和template三个动态值
根据获取的三个动态值,计算cookie:wzwstemplate和wzwschanllenge
携带cookie:之前响应的wzwsconfirm和wzwsvtime以及计算得到的wzwstemplate和wzwschanllenge,向验证URL发送请求,然后获取响应的cookie:ccpassport以及其他cookie值
携带上述几步得到的cookie值,发送正常页面的请求
脚本实现(python)
利用[urllib2]发送请求和接收响应内容,注意模拟浏览器请求头构造请求header,另外,由于需要获取响应的cookie,因此需要用到[cookielib] 来保存响应的cookie。响应内容用了gzip进行压缩,因此需要用到zlib模块中的decompress进行解压还原
由于替身页面中的JS代码经过了混淆,因此需要首先进行解混淆,这里我用了[jsbeautifier]在github上开源的支持python的[jsbeautifier]模块,根据其中的测试用例,需要首先调用jsbeautifier.unpackers.packer中的unpack函数进行解混淆,然后调用jsbeautifier.beautify函数,最终得到反混淆和格式化后的js代码,然后用三个正则,取出三个动态随机值
如之前分析的,js代码中的计算函数不变,因此,我直接事先将js代码中的两个函数QWERTASDFGXYSF(),KTKY2RBD9NHPBCIHV9ZMEQQDARSLVFDU(),人工转为python语法的函数,这里并不用关心函数中的代码逻辑,只需要将js中的API转为python中的API, js语法转为python语法即可(比如,代码中的charCodeAt对应python中的ord...,其实,也可以实现一个工具来进行js代码到python代码的转换,后面稍微做说明)。
另外,验证URL,验证成功后,返回的响应码为302,因此这里需要注意捕获urllib2.HTTPError
bypass代码:
上述代码略微有点不稳定,验证URL偶尔会得到响应404(纳闷~),稳定后其实还可以做为一个python模块,比如antiantiXXXcrawler,输入为一个被该反爬虫保护的站点URL,输出为:验证后得到的cookies。
【0x03 如何加固基于JS-Injection的反爬虫方案?】
该安全公司的反爬方案之所以很容易绕过,总结有两点:
注入的JS模板一尘不变,js代码中的算法逻辑唯一,仅依靠三个动态值来增加js代码的随机性,而这些动态值很容易通过正则匹配提取
注入的JS模板中,执行的算法逻辑不依赖js脚本语言,不依赖浏览器。因此,很容易被转换为其他脚本语言的语法代码
加固讨论
注入的JS版本多样性,不仅仅加强某些变量的动态随机性,在算法逻辑上,也需要动态随机.那么反爬引擎就需要维护一个算法库,随机选取一个或几个算法保证js代码版本的多样性,且算法库需要定时更新,以免遭到攻击者穷举。
其实,理论上来说版本的多样性,也很容易bypass。正如之前提到,js代码到python代码的转换,也可以借助工具实现。于是查了下,发现有个开源工具:[jiphy] 能够将js代码转换为python代码,但其正确率取决于支持的语法结构,于是利用jiphy测试了下,将上面该反爬js转成python代码如下(还是有很多错误,看来jiphy还很年轻。。。以后有时间可以扩充下):
注入的JS模板中,确保执行的算法逻辑依赖浏览器特点,或js脚本语言特性,比如,在js代码中不仅仅做单纯的运算,还可以去调用浏览器环境下的API,比如浏览器版本信息,屏幕分辨率,时区,浏览器字体等浏览器指纹信息,以增加攻击者的分析难度和代码转换难度。
最后,没有绝对的防御,即使上述的加固方案也是可以被bypass的。但是攻防就是一个博弈的过程,道高一尺,魔高一丈。防御能做的也就是一步一步提高攻击者门槛,尽量使得攻击花费的价值与攻击成功后的价值相当~
【尾声】
文中提到的华为云WAF,是华为云最近上线的正在公测的一款保障网站安全的web应用防火墙服务。WAF可以精准过滤海量攻击流量(如SQL注入、XSS跨站脚本攻击、网页木马上传、第三方应用漏洞攻击、CC攻击、恶意爬虫扫描等),保障您的网站安全稳定运行,避免数据泄露。更多详情请您点击链接了解:http://www.huaweicloud.com/product/waf.html
本文作者by:九日可可
- 点赞
- 收藏
- 关注作者
评论(0)