参数加密签名 & JS逆向

这首歌是前一段时间旅行时候听当地少数民族的哥们儿唱的,虽然只是作为席间祝酒歌,同时音响声音挺杂的,但是他唱出来超级好听,找了好几天才找到歌名,现在推荐给大家

0x00 背景

年少无知的时候经常在渗透测试报告上写一个漏洞,叫做明文传输漏洞。现在妥了,网站不仅开始采用 https,请求和响应包中的数据也加入了有效的加密措施,同时还会配有一些自定义的 http header 或者 cookie、前端混淆、字体加密等措施,感觉像是回旋镖,一下打中了多年后的自己

当然,上面的描述主要是玩笑,企业做这些操作的主要目的是与搞爬虫的这帮人进行对抗,给安全人员带来困扰只是顺带的

采用数据加密和签名技术可能会给安全人员的工作带来哪些困扰呢?

  • 无法使用 burpsuite 等工具进行包重放攻击
  • 绝大多数的自动化工具均无法正常使用
  • 渗透测试过程中又要重新面对前端的防护措施
  • 无法有效确定网站隐藏的风险点
  • ...

目前对请求参数全做加密的网站数量并不多,我猜测其中一部分原因是一旦这么做,绝大多数的waf和态势感知类产品防护都会失效,大多数目前主要是对网站返回数据进行了加密,同时加上一些请求头加密、风控措施等

但我预测在未来几年,js 逆向技术会成为渗透测试工程师与红队检测相关人员的必备技能,所以目前相关文章和视频主要是搞爬虫那帮人在写这件事让我感到十分不安,于是有了这篇文章

下面是一些案例

可以看到,同样的参数,包重放就会导致 403 错误

0x01 技术点

想要解决加密和签名问题,主要就是两个方面,第一能够写出加密过程;第二就是能够对数据进行解密。对于安全人员来说,前一部分更加重要,如果只是手工测试的话,后一部分可以交给浏览器,其中涉及的技术在不断更新,就看网站到底“变态”到什么程度,一般需要了解以下技术

  • 浏览器开发者工具使用方法
  • javascript 代码调试
  • webpack 打包技术
  • javascript 混淆技术

0x02 找出加密的值

以某个网站为例,主要完成编写请求加密过程,主要思路是找出所有网站用于校验的加密参数以及请求头,通过断点调试的方式找到加密过程,本地复现该加密过程,成功发起请求,如果时间允许,再完成返回包解密工作

找出加密的值

可以看到主要有四个值是加密的,三个 http 头,一个请求体

  • X-K-Header
  • X-Ss-Req-Header
  • X-S-Header
  • data

首先我们测试一下如果更换搜索词或者分页这几个值是否会改变

字符比较长,直接肉眼看可能不能完全准确,放到 compare 中进行对比

通过对比可以发现,其中 X-K-HeaderX-Ss-Req-Header 是没有发生变化的,可能是这个参数就是不变的,也有可能这个参数是有时效性的,若干时间后会发生变化,也可能是与功能接口有关

此时对于搞爬虫的人员来说,大概率就可以不管这两个参数了,但是我们作为安全人员,需要对参数原始值进行探索,看看是否可能存在安全隐患,举个例子,像原始值中添加一段 log4j 漏洞利用代码,经过加密传输到后端后,可能就会导致漏洞利用

0x03 js 逆向请求过程

这个过程中有很多技巧,最原始的方式就是一点一点跟栈

本地先安装 nodejs 环境,用于本地执行 js 文件,本次用于解密的 js 名称为 js_rev.js

1. X-K-Header

如果服务器想让客户端发起一个请求,并携带特定的请求头,那肯定是在 js 中定义好的,要么是访问即加载的js,要么是服务器远程返回的js,我们直接在开发者工具中搜索该字符 (Ctrl + f) 打开搜索

这一步的目的是在服务器 js 文件中(或者服务器返回的js代码)找到我们希望的字符,所以可以看到,这里只有一个 main.js 中包含该字符,我们点进去

搜索相关字符

有两个结果,很显然第一个是我们要找的,因为在这里是设置 http header 的部分,其他两个请求头也在这里,将代码一起扒下来

我们将 e.headers 这种格式直接改成变量

代码语言:javascript
复制
// e.headers["X-S-HEADER"] = (0,T.A2)(e),
// e.headers["X-K-HEADER"] = (0,T.G5)(),
// e.headers["X-SS-REQ-HEADER"] = (0,T.cz)()

// var X_S_HEADER = (0,T.A2)(e)
var X_K_HEADER = (0,T.G5)()
// var X_SS_REQ_HEADER = (0,T.cz)()

我们先处理 X-K-HEADER ,把其他两个先注视,很明显我们代码里缺少 (0,T.G5)() ,我们在浏览器中打上断点,看一下这个内容具体是什么

刷新页面

网页断在了这里,我们在控制台打印 (0,T.G5)

可以看到 (0,T.G5) 是一个函数,函数的返回结果就是我们要的头,点击上图中的第一个箭头的位置,也就是函数本身的位置,就会跳转到函数定义位置

可以看到 (0,T.G5) 其实就是函数 Ft,将其代码扒下来

代码语言:javascript
复制
Ft = function() {
return Jt("secretKeyValue") || ""
}

var X_K_HEADER = Ft()

// var X_S_HEADER = (0,T.A2)(e)
// var X_SS_REQ_HEADER = (0,T.cz)()

直接执行代码的话,会找不到 Jt 函数,那我们就需要去找 Jt 函数

在调用 Jt 函数这行打上断点,注意看嗷,断点打在了行里面的具体函数调用处,点击右侧箭头所指向的按钮,让代码向下执行

程序执行到Jt 函数处停下,此时将鼠标悬停在 Jt 函数上,就会出现关于该函数的信息,点击 FunctionLocation 后面的地址就会跳转到函数定义位置

继续拷贝

代码语言:javascript
复制
Jt = function(t) {
return sessionStorage.getItem(t)
}

Ft = function() {
return Jt("secretKeyValue") || ""
}

var X_K_HEADER = Ft()
console.log(X_K_HEADER)

此时可以看出,X_K_HEADER 头的值是从浏览器的 sessionStorage 中取的 secretKeyValue key所对应的值

要么我们直接把值拿过来,要么探索一下 js 向 sessionStorage 的写入过程,我们尝试探索一下

我们需要取消所有断点,之后重新搜索 secretKeyValue

还是在 main.js

通过搜索,发现有 7 处存在该字符,我们查询一下 javascriptsessionStorage 的写入键值对所使用的方法

将查询到的 7 处与其进行比对

发现此处较为类似,在此打上断点,清除 sessionStorage 执行到此处停下,控制台打印 sessionStorage

