CVE-2018-6616
本文将介绍CVE-2018-6616这一漏洞的背景、原理和复现方式,仅为个人学习笔记,供大家学习参考。
申明:本工作是A.S.E (AI Code Generation Security Evaluation)开源项目的一部分,很荣幸能作为contributor参与这一开源项目,为大模型的安全评估做出贡献;
笔记汇总在CVE_Binary_Reproduction。
漏洞卡片
| 字段 | 内容 |
|---|---|
| CVE-ID | CVE-2018-6616 |
| CWE-ID | CWE-400 : Uncontrolled Resource Consumption |
| NVD公开日期 | 2018-02-04 |
| 评分 | 5.5 MEDIUM (CVSS v3) |
| 影响组件 | OpenJPEG (openjp2) ≤ 2.2.0 |
| 受影响模块 | openjp2(JPEG2000 编码器) |
| 漏洞类型 | 声明尺寸伪造 → 算法级DoS |
| 利用后果 | 远程代码执行 / DoS |
| 补丁 Commit | 8ee335227bbcaf1614124046aa25e53d67b11ec3 (openjpeg) |
背景介绍
- OpenJPEG(openjp2)是 JPEG 2000(ISO/IEC 15444)的一种开源实现,包含编解码器与命令行工具(opj_compress、opj_decompress 等),广泛用于图像查看器、PDF 渲染器与医疗影像等领域。
- 2018 年 2 月,ProbeFuzzer 在 2.3.0 及 master 分支发现:用一个仅 144 字节的 BMP 即可让
opj_compressCPU 100% 超过 15 分钟,该 issue 被分配 CVE-2018-6616,CVSS 3.x 5.5(MEDIUM),CWE-400(Uncontrolled Resource Consumption)。 - 漏洞根因是 BMP 解析器未校验“头声明尺寸 vs. 实际像素数据”,导致后续 JPEG 2000 编码核心
opj_t1_encode_cblks按恶意宽高分配 precinct/code-block,迭代次数爆炸,形成算法级拒绝服务。
漏洞原理分析
- 触发点(观察到的行为)
- opj_compress 在处理仅 144 字节的 BMP 时 CPU 单核 100%,持续 15 min 以上;gdb 堆栈永远停在
opj_t1_enc_sigpass_step→opj_t1_encode_cblks的 5 层 for-loop 内。
- opj_compress 在处理仅 144 字节的 BMP 时 CPU 单核 100%,持续 15 min 以上;gdb 堆栈永远停在
- 根本原因(两类问题共同导致)
- BMP 解析器未校验“实际像素数据量 vs. 头声明宽高”:
bmp_read_rle8_data对恶意声明的 16384001×10 图像仍返回OPJ_TRUE。 opj_t1_encode_cblks按头声明的pw×ph分配 precinct/code-block 结构,迭代次数 =numcomps × numresolutions × numbands × pw × ph × cw × ch,可达 1×10^10 以上;循环体内纯 CPU 运算,无 I/O 阻塞,造成 DoS。
- BMP 解析器未校验“实际像素数据量 vs. 头声明宽高”:
- 如何被利用(攻击面)
- 远程上传场景:Web 渲染、PDF 打印机、医学 PACS 等只要调用
opj_compress转码即可触发,无需本地交互。
- 远程上传场景:Web 渲染、PDF 打印机、医学 PACS 等只要调用
漏洞复现
环境准备
本次复现是在docker容器环境下进行的,保证了环境的精确、纯粹,我们可以随意指定依赖版本,而不会被主机的环境干扰。
首先,拉取openjpeg官方github仓库。
|
|
然后,根据编译所需相关依赖创建docker镜像,注意依赖要尽量贴合当年的环境,以下是dockerfile。
|
|
根据dockerfile,我们创建镜像。
|
|
至此环境准备就完成了。
编译/触发
首先,使用先前创建的镜像启动容器,建议使用docker容器挂载宿主机目录,方便进行文件的观测和修改。
|
|
宿主机目录会被挂载到容器的/workspace目录下,注意所有改变也会同步到宿主机目录上。
进入容器后,我们先将项目切换到修复前版本。
|
|
接着我们编译出供漏洞复现使用的组件,注意需要启用 ASan 与 debug 编译标志以获得清晰崩溃信息,以下是我撰写使用的编译脚本setup.sh。
|
|

