某医院小程序接口逆向分析
最近使用某医院小程序时,发现只能查询最近半年的检查报告。那么有没有可能通过修改请求参数,获取到更早时间的检查报告呢?
环境&工具
- WeChat 3.8.9
- Proxifier
- Brup Suite
pip install pycryptodome
- Node.js
1
2
3
4node -v
v18.17.0
npm -v
10.9.1
配置Brup Suite
- Proxy -> Proxy Settings -> Import/export CA certificate -> Certificate in DER format,选择CA文件的路径并命名为
xxx.crt
- 双击生成的CA文件,导入钥匙串
- 双击
PortSwiger CA
,信任设置为"始终信任"
配置Proxifier
- 配置Proxy: 依次点击
Rules
->Add
,地址为127.0.0.1
,端口为Brup Suite
对应的端口 - 配置Rule: 依次点击
Rules
->Add
->+
,选择WeChatAppEx Helper.app
快捷键command+shift+G搜索
/Applications/WeChat.app/Contents/MacOS/WeChatAppEx.app/Contents/Frameworks/WeChatAppEx Framework.framework/
,点击Helpers
- 打开Brup Suite和WeChat小程序,此时已经可以看到小程序发出的网络请求
小程序反编译
- 进入目录
~/Library/Containers/com.tencent.xinWeChat/Data/.wxapplet/packages/
,会看到一堆命名为wx开头的{AppID}
的文件夹,找到对应🏥小程序所在的目录- 可删除所有wx开头的
{AppID}
文件夹后,重新打开🏥小程序 - 也可在移动端
Wechat
->开发者资料->AppID
中直接获取AppID
- 可删除所有wx开头的
- 进入
{AppID}
所在目录,找到名为__APP__.wxapkg
的文件 - 准备
wxappUnpacker
环境1
2
3
4
5
6
7
8
9git clone https://gitee.com/ksd/wxappUnpacker.git
cd wxappUnpacker
npm install
npm install esprima
npm install css-tree
npm install cssbeautify
npm install vm2
npm install uglify-es
npm install js-beautify - 执行反编译,得到小程序源码,可能会出现
TypeError: subPackage.pages is not iterabl
的报错,忽略即可1
./bingo.sh ../__APP__.wxapkg
代码审计
- 比较相邻2次请求,发现除请求参数外,只有
X-Api-Key
和Request-No
不同 Request-No
盲猜是请求序号,直接在app-server.js
中搜索X-Api-Key
- 找到
a
对应的n(465)
函数,发现最终调用了i.encrypt
方法,p
应该是用来计算hashDigest
- 分析
p(e, t)
函数,f
数组由以下几部分组成:
pathname + query
i.REQUEST_HASH_HEADERS
中的header按照{key}={value}
的格式用,
连接PUT
和POST
请求的bodyJSON.stringify
后的字符串过滤掉数组中的空字符串,用
&&
连接剩余部分,然后计算MD5值
5. 找到i
对应的n(392)
函数,发现被混淆了;
将n(392)
函数的代码拷贝至n392.js
,模拟混淆过程,用正则批量替换得到反混淆后的n391_decrypt.js
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
32var n = i => {
return e[i - 224];
};
var r = e;
for (let index = 0; index < e.length; index++) {
try {
if (
773152 == -parseInt(n(243)) / 1 +
(parseInt(n(254)) / 2) * (-parseInt(n(250)) / 3) +
(-parseInt(n(226)) / 4) * (-parseInt(n(237)) / 5) +
parseInt(n(252)) / 6 + parseInt(n(231)) / 7 +
(-parseInt(n(227)) / 8) * (-parseInt(n(248)) / 9) +
(parseInt(n(235)) / 10) * (-parseInt(n(258)) / 11)
)
break;
r.push(r.shift());
} catch (e) {
r.push(r.shift());
}
}
const fs = require('fs');
let data = fs.readFileSync('n392.js', 'utf8');
for (let [index, item] of e.entries()) {
data = data.replace(new RegExp(`r\\(${224 + index}\\)|a\\(${224 + index}\\)`, 'g'), `"${item}"`);
}
console.log(data);
fs.writeFileSync('n392_decrypted.js', data);
- 分析反混淆后的
n392_decrypt.js
,得到AES加密用的KEY
和IV
、计算MD5用的REQUEST_HASH_HEADERS
DEFAULT_ENCRYPT_KEY = "
" DEFAULT_ENCRYPT_IV = "
" REQUEST_HASH_HEADERS = ["X-User-Id", "X-Hos-Id", "X-Auth-Token', "X-Api-Ver"]
尝试解密一个
X-Api-Key
的值,得到被AES加密前的数据1
2
3
4
5
6
7
8
9
10
11from Crypto.Cipher import AES
from base64 import b64decode, b64encode
from Crypto.Util.Padding import pad, unpad
DEFAULT_ENCRYPT_KEY = b"<DEFAULT_ENCRYPT_KEY>"
DEFAULT_ENCRYPT_IV = b"<DEFAULT_ENCRYPT_IV>"
encrypted_b64 = "<X-Api-Key>"
cipher = AES.new(DEFAULT_ENCRYPT_KEY, AES.MODE_CBC, iv=DEFAULT_ENCRYPT_IV)
decrypted = unpad(cipher.decrypt(b64decode(encrypted_b64)), AES.block_size).decode("utf-8")json.loads(decrypted)
后的数据:1
2
3
4
5
6{
"accessEntry": "local-patient-miniapp",
"timestamp": 1735565655493,
"replayNo": "4a43104a-5b76-4e21-9e7f-dee01c45b648",
"hashDigest": "91e260d5bf94fc1eed38769b6cc71cd6",
}
- 综上,如果想要获取其他时间范围的检查报告,只需修改查询参数后,按照新的查询参数重新计算MD5值,替换被签名的原始数据中的
hashDigest
后,重新AES加密计算新的<X-Api-Key>
即可1
2
3
4data["hashDigest"] = "<NEW MD5>"
plain = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
encrypted = cipher.encrypt(pad(plain.encode("utf-8"), AES.block_size))
encrypted_b64 = b64encode(encrypted).decode("utf-8") - 参照
p(e, t)
和REQUEST_HASH_HEADERS
计算MD5值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import hashlib
def calculate_md5(input_string):
md5_hash = hashlib.md5()
md5_hash.update(input_string.encode("utf-8"))
return md5_hash.hexdigest()
headers = {
"X-User-Id": 12345678,
"X-Hos-Id": 12345678,
"X-Auth-Token": '<X-Auth-Token>',
"X-Api-Ver": "2.26.7",
}
lst = list()
lst.append(
"/patient/v1/report/record?inspectType=UC&reportType=3&cardNos=xxxxx&startTime=2024-12-24&endTime=2024-12-31"
)
lst.append(",".join([f"{k.lower()}={v}" for k, v in headers.items()]))
string_to_sign = "&&".join(lst)
md5 = calculate_md5(string_to_sign)
修改请求
- 打开
BrupSuite
的Proxy Intercept
开关,以拦截小程序的请求 - 打开🏥小程序,点击"查报告"
- 从
Brup Suite
中复制Path+Query
、REQUEST_HASH_HEADERS
、<X-Auth-Token>
和<X-Api-Key>
对应的值 - 执行Python脚本计算新的
<X-Api-Key>
,然后更新Brup Suite
中的请求参数和X-Api-Key
,点击Forward
参考
完整代码
1 | import hashlib |