发现并没有 secretKeyValue,此时我们让程序一步一步向下走

在此输出 sessionStorage 时发现这回 sessionStorage 中已经存在 secretKeyValue 键值对了,此时可以确定,用于设置该键值对的函数为 Wt,用上面的方法,找到 Wt 的定义位置,并拷贝代码

代码语言:javascript
复制
Wt = function(t, e) {
sessionStorage.setItem(t, e)
}

可以看到,这个函数并没有加密过程,只是设置键值对,因此找到形式参数 e 的传递值以及其生成位置就可以找到参数加密过程

代码语言:javascript
复制
Wt("secretKeyValue", null == r || null === (t = r.content) || void 0 === t ? void 0 : t.secretKeyValue)

可以看到,形式参数e 的值为 null == r || null === (t = r.content) || void 0 === t ? void 0 : t.secretKeyValue

我们清除 sessionStorage 在此执行到此处停下,在控制台打印相关变量的值

可以看出,这个加密值是从 t.secretKeyValue 来的,那接下来就去找一下 t.secretKeyValue 的生成过程

t 是一个对象,t 的值来自 t = r.content ,打印一下 r

跟踪 r 对象

r 的值是由一个请求返回的响应包中的数据转化而来的,请求的地址为 https://gate.lagou.com/system/agreement 在网络栏寻找该请求

由此捋清楚该加密参数的逻辑,其加密参数是由客户端向 https://gate.lagou.com/system/agreement 发起请求后获得的

这是一个 POST 请求,请求头并没有加密值

请求体包含一加密参数,格式可能为 json

继续寻找该加密值 secretKeyDecode 的生成方式,在发起该请求的地方打上断点,刷新页面

secretKeyDecode 的值由变量 e 存储,跟踪 e 的赋值过程

因此 e 其实是 Ot 函数的参数,是由调用者传递过来的,经过调试发现生成 secretKeyDecode 的位置为

代码语言:javascript
复制
e = {
secretKeyDecode: Jt("rsaEncryptData") || Rt()
}

在此处设置断点,清空 sessionStorage 并在此处停下

此时可以看到, e 是空的, Jt("rsaEncryptData") 的结果是 null,因此 secretKeyDecode 的值来自于 Rt 函数,按照上面的方法,找到 Rt 函数的位置,之后分析 Rt 函数

代码语言:javascript
复制
Rt = function() {
var t = function(t) {
for (var e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", r = "", n = 0; n < t; n++) {
var i = Math.floor(Math.random() * e.length);
r += e.substring(i, i + 1)
}
return r
}(32);
Jt("aesKey") || Wt("aesKey", t);
var e = new wt;
e.setPublicKey("-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnbJqzIXk6qGotX5nD521Vk/24APi2qx6C+2allfix8iAfUGqx0MK3GufsQcAt/o7NO8W+qw4HPE+RBR6m7+3JVlKAF5LwYkiUJN1dh4sTj03XQ0jsnd3BYVqL/gi8iC4YXJ3aU5VUsB6skROancZJAeq95p7ehXXAJfCbLwcK+yFFeRKLvhrjZOMDvh1TsMB4exfg+h2kNUI94zu8MK3UA7v1ANjfgopaE+cpvoulg446oKOkmigmc35lv8hh34upbMmehUqB51kqk9J7p8VMI3jTDBcMC21xq5XF7oM8gmqjNsYxrT9EVK7cezYPq7trqLX1fyWgtBtJZG7WMftKwIDAQAB-----END PUBLIC KEY-----");
t = e.encrypt(t);
return Wt("rsaEncryptData", t),t
}

Rt 函数先是通过一个匿名函数生成了长度为 32 的由 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= 中字符组成的随机字符串

之后便进入到 JtWt 方法,根据我们之前的分析,此处的含义为:如果 sessionStorage 存在名为 aesKeykey 就算了,不存在的话,就将刚才生成的32位字符串作为值存储进 sessionStorage

我们在此行设置断点,让程序走过来,看看此时 sessionStorage 中是否存在该 key

可以看到此时并不存在,于是程序会将 t 的值设置为该 key 的值

代码语言:javascript
复制
var e = new wt;
e.setPublicKey("-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnbJqzIXk6qGotX5nD521Vk/24APi2qx6C+2allfix8iAfUGqx0MK3GufsQcAt/o7NO8W+qw4HPE+RBR6m7+3JVlKAF5LwYkiUJN1dh4sTj03XQ0jsnd3BYVqL/gi8iC4YXJ3aU5VUsB6skROancZJAeq95p7ehXXAJfCbLwcK+yFFeRKLvhrjZOMDvh1TsMB4exfg+h2kNUI94zu8MK3UA7v1ANjfgopaE+cpvoulg446oKOkmigmc35lv8hh34upbMmehUqB51kqk9J7p8VMI3jTDBcMC21xq5XF7oM8gmqjNsYxrT9EVK7cezYPq7trqLX1fyWgtBtJZG7WMftKwIDAQAB-----END PUBLIC KEY-----");
t = e.encrypt(t);
return Wt("rsaEncryptData", t),t

接下来这几行看起来像是创建一个对象事例,之后调用其方法,设置公钥,之后对生成的 32 位随机字符串进行加密,在 sessionStorage 设置一个新的键值对, keyrsaEncryptData ,值为加密后的字符串,最后返回该字符串

因此可以明确,secretKeyDecode 就是 t

其实到这里很多朋友就很熟悉了,这就是 RSA + AES 的加密传输方式

参考以下文章
https://blog.csdn.net/weixin_44864084/article/details/109898434

对于标准加密方式我们不需要扒代码,直接采用现成的库就可以,但是我们需要确定两件事

  • 加密的参数格式
  • 是否为标准加密方式,会不会存在魔改的情况

那我们就需要清空 sessionStorage 重新下断点去跟一下 e ,确定上面两件事

生成 e 的部分代码中 wt 是一个函数xt,这种语法的意思就是创建一个 xt 实例,并将内部成员值传递给该实例,我们可以在实例创建结束后打印 e

一般我这么说的意思就是在创建完 e 后的代码打上断点,之后让代码执行到此处

一般来说,RSA 加密需要三个值

  • 被加密内容
  • 公钥
  • 填充方式

通过将加密相关代码在搜索引擎中进行搜索,可以推测该前端使用了 jsencrypt 这个库完成的长明文分段加密,这个库的填充方式为 pkcs#1

可以参考
https://juejin.cn/post/7039016811299340319

这条链到现在已经比较长了,简单帮大家总结一下

代码语言:javascript
复制
1. 通过使用固定范围的字符集生成一段随机的长度为32个字符的字符串
2. 通过 RSA 对该值进行加密,将加密后的值写入 sessionStorage 并返回
3. 将 RSA 加密后的值作为 POST 请求中 json 数据中key为 secretKeyDecode 的值
4. 向 https://gate.lagou.com/system/agreement 发送 POST 请求获取返回值
5. 将返回值中的 secretKeyValue 存储进入 sessionStorage
6. 前端从 sessionStorage 取值并作为 X-K-Header 的值

这里我们思考一个问题,从测试角度来说,我们有可以将随机生成的32位字符串替换成恶意内容,溢出代码也好,log4j 代码也好

但在后续的请求中,是不是可以直接使用服务器生成的,因为目前来看,这个 header 是没有变化的,因此我们这一系列操作目的主要就是查看前后端一起加解密过程中是否存在安全隐患,如果我们直接对加密后的请求头进行测试,可能就会错过一些机会

由于篇幅问题,就不再展示对 X-Ss-Req-Header 值的逆向过程了

2. X-S-Header

一样的方法,尝试全局搜索

还是只有 main.js 中存在相关字符,如果代码经过了混淆,根本就搜索不到,进入该文件,找到相关位置

代码语言:javascript
复制
e.headers["X-S-HEADER"] = (0,T.A2)(e)

清空 secretKeyValue ,下断点,找到 (0,T.A2)函数以及 e

此时发现该位置被多次调用,如果想要断点到我们想要的位置,需要进行条件断点,此时可以通过对 e.url 进行判断,值的话直接在网络中查看

https://www.lagou.com/jobs/v2/positionAjax.json

点击搜索按钮或者分页

e 是一个对象,其中内容就是发起http 请求的参数,可以观察到此时 body 还没有被加密

进入 (0,T.A2) 函数

(0,T.A2) 函数其实就是 Ut 函数,拷贝函数到 js 文件

代码语言:javascript
复制
Ut = function() {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {}
, e = t.url
, r = (void 0 === e ? "" : e).replace("https://gate.lagou.com", "").replace("https://activity.lagou.com", "")
, e = {
deviceType: 1
}
, t = "".concat(JSON.stringify(e)).concat(Kt(r)).concat(Zt(t))
, t = (t = t,
null === (t = Tt.SHA256(t).toString()) || void 0 === t ? void 0 : t.toUpperCase());
return It(JSON.stringify({
originHeader: JSON.stringify(e),
code: t
}))
}

这个函数默认是没有参数的,但是我们是传递了e 参数的,没关系,从刚才断点的地方单步调试进入该函数

在控制台打印arguments

arguments 是一个对象,其中成员 0 对应的内容就是传入的 e 的内容,我们给本地的该函数补充一个参数 arguments

代码语言:javascript
复制
Ut = function(arguments) {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {}
, e = t.url
, r = (void 0 === e ? "" : e).replace("https://gate.lagou.com", "").replace("https://activity.lagou.com", "")
, e = {
deviceType: 1
}
, t = "".concat(JSON.stringify(e)).concat(Kt(r)).concat(Zt(t))
, t = (t = t,
null === (t = Tt.SHA256(t).toString()) || void 0 === t ? void 0 : t.toUpperCase());
return It(JSON.stringify({
originHeader: JSON.stringify(e),
code: t
}))
}

args_obj = {
'async' : true,
'body' : "first=true&cl=false&fromSearch=true&labelWords=&suginput=&city=&kd=ceo",
'headers' : {
'accept' : 'application/json, text/plain, */*',
'content-type' : 'application/x-www-form-urlencoded; charset=UTF-8',
'x-anit-forge-token' : 'None',
'x-anit-forge-code' : '0'
},
'method' : "POST",
'password' : undefined,
'url' : "https://www.lagou.com/jobs/v2/positionAjax.json",
'user' : undefined,
'withCredentials' : true,
}

args = {
0: args_obj
}
var X_S_HEADER = Ut(args)
console.log(X_S_HEADER)

执行后发现其中存在 KtZtTtIt 的值还没有,继续断点调试,找到定义位置,拷贝其代码

代码语言:javascript
复制
Kt = function() {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : ""
, r = "";
return -1 < t.indexOf("?") && t.split("?")[1].split("&").filter(function(t) {
return !!t
}).map(function(t, e) {
t && (t = t.split("="),
r += (0 === e ? "?" : "&").concat(t[0], "=").concat(window.encodeURIComponent(window.decodeURIComponent(t[1]))))
}),
r = t.split("?")[0] + r
}

看着情况也需要补一个形式参数 arguments

代码语言:javascript
复制
Kt = function(arguments) {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : ""
, r = "";
return -1 < t.indexOf("?") && t.split("?")[1].split("&").filter(function(t) {
return !!t
}).map(function(t, e) {
t && (t = t.split("="),
r += (0 === e ? "?" : "&").concat(t[0], "=").concat(window.encodeURIComponent(window.decodeURIComponent(t[1]))))
}),
r = t.split("?")[0] + r
}
代码语言:javascript
复制
Zt = function() {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {}
, e = ""
, r = t.method
, r = void 0 === r ? "get" : r
, t = t.body;
return "post" === r.toLowerCase() && t && (t = Gt(t) ? JSON.parse(t) : (0,
_t.$Z)("?".concat(t)),
e = Object.keys(t).length ? JSON.stringify(t) : ""),
e
}

也需要补参数

继续向下寻找未定义的内容

代码语言:javascript
复制
t = Tt.SHA256(t).toString()

这种一看就是标准方法,但是还是测试一下,我们通过控制台的 Tt.SHA256 方法和在线网站分别对 admin 进行加密,看看结果是否相同

通过对比,可以确定是标准方法,可以使用 crypto-js 库来进行完成

使用 Crypto_Obj 替换 Tt

代码语言:javascript
复制
It = function(t) {
kt = Tt.enc.Utf8.parse(Jt("aesKey")),
t = Tt.enc.Utf8.parse(t);
t = Tt.AES.encrypt(t, kt, {
iv: Pt,
mode: Tt.mode.CBC,
padding: Tt.pad.Pkcs7
});
return t.toString()
}

Tt 替换后,引入了一个新的函数叫 Jt,这个函数之前就提到过,是根据参数从 sessionStorage 获取值的方法,而且获取的这个 aesKey 我们也很眼熟,就是之前生成的,如果你想严谨一些,可以分别打上断点,查看一下在生成到此处调用值是否发生过改变

没错,我就是个严谨的人,我要验证一下

清空 sessionStorage ,分别在两个断点处打印该值

可以看到,两次结果一致,这个这个值是我们生成的,脚本里保持一致即可,先定义为一个常量

再次执行脚本,发现 Pt 不存在,我们寻找一下 Pt

