某知笔记服务端docker镜像授权分析

本文为看雪论坛优秀文章看雪论坛作者ID:HOWMP

1

启动docker镜像

根据其官网 https://www.wiz.cn/zh-cn/docker 说明,使用如下命令即可启动。docker pull wiznote/wizserver:latestdocker run --name wiz --restart=always -it -d \ -v `pwd`/wizdata:/wiz/storage \ -v /etc/localtime:/etc/localtime \ -p 80:80 -p 9269:9269/udpwiznote/wizserver

2

初探授权

启动好之后,默认授权限制只能有5个用户。

进入docker容器,查看基本信息。

执行 docker exec -it wiz bash 进入容器,可以看到:

后端主要使用node实现并用pm2管理,nginx反代,数据库mysqlnode版本v8.11.2,v8版本6.2.414.54。

# ps -aefUIDPIDCMDroot 1bash /wiz/app/entrypoint.shroot33/usr/bin/redis-server 127.0.0.1:6379mysql 53/usr/sbin/mysqldroot59nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.confnginx 60nginx: worker processnginx 61nginx: worker processroot 119PM2 v4.5.0: God Daemon (/root/.pm2)root 129node /root/.pm2/modules/pm2-logrotate/node_modules/pm2-logrotate/app.jsroot 137node /wiz/app/wizserver/app.jsroot 157node /wiz/app/wizserver/app.jsroot 173node /wiz/app/wizserver/app.jsroot 189node /wiz/app/wizserver/app.jsroot 205node /wiz/app/wizserver/app.jsroot 221node /wiz/app/wizserver/app.jsroot 240node /wiz/app/wizserver/app.jsroot 246crond -nroot 256bashroot 270ps -aef# node -vv8.11.2# node -p process.versions.v86.2.414.54# pm2 list┌─────┬──────────────────┬│ id│ name │├─────┼──────────────────┼│ 1 │ as ││ 6 │ cloud││ 4 │ index││ 2 │ note ││ 7 │ office ││ 5 │ search ││ 3 │ ws │└─────┴──────────────────┴

分析代码发现后缀为jsc的文件:

# ll /wiz/app/wizserver/common/total 536-rw-r--r-- 1 root root8792 Dec 232020 alicloud_tools.jsc-rw-r--r-- 1 root root9984 Dec 232020 client_tools.jsc-rw-r--r-- 1 root root7024 Dec 232020 cookie_tools.jsc-rw-r--r-- 1 root root2352 Dec 232020 date_tools.jsc-rw-r--r-- 1 root root 27528 Dec 232020 db_tools.jsc......进一步分析发现,这是通过bytenode工具编译成了v8的字节码。在各种搜索之后发现,目前还没有相关的反汇编、反编译工具。唯一相关项目是GHIDRA的插件,但node版本和我们不匹配。

3

反汇编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原型: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来进行反汇编。BytecodeArray* SharedFunctionInfo::bytecode_array() const {DCHECK(HasBytecodeArray());return BytecodeArray::cast(function_data());}于是思路自然而然就有了:读取jsc,构造ScriptData对象反序列化,获取SharedFunctionInfo对象反汇编,通过bytecode_array获取BytecodeArray,并调用Disassemble反汇编

搭建v8编译环境

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.gitexport PATH=`pwd`/depot_tools:"$PATH"export HTTPS_PROXY=:18080# 设置代理gclient sync# 更新工具链fetch v8# 获取v8代码cd v8git 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# 编译d8v8_enable_disassembler和v8_enable_object_print一定要开启,否则反汇编时不显示常量内容。

修改d8代码(d8.cc)

