目录
- 前言
- 一、分析
- 二、验证
- 借鉴
前言
听说这是个抓的比较严格的网址,这里就不作太深的分析,仅是将一些环境点给分析出来。
详细的可以看这位大佬的(玩的就是一个风险转移)
小红书x-s新版分析(2023-05-30失效)
一、分析
看波封面
加密点在这哈,没啥技巧,往上跟就看到了
window._webmsxyw
这个就是一整个js抠出来就行了,补环境,讲究粗暴!
l和a在网页上复制下来即可,大概就是这么个样子。
先让他正常跑起来再说吧,跑个结果试试,保险起见,先用vm2去跑,防止它检测我们的node环境
const fs = require('fs')
const { VM, VMScript } = require('vm2');
// console.time('myTimer');
const sandbox = {
require: require,
console: console
};
const codeFile = `${__dirname}/test.js`;
const vm = new VM({
sandbox: sandbox,
require: {
external: true
}
});
const script = new VMScript(fs.readFileSync(codeFile), `${__dirname}/我正在调试的代码.js`);
debugger;
result = vm.run(script);
console.log(result)
先补一些最基本的对象吧,然后挨个上个简单代理(为什么不用自己的链式代理,那玩意太长了,我看着烦,偷个懒随机整个)
然后就可以开始造了,下面是简单代理
better.proxy = function (o, callerName){
return new Proxy(o, {
set(target, property, value){
console.table([{"类型":"set-->","调用者":callerName,"属性":property,"值":value}]);
return Reflect.set(...arguments);
},
get(target, property, receiver){
if (property!=="Math"){
console.table([{"类型":"get<--","调用者":callerName,"属性":property,"值":target[property]}]);
}
return Reflect.get(...arguments);
},
}
);
};
运行一下,可以看到是检测了它这个Symbol(Symbol.toStringTag)属性的
打开浏览器去看一下,这个属性是有值的,那我们可以去补一下,上面那两个属性都是取不到值的, Symbol(nodejs.util.inspect.custom)这属性,是 Node.js 中用于自定义对象的字符串表示形式的 Symbol。
所以我们接下来就是要补这个Symbol(Symbol.toStringTag)了,顺带把他原型链也给补了(这里就简单补了一层,并没有太深,问,就是我懒)
var Window = function Window(){
throw new TypeError("Illegal constructor");
};
Object.defineProperties(Window.prototype, {
[Symbol.toStringTag]:{
value:"Window",
configurable:false, // 通常为 false,不允许删除属性并修改其特性。
writable:true, // 通常为 true,允许重新赋值。
enumerable:true // 通常为 true,可以出现在 for...in 循环或 Object.keys() 方法中。
}
});
window.__proto__ = Window.prototype;
上面那个解决了,往下看,这三个又缺了。补呗,navigator和window是一样的补法。
这里留个小细节Navigator,后续会遇到,先正常配置就行。剩下两个方法,直接补上去就行了。不给代码了哈,自己老实补。
后面遇到这种,想必大家都已经学会怎么补了,接下来的就不带大家补这个toString了
这是上面的一些toString补完后,出现的一个需要补的参数AudioContext
方法,在这里呢,我们秉承的原则就是,是对象就代理,直接给他扔一个上去(接下来有关对象的操作都得这么干)
接下来就到了这一步,很多人在这个方法上出现了不同的歧义
我就展示一下,我的理解,这个方法是在Document的原型上面的,他的作用就是创建一个元素,也就是对象,俗话说万物皆对象,那么我就返回一个对象回去,然后代理它,看看它被取了什么值不就行了吗,这里以canvas为例代理了一下这个canvas_obj
对象。(如果是div或者span对象怎么办,创建呗,返回对象的对象过去就行了,有检测原型就补原型就完事了。缺啥补啥,没检测就别浪费时间补了)
通过我上面的补环境思路,大概随便补一点,数据就出来了,这个时候拿去用肯定是过不了的(我试过),大家观察这个XYW后面的ey开头像不像base64加密,既然像,就拿去解密看看是什么东西
base64解密网址
可以看到,其中只有这个payload需要我们去搞清楚是怎么来的,其他的参数几乎是可以固定死的。那我们就需要回到网页上去看看,这是咋加密出来的。
先把js放上去,然后可以看到第1点就是出我们的X-S的地方。第2点就是返回值了,那么我们就需要在第1点的时候对这个_ace_dcca5[0]
做一个hook操作
_ace_dcca5 = new Proxy(_ace_dcca5,{
get: function(target, prop) {
return target[prop];
// 返回属性的值
},
set: function(target, prop, value) {
// debugger; // 在这里设置断点或执行自定义逻辑
target[prop] = value;
// 设置属性的值
try {
if (value['_ace_4de55'] && value['_ace_4de55'].startsWith("XYW")) {
debugger ;
}
} catch (error) {
}
return true;
}
});
这里要看好了。第一次是XYW。
第二次就是XYW_了,那下一次就是我们要的经过base64加密的东西了
那在这里,我们就要一步步跟进去看了。一直跟到这里就可以发现,这个base64出现了。
我们接着跟_ace_34d1(3, 49)
,参数别忘了带进去。
跟到这一步,其实就能发现一些端倪了。再里面我已经跟过了,这个效果就是把_ace_66
赋值给_ace_936
这个值,可以发现,这个_ace_66
不是赋值进来的,那就代表了什么,这东西是个全局的啊。
这个里面的下标49就是我们要的base64加密的东西,那接下来就hook它了。
这是hook代码,你直接拿去无脑hook是行不通的。要跟进去搞。
_ace_66 = new Proxy(_ace_66,{
get: function(target, prop) {
return target[prop];
// 返回属性的值
},
set: function(target, prop, value) {
if (prop === "49") {
debugger; // 在第 49 个下标被设置时设置断点或执行自定义逻辑
}
console.log(value)
target[prop] = value; // 设置属性的值
return true; // 表示设置成功
}
});
这是成功hook住的样子,这个数据熟悉吧,就是被拿来base64的源数据,
二话不说,hook它
_ace_66 = new Proxy(_ace_66,{
get: function(target, prop) {
return target[prop];
// 返回属性的值
},
set: function(target, prop, value) {
try {
if (value && value.startsWith("cd")) {
debugger ;
}
} catch (error) {}
if (prop === "49") {
debugger ;// 在第 49 个下标被设置时设置断点或执行自定义逻辑
}
console.log(value)
target[prop] = value;
// 设置属性的值
return true;
// 表示设置成功
}
});
往上跟,可以发现是又是这个_ace_936,跟到这里又到头了,看了一下这个_ace_dcca5
是个全局的,好家伙,继续hook。
继续往上走
又回到这里了,好家伙
其实可以发现又跟到死胡同里面了,那我们就接着hook,直接把这个全局变量的值都打印出来,通过打印日志可以发现有个stackInput
和stackOutput
,好家伙这不就真相出来了
就是根据x1=f060e018ee22aede6cdd2393dadd54f9;x2=0|0|0|1|0|0|1|0|0|0|1|0|0|0|0;x3=18995b7979bwbgf099rbj41o4mscuki8wn6a4ep3250000419053;x4=1690954928490;
这个加密来的
那分解一下这个参数,可以得到x1,x2,x3,x4,x4明显就是时间戳,不用管,x3是cookie的a1,也不用管,只有x1和x2还不知道怎么获取的。但是不用慌,在上面打印的过程中,想必大家也看到了x1,x2,x3,x4,就是在_ace_66
hook的时候,接下来我们回去再hook一次_ace_66 看看。
可以发现,这个url这串字符串出现后,才出现了x1,x1这个长度,有经验的读者应该就会联想到md5,确实这个也就是url这串字符串加密后的md5,这下x1就解决了,只剩一个x2了。
MD5加密地址
不用着急,打印信息不仅仅只有x1,x2也出来了
不过往上面看,x2没啥头绪,那么我们就可以回到我们自己的nodejs环境去看看,和浏览器有什么不一样了
二、验证
不需要回去hook我们自己的程序了,根据hook_ace_66
的时候,可以发现都是统一经过这个函数出来的,我们回去打印这个函数就可以了。
记得用cmd看,并且筛选一下信息,不然你看不过来
可以发现nodejs里面只有x2是和浏览器不一样的,那我们再往上看看打印信息,你会发现,经过了这个set->userAgent
x2的某个值就变成1了,这就是之前说的小细节,在浏览器这么做可是不行的噢。
所以我们就需要做点措施,直接定为不可被set
,再看看
Object.defineProperty(Navigator.prototype, "userAgent", {
value:'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36',
writable: false
});
看,这就变为0了,还有一个1和浏览器的不一样,我们再往上看看。浏览器的是0|0|0|1|0|0|1|0|0|0|1|0|0|0|0
我们的是0|0|0|1|0|0|1|0|0|1|1|0|0|0|0
往上看看,那个不一样的1在哪,看,经过调用window.Window就变成1了,那必然有问题啊,我们的nodejs的window.Window是
var Window = function Window(){
throw new TypeError("Illegal constructor");
};
浏览器的是window.Window是window本身,那就浅浅瞎操作一下window.Window=window
,好家伙这次就和浏览器一样了
然后把结果拿去请求。到这里小红书的X-S环境就补完了,嘎嘎乱杀。记得请求的data
要和加密的一样
噢!
大概js环境有个300行,还有一些写的没用的。挺简单的,大家可以动手试一下。
借鉴
小红书x-s新版分析(2023-05-30失效)