CVE-2025-25185 深度解析:GPT Academic 软链接解析缺陷导致任意文件读取
摘要在处理用户上传的归档文件时,安全校验的边界一旦出现偏差,就可能引发灾难性的后果。本文将深入分析开源的学术优化工具 GPT Academic(受影响版本 ≤ 3.91)中存在的任意文件读取漏洞(CV 2026-11-11 12:26:24 Author: xz.aliyun.com(查看原文) 阅读量:0 收藏

摘要

在处理用户上传的归档文件时,安全校验的边界一旦出现偏差,就可能引发灾难性的后果。本文将深入分析开源的学术优化工具 GPT Academic(受影响版本 ≤ 3.91)中存在的任意文件读取漏洞(CVE-2025-25185)。文章依托真实的漏洞复现截图,从核心源码的逐行拆解、恶意 Payload 的底层构造,到 Web 交互与文件读取的最终触发,完整还原了从代码审计到实战利用的全生命周期。本文旨在为安全从业者提供一种关于压缩包解压场景的深度思考与防御思路。

一、 漏洞背景与攻击面概述

现代 Web 应用中,文件上传与处理是极为常见的业务需求。GPT Academic 作为一款深受科研人员喜爱的 LLM 辅助工具,提供了“上传论文/代码压缩包进行智能解析”的功能。为了支持这一功能,开发者必须实现服务端的解压逻辑。

CVE-2025-25185 的核心正是出在这个解压环节。在 Linux 与 Windows 操作系统中,除了常规的文本文件和可执行文件,还存在一种特殊的文件类型——符号链接(Symbolic Link,简称软链接)。软链接本质上是一个指向其他文件或目录的“快捷方式”。

由于项目在处理 .tar.gz 压缩包时,底层调用了 Python 的 tarfile 模块,且未对解压成员的文件属性进行安全过滤,导致攻击者可以向服务器植入一个指向系统敏感文件(如 win.ini、/etc/passwd 或 .env 等)的软链接。当 Web 服务后续通过路由访问该解压目录时,由于操作系统的底层文件系统解析机制,服务端会跟随这个软链接,将目标敏感文件的真实内容直接回显给攻击者,造成严重的信息泄露。

  • 漏洞名称:GPT Academic 符号链接跟随导致任意文件读取

  • CVE 编号:CVE-2025-25185

  • 漏洞类型:CWE-59(链接解析不当)

  • 影响版本:GPT Academic 3.91 及以下

  • CVSS 3.1 评分:7.5(High)

二、 漏洞现场:探寻深藏的“伪防御”

要彻底理解漏洞,必须回归代码。图1展示了该项目存在漏洞的核心源码,这也是我们整个代码审计工作的起点。

with tarfile.open(file_path, "r:*") as tarobj:
    # 清理提取路径,移除任何不安全的元素
    for member in tarobj.getmembers():
        member_path = os.path.normpath(member.name)
        full_path = os.path.join(dest_dir, member_path)
        full_path = os.path.abspath(full_path)
        if not full_path.startswith(os.path.abspath(dest_dir) + os.sep):
            raise Exception(f"Attempted Path Traversal in {member.name}")

    tarobj.extractall(path=dest_dir)
    logger.info(f"Successfully extracted tar archive to {dest_dir}")

2.1 逐行代码审计与“善意”的防御

在截图中的 109 到 115 行,我们能看出开发者其实是具备一定安全意识的。这段代码试图防御经典的 路径穿越漏洞(Zip-Slip):

  • os.path.normpath(member.name):开发者遍历了压缩包内的所有成员,并试图规范化文件名,消除 ../ 这类危险字符。

  • full_path = os.path.abspath(full_path):计算出最终解压的绝对物理路径。

  • if not full_path.startswith(...):这是一个经典的防御策略。它判断解压后文件的物理路径,是否依然以预设的安全解压目录 dest_dir 开头。如果不是,就抛出异常阻断攻击。

如果攻击者试图在压缩包内放入名为 ../../../../Windows/System32/evil.txt 的文件,normpath 和 abspath 计算后,其路径就会脱离 dest_dir 的控制范围,从而被 startswith 逻辑拦截。这套针对文件名和路径的校验逻辑本身是严密且合理的。

2.2 破绽在哪?被忽视的“文件类型”

这套看似完美的防御,恰恰暴露了最大的盲区。开发者只校验了文件的“名字”,却忽略了文件的“物理属性(类型)”。

在 118 行,代码安全校验通过后,直接调用了 tarobj.extractall(path=dest_dir)。在 Python 的 tarfile 模块中,extractall 方法极为强大,它不但会解压普通文件,还会忠实地按照 Tar 归档包内的元数据,为 符号链接(Symlink) 和 硬链接(Hardlink) 创建映射。

攻击者的思路由此豁然开朗:既然无法通过命名(如 ../../)逃逸目录,那不如在安全目录内放置一个“特洛伊木马”软链接。比如,这个软链接名字就叫 hacked.txt,它的路径完全合法,能轻松通过所有的 startswith 校验;但是,这个 hacked.txt 的物理指向,却是系统深处的 C:\Windows\win.ini。当后续业务读取 hacked.txt 时,操作系统顺藤摸瓜,就把 win.ini 的内容交了出来。

这就是典型的逻辑漏洞。开发者在路径层面上了锁,却没有发现攻击者直接在屋里挖了个地道。

三、 靶场环境确认与攻击准备

在进行漏洞复现之前,必须确保靶场的版本处于“脆弱状态”。图2展示了靶场的运行环境。


页面顶端赫然写着 “GPT 学术优化 3.91”,这是存在该漏洞的核心版本。界面正常渲染,右侧展示了丰富的基础功能与函数插件区,上方有“上传文件”的选项卡。这与真实的业务场景完全吻合,确保了漏洞利用链的完整性。在这个界面之下,承载着上述漏洞代码的 Web 后端正在静默运行。

为了触发漏洞,我们需要构造一个恶意的 .tar.gz 压缩包。在常规的 Windows 环境下,普通用户权限无法直接使用 mklink 命令创建软链接,直接通过 7-Zip 等可视化压缩软件也无法直接打包软链接结构。

因此,必须从代码层面直接操作数据结构,编写专用的 PoC 生成脚本。

四、 构造“数字特洛伊木马”:Payload 深剖

图3展示了通过命令行执行 Python 脚本生成恶意压缩包的全过程。

D:\gpt_academic>python poc.py
正在生成 payload.tar.gz ...
✅ 恶意压缩包 payload.tar.gz 生成成功!可以去上传了。

看似简单的一行输出,背后却蕴含着精妙的底层逻辑。这个恶意包是如何欺骗系统的?其生成的核心原理如下:

import tarfile

target_file = r"C:\Windows\win.ini" 
link_name = "hacked.txt" 

with tarfile.open("payload.tar.gz", "w:gz") as tar:
    info = tarfile.TarInfo(link_name) # 创建一个归档成员对象
    info.type = tarfile.SYMTYPE       # 【核心】声明该文件为符号链接
    info.linkname = target_file       # 【核心】设置该链接指向的真实物理路径
    tar.addfile(info)                 # 写入压缩包
  • 构建 TarInfo 对象:正常的压缩软件写入的是文件的二进制流,而我们构建的是一个“描述信息”。我们指定了文件的名称 hacked.txt。

  • 篡改文件头标识:在 Tar 文件的二进制格式中,每个文件头都有一个类型标识位(Typeflag)。info.type = tarfile.SYMTYPE 的作用是强行将这一位改为代表“符号链接”的字符(通常是 2)。

  • 植入目标指针:info.linkname 将这个链接的目标锁死为 C:\Windows\win.ini。

生成的 payload.tar.gz 文件大小仅为 132 字节。因为在这个压缩包内部,根本没有任何实质性的数据内容,它仅仅包含了一条极度精简的文件头指令:“请在这里创建一个名叫 hacked.txt 的快捷方式,指向 win.ini。”

随后,通过 tarfile 库将这个带有恶意标识的包头重新打包成 gz 格式,一个致命的数字特洛伊木马便诞生了。

五、 突破边界:上传与解析的化学作用

有了武器,下一步就是投递。图4展现了网页端上传恶意压缩包后的回显结果。


我们可以看到,在上传了 payload.tar.gz(大小 132.0 B)后,界面下方的 Local Message 返回了文件的处理状态:

收到以下文件(上传到路径:private_upload\default_user\2026-11-11-16-23-00):
文件:
private_upload\default_user\2026-11-11-16-23-00\payload.tar.gz
private_upload\default_user\2026-11-11-16-23-00\payload.tar.gz.extract
private_upload\default_user\2026-11-11-16-23-00\payload.tar.gz.extract\hacked.txt

这一张图,完美闭环了我们的代码审计逻辑:

  • 自动解压触发:服务端接收到压缩包后,果然调用了我们在图1中分析的解压函数。它为目标文件分配了以当前时间戳(2026-11-11-16-23-00)命名的安全目录 private_upload\default_user\。

  • 校验绕过成功:解压函数开始遍历压缩包,读取到了 hacked.txt 这个成员。由于名字合法,图1中的 abspath 与 startswith 校验一路绿灯。

  • 幽灵文件落地:最关键的一步发生在一瞬间。服务端底层执行 extractall,操作系统在 private_upload...extract 目录下,忠实创建了一个指向 C:\Windows\win.ini 的软链接,并命名为 hacked.txt。

从回显上看,服务器天真地以为这只是个普通的文本文件,并热情地把它的绝对路径暴露在了 Web 页面上。攻击所需的最后一块拼图就位,直指系统核心。

六、 收割战果:任意文件读取成功

最后一步,触发读取。由于 GPT Academic 在处理文件路径时有自己的路由设计,通常使用 /file= 参数来定位静态资源。


在浏览器的地址栏输入构造好的请求:
http://localhost:44156/file=private_upload/default_user/2026-11-11-16-23-00/payload.tar.gz.extract/hacked.txt

回车后,没有任何阻拦,浏览器直接渲染出了如下内容:

; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
[Mail]
MAPI=1

致命一击完成!
这正是 Windows 操作系统历史遗留的核心配置文件 C:\Windows\win.ini 的真容。攻击者通过 Web 服务越权访问了本地操作系统的文件。

在底层,究竟发生了什么?
当 Web 框架接收到 /file=... 的请求时,程序拿着 hacked.txt 的相对路径去定位文件。操作系统在 open 这个文件时,发现它不是一个真实的文件(没有 inode 数据块),而是一个被标记为 SYMTYPE 的符号链接。根据文件系统协议,操作系统自动解析了这个链接,把读取请求重定向到了 C:\Windows\win.ini。随后,Python 的底层 IO 流将 win.ini 的字节流读取出来,由 Web 框架原封不动地作为 HTTP 响应体返回给了浏览器。

如果在这里将 win.ini 替换为诸如 .env 数据库配置、config_private.py(OpenAI API Key)、甚至是 SSH 私钥或 Linux 下的 /etc/passwd,后果将不堪设想,攻击者可以借此跳板直接接管服务器。

七、 深度防御:如何封堵链接解析漏洞

此漏洞的成因并非底层逻辑的严重错误,而是因为防御边界的缺失。开发者防住了“路径穿越”,却没有防住“链接引用”。对于此类涉及压缩包解压的操作,必须遵循“零信任”原则,施加严苛的白名单校验。

7.1 核心代码修复思路

在调用 tarobj.extractall() 之前,必须对每一个 member 进行多维度的属性检查。正确的防御代码应当如下:

import os
import tarfile

def safe_extract_tar(tar_path, dest_dir):
    with tarfile.open(tar_path, "r:*") as tar:
        for member in tar.getmembers():
            # 1. 拦截软链接 (Symlink) 与 硬链接 (Hardlink)
            # 这是修复 CVE-2025-25185 的致命一击
            if member.issym() or member.islnk():
                raise Exception(f"Security Alert: Link detected in {member.name}")
            
            # 2. 依然保留原有的路径穿越校验,防范 Zip-Slip
            member_path = os.path.normpath(member.name)
            full_path = os.path.abspath(os.path.join(dest_dir, member_path))
            if not full_path.startswith(os.path.abspath(dest_dir) + os.sep):
                raise Exception(f"Security Alert: Path traversal in {member.name}")
            
            # 3. 限制解压后文件的大小,防压缩炸弹(Zip Bomb)
            if member.size > 100 * 1024 * 1024: # 限制单文件 100MB
                raise Exception(f"Security Alert: File too large: {member.name}")
                
        # 校验全部通过,才允许真正解压
        tar.extractall(path=dest_dir)

(注:Python 3.12+ 版本在原生 tarfile 库中引入了 filter="data" 参数,可自动剥离高危属性,建议升级使用。)

7.2 架构与运维层的纵深防御

仅靠代码层的修补是不够的,在企业级架构中,应当构建纵深防御体系:

  • 沙箱化隔离:业务解压的目录应通过 Docker Volume 挂载等方式进行隔离,确保该目录的底层用户权限极低,即使解压出软链接,也无法读取如 /etc/shadow 等 root 权限的系统文件。

  • 权限控制:不要使用 root 用户运行 Python Web 服务,遵循最小权限原则。针对 Web 框架中直接通过 HTTP 返回文件内容的路径(如 /file=),必须使用真实的路径剥离逻辑(如 os.path.realpath 解析后再做白名单比对),防止跟随任何系统符号链接。

  • 文件清理:临时上传与解压的文件应设定极短的生命周期,定时脚本清理,防止攻击者将服务器变为文件驻留的跳板。

八、 总结

通过对本次 CVE-2025-25185 的完整审计与复现,我们清晰地看到了由于代码中对“文件属性”校验的疏漏所引发的连锁反应。图1至图5的复盘,不仅展示了将一条死路走活的攻击思路,更是对每一位安全研发人员的警示:

在处理不受信任的输入(尤其是复杂的归档文件格式)时,永远不要信任任何文件表面上的温顺。对类型、路径、大小进行严苛的白名单校验,才是构建安全代码的唯一途径。


文章来源: https://xz.aliyun.com/news/93253
如有侵权请联系:admin#unsafe.sh