代码语言:javascript
复制
Pt = Tt.enc.Utf8.parse("c558Gq0YQK2QUlMc")

脚本执行成功,帮我们生成了 X-S-Header 的值,代码如下

代码语言:javascript
复制
const Crypto_Obj = require('crypto-js')

// console.log(Crypto_Obj)

const aes_key = '1H=lYnM9Ubnm+BZ=YUhNfqWaE8xhTeOd'
Pt = Crypto_Obj.enc.Utf8.parse("c558Gq0YQK2QUlMc")

It = function(t) {
kt = Crypto_Obj.enc.Utf8.parse(aes_key),
t = Crypto_Obj.enc.Utf8.parse(t);
t = Crypto_Obj.AES.encrypt(t, kt, {
iv: Pt,
mode: Crypto_Obj.mode.CBC,
padding: Crypto_Obj.pad.Pkcs7
});
return t.toString()
}

Zt = function(arguments) {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {}
, e = ""
, r = t.method
, r = void 0 === r ? "get" : r
, t = t.body;
return "post" === r.toLowerCase() && t && (t = Gt(t) ? JSON.parse(t) : (0,
_t.$Z)("?".concat(t)),
e = Object.keys(t).length ? JSON.stringify(t) : ""),
e
}

Kt = function(arguments) {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : ""
, r = "";
return -1 < t.indexOf("?") && t.split("?")[1].split("&").filter(function(t) {
return !!t
}).map(function(t, e) {
t && (t = t.split("="),
r += (0 === e ? "?" : "&").concat(t[0], "=").concat(window.encodeURIComponent(window.decodeURIComponent(t[1]))))
}),
r = t.split("?")[0] + r
}

Ut = function(arguments) {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {}
, e = t.url
, r = (void 0 === e ? "" : e).replace("https://gate.lagou.com", "").replace("https://activity.lagou.com", "")
, e = {
deviceType: 1
}
, t = "".concat(JSON.stringify(e)).concat(Kt(r)).concat(Zt(t))
, t = (t = t,
null === (t = Crypto_Obj.SHA256(t).toString()) || void 0 === t ? void 0 : t.toUpperCase());
return It(JSON.stringify({
originHeader: JSON.stringify(e),
code: t
}))
}

args_obj = {
'async' : true,
'body' : "first=true&cl=false&fromSearch=true&labelWords=&suginput=&city=&kd=ceo",
'headers' : {
'accept' : 'application/json, text/plain, */*',
'content-type' : 'application/x-www-form-urlencoded; charset=UTF-8',
'x-anit-forge-token' : 'None',
'x-anit-forge-code' : '0'
},
'method' : "POST",
'password' : undefined,
'url' : "https://www.lagou.com/jobs/v2/positionAjax.json",
'user' : undefined,
'withCredentials' : true,
}

args = {
0: args_obj
}
var X_S_HEADER = Ut(args)
console.log(X_S_HEADER)

3. data

前面三个 header 通过搜索可以直接根据特征找到值生成位置,对于 data 这种比较普遍的字符串,搜索可能效果不会很好

比较难以直接找到生成位置,但是有一点,在发送请求的时候,加密值 data 已经生成了,此时我们可以从该位置,一点一点向前寻找加密过程

我们在源代码界面,添加 xhr 断点

点击搜索按钮

此时请求体参数已经是加密的了,我们向前追踪一下(当然,之前处理其他参数的时候,当时还没有加密,可以在两点之间打上断点,以后进行查找,这里还是一步一步来)

datan参数里是加密状态,看一下 n 的生成

可以看到 var n = e.data.target; 也就是说 n 是由 e 来的,而 e 是函数的参数,代码很长,在函数内不断向上追踪 e 的值

代码语言:javascript
复制
function Xt(t) {
var e = XMLHttpRequest.prototype;
if (e && e.addEventListener)
var n = _t(e, "open", (function() {
return function(t, e) {
return t[St] || (t[Tt] = e[0],
t[wt] = e[1],
t[bt] = !1 === e[2]),
n.apply(t, e)
}
}
))
, r = _t(e, "send", (function() {
return function(e, n) {
if (e[St])
return r.apply(e, n);
var a = {
source: "xmlhttprequest",
state: "",
type: "macroTask",
data: {
target: e,
method: e[Tt],
sync: e[bt],
url: e[wt],
status: ""
}
};
try {
return function(e) {
if ("schedule" !== e.state) {
e.state = "schedule",
t("schedule", e);
var n = e.data.target;
r("readystatechange"),
r("load"),
r("timeout"),
r("error"),
r("abort")
}
function r(t) {
n.addEventListener(t, (function(t) {
var r = t.type;
"readystatechange" === r ? 4 === n.readyState && 0 !== n.status && i(e, "success") : i(e, "load" === r ? "success" : r)
}
))
}
}(a),
r.apply(e, n)
} catch (t) {
throw i(a, "error"),
t
}
}
}
));
function i(e, n) {
e.state !== X && (e.state = X,
e.data.status = n,
t(X, e))
}
}

可以说 en 两个参数互相纠缠,难解难分,经过测试,其实在调用 Xt函数之前,加密的 data 就已经生成了,此时我们采用一种朴素的方法,一步一步向上跟栈,就从 xhr 断点开始

方法就是点击调用堆栈中的项,最上方的是最后调用的,越往下越是之前的,在每次点击的时候都看一下作用域,尤其是本地的,看看什么时候才会出现明文的 data

在向上查找到第二个匿名函数是,发现了明文的 data 数据

在此处打上断点

os 的值是 data 加密前的明文状态

因此我们追着 os ,看看它们到底经历了什么