在Shell类增加LoadJSC方法: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中添加代码: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: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)"Parameter count 1Frame size 80 E> 0x17c42dd2c522 @0 : 91StackCheck0 S> 0x17c42dd2c523 @1 : 6e 00 00 00 CreateClosure [0], [0], #0 0x17c42dd2c527 @5 : 1e fb Star r0115 S> 0x17c42dd2c529 @7 : 95ReturnConstant pool (size = 1)0x17c42dd2c531: [FixedArray] in OldSpace - map = 0x1b27f0f82309 <Map(HOLEY_ELEMENTS)> - length: 1 0: 0x17c42dd2c549 <SharedFunctionInfo>Handler Table (size = 16)Function nameParameter count 6Frame size 8 0x17c42dd2c742 @0 : 6e 00 00 02 CreateClosure [0], [0], #2 0x17c42dd2c746 @4 : 1e fb Star r0 10 E> 0x17c42dd2c748 @6 : 91StackCheck106 S> 0x17c42dd2c749 @7 : 4f fb 01CallUndefinedReceiver0 r0, [1] 0x17c42dd2c74c @ 10 : 04LdaUndefined113 S> 0x17c42dd2c74d @ 11 : 95ReturnConstant pool (size = 1)0x17c42dd2c751: [FixedArray] in OldSpace - map = 0x1b27f0f82309 <Map(HOLEY_ELEMENTS)> - length: 1 0: 0x17c42dd2c769 <SharedFunctionInfo xxx>Handler Table (size = 16)Function name xxxParameter count 1Frame size 24 74 E> 0x17c42dd2c862 @0 : 91StackCheck 82 S> 0x17c42dd2c863 @1 : 0a 00 02LdaGlobal [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 00CallProperty1 r0, r1, r2, [0] 0x17c42dd2c877 @ 21 : 04LdaUndefined104 S> 0x17c42dd2c878 @ 22 : 95ReturnConstant 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)

4

分析授权逻辑

前台获取授权信息的api接口

返回数据:{"returnCode": 200,"returnMessage": "OK","externCode": "","result": {"start": 36,"end": 36,"count": 5,"oem": "wiz","type": "license_free","version": 1,"key": "4bc0cc40-efbb-11e9-bef0-0faf68675f7c","ext": {}}}

定位接口对应的文件

根据上述url,?clientType=web&clientVersion=4.0&lang=zh-cn 结合后端的目录结构,首先对admin_router.jsc文件进行反汇编分析:# pwd/wiz/app/wizserver/as/admin# lltotal 128-rw-r--r-- 1 root root7408 Dec 232020 admin_file_utils.jsc-rw-r--r-- 1 root root 80720 Dec 232020 admin_router.jsc-rw-r--r-- 1 root root1680 Dec 232020 admin_static_files.jsc-rw-r--r-- 1 root root3384 Dec 232020 ldap_settings.jscdrr-xr-x 4 root root4096 Dec 232020 meta_files-rw-r--r-- 1 root root3304 Dec 232020 middleware.jsc-rw-r--r-- 1 root root 15240 Dec 232020 oem_utils.jsc-rw-r--r-- 1 root root6304 Dec 232020 wizbox_search.jscadmin_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的构造函数,来打印一下公钥。

pm2 restart as#pm2重启as服务,并访问网页授权页面tail -f /root/.pm2/logs/as-out.log #查看as日志

为了进一步验证,我们修改解密函数,打印返回值。

再次重启as服务,并访问网页授权页面。2021-06-29 18:15 +08:00: {"start":36,"end":36,"count":5,"oem":"wiz","type":"license_free","version":1,"key":"4bc0cc40-efbb-11e9-bef0-0faf68675f7c","ext":{}}2021-06-29 18:15 +08:00: eba7aab345f解密后的数据与网页接口返回的数据一致,证明猜测位置正确。

5

解除授权封印

通过上面的分析,思路就很清楚了。我们只要在特定的时候修改NodeRSA.prototype.decryptPublic的返回值可以控制授权。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;};最终结果:

 

看雪ID:HOWMP

https://bbs.pediy.com/user-home-353809.htm

  *本文由看雪论坛 HOWMP 原创,转载请注明来自看雪社区

# 往期推荐

1. angr学习(三)一道自己模仿着出的简单题和angr-ctf符号化输入相关题目

2. 记一次Word宏Downloader样本分析

3. XX之NTDLL随机化“逆向”(XP系统)

4. Frida分析违法应用Native层算法

5. 关于一次在pwnable.kr中input题目的经历(python3)

6. Pwn堆利用学习——Unsortedbin Attack——HITCON_Training_lab14_magicheap

ID:ikanxue官方微博:看雪安全商务合作:[email protected]

球分享

球点赞

球在看

点击“阅读原文”,了解更多!