写在前面
本人没有深入钻研过前端技术,JS属于亚文盲水平,这篇分析的目的是分享解决解决问题的思路。
- 提出假设,证明它或推翻它,如果推翻,就再提出一个新的假设。该走的弯路,一米都不能少
- 抛开所有不可能的,剩下的,不管多么匪夷所思,那都是事实
- 坚信JS是纯客户端逻辑,理论上一定可破解,需要的是时间和耐心
- 凡是不能杀死你的,最终都会让你更强
问题描述
智联招聘企业端登陆,输入验证码后,点击验证按钮,抓包获得以下请求
https://passport.zhaopin.com/chk/verify?callback=jsonpCallback&MmEwMD=12quxKHKjs0Ueapv9t.fUcurtt8lkuPKNW.iigEyOYlrTOxwONa0ImcCQ8gvRZic3MnqU1WUPdIESozanNi1FCB38raqp0l5vVLXlYgSCIWxh6rR.3q0lK304tlBMH49LDm8FUL4hkfboCm_lhWv9qO7coH_FfFRY1fC8xIZ7_NljGkOJvIWaaUFwhGF2SVwN58UkzX6GuABWNvbUzzVVq5Kzj45tS_p4U2fo.re9LOuwPpp99H5bLKIgmNsI_RALV39ujuLsqhbHTJAgCtQeZzOkhMbjJKciuKPp97TkbIpDpGr_ZqTBghcjRgr_gz1CkGCCoUTn24dIPfVB0tTDenbTTuOot.rT_RmTiIsA0zY5ac445PXgJZXaXHxrV_ASY0
问题:如何构建MmEwMD参数?
初步分析
查看智联招聘login页面的相关JS文件,分析query param的构建方法
从上图中我们可以看到调用JQuery的$.ajax方法来发送请求,发送的url只有 callback
一个query param,并没有看到 MmEwMD
的踪迹,说明不是在zhilian自己的JS文件中加这个参数,猜测是给JQuery加了 prefilter
或者 beforeSend
之类的钩子
构建本地JS Debug环境
-
通过
wget
抓取html和相关资源文件wget --mirror --page-requisites --adjust-extension --no-parent --convert-links --directory-prefix=sousers https://passport.zhaopin.com/org/login
-
使用python启动simple web server
python -m SimpleHTTPServer 8081
-
访问本地login页面
http://localhost:8081/org/login.html
-
智联招聘使用的JQuery版本是1.7.1,我们从官网下载非min版本方便debug。下载完成后放到script目录下,然后修改login.html中include script代码
http://code.jquery.com/jquery-1.7.1.js
Debug JQuery
// Do send the request // This may raise an exception which is actually // handled in jQuery.ajax (so no try/catch here) xhr.send( ( s.hasContent && s.data ) || null );
从 scripts/chk/captcha.js
的 $.ajax
入手一路debug进去,直到上面这行代码,我们inspector xhr对象发现,并没有任何JQuery的钩子,此时,我们怀疑xhr (XMLHttpRequest) 被hook了。
验证方法如下:
在浏览器console中执行以下代码
var xhr = new window.XMLHttpRequest(); xhr.open('POST','/hello',true); xhr.send('abc');
出现了以下请求,说明在当前页面中的xhr肯定被hook了,同时解除对JQuery的怀疑。
POST http://localhost:8081/hello?MmEwMD=1zXMRFfiFy00rBpdSoJ2cJ4EvS6mQ1dvl5jZLrqB…oRVu5t.x8Y3GVQLmqdG.lmuE2snbEk.YHif4rmHwOtKQoTiHpODCa6HikgWg1X2S._aq8rKTLr
分析Login HTML
Login页面的html中有三段非常有趣的代码
第一段代码是:
<meta name="renderer" content="webkit"><meta id="9DhefwqGPrzGxEp9hPaoag" content="{qqqkkGZAkCAGc60cEqNGqD3DD5aef8al4605kUdZ}FH00lxmQZlsYUWYqfYOapWmqyrK0HMVZCsczHJ90Xl1mUHYqzYOqpHmqJrYVhVAAsUfwWmYqV102mqmgDsaJppYakV2QoUGAwKTymDYgwsSNpK2aFV9aopaAtK07pqmQFVqSKlSlwVrEmXq5_s8RWy24CUi0pdmIbVHfKySoNVi3mzq5CJ1489597716416YipgfCWy.0N5u81OO.MzYPhz_sUNCdYZHoc64qqqVqDASVkfQJYZ6Vkm8J20yqqqqqqqqqqqqqqqqKBgyFY2AtnVVvvzAyQm_MlqqqiR9xU82xnVUwUIqc80{BxLZUzp12oHeoyld0p5rxyaOOYIxVTlI6A8eodlHZm_QVjSuSpeRHS9IK1wlE0YUqYiNonVowpZyknf8pmx0Qa0I3sZfoSlvkcJJKa6kpYWRHCVGkl3W8iplVtALfc1VJxa07kPgqEa9Nk10HhAh1CI9DpmpzDfqr0k130qqqr0qqqqqqqqqqqqqqqq">
- 好消息是这套机制不是智联独创的,别的网站也有用。
- 坏消息是网上的资料非常非常非常稀少
第二段代码是:
<script type="text/javascript" src="/4QbVtADbnLVIc/d.FxJzG50F.js?D9PVtGL=5aef8a">script> https://passport.zhaopin.com/4QbVtADbnLVIc/d.FxJzG50F.js?D9PVtGL=5aef8a
- 和上面情形差不多,有别的网站在用,但资料非常稀缺
- 差不多可以推断出这个JS是用来做混淆的
第三段代码是:
- 这段代码的是经过混淆的JS,并且每次请求login页面,拿到的JS内容都会变化,意味每次会随机用于混淆的变量名和函数名,真是太贱了。
- 接下来我们挑选一个版本,然后解剖下这段JS究竟在做什么
做到这一步,已经产生了一些敬畏之心,既然搜了这么多冷门的关键字,不妨搜一下主角 MmEwMD
百度终于比谷歌牛逼了一回,谷歌完全搜不到,百度可以搜到一条论坛信息,时间还挺近的,是有人在论坛上求助咪咕的login请求问题,再一次印证了这东西不是智联原创,但网络上相关资料非常稀少,可能是一个非公开,非开源的东西
分析被混淆过的outer.js
由于代码有500多行,我将完整代码贴在文章最后的附录中,我们称这个js为 outer.js
为了debug这个混淆过的 outer.js
文件,我们需要:
- 在login.html文件中注释掉这段JS的
- 将format后的JS保存到文件中放到script/outer.js
- 在login.html中include这个JS文件
准备工作完成后,开始debug这个JS,初步猜想,这个JS中应该有一个callback函数,当点击验证按钮的时候,可以到这个函数中。我们给所有的function的入口出都打上断点,但按下验证按钮后,居然没有一个function被触发,这说明callback函数不在这些function中!
经过逐行跟踪,我们猜测callback function的代码,以及inject hook的代码可能都是运行过程中产生的,然后通过 eval()
或者 window['execScript']
之类的来动态加载的,最后我们定位到下面这个函数
outer.js line 405~417 function _$tx(_$dq) { if (_$dq === undefined || _$dq === "") return; var _$dL = _$h1[_$k1][_$ls], _$kD; if (!_$jr) _$jr = _$dL.push; if (_$h1[_$o4]) _$kD = _$h1[_$o4](_$dq); else { var _$sD = _$h1[_$bn]; _$kD = _$sD[_$hm](_$h1, _$dq); } if (_$jr !== _$dL.push) _$dL.push = _$jr; return _$kD; }
_$dq
里面就是被动态加载的代码,接下来我们将debug这段被动态加载的代码。- 我们将他保存为
inner.js
,完整代码参见最下面的附录(有2000多行,太长贴不下,分了卷一和卷二) - 做到这一步已经有些心力憔悴,因为我们不知道前方会有几层迭代,会不会不停地产生动态代码并且加载
字符串变量替换表
在分析 outer.js
过程中,我们发了一张字符串和混淆变量之间的对应关系表,将下面这段代码执行后,可以得到以下变量定义(完整定义请见附录),这里定义了 inner.js
用到的所有全局变量,对我们分析2000+行的 inner.js
非常有帮助
...
_$eS = "XMLHttpRequest", _$ml = "send", _$ij = "MmEwMD", ...
outer.js line 52
_$h1[_$w6(101, 118, 97, 108)](_$kw("FSSBBIl1UgzbN7N`use strict`document`localStorage`globalStorage`sessionStorage`indexedDB`mozIndexedDB`webkitIndexedDB`msIndexedDB`name`top`vdFm`unload`prototype`get`call`set`openDatabase`EkcP`windowData`sessionData`globalData`localData`userData`dbData`idbData`tests`function`setItem`getItem`div`userdata_el`addBehavior`style`behavior`url(#default#userdata)`setAttribute`save`load`getAttribute`transaction`executeSql`CREATE TABLE IF NOT EXISTS `EkcP_t`id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `name TEXT NOT NULL, `value TEXT NOT NULL, `UNIQUE (name)`INSERT OR REPLACE INTO `(name, value) `VALUES(?, ?)`SELECT value FROM ` WHERE name=?`rows`length`item`value`open`onerror`onupgradeneeded`target`result`createObjectStore`onsuccess`objectStoreNames`contains`readwrite`objectStore`put`close`vlaue`escape`indexOf`substr`string`split`charAt`substring`unescape`location`host`replace`getElementById`createElement`visibility`hidden`position`absolute`id`body`appendChild`slice`concat`splice`floor`t__`B_`fontList`$b_onBridgeReady`$b_callHandler`$b_setup`$b_platform`android`iframe`display`none`documentElement`cb_`getTime`src`jbscheme://`stringify`jbscheme://queue_has_message`$b_fetchQueue`$b_onNativeResponse`navigator`platform`test`Uint8Array`subarray`lastIndexOf`XMLHttpRequest`send`Microsoft.XMLHTTP`Array`charCodeAt`from`language`browserLanguage`zh-CN`_tCbyRDtiXFzNibfz8bwLXK67X84O5PPj58BL8FVX1uL.4I9XE6LNLoLBNH2fFUb23dbfxd9B_obGFdbPxMvG.v2f4B9X4B9BRUN944bOjVf`alert`apply`GET`href`responseType`arraybuffer`setRequestHeader`X-sOYOcALfiiw`onreadystatechange`readyState`status`response`VBArray`responseBody`toArray`MmEwMD`Math`ceil`JSON`0000`toString`number`null`boolean`object`[object Array]`hasOwnProperty`%20`addEventListener`on`attachEvent`random`assert failed with condition: `assert failed: ` is not same as `acceleration`accelerationIncludingGravity`alpha`beta`gamma`battery`getBattery`then`level`charging`chargingTime`Infinity`$_YWTU`$_cDro`isNaN`keyCode`button`offsetX`offsetY`touches`screenX`screenY`clientX`clientY`userAgent`standalone`$PreUCBrowserClassic,UCBrowserMessageCenter`__firefox__,_firefox_ReaderMode`__mttCreateFrame,mttCumstomJS`__crWeb,__gCrWeb`MicroMessenger`SeMobFillFormTool,SogouMse`Sogou`ApplePaySession`Safari`PointerEvent`MSPointerEvent`msCredentials`webkitPersistentStorage`browser_parameters,item`FaveIconJavaInterface,jesion` OPR/`chrome`runtime`webstore`onautocomplete`PerformanceObserver`PerformanceObserverEntryList`Entity`AnalyserNode`external`AddSearchProvider`dumpAll`MozAppearance`$_ts`DeviceStorage`controllers`UCWebExt,ucweb`qb_bridge,qbbookshelf`dolphin,dolphininfo,dolphinmeta`safari`pushNotification`orientation`callPhantom,_phantom`$hook$,$$logger,$$lsp,$$lsrb`netsparker,__ns`hp_identifier`spi_hooked,mozAnimationStartTime,mozIndexedDB,mozRequestAnimationFrame`Gamepad`c[`a[b](`WebXMLogMsg_UNIQUE_`stack`pop`Object.InjectedScript.evaluate`@debugger`evaluate`setInterval`eval`var a = new Date(); debugger; new Date() - a > 100;`$_ck`_Selenium_IDE_Recorder,_selenium,callSelenium`__driver_evaluate,__webdriver_