代码语言:javascript
复制
(window._xhrHook = !0, function(t) {
if (o)
throw "Proxy already exists";
o = new B(t)
}({
onRequest: (R = (0,
n.Z)(S().mark(function t(e, r) {
var n, i, o, s, a, h, c, u, f, l, p, d, g, y, v, m, b, w;
return S().wrap(function(t) {
for (; ; )
switch (t.prev = t.next) {
case 0:
if (e && e.url && !e.url.includes("/agreement") && (0,
T.aB)())
return t.next = 4,
(0,
T.Fw)("axios");
t.next = 6;
break;
case 4:
t.sent;
case 6:
if (!e.url || !((0,
T.s8)(e.url) || -1 < e.url.indexOf("/company/imgPreview") && (0,
A.$Z)(e.url).imageFileId)) {
t.next = 53;
break
}
if (i = "get" === e.method.toLowerCase(),
e.headers["X-S-HEADER"] = (0,
T.A2)(e),
e.headers["X-K-HEADER"] = (0,
T.G5)(),
e.headers["X-SS-REQ-HEADER"] = (0,
T.cz)(),
i || "post" != (null == e || null === (n = e.method) || void 0 === n ? void 0 : n.toLowerCase())) {
t.next = 52;
break
}
if (null === (b = e.headers["content-type"]) || void 0 === b || !b.includes("application/json") || !e.body) {
t.next = 16;
break
}
e.body = JSON.stringify({
data: (0,
T.q6)(e.body && e.body)
}),
t.next = 52;
break;
case 16:
if ((s = null) == e || null === (o = e.body) || void 0 === o || !o.forEach) {
t.next = 50;
break
}
a = new FormData,
h = new FormData,
u = 0,
f = !(c = {}),
l = D(null == e ? void 0 : e.body);
try {
for (l.s(); !(p = l.n()).done; )
d = (0,
x.Z)(p.value, 2),
g = d[0],
y = d[1],
u++,
y instanceof File ? (h.append(g, y),
f = !0) : c[g] = y
} catch (t) {
l.e(t)
} finally {
l.f()
}
if (!u) {
t.next = 48;
break
}
if (!f) {
t.next = 46;
break
}
v = D(h),
t.prev = 28,
v.s();
case 30:
if ((b = v.n()).done) {
t.next = 38;
break
}
return b = (0,
x.Z)(b.value, 2),
m = b[0],
b = b[1],
t.next = 34,
(0,
T.Po)(b);
case 34:
w = t.sent,
a.append(m, w);
case 36:
t.next = 30;
break;
case 38:
t.next = 43;
break;
case 40:
t.prev = 40,
t.t0 = t.catch(28),
v.e(t.t0);
case 43:
return t.prev = 43,
v.f(),
t.finish(43);
case 46:
0 < Object.keys.length && a.append("data", (0,
T.q6)(JSON.stringify(c))),
e.body = a;
case 48:
t.next = 52;
break;
case 50:
(s = e.body) && (w = (0,
T.q6)(JSON.stringify((0,
A.$Z)("?".concat(s)))),
e.body = "data=".concat(encodeURIComponent(decodeURIComponent(w))));
case 52:
e.url && -1 < e.url.indexOf("/company/imgPreview") && (0,
A.$Z)(e.url).imageFileId && (e.url = "".concat(e.url.substr(0, e.url.indexOf("?")), "?imageFileId=").concat(encodeURIComponent((0,
T.q6)((0,
A.$Z)(e.url).imageFileId))));
case 53:
r.next(e);
case 54:
case "end":
return t.stop()
}
}, t, null, [[28, 40, 43, 46]])
})),
function(t, e) {
return R.apply(this, arguments)
}
),
onError: function(t, e) {
e.next(t)
},
onResponse: function(t, e) {
var r = t.headers["x-ss-req-header"] ? JSON.parse(t.headers["x-ss-req-header"]) : {}
, r = t.headers["x-s-header"] || r.encrypted;
if (-1 < t.config.url.indexOf("/company/imgPreview") || -1 < t.config.url.indexOf("/nearBy/previewResume"))
return t.response = (0,
T.Zy)({
response: t,
isEncrypted: r
}),
void e.next(t);
r && (r = JSON.parse(t.response),
t.response = JSON.stringify((0,
T.ow)(r.data))),
e.next(t)
}
}))

经过搜索,发现与 o 相关的赋值只有几处

  • o = new B(t)
  • if ((s = null) == e || null === (o = e.body) || void 0 === o || !o.forEach)

分别设置断点进行分析

此处不是我们要找的

此时 e.body 的值还是明文的,我们在该函数结束的时候打个断点,再看看 e.body

此时已经是加密状态,因此我们将范围缩小到这段代码中了,而且被加密的变量名为 e.body

我们从这段代码开始处开始跟

此处是一个比较好的地方,因为每一个 case 进入前都会在此进行赋值,可以通过在此处设置 e.body 等于最终加密后的值,来快速找到加密的 case ,但是为了给大家稍微多介绍一点知识,我们采用新增记录点的方式,在此处记录 t.next 的值以及 e.body

这里看到,还挺复杂,时而明文,时而密文,因为不止一个请求用了此处代码,所以这里仅作为展示日志点功能来用,接下来我们要上个大招了

既然是 switch..case 这种代码,那就很简单粗暴了,直接把所有的 break 都打上断点,之后不断查看 e.body 值的变化,直到它变成加密的值

找到了, e.body 的值来自于 w

