一次微信小程序云开发数据库的渗透测试

前言

某日在一个小程序闲逛时,发现有一些请求抓不到,这引起了我的注意,经过一番研究后发现这是微信提供的云开发能力,包括云函数云数据库等,本文主要是对云数据库的一个渗透尝试记录,微信小程序渗透基础可以参考这篇微信小程序的渗透五脉,写的很好

微信抓包

Root环境就不说了,我给出一套自己使用的工具组,能解决大部分抓包问题

TrustMeAlready(1.11) + mitmweb(9.0) + MITMProxy cert

这组工具解决了三个问题:

  1. 单向证书认证
  2. 抓包数据可视化
  3. Android6以上应用不再信任用户证书

其中MITMProxy cert可以在启动mitmweb后,手机配置代理后通过mitm.it下载到Magisk模块,直接刷入即可

小程序代码包获取

这时我们已经有了微信的部分抓包能力,这时可以通过在微信里把目标小程序长按-拖动-删除,来删掉这个小程序的缓存,然后就可以再打开目标小程序抓包了,你会发现抓到了一个包含res.servicewechat.com/weapp/release_encrypt/的链接。当然,也可能是多个,比如小程序分包和微信小程序运行环境基础包

image-20230531161548731.png

把这个链接复制一下,下载下来就是小程序代码的主包了,但还要经过一些处理,比如图中这个就是zstd格式的,要先解压一下,它就是wxapkg格式了,然后用unveilr解包就可以了

image-20230531170336348.png

wx.cloud.database() 的出现

在解包的代码中有一段代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var s = n(56),
o = n(5);
"wx" === global.mpvuePlatform &&
wx.cloud.init({ env: "xxxxxxxxxx", traceUser: !0 });
var i = wx.cloud.database().collection("checkUser");

checkUser: function () {
var t = this;
i.where({ userId: this.userInfo.id })
.get()
.then(function (e) {
e.data.length > 0 && (t.isCheckUser = !0);
});
},

一眼看上去就是类mongodb的collection以及它的where查询语句

一眼我就觉得它可能有问题。众所周知,微信小程序是前端代码,前端中直接查数据库是非常危险的,一般这种情况下都伴随着前端用用户名密码直连数据库,但这里微信包了一层,通过小程序appid绑定的env来在小程序运行环境中使用一些变量,同时微信限制了env的使用权限,只有绑定了该appid的微信账号才能使用对应的env,看起来比前端直连数据库好了很多。但是,我还是觉得有越权查询的风险,当小程序执行 i.where({userId:1}) 时,是不是查询到的就是用户1的信息?接下来就是验证这个想法的过程

wx.cloud.database() 安全性官方介绍

官方文档可以参考 权限控制|微信开放文档

