启动docker镜像
根据其官网 https://www.wiz.cn/zh-cn/docker 说明,使用如下命令即可启动
1 2 3 4 5
| docker pull wiznote/wizserver:latest docker run --name wiz --restart=always -it -d \ -v `pwd`/wizdata:/wiz/storage \ -v /etc/localtime:/etc/localtime \ -p 80:80 -p 9269:9269/udp wiznote/wizserver
|
初探授权
启动好之后,默认授权限制只能有5个用户。
进入docker容器,查看基本信息。
执行 docker exec -it wiz bash
进入容器,可以看到
- 后端主要使用node实现并用pm2管理,nginx反代,数据库mysql
- node版本v8.11.2,v8版本6.2.414.54
点击查看执行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| UID PID CMD root 1 bash /wiz/app/entrypoint.sh root 33 /usr/bin/redis-server 127.0.0.1:6379 mysql 53 /usr/sbin/mysqld root 59 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf nginx 60 nginx: worker process nginx 61 nginx: worker process root 119 PM2 v4.5.0: God Daemon (/root/.pm2) root 129 node /root/.pm2/modules/pm2-logrotate/node_modules/pm2-logrotate/app.js root 137 node /wiz/app/wizserver/app.js root 157 node /wiz/app/wizserver/app.js root 173 node /wiz/app/wizserver/app.js root 189 node /wiz/app/wizserver/app.js root 205 node /wiz/app/wizserver/app.js root 221 node /wiz/app/wizserver/app.js root 240 node /wiz/app/wizserver/app.js root 246 crond -n root 256 bash root 270 ps -aef
v8.11.2
6.2.414.54
┌─────┬──────────────────┬ │ id │ name │ ├─────┼──────────────────┼ │ 1 │ as │ │ 6 │ cloud │ │ 4 │ index │ │ 2 │ note │ │ 7 │ office │ │ 5 │ search │ │ 3 │ ws │ └─────┴──────────────────┴
|
分析代码发现后缀为jsc的文件
1 2 3 4 5 6 7 8
| total 536 -rw-r--r-- 1 root root 8792 Dec 23 2020 alicloud_tools.jsc -rw-r--r-- 1 root root 9984 Dec 23 2020 client_tools.jsc -rw-r--r-- 1 root root 7024 Dec 23 2020 cookie_tools.jsc -rw-r--r-- 1 root root 2352 Dec 23 2020 date_tools.jsc -rw-r--r-- 1 root root 27528 Dec 23 2020 db_tools.jsc ......
|
进一步分析发现,这是通过bytenodehttps://github.com/bytenode/bytenode工具编译成了v8的字节码。
在各种搜索之后发现,目前还没有相关的反汇编、反编译工具。
唯一相关项目https://github.com/PositiveTechnologies/ghidra_nodejs是GHIDRA的插件,但node版本和我们不匹配
反汇编v8字节码,改造d8
通过v8源码分析发现,v8本身是有反汇编功能,node可通过--print-bytecode
参数开启。
但只能反汇编源码,无法反汇编jsc文件。为了反汇编字节码只能通过修改v8来实现。
d8 https://v8.dev/docs/d8 是v8的开发工具,为了简单起见决定对d8进行修改。
反汇编jsc思路
- jsc实际是由
v8::internal::CodeSerializer::Serialize
方法生成
- 反汇编需要调用
v8::internal::BytecodeArray::Disassemble
方法生成
反序列v8::internal::CodeSerializer::Deserialize
原型
1 2
| MUST_USE_RESULT static MaybeHandle<SharedFunctionInfo> Deserialize( Isolate* isolate, ScriptData* cached_data, Handle<String> source);
|
其中参数cached_data
,可通过ScriptData的构造函数ScriptData(const byte* data, int length);
构造
其中返回值SharedFunctionInfo
对象有bytecode_array
方法,可以获得BytecodeArray来进行反汇编
1 2 3 4
| BytecodeArray* SharedFunctionInfo::bytecode_array() const { DCHECK(HasBytecodeArray()); return BytecodeArray::cast(function_data()); }
|
于是思路自然而然就有了
- 读取jsc,构造
ScriptData
对象
- 反序列化,获取
SharedFunctionInfo
对象
- 反汇编,通过
bytecode_array
获取BytecodeArray
,并调用Disassemble
反汇编
搭建v8编译环境
1 2 3 4 5 6 7 8 9 10 11 12
| git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git export PATH=`pwd`/depot_tools:"$PATH" export HTTPS_PROXY=http://172.31.0.1:18080 gclient sync fetch v8 cd v8 git checkout 6.2.414.46 gclient sync tools/dev/v8gen.py x64.release -- \ v8_enable_disassembler=true \ v8_enable_object_print=true ninja -C out.gn/x64.release d8
|
v8_enable_disassembler
和v8_enable_object_print
一定要开启,否则反汇编时不显示常量内容
修改d8代码(d8.cc)
在Shell类增加LoadJSC方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| static void Disassemble(v8::internal::BytecodeArray* bytecode) { internal::OFStream os(stdout); bytecode->Disassemble(os); auto consts = bytecode->constant_pool(); for (int i = 0; i < consts->length(); i++) { auto obj = consts->get(i); if (obj->IsSharedFunctionInfo()) { auto shared = v8::internal::SharedFunctionInfo::cast(obj); os << "Function name " << shared->name()->ToCString().get() << "\n"; Disassemble(shared->bytecode_array()); } } } void Shell::LoadJSC(const v8::FunctionCallbackInfo<v8::Value>& args) { auto isolate = reinterpret_cast<i::Isolate*>(args.GetIsolate()); for (int i = 0; i < args.Length(); i++) { String::Utf8Value filename(args.GetIsolate(), args[i]); if (*filename == NULL) { Throw(args.GetIsolate(), "Error loading file"); return; } int length = 0; auto filedata = reinterpret_cast<uint8_t*>(ReadChars(*filename, &length)); if (filedata == NULL) { Throw(args.GetIsolate(), "Error reading file"); return; } auto scriptdata = new i::ScriptData(filedata, length); auto source = isolate->factory() ->NewStringFromUtf8(i::CStrVector("source")) .ToHandleChecked(); auto fun = i::CodeSerializer::Deserialize(isolate, scriptdata, source) .ToHandleChecked(); Disassemble(fun->bytecode_array()); } }
|
注册为全局函数,在Shell::CreateGlobalTemplate中添加代码
1 2 3 4
| global_template->Set( String::NewFromUtf8(isolate, "loadjsc", NewStringType::kNormal) .ToLocalChecked(), FunctionTemplate::New(isolate, LoadJSC));
|
由于目标v8版本为6.2.414.54,但v8的git仓库中并没有这个版本
所以用了最接近的版本6.2.414.46,但反序列时一些校验无法通过
- SerializedCodeData::SanityCheck
- Deserializer::Initialize
需要修改以上两个方法,代码过于丑陋就不贴了。
修改后使用ninja -C out.gn/x64.release d8
命令重新编译即可。
使用loadjsc进行反汇编
使用命令v8/out.gn/x64.release/d8 -e "loadjsc('xxx.jsc')"
即可反汇编jsc文件
例如源码test.js
1 2 3 4
| function xxx(){ console.log("asdasd") } xxx()
|
其通过wiz的docker里bytenode编译为test.jsc
命令 /wiz/app/wizserver/node_modules/.bin/bytenode -c test.js
再通过d8反汇编,结果如下:
命令 ./d8 -e "loadjsc('test.jsc')"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| Parameter count 1 Frame size 8 0 E> 0x17c42dd2c522 @ 0 : 91 StackCheck 0 S> 0x17c42dd2c523 @ 1 : 6e 00 00 00 CreateClosure [0], [0], #0 0x17c42dd2c527 @ 5 : 1e fb Star r0 115 S> 0x17c42dd2c529 @ 7 : 95 Return Constant pool (size = 1) 0x17c42dd2c531: [FixedArray] in OldSpace - map = 0x1b27f0f82309 <Map(HOLEY_ELEMENTS)> - length: 1 0: 0x17c42dd2c549 <SharedFunctionInfo> Handler Table (size = 16) Function name Parameter count 6 Frame size 8 0x17c42dd2c742 @ 0 : 6e 00 00 02 CreateClosure [0], [0], #2 0x17c42dd2c746 @ 4 : 1e fb Star r0 10 E> 0x17c42dd2c748 @ 6 : 91 StackCheck 106 S> 0x17c42dd2c749 @ 7 : 4f fb 01 CallUndefinedReceiver0 r0, [1] 0x17c42dd2c74c @ 10 : 04 LdaUndefined 113 S> 0x17c42dd2c74d @ 11 : 95 Return Constant pool (size = 1) 0x17c42dd2c751: [FixedArray] in OldSpace - map = 0x1b27f0f82309 <Map(HOLEY_ELEMENTS)> - length: 1 0: 0x17c42dd2c769 <SharedFunctionInfo xxx> Handler Table (size = 16) Function name xxx Parameter count 1 Frame size 24 74 E> 0x17c42dd2c862 @ 0 : 91 StackCheck 82 S> 0x17c42dd2c863 @ 1 : 0a 00 02 LdaGlobal [0], [2] 0x17c42dd2c866 @ 4 : 1e fa Star r1 90 E> 0x17c42dd2c868 @ 6 : 20 fa 01 04 LdaNamedProperty r1, [1], [4] 0x17c42dd2c86c @ 10 : 1e fb Star r0 0x17c42dd2c86e @ 12 : 09 02 LdaConstant [2] 0x17c42dd2c870 @ 14 : 1e f9 Star r2 90 E> 0x17c42dd2c872 @ 16 : 4c fb fa f9 00 CallProperty1 r0, r1, r2, [0] 0x17c42dd2c877 @ 21 : 04 LdaUndefined 104 S> 0x17c42dd2c878 @ 22 : 95 Return Constant pool (size = 3) 0x17c42dd2c881: [FixedArray] in OldSpace - map = 0x1b27f0f82309 <Map(HOLEY_ELEMENTS)> - length: 3 0: 0x2e3b60d34a19 <String[7]: console> 1: 0x2e3b60d08e41 <String[3]: log> 2: 0x17c42dd2c8e9 <String[6]: asdasd> Handler Table (size = 16)
|
分析授权逻辑
前台获取授权信息的api接口
返回数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "returnCode": 200, "returnMessage": "OK", "externCode": "", "result": { "start": 1571192332036, "end": 4733432332036, "count": 5, "oem": "wiz", "type": "license_free", "version": 1, "key": "4bc0cc40-efbb-11e9-bef0-0faf68675f7c", "ext": {} } }
|
定位接口对应的文件
根据上述url,http://127.0.0.1/as/admin/licence?clientType=web&clientVersion=4.0&lang=zh-cn
结合后端的目录结构,首先对admin_router.jsc
文件进行反汇编分析
1 2 3 4 5 6 7 8 9 10 11 12
| /wiz/app/wizserver/as/admin
total 128 -rw-r--r-- 1 root root 7408 Dec 23 2020 admin_file_utils.jsc -rw-r--r-- 1 root root 80720 Dec 23 2020 admin_router.jsc -rw-r--r-- 1 root root 1680 Dec 23 2020 admin_static_files.jsc -rw-r--r-- 1 root root 3384 Dec 23 2020 ldap_settings.jsc drwxr-xr-x 4 root root 4096 Dec 23 2020 meta_files -rw-r--r-- 1 root root 3304 Dec 23 2020 middleware.jsc -rw-r--r-- 1 root root 15240 Dec 23 2020 oem_utils.jsc -rw-r--r-- 1 root root 6304 Dec 23 2020 wizbox_search.jsc
|
admin_router.jsc
粗略分析,发现admin_router
调用了addLicence
和getLicence
两个方法
而同时包含这两个方法的文件是wiz_name_utils.jsc
wiz_name_utils.jsc
发现wiz_name_utils.jsc应该是调用了node-rsa库,通过RSA算法来计算相关授权
node-rsa
为了验证猜测,通过修改/wiz/app/wizserver/node_modules/node-rsa/src/NodeRSA.js
的构造函数,来打印一下公钥
1 2
| pm2 restart as tail -f /root/.pm2/logs/as-out.log
|
为了进一步验证,我们修改解密函数,打印返回值
再次重启as服务,并访问网页授权页面
1 2
| 2021-06-29 18:15 +08:00: {"start":1571192332036,"end":4733432332036,"count":5,"oem":"wiz","type":"license_free","version":1,"key":"4bc0cc40-efbb-11e9-bef0-0faf68675f7c","ext":{}} 2021-06-29 18:15 +08:00: e58678339769ba7a9139202655ab345f
|
解密后的数据与网页接口返回的数据一致,证明猜测位置正确
解除授权封印
通过上面的分析,思路就很清楚了。我们只要在特定的时候修改NodeRSA.prototype.decryptPublic
的返回值可以控制授权
1 2 3 4 5 6 7 8 9 10 11 12 13
| NodeRSA.prototype.decryptPublic = function (buffer, encoding) { var data = this.$$decryptKey(true, buffer, encoding); try{ var v = JSON.parse(data); if(v.count == 5){ v.count = 99999; v.type = 'license_vip'; data = Buffer.from(JSON.stringify(v)); } }catch(e){ } return data; };
|
最终结果: