漏洞
Ghostscript:一张 PDF 预览,怎样写穿内存
Wordfence 的测试 PDF 使 Ghostscript 向 256 字节的行缓冲写入 1,024 字节,研究者又在专建的 WordPress 上传站点中把这项错误发展成代码执行。风险随上传文件抵达系统转换程序,更新插件不会自动修补这条路径。

文章导航
网站收到一份 PDF,给它生成一张缩略图。用户看到的是附件封面,服务器却可能已经把文件交给另一个拥有文件读取和网络权限的程序。这项方便的小功能,把网页上传与系统内存连在了一起。
Wordfence 在 10 月 12 日公开的研究沿着这条路径找到 CVE-2026-39919。漏洞藏在 Ghostscript 处理 JPEG 2000 图像的代码里。研究者在专建的 WordPress 测试站点提供匿名上传入口,最终证明上传的 PDF 能让服务器执行代码。这个入口和环境属于该测试站点,默认 WordPress 并不向匿名访客开放媒体上传。
这件事值得网站维护者关注,是因为修补位置很容易找错。Ghostscript 通常由操作系统包或容器镜像带入,WordPress 与插件都更新了,它仍可能留在旧版本。真正需要查的是,外来文件会不会进入预览程序,以及正在运行的那个程序有没有包含修复。公开研究给出了足够清楚的内存错误,也展示了一个现实限制,能使程序崩溃的 PDF 与能接管某个环境的 PDF 之间,还有一段依赖具体构建的距离。
1 上传结束,文件处理才开始
Ghostscript 是解释 PostScript 和 PDF 的系统程序,可以把文档页变成图片。WordPress 生成 PDF 附件预览时,可能先通过 PHP 的 Imagick 扩展调用 ImageMagick,再由 ImageMagick 启动 Ghostscript。应用代码里看不到 gs,也可能正在间接使用它。
这条链需要几个条件同时成立。文件进入 WordPress 的附件处理,PDF 分支确实需要生成预览,图像编辑器选择 Imagick,ImageMagick 的策略允许读 PDF,最终使用的 Ghostscript 构建带有易受影响的 OpenJPEG 解码路径。直接储存附件但不做转换的入口,与自动预览的入口承担着不同风险。站点健康页面显示 Imagick 认识 PDF,只回答了格式识别的一步,仍需核对转换策略和实际程序包。
WordPress 7.1 的 wp_generate_attachment_metadata(),image.php 第 669–725 行,先确定要生成的图片尺寸,merged_sizes 非空时才调用 wp_get_image_editor(),随后保存 JPEG 预览并生成其他尺寸。研究者另外提供了匿名上传表单,才把陌生访客送来的文件接到这个核心功能上。因此,不能凭安装了 WordPress 就认定任何访客都能触发漏洞。插件、表单、业务 API 或已有账户分别决定谁可以把文件送到转换器。
转换器按服务器安排的进程身份工作。它可能能读到网站配置、数据库凭据和挂载目录,也可能获准连接内部服务。对攻击者而言,获得的是这些权限范围内的代码执行机会。转换服务若与主站分开运行,秘密不挂载进去,外联也限制到实际用途,后果会随可达资源缩小。隔离在限制后果,永久修复仍需替换那段错误的图像处理代码。
2 256 字节的容量,为什么写进 1,024 字节
JPEG 2000 把图像中的不同信息称为分量。灰度分量描述明暗,透明度分量描述像素是否透明。各分量可以有不同的分辨率,每个采样值也可以只占几个 bit。若每个值占 2 bit,一个字节就能放下四个值。这些格式能力本身是合法的。
在固定源码 sjpx_openjpeg.c 第 425–444 行,decode_image() 比较分量的水平和垂直采样间隔 dx、dy。间隔有差异时,state->samescale 变成 false,输出阶段便需要按各自间隔找到对应样本。Wordfence 那一字节的对照差异改变了透明度分量的分辨率,也改变了程序选择的路径。相同缩放的低精度分支本来用 bit_pos 累积位数,凑满一个字节再输出,出错的是后来那个需要重新取样的分支。
process_one_trunk() 第 570–581 行按 (width × out_numcomps × bpp + 7) >> 3 得到 row_size,再通过 gs_alloc_byte_array() 分配 row_data。Wordfence 的最小测试图像每行包含 1,024 个 2-bit 样本,打包后需要 1024 × 2 ÷ 8 = 256 字节。容量计算是正确的,填写缓冲的循环却把另一种单位带进来了。
错误落在 process_one_trunk() 的两层循环。Artifex 补丁中的原第 688–703 行让外层 i 从零走到图像宽度,每走一步输出一个字节。内层按 ppbyte1 = 8 / state->bpp 取样,2-bit 时取四次,却没有推进 i,于是同一位置的数据被取四次再打包。外层仍然执行 1,024 次,最终写入 1,024 字节,比行缓冲多出 768 字节。它把“样本位置”和“已输出字节数”混在了一个循环计数里。
修复后的第 691–707 行保留图像宽度作为样本终点,把 i++ 移进内层。每取一个样本就前进一步,取满四个才输出一个字节,1,024 个样本因此产生 256 个输出字节。末尾不足四个的情况仍要完成位移打包,但 i < state->width 会阻止额外读取。
// 旧循环的两个计数位置,来自固定补丁
for (i = 0; i < state->width; i++)
for (b = 0; b < ppbyte1; b++)
// 修复后的两个计数位置
for (i = 0; i < state->width; )
for (b = 0; b < ppbyte1; b++, i++)
// 取值前另检查 i < state->width
修补同时处理采样精度。decode_image() 第 509–514 行先沿用把 12-bit 数据扩到 16-bit 的兼容处理,再只接受 1、2、4、8、16 的内部精度。补丁说明提到一个 10-bit 回归文件,旧循环没有读出数据,修正计数后却会读得过多。新的精度检查让不支持的输入在进入输出循环前失败。这一道检查与计数修正各自解决具体问题。