总结一下就是:

  1. 微信早期有一个简易权限配置,其有以下几个特点

    1. 在小程序中创建的每个数据库记录都会带有该记录创建者(即小程序用户)的信息,以 _openid 字段保存用户的 openid 在每个相应用户创建的记录中

    2. 以下按照权限级别从宽到紧排列如下:

      1. 仅创建者可写,所有人可读:数据只有创建者可写、所有人可读;比如文章。
      2. 仅创建者可读写:数据只有创建者可读写,其他用户不可读写;比如用私密相册。
      3. 仅管理端可写,所有人可读:该数据只有管理端可写,所有人可读;如商品信息。
      4. 仅管理端可读写:该数据只有管理端可读写;如后台用的不暴露的数据。
  2. 微信开发者工具 1.02.1911252 起支持配置安全规则,其在简易权限配置的基础上,进一步精细化的控制权限:

    1. 灵活自定义集合记录的读写权限:获得比基础的四种基础权限设置更灵活、强大的读写权限控制,让读写权限控制不再强制依赖于 _openid 字段和用户 openid

    2. 防止越权访问和越权更新:用户只能获取通过安全规则限制的用户所能获取的内容,越权获取数据将被拒绝

    3. 限制新建数据的内容:让新建数据必须符合规则,如可以要求权限标记字段必须为用户 openid

    4. 比如定义一个读写访问规则是 auth.openid == doc._openid,则表示访问时的查询条件(doc)的 openid 必须等于当前用户的 openid (由系统赋值的不可篡改的 auth.openid 给出),如果查询条件没有包含这项,则表示尝试越权访问 _openid 字段不等于自身的记录,会被后台拒绝访问。

    5. 在新的安全规则体系下,要求显式传入 openid,假如以上述checkUser函数为例子,应用了新安全规则时,该段代码应该改为

      1
      2
      3
      4
      5
      6
      7
      checkUser: function () {
      var t = this;
      i.where({ userId: this.userInfo.id, _openid: '{openid}'})
      .get()
      .then(function (e) {
      e.data.length > 0 && (t.isCheckUser = !0);
      });

通过官方的文档介绍,我们可以看出来目标小程序应用的还是简易权限配置。同时也知道了微信官方对这个前端可以调用的云开发数据库是有安全防范的,本身的安全性没有问题,那么安全问题就有可能出在开发者身上,比如设置了错误的权限管理规则

如何实现 i.where({userId:1})?

首先我尝试了微信官方的微信开发者工具,想通过更改为目标小程序的appid,直接init目标的env,然后写段代码直接调用wx.cloud.database,但不出意外的失败了

然后我打算从微信小程序本身入手,既然我已经有了源代码,那么我可以直接更改这段查询代码,比如改成这样

1
2
3
4
5
6
7
8
9
10
11
12
checkUser: function () {
var t = this;
i.where({ userId: 1})
.get()
.then(function (e) {
wx.showToast({
title: Json.stringify(e.data),
icon: "none",
duration: 10000
})
e.data.length > 0 && (t.isCheckUser = !0);
});

这样当小程序执行到这个函数的时候,就会去查UserId为1的数据,然后把结果通过wx.showToast显示出来。为了实现这个目标,我需要把代码重打包,然后替换掉目标小程序的原始包

代码重打包与包体替换

代码重打包很简单,用微信开发者工具直接导入解包出的代码,然后登录自己的微信账号,修改好代码,点预览进行编译运行即可,小程序会在手机微信上自动打开。需要注意的是编译有可能会不通过,一般是因为少了代码,可能这个小程序还有分包,需要把分包也下载下来再手动合并进主包目录中

包体替换比较复杂,我尝试了两种方式,但都失败了

  1. 新编译的包在手机上运行后,去手机的小程序缓存目录(/data/data/com.tencent.mm/MicroMsg/appbrand/pkg/general/)下会看到它的wxapkg包,把这个包重命名替换目标小程序的原始缓存包-> 微信启动小程序时会校验本地缓存的小程序包的hash,当发现被替换后会再去下载一次原版包并覆盖
  2. zstd压缩新编译的包,用抓包软件替换对应下载链接的返回内容,让微信下载下来的就是我编译过的包-> 用微信开发者工具生成的包是debug包,同时zstd的压缩好像也不太对,微信并不认这个包,替换返回内容后微信会报小程序运行环境异常的错误

这两种方式都有改进的思路,比如方式一可以尝试干掉微信的校验,方式二可以去上架一个正式环境的小程序再抓包替换试试

这些方式都太麻烦了,于是我找到了一个新思路:在微信校验小程序的hash后,打开小程序前这个时间点,新包替换原包。命令如下:

1
/data/data/com.termux/files/usr/bin/inotifywait -m -e open /data/data/com.tencent.mm/MicroMsg/appbrand/pkg/general/_1718118489_117.wxapkg | awk '{if(++count==2) system("cp /data/local/tmp/11.wxapkg /data/data/com.tencent.mm/MicroMsg/appbrand/pkg/general/_1718118489_117.wxapkg");}'

当inotify监听到open操作被执行到第二次时就会去替换这个包,可能因为命令执行时间的问题有时没成功,但一般最多试两三次就可以了

代码注入效果

image-20230601162407168.png

这时已经可以看到我写的这段代码已经生效了,但wx.showToast显示的内容有限

1
2
3
4
5
wx.showToast({
title: Json.stringify(e.data),
icon: "none",
duration: 10000
})

数据外带方案

本来试了试能不能嵌入一个vConsole,把数据打到console里,但vConsole嵌入后小程序直接白屏了。。。

2023-06-08更新:

  • r3x5ur大佬提供了一种方法,可以打开小程序自带的vConsole

换了个方法把数据带出来:

  1. 本地启动一个WebServer,把POST的数据打印出来
  2. 小程序内使用wx.request函数把数据POST到WebServer上

image-20230601162300958.png

最终效果如下

image-20230601162011321.png

总结

大致过程就是这样了,这个测试的方法实操起来很复杂,但这次测试也算有所收获,希望这篇文章能抛砖引玉,期待大佬们能找到更方便的测试方法