执行完毕后,需要用到的编码器组件opj_compress应当在以下路径。
|
|
接下来就可以结合poc触发文件,参考bug_report的中的漏洞触发方式进行漏洞复现。
我使用了如下poc.sh脚本进行测试:
|
|
我使用的poc文件链接附上:2018-6616.bmp
调整路径并执行后就能成功触发漏洞,即触发命令执行超时,具体结果如下:

|
|
当我们切换到修复后版本尝试漏洞复现:
|
|
这次就不再触发超时。

|
|
PoC分析
我们来分析一下PoC文件是如何触发漏洞的:
PoC文件2018-6616.bmp仅144字节,却能让opj_compress陷入十亿级循环、CPU 100%持续15分钟以上,其关键在于**“头体严重不符”** + RLE8压缩的双重欺骗。
文件结构陷阱
- biWidth设为
0x00FA0001=16,384,001像素,biHeight仅10像素,总“声明”像素≈1.6亿。 - biCompression=
1(BI_RLE8),biSizeImage=4(仅4字节),bfSize伪造为0x4000008E(≈1 GB),制造“体积足够”假象。 - 真实像素数据从偏移
0x5500开始,长度仅144字节,全部为RLE8压缩块,可迅速结束解码。
解码路径绕过
bmp_read_rle8_data按声明宽度分配stride,却因数据不足,实际只写入144像素;由于未校验written != width\*height,函数仍返回OPJ_TRUE,成功通过BMP解析关。
编码阶段爆炸
openjpeg在-n 1(1层小波)模式下,将整图视为单tile:
res->pw = (16384001 +1)>>1 = 8,192,001res->ph = (10 +1)>>1 = 5
默认cw=ch=64,单分量单band即产生
8192001×5×64×64 ≈ 1.68×10^11个code-block;再乘以3分量×3子带,总迭代次数**>1.5×10^12**。
循环体内全是位运算,无I/O阻塞,表现为永久卡死——经典算法级DoS。
补丁对比
修复后bmp_read_rle8_data新增written计数,发现144 ≠ 163840010立即return OPJ_FALSE,opj_compress提前退出,不再进入opj_t1_encode_cblks,十亿级循环被扼杀在解析层。
补丁分析
修复commit详见
-
主要改动 commit 8ee3352 仅在
src/bin/jp2/convertbmp.c的bmp_read_rle8_data()增加:- 变量
written计数已解码像素; - 每次写入像素后
written++; - 函数返回前校验
written == width*height,不符即fprintf警告并返回OPJ_FALSE。
- 变量
-
为什么能修复问题
在 BMP→RAW 阶段就拒绝“巨宽高+少数据”的恶意文件,后续
opj_t1_encode_cblks根本不会拿到离谱的pw/ph,迭代炸弹被扼杀在解析层。 -
补丁的局限与建议
- 仅防御“头体不符”类攻击;若攻击者构造合法宽高但内部 JPEG 2000 参数(tile 数、resolutions、dwt 级数)极端,仍可能造成算法级 DoS,需额外对
numtiles、numresolutions、cblk_w/h等设上限。 - 建议在上层应用再加一次“总像素数 > 阈值直接拒绝”的保险,防止类似逻辑炸弹变种。
- 仅防御“头体不符”类攻击;若攻击者构造合法宽高但内部 JPEG 2000 参数(tile 数、resolutions、dwt 级数)极端,仍可能造成算法级 DoS,需额外对
复现镜像
以上复现过程已打包为docker镜像,可通过以下命令拉取:
|
|
内含:
- openjpeg(项目文件夹)
- setup.sh
- image_status_check.sh
- test_case.sh
- poc.sh
- poc文件
首先进入项目文件夹,按需切换到修复前/后版本:
|
|
然后按顺序执行四个脚本,即可复现和验证漏洞,预期结果为:
|
|
总结和启示
小结:
CVE-2018-6616 是典型的“声明尺寸 vs. 实际数据”不匹配导致的算法级 DoS;官方通过在最早的数据入口增加廉价校验,把 O(10^10) 次循环消灭在 O(1) 判断里。
启示:
- 对任何外部输入的“count/length/width/height”字段,务必在第一时间与真实数据长度交叉校验;
- 核心算法层不要假设上游已过滤,防御深度永远优于事后补洞。