越界写入之后,受损的是相邻内存中的数据。Wordfence 把这些数据追到了 Ghostscript 自己的内存管理结构,程序稍后清理对象时还会使用其中的地址。图像制作者可以影响溢出的内容,这才给代码执行留下机会。完整接管还取决于结构布局、可用地址与运行构建,单凭“越界了 768 字节”无法算出某台服务器的利用成功率。
研究者给出的危险 PDF 只有 1,029 字节,与无害对照文件只差一个描述透明度分量分辨率的字节。两份文件都可以是合法 PDF,内部也包含合法 JPEG 2000 图像。扩展名、MIME 类型和一般文件结构检查能检查它是不是 PDF,无法替转换器判断这组图像参数会不会走入错误分支。
3 能崩溃,与能接管,是两项结果
Wordfence 在 9 月 15 日测试的十个 Linux 包环境都存在该错误。这是一个有明确日期的历史样本,不能拿来描述今天十套系统仍未修补。更有意义的是随后做的交叉构建研究,同一个根因在不同发行包里成立,已经做成的两份利用文档却没有自动适应其他程序布局。
研究者把两份利用文档用于十个其他包环境,累计 10,240 次跨构建尝试没有得到代码执行。这限制了那两份文档的直接可移植性。另一次研究保留同一个 Ghostscript 可执行文件,改变字体、语言设置、字体配置及一个核心系统库。在这四种设置中,匹配该构建的利用都曾成功,不过仍有尝试失败。周围环境变了,处理文档的二进制仍相同,共用基础镜像的主机群因此值得优先检查。
修补应优先覆盖处理外来文档的转换入口,调查中则要保留实际可执行文件和包身份。转换失败时,先看对应 Ghostscript 进程是否崩溃,再检查有没有异常子进程与外联。崩溃记录说明处理过程出了错,后两项记录才继续回答错误之后有没有发生其他执行和连接。网站返回上传成功,也可能掩盖后台转换的失败。若历史记录已出现可疑转换或后续执行,应保留文件、时间、父子进程和网络记录,再按转换进程能访问的范围检查和处理凭据。
Wordfence 的公开研究还解释了失败为何可能带来第二次处理。其展示的 WordPress Imagick PDF 加载流程先尝试裁剪设置,遇到异常后换一个设置重试。研究者实测无害 PDF 对应一次 Ghostscript 启动,崩溃 PDF 对应两次,三份附件带来六个进程。这些数字属于其测试环境。限制每次上传文件数会减少处理机会,却没有修正内存写入。
-dSAFER 限制文档通过正常解释器功能访问资源。这里的错误发生在 C 代码操作内存时,Wordfence 的演示在启用该选项后仍触发漏洞。继续保留沙箱选项有价值,修复这次越界仍需更新程序。Wordfence 在 10 月 12 日发表的研究中表示,尚未找到该漏洞正遭利用的公开证据。
4 要更新的是实际处理文档的程序
Wordfence 将上游易受影响范围明确为 9.21–10.07.1,条件是构建包含相应 OpenJPEG 解码器。负责发布该漏洞记录的 VulnCheck 使用更宽的 before 10.08.0 描述范围,两者粒度不同。Artifex 在 9 月 8 日发布的正式 10.08.0包含修复,也是 10 月 14 日官方发行页的最新正式版。研究里未修的 10.08.0rc1 曾显示相同的基础版本号,因此不能只抄一行简短版本输出就完成验收。
发行版会把修补回到自己的旧版本。应根据所属发行版的安全公告核对完整包号,不能要求所有机器的上游版本都达到 10.08.0。截至 10 月 14 日,Debian 的当前表列出 trixie security 的 10.05.1~dfsg-1+deb13u3 为 fixed,历史首修是 u2。bookworm 的 10.0.0~dfsg-11+deb12u8仍标为 vulnerable。这说明发行分支比一个裸版本数字更有用。可用下面的只读查询取得完整包身份,再与所属分支公告比对。
# Debian / Ubuntu
dpkg-query -W -f='${Package} ${Version}\n' ghostscript
# Fedora / Red Hat 系列
rpm -q ghostscript
Ubuntu、Red Hat 和 SUSE 同样有自己的修复包表,安装动作应跟着具体系统执行。直接使用上游 Ghostscript 的环境,先确认拿到正式修复制品,再核对调用程序实际使用的路径。容器则要重建并重新部署,旧容器继续使用旧层,新基础镜像不会倒过来替换已经运行的进程。共享镜像的站点还应核对各副本,避免只更新测试容器。最后用正常文件走完原来的上传与预览流程,确认业务调用确实进入新程序,无需把危险 PDF 投向生产。
暂时无法更新时,最直接的临时动作是让不可信 PDF 到不了转换器。可以暂停外部 PDF 上传,或在 ImageMagick 安全策略中禁止 PDF 读取。前者会影响文档提交,后者会让相应预览停止生成,需要按实际业务选一种并验证。只在前台隐藏上传按钮会留下 API 与队列入口,限速也只是减少尝试次数。升级失败期间应继续保持转换入口受限,不能为恢复缩略图而回到未修程序。