代码语言:javascript
复制
w = (0,T.q6)(JSON.stringify((0,A.$Z)("?".concat(s)))

也就是说 w 来自于 s

代码语言:javascript
复制
s = e.body

s 就等于 e.body

现在开始扒代码

s 直接赋值就可以了

代码语言:javascript
复制
var s = "first=true&cl=false&fromSearch=true&labelWords=&suginput=&city=&kd=ceo"

w 涉及到一些函数

代码语言:javascript
复制
w = (0,T.q6)(JSON.stringify((0,A.$Z)("?".concat(s)))

这函数也叫 s ,我们先改一个名字,叫 s_f

代码语言:javascript
复制
var s_f = function(t) {
var e = 1 < arguments.length && void 0 !== arguments[1] && arguments[1]
, t = t.substr(t.indexOf("?"))
, r = new Object;
if (-1 != t.indexOf("?"))
for (var n = t.substr(1).split("&"), i = 0; i < n.length; i++) {
var o = n[i].split("=");
if ("null" !== o[1] && "undefined" !== o[1])
try {
r[o[0]] = e ? o[1] : decodeURIComponent(o[1])
} catch (t) {
r[o[0]] = o[1]
}
}
return r
}
代码语言:javascript
复制
It = function(t) {
kt = Tt.enc.Utf8.parse(Jt("aesKey")),
t = Tt.enc.Utf8.parse(t);
t = Tt.AES.encrypt(t, kt, {
iv: Pt,
mode: Tt.mode.CBC,
padding: Tt.pad.Pkcs7
});
return t.toString()
}

再将之前的已经修改过的内容进行同步修改

代码语言:javascript
复制
const Crypto_Obj = require('crypto-js')

// // console.log(Crypto_Obj)

const aes_key = '1H=lYnM9Ubnm+BZ=YUhNfqWaE8xhTeOd'
Pt = Crypto_Obj.enc.Utf8.parse("c558Gq0YQK2QUlMc")

It = function(t) {
kt = Crypto_Obj.enc.Utf8.parse(aes_key),
t = Crypto_Obj.enc.Utf8.parse(t);
t = Crypto_Obj.AES.encrypt(t, kt, {
iv: Pt,
mode: Crypto_Obj.mode.CBC,
padding: Crypto_Obj.pad.Pkcs7
});
return t.toString()
}

var s_f = function(t) {
var e = 1 < arguments.length && void 0 !== arguments[1] && arguments[1]
, t = t.substr(t.indexOf("?"))
, r = new Object;
if (-1 != t.indexOf("?"))
for (var n = t.substr(1).split("&"), i = 0; i < n.length; i++) {
var o = n[i].split("=");
if ("null" !== o[1] && "undefined" !== o[1])
try {
r[o[0]] = e ? o[1] : decodeURIComponent(o[1])
} catch (t) {
r[o[0]] = o[1]
}
}
return r
}

var s = "first=true&cl=false&fromSearch=true&labelWords=&suginput=&city=&kd=ceo"
var w = It(JSON.stringify(s_f("?".concat(s))))
var data = "data=".concat(encodeURIComponent(decodeURIComponent(w)))
console.log(data)

成功生成了加密后的 data

4. 编写http请求代码

静态值直接使用服务器默认的,动态值使用动态生成

代码语言:javascript
复制
const Crypto_Obj = require('crypto-js')
const axios = require('axios')

const X_K_HEADER = 'qUbv5QqijUsjGSKO0uqy9URHswCBvfWyuWUxV2U9R3OLnygt8F+ogRPnch8omsLF'
const X_Ss_Req_Header = '{"secret":"qUbv5QqijUsjGSKO0uqy9URHswCBvfWyuWUxV2U9R3OLnygt8F+ogRPnch8omsLF"}'
const AES_KEY = '1H=lYnM9Ubnm+BZ=YUhNfqWaE8xhTeOd'

const Pt = Crypto_Obj.enc.Utf8.parse("c558Gq0YQK2QUlMc")

const Zt = function(arguments) {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {}
, e = ""
, r = t.method
, r = void 0 === r ? "get" : r
, t = t.body;
return "post" === r.toLowerCase() && t && (t = Gt(t) ? JSON.parse(t) : (0,
_t.$Z)("?".concat(t)),
e = Object.keys(t).length ? JSON.stringify(t) : ""),
e
}

const Kt = function(arguments) {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : ""
, r = "";
return -1 < t.indexOf("?") && t.split("?")[1].split("&").filter(function(t) {
return !!t
}).map(function(t, e) {
t && (t = t.split("="),
r += (0 === e ? "?" : "&").concat(t[0], "=").concat(window.encodeURIComponent(window.decodeURIComponent(t[1]))))
}),
r = t.split("?")[0] + r
}

const Ut = function(arguments) {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {}
, e = t.url
, r = (void 0 === e ? "" : e).replace("https://gate.lagou.com", "").replace("https://activity.lagou.com", "")
, e = {
deviceType: 1
}
, t = "".concat(JSON.stringify(e)).concat(Kt(r)).concat(Zt(t))
, t = (t = t,
null === (t = Crypto_Obj.SHA256(t).toString()) || void 0 === t ? void 0 : t.toUpperCase());
return It(JSON.stringify({
originHeader: JSON.stringify(e),
code: t
}))
}

const It = function(t) {
kt = Crypto_Obj.enc.Utf8.parse(AES_KEY),
t = Crypto_Obj.enc.Utf8.parse(t);
t = Crypto_Obj.AES.encrypt(t, kt, {
iv: Pt,
mode: Crypto_Obj.mode.CBC,
padding: Crypto_Obj.pad.Pkcs7
});
return t.toString()
}

const s_f = function(t) {
var e = 1 < arguments.length && void 0 !== arguments[1] && arguments[1]
, t = t.substr(t.indexOf("?"))
, r = new Object;
if (-1 != t.indexOf("?"))
for (var n = t.substr(1).split("&"), i = 0; i < n.length; i++) {
var o = n[i].split("=");
if ("null" !== o[1] && "undefined" !== o[1])
try {
r[o[0]] = e ? o[1] : decodeURIComponent(o[1])
} catch (t) {
r[o[0]] = o[1]
}
}
return r
}

args = {
'async' : true,
'body' : "first=true&cl=false&fromSearch=true&labelWords=&suginput=&city=&kd=ceo",
'headers' : {
'accept' : 'application/json, text/plain, */*',
'content-type' : 'application/x-www-form-urlencoded; charset=UTF-8',
'x-anit-forge-token' : 'None',
'x-anit-forge-code' : '0'
},
'method' : "POST",
'password' : undefined,
'url' : "https://www.lagou.com/jobs/v2/positionAjax.json",
'user' : undefined,
'withCredentials' : true,
}

let X_S_HEADER = Ut({0: args})
// console.log(X_S_HEADER)

const sfunc = function(t) {
var e = 1 < arguments.length && void 0 !== arguments[1] && arguments[1]
, t = t.substr(t.indexOf("?"))
, r = new Object;
if (-1 != t.indexOf("?"))
for (var n = t.substr(1).split("&"), i = 0; i < n.length; i++) {
var o = n[i].split("=");
if ("null" !== o[1] && "undefined" !== o[1])
try {
r[o[0]] = e ? o[1] : decodeURIComponent(o[1])
} catch (t) {
r[o[0]] = o[1]
}
}
return r
}

const s = "first=true&cl=false&fromSearch=true&labelWords=&suginput=&city=&kd=ceo"
const w = It(JSON.stringify(sfunc("?".concat(s))))
const data = "data=".concat(encodeURIComponent(decodeURIComponent(w)))
// console.log(data)

async function sendSyncPostRequest() {
try {
const response = await axios.post(
'https://www.lagou.com/jobs/v2/positionAjax.json', data,
{
headers: {
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Cookie': 'X_MIDDLE_TOKEN=797bc148d133274a162ba797a6875817; JSESSIONID=ABAAABAABEIABCI61622272032040D969240E925E5223CF; WEBTJ-ID=20231014001623-18b29d31eede79-00f4d0f0dac513-4c6d1379-2073600-18b29d31eee18ec; RECOMMEND_TIP=true; _putrc=A81646AD3A1E7E5F123F89F2B170EADC; gate_login_token=v1####7b0a843e7159b66771dd41a7c981850988c807d1f167505562200c8db1da14b8; login=true; unick=%E7%94%A8%E6%88%B73029; showExpriedIndex=1; showExpriedCompanyHome=1; showExpriedMyPublish=1; hasDeliver=0; user_trace_token=20231014001624-68ff7b99-d051-439b-a974-92537e5a83af; LGUID=20231014001624-d0c4815a-8be2-458b-b208-8c0e32632c14; Hm_lvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1697213784; _ga=GA1.2.78104773.1697213784; _gid=GA1.2.263005079.1697213784; sajssdk_2015_cross_new_user=1; sensorsdata2015session=%7B%7D; index_location_city=%E5%85%A8%E5%9B%BD; X_HTTP_TOKEN=5eca3e2beb16a5e55873127961e18af9ab6f5c0e5d; __SAFETY_CLOSE_TIME__26543112=1; Hm_lpvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1697213785; LGRID=20231014001625-12533f75-16ed-448d-a125-7b05c44e822a; _ga_DDLTLJDLHH=GS1.2.1697213785.1.0.1697213785.60.0.0; privacyPolicyPopup=false; TG-TRACK-CODE=index_search; __lg_stoken__=8dd29488d1e08be0eb24c6e2c853c9ae6922c526edba40d6639f2556012c673341d38f8d38bc889ad550ad9d0e1a431c4bc1e0708be6c66c00b085304f03b070a5704378d859; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2226543112%22%2C%22first_id%22%3A%2218b29d322311b8-0b0199033d26bb-4c6d1379-2073600-18b29d32232d18%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%2C%22%24os%22%3A%22MacOS%22%2C%22%24browser%22%3A%22Chrome%22%2C%22%24browser_version%22%3A%22117.0.0.0%22%7D%2C%22%24device_id%22%3A%2218b29d322311b8-0b0199033d26bb-4c6d1379-2073600-18b29d32232d18%22%7D',
'Origin': 'https://www.lagou.com',
'Pragma': 'no-cache',
'Referer': 'https://www.lagou.com/wn/jobs?cl=false&fromSearch=true&kd=ceo',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.55',
'X-K-HEADER': X_K_HEADER,
'X-S-HEADER': X_S_HEADER,
'X-SS-REQ-HEADER': X_Ss_Req_Header,
'accept': 'application/json, text/plain, */*',
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'sec-ch-ua': '"Microsoft Edge";v="117", "Not;A=Brand";v="8", "Chromium";v="117"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'traceparent': '00-0c88a9683be07b2c174344de54456797-e8c4e28cbc4d61b5-01',
'x-anit-forge-code': '0',
'x-anit-forge-token': 'None'
}
}
);
console.log(response.data);
} catch (error) {
console.error(error);
}
}

sendSyncPostRequest();

成功发起请求,获取到加密的返回值,状态码是 200 ,但是严谨来说,也不知道这结果到底是啥样,所以我们得想办法把这内容给解密了

0x04 js 逆向解密返回值

对于安全人员来说,返回值不见得一定要逆向,毕竟浏览器会帮我们显示出来

如果我们从xhr 断点处一点一点向下进行调试,是可以找到解密方法的,但是这种方法效率太低了

从返回内容和头来看,返回内容超级长,返回值为 json 格式,那么前端在解密的时候肯定会涉及一个方法 JSON.parse;同时这个 js 文件应该还得包含 data 关键字;整体来看,代码没有混淆,所以可能存在decrypt 这种字符

按照这种思路,我们对文件进行搜索

经过多重搜索,推测可能在 main.js

一共有四个位置,可以挨个断点分析一下,可以先看一下界面中的内容,找个特征字符,之后在断点处设置条件断点,这样就可以找到是否是我们需要的值了

在此处设置一个记录点,先看一下是否会有明文data返回值

确实有记录到值,也是明文的,但是似乎是很多接口都是用此处代码进行返回

尝试在四处均打上断点,发现只有上面这处能够成功拦截,因此此处基本上就是解密点,在此处打上断点,分析一下代码执行后的解密值

可以看到,内容就是我们请求的返回值,这些值直接显示在页面中,此时我们需要看一下函数最开始传递的参数是什么形式,是否这个函数直接就解密了

可以看到直接就是我们的加密的返回值呀,那倒是简单了,直接扒代码

代码语言:javascript
复制
Mt = function(t) {
kt = Tt.enc.Utf8.parse(Jt("aesKey"));
t = Tt.AES.decrypt(t, kt, {
iv: Pt,
mode: Tt.mode.CBC,
padding: Tt.pad.Pkcs7
}).toString(Tt.enc.Utf8);
try {
t = JSON.parse(t)
} catch (t) {}
return t
}

看起来好像就是一个标准的 AES 解密,那到简单了,直接来吧

代码语言:javascript
复制
const Crypto_Obj = require('crypto-js')
const axios = require('axios')

const X_K_HEADER = 'qUbv5QqijUsjGSKO0uqy9URHswCBvfWyuWUxV2U9R3OLnygt8F+ogRPnch8omsLF'
const X_Ss_Req_Header = '{"secret":"qUbv5QqijUsjGSKO0uqy9URHswCBvfWyuWUxV2U9R3OLnygt8F+ogRPnch8omsLF"}'
const AES_KEY = '1H=lYnM9Ubnm+BZ=YUhNfqWaE8xhTeOd'

const Pt = Crypto_Obj.enc.Utf8.parse("c558Gq0YQK2QUlMc")

const Zt = function(arguments) {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {}
, e = ""
, r = t.method
, r = void 0 === r ? "get" : r
, t = t.body;
return "post" === r.toLowerCase() && t && (t = Gt(t) ? JSON.parse(t) : (0,
_t.$Z)("?".concat(t)),
e = Object.keys(t).length ? JSON.stringify(t) : ""),
e
}

const Kt = function(arguments) {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : ""
, r = "";
return -1 < t.indexOf("?") && t.split("?")[1].split("&").filter(function(t) {
return !!t
}).map(function(t, e) {
t && (t = t.split("="),
r += (0 === e ? "?" : "&").concat(t[0], "=").concat(window.encodeURIComponent(window.decodeURIComponent(t[1]))))
}),
r = t.split("?")[0] + r
}

const Ut = function(arguments) {
var t = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {}
, e = t.url
, r = (void 0 === e ? "" : e).replace("https://gate.lagou.com", "").replace("https://activity.lagou.com", "")
, e = {
deviceType: 1
}
, t = "".concat(JSON.stringify(e)).concat(Kt(r)).concat(Zt(t))
, t = (t = t,
null === (t = Crypto_Obj.SHA256(t).toString()) || void 0 === t ? void 0 : t.toUpperCase());
return It(JSON.stringify({
originHeader: JSON.stringify(e),
code: t
}))
}

const It = function(t) {
kt = Crypto_Obj.enc.Utf8.parse(AES_KEY),
t = Crypto_Obj.enc.Utf8.parse(t);
t = Crypto_Obj.AES.encrypt(t, kt, {
iv: Pt,
mode: Crypto_Obj.mode.CBC,
padding: Crypto_Obj.pad.Pkcs7
});
return t.toString()
}

const s_f = function(t) {
var e = 1 < arguments.length && void 0 !== arguments[1] && arguments[1]
, t = t.substr(t.indexOf("?"))
, r = new Object;
if (-1 != t.indexOf("?"))
for (var n = t.substr(1).split("&"), i = 0; i < n.length; i++) {
var o = n[i].split("=");
if ("null" !== o[1] && "undefined" !== o[1])
try {
r[o[0]] = e ? o[1] : decodeURIComponent(o[1])
} catch (t) {
r[o[0]] = o[1]
}
}
return r
}

const Mt = function(t) {
const kt = Crypto_Obj.enc.Utf8.parse(AES_KEY);
t = Crypto_Obj.AES.decrypt(t, kt, {
iv: Pt,
mode: Crypto_Obj.mode.CBC,
padding: Crypto_Obj.pad.Pkcs7
}).toString(Crypto_Obj.enc.Utf8);
try {
t = JSON.parse(t)
} catch (t) {}
return t
}

args = {
'async' : true,
'body' : "first=true&cl=false&fromSearch=true&labelWords=&suginput=&city=&kd=ceo",
'headers' : {
'accept' : 'application/json, text/plain, */*',
'content-type' : 'application/x-www-form-urlencoded; charset=UTF-8',
'x-anit-forge-token' : 'None',
'x-anit-forge-code' : '0'
},
'method' : "POST",
'password' : undefined,
'url' : "https://www.lagou.com/jobs/v2/positionAjax.json",
'user' : undefined,
'withCredentials' : true,
}

let X_S_HEADER = Ut({0: args})
// console.log(X_S_HEADER)

const sfunc = function(t) {
var e = 1 < arguments.length && void 0 !== arguments[1] && arguments[1]
, t = t.substr(t.indexOf("?"))
, r = new Object;
if (-1 != t.indexOf("?"))
for (var n = t.substr(1).split("&"), i = 0; i < n.length; i++) {
var o = n[i].split("=");
if ("null" !== o[1] && "undefined" !== o[1])
try {
r[o[0]] = e ? o[1] : decodeURIComponent(o[1])
} catch (t) {
r[o[0]] = o[1]
}
}
return r
}

const s = "first=true&cl=false&fromSearch=true&labelWords=&suginput=&city=&kd=ceo"
const w = It(JSON.stringify(sfunc("?".concat(s))))
const data = "data=".concat(encodeURIComponent(decodeURIComponent(w)))
// console.log(data)

async function sendSyncPostRequest() {
try {
const response = await axios.post(
'https://www.lagou.com/jobs/v2/positionAjax.json', data,
{
headers: {
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Cookie': 'X_MIDDLE_TOKEN=797bc148d133274a162ba797a6875817; JSESSIONID=ABAAABAABEIABCI61622272032040D969240E925E5223CF; WEBTJ-ID=20231014001623-18b29d31eede79-00f4d0f0dac513-4c6d1379-2073600-18b29d31eee18ec; RECOMMEND_TIP=true; _putrc=A81646AD3A1E7E5F123F89F2B170EADC; gate_login_token=v1####7b0a843e7159b66771dd41a7c981850988c807d1f167505562200c8db1da14b8; login=true; unick=%E7%94%A8%E6%88%B73029; showExpriedIndex=1; showExpriedCompanyHome=1; showExpriedMyPublish=1; hasDeliver=0; user_trace_token=20231014001624-68ff7b99-d051-439b-a974-92537e5a83af; LGUID=20231014001624-d0c4815a-8be2-458b-b208-8c0e32632c14; Hm_lvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1697213784; _ga=GA1.2.78104773.1697213784; _gid=GA1.2.263005079.1697213784; sajssdk_2015_cross_new_user=1; sensorsdata2015session=%7B%7D; index_location_city=%E5%85%A8%E5%9B%BD; X_HTTP_TOKEN=5eca3e2beb16a5e55873127961e18af9ab6f5c0e5d; __SAFETY_CLOSE_TIME__26543112=1; Hm_lpvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1697213785; LGRID=20231014001625-12533f75-16ed-448d-a125-7b05c44e822a; _ga_DDLTLJDLHH=GS1.2.1697213785.1.0.1697213785.60.0.0; privacyPolicyPopup=false; TG-TRACK-CODE=index_search; __lg_stoken__=8dd29488d1e08be0eb24c6e2c853c9ae6922c526edba40d6639f2556012c673341d38f8d38bc889ad550ad9d0e1a431c4bc1e0708be6c66c00b085304f03b070a5704378d859; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2226543112%22%2C%22first_id%22%3A%2218b29d322311b8-0b0199033d26bb-4c6d1379-2073600-18b29d32232d18%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%2C%22%24os%22%3A%22MacOS%22%2C%22%24browser%22%3A%22Chrome%22%2C%22%24browser_version%22%3A%22117.0.0.0%22%7D%2C%22%24device_id%22%3A%2218b29d322311b8-0b0199033d26bb-4c6d1379-2073600-18b29d32232d18%22%7D',
'Origin': 'https://www.lagou.com',
'Pragma': 'no-cache',
'Referer': 'https://www.lagou.com/wn/jobs?cl=false&fromSearch=true&kd=ceo',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.55',
'X-K-HEADER': X_K_HEADER,
'X-S-HEADER': X_S_HEADER,
'X-SS-REQ-HEADER': X_Ss_Req_Header,
'accept': 'application/json, text/plain, */*',
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'sec-ch-ua': '"Microsoft Edge";v="117", "Not;A=Brand";v="8", "Chromium";v="117"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'traceparent': '00-0c88a9683be07b2c174344de54456797-e8c4e28cbc4d61b5-01',
'x-anit-forge-code': '0',
'x-anit-forge-token': 'None'
}
}
);
// console.log(response.data.data);
return response.data.data
// encrypt_data = response.data.data
} catch (error) {
console.error(error);
}
}

sendSyncPostRequest().then(data => {
const plaintext = Mt(data);
console.log(plaintext);
}).catch(error => {
console.error(error);
})

深入打印其中的数据

至此成功对该网站进行加密和解密

0x05 总结

我的前端加解密知识主要来自于一位讲爬虫的讲师,虽然没有报班,只是看了些公开课和录屏,还是非常感谢老师付出,本文不想做商业宣传,故不提相关名字
这篇文章是这位老师的一个案例,作为安全人员,在一些技术问题的视角上是不同于搞爬虫的人员的,因此文中包含大量的不同视角的自我思考

文章中主要涉及的加解密知识在整体加解密领域内属于相对简单明了,采用标准加密算法、没有代码混淆、反debug、虚拟机等,但是其实前端加解密的对抗还有非常非常大的空间,安全人员作为被动的一方,最大的优势就是网站必须把加密的代码拿到我们浏览器本地执行,因此无论多复杂,都有被破解的可能