一个计数器没清零:7-Zip 解压 CAB 就能拿到 shell
Ots安全
2026年11月11日 15:37
广东
去阅读
2026 年 11 月 10 日,一篇个人技术博客放出两段演示视频。画面里没有钓鱼页,没有宏,没有需要点「启用内容」的文档。只有一条解压命令。
命令跑完,另一台机器上多了一个 shell。
让人停下来的是两个数字:目标缓冲区 32 KB,写入位置在它末尾之外约 1.3 MB。
而修好这个洞,上游只加了四行赋值。
7-Zip 26.04 — 远程反向shell,ASLR 已开启:
已关注
关注
重播 分享 赞
关闭
观看更多
更多
退出全屏
切换到竖屏全屏退出全屏
Ots安全已关注
分享视频
,时长01:27
0/0
00:00/01:27
切换到横屏模式
继续播放
[ ]
进度条,百分之0
播放
00:00
/
01:27
01:27
倍速
全屏
倍速播放中
0.5倍0.75倍1.0倍1.5倍2.0倍
超清流畅
继续观看
一个计数器没清零:7-Zip 解压 CAB 就能拿到 shell
观看更多
转载
,
一个计数器没清零:7-Zip 解压 CAB 就能拿到 shell
Ots安全已关注
分享点赞在看
已同步到看一看写下你的评论
视频详情
7-Zip 26.05 — 远程反向shell,ASLR 已开启:
已关注
关注
重播 分享 赞
关闭
观看更多
更多
退出全屏
切换到竖屏全屏退出全屏
Ots安全已关注
分享视频
,时长02:09
0/0
00:00/02:09
切换到横屏模式
继续播放
[ ]
进度条,百分之0
播放
00:00
/
02:09
02:09
倍速
全屏
倍速播放中
0.5倍0.75倍1.0倍1.5倍2.0倍
超清流畅
继续观看
一个计数器没清零:7-Zip 解压 CAB 就能拿到 shell
观看更多
转载
,
一个计数器没清零:7-Zip 解压 CAB 就能拿到 shell
Ots安全已关注
分享点赞在看
已同步到看一看写下你的评论
视频详情
先看问题:一个 CAB,两个数字,四行补丁
先把能坐实的摆平。这次不是传闻,也不是某个转述里被放大过的「重大漏洞」。
| 事项 | 可以写实的 | 依据 |
|---|---|---|
| 编号与定性 | CVE-2026-125742,CWE-787 越界写,CNA 为 VulnCheck,2026-11-10 发布 | CVE 官方记录 |
| 评分 | CVSS 4.0 8.5 HIGH、CVSS 3.1 7.8 HIGH;两条向量都要求本地触发与用户交互 | CVE 官方记录、Rapid7 |
| 影响范围 | 7-Zip 26.06 之前版本,源文写为不高于 26.05 | CVE 产品状态、源文 |
| 修复 | 26.06,2026-11-06 发布;官网变更日志只写「Some bugs and vulnerabilities were fixed.」,未点名该 CVE | 7-zip.org 与 history.txt |
| 根因 | 续卷 CAB 声明更小的 LZX 窗口时,旧 _pos 未清零,无符号减法回绕使边界检查失效 |
CVE 描述、阿里云 AVD、源文代码 |
| 在野状态 | CISA KEV 2026.11.10 版共 1779 条,不含此编号;7-Zip 相关仅 CVE-2025-0411 | CISA KEV 官方 JSON |
| 公开材料 | 源文被 CVE 参考列为 technical-description 与 exploit,附两段演示视频 | CVE 参考列表 |
四源以上交叉下来,机制、版本、补丁、评分都没有分歧。需要挂起来的是另外几件事,放在后面「偏差清单」章逐条钉。
有一处口径要先讲清:源文把危害写成 Critical,官方评分是 HIGH。研究文章描述的是「在作者那台机器上打出了 reverse shell」,CVE 评的是「通用环境下的严重程度」,两者不是同一把尺子。本文按官方评分叙述,同时不低估作者演示出来的后果。
还有一处是平台。演示跑在 Arch Linux x86-64、glibc 2.44、7-Zip 26.04 与 26.05 上,作者自己列了五项需要重新标定的变量:7-Zip 版本、glibc 版本、内核、归档名、embed size。Windows 上并没有公开的验证结果,而 7-Zip 的绝大多数用户恰恰在 Windows。这层空白不能拿「跨平台通杀」去填。
一个计数器没被清零
CAB 是微软 1995 年前后就有的归档格式,Windows 更新、驱动包、安装包里还在用。它允许一个归档跨多个卷,后一卷声明「接着上一卷继续」,解码器就保留历史字典,不重置状态。
LZX 是 CAB 支持的压缩编码之一。它带一个窗口参数,从 32 KB(bits=15)到 2 MB(bits=21)。解码器按窗口大小分配一块缓冲区 _win,用位置计数器 _pos 记录写到了哪里。
问题就出在「接着上一卷继续」和「窗口变小」同时发生的时候。
2.1 续卷时,只在刚好到边界才重置
原文给出的这段代码在 Code_WithExceedReadWrite(),位置标为 LzxDecoder.cpp:1446-1454:
// LzxDecoder.cpp:1446-1454if (!_keepHistory){ _pos = 0; // Fresh start: always reset}else if (_pos == _winSize){ _pos = 0; // Continuation: only reset at window boundary _overDict = true;}
代码块:原文照录,为便于阅读作者补了英文注释。来源 Nguyễn Trần Thanh Lâm 同名文章(CC BY 4.0)。
逻辑读起来很顺:新开一次解压就把 _pos 清零;续卷时只在 _pos 恰好等于窗口大小,也就是刚好写满一圈时清零。
「恰好」这两个字是关键。只要 _pos 不等于 _winSize,续卷分支就什么都不做。
2.2 换窗口只换 buffer,不换 \_pos
第二段在 Set_DictBits_and_Alloc(),位置标为 LzxDecoder.cpp:1496-1513:
// LzxDecoder.cpp:1496-1513if (!_win || newWinSize != _winSize){ z7_AlignedFree(_win); _winSize = 0; _win = (Byte *)z7_AlignedAlloc(newWinSize + k_OutBufSize_Add); // ...}_winSize = newWinSize;
代码块:原文照录,作者做了精简。逐字的上游源码见 26.05 tag 的 CPP/7zip/Compress/LzxDecoder.cpp,该函数外层还有 _needAlloc 判断与 RINOK(SetParams2(...)),语义一致但并非逐字相同。
旧缓冲区被释放,按新窗口重新分配,_winSize 更新。三个动作都做了,唯独 _pos 留着上一卷用大窗口累积出来的值。
源文构造的卷一用 LZX:21(2 MB 窗口)把 _pos 推到 0x14A420,约 1.3 MB;随后放一个 LZX:15(32 KB 窗口)的空 folder。于是 2 MB 的缓冲区被换成 32 KB,_winSize 变成 0x8000,_pos 还是 0x14A420。
2.3 无符号减法把边界检查绕了过去
第三段只有两行,位置标为 LzxDecoder.cpp:1463-1464:
// LzxDecoder.cpp:1463-1464if (outSize > _winSize - _pos) return S_FALSE;
代码块:原文照录。来源同上。
_winSize 与 _pos 都是无符号数。0x8000 - 0x14A420 不会得到负数,它回绕成一个很大的正数。边界检查因此通过,解码器把数据写到 _win + _pos,也就是 32 KB 缓冲区末尾之外约 1.3 MB 处。
三处代码单独看都说得过去。合起来才出事:状态保留是续卷功能要的,重分配是窗口变化要的,边界检查本身也没写错,只是它假设 _pos 一定小于 _winSize。这个假设在换窗口的那一刻悄悄失效了。
图:原文机制图,左右两栏分别是卷一的 2 MB 窗口与卷二的 32 KB 窗口,标出 _pos 未重置与无符号回绕。原图为 SVG,本文转为 PNG 以便阅读;作者 Nguyễn Trần Thanh Lâm,CC BY 4.0,仅作机制说明引用。
1.3 MB 落在哪儿
越界写不是终点,落点才是。
源文用堆布局把目标固定在一个 COutFileStream 对象上:卷一先写出大量填充文件,让分配器把后续对象摆到可预测的位置;卷二里一个 462 字节的 CFDATA 块被解码时,写入位置正好覆盖这个流对象的虚表指针与 FILE 相关字段。
图:原文机制图,显示 _win、越界写入偏移与 COutFileStream 的相对位置。原图为 SVG,本文转为 PNG;作者同上,CC BY 4.0,仅作机制说明引用,不展开载荷构造。
这一步之后发生的事情属于利用技术,不属于漏洞成因:改写虚表让下一次 Write() 调用落到 glibc 的 FILE 处理函数,再借伪造的 stdin 结构在退出清理时走到 system()。原文把它归在 House of Apple 2 这一类 FSOP 手法下。
这里要把话说完整。能确认的是「越界写可以覆盖堆上的流对象」,这是漏洞本身给的;「覆盖之后一定能稳定拿到 shell」则是作者在单一环境下、针对特定 glibc 与堆布局标定出来的结果。两者不能划等号。
顺带一个可观测的细节:原文附的演示视频里,画面出现过 ERROR: errno=473 与 segmentation fault (core dumped) 之类的解压异常。对防守方来说,7-Zip 解压进程异常退出、生成 core dump,本身就是比「有没有杀软报毒」更稳的线索。
链子走到哪一步,在哪一步停手
原文的流程图把整条路径拆成五段。放进来看全局是必要的,逐段展开则不是。
图:原文流程总览,五个阶段依次为堆布局准备与脚本落盘、窗口不同步、越界堆写、虚表劫持、退出清理触发执行。原图为 SVG,本文转为 PNG;作者同上,CC BY 4.0。本图仅用于说明阶段划分,文中不提供任何阶段的实现参数、载荷或脚本。
阶段名本身就说明了这条链子的形状:前面两步在准备内存与磁盘,中间两步是漏洞给的写能力,末尾一步借的是 glibc 退出清理流程。漏洞只负责第三步,剩下的是利用工程。
作者公开的成功率也很值得记住:
| 模式 | 结果 | 原文附注 |
|---|---|---|
| ASLR 关闭 | 5/5,全部成功 | setarch -R 关闭随机化 |
| ASLR 开启 | 19/64,约 30% | 单一字节变体;理论值 12.5% |
不需要信息泄露是这条链子里技术含量较高的一处。堆上残留的 arena 指针带着 libc 地址的高位字节,LZX 的 match 指令又能从已解码数据里复制字节,作者用它拼出完整的 libc 地址;中间那一个字节靠猜,生成 8 个变体。原文说猜错的变体通常只是解压报错退出,不崩溃,可以重试。
本文在这里停手。原文的 12 个代码围栏里,有 6 个只是行号,3 段是上面的根因代码,另外 3 段分别是 shell 脚本、虚表偏移与调用链表达式。后面这三段、伪造 FILE 结构的字段表、堆布局的具体距离与拼接字节,都不转载。理由不是它们不公开,是公众号不是利用代码分发渠道。要读懂成因与修复,前面三段代码已经够。
另外一个必须写清的边界:这是作者在自有实验环境里的研究记录,公开 PoC 不等于在野利用。CISA KEV 到 2026-11-10 的版本里没有这条编号,阿里云漏洞库的「利用情况」也标为暂无。反过来,漏洞细节与演示视频已经公开,把「暂无在野」读成「不用急着修」同样不对。
补丁只有四行
7-Zip 26.06 在 2026-11-06 发布,11-10 的 CVE 记录把修复指向 commit d3296e3。那个 commit 改了 73 个文件,其中 LzxDecoder.cpp 的 diffstat 是 4 行新增、0 行删除。
我把 26.05 与 26.06 两个 tag 的该文件拉下来逐行比对,改动如下:
--- 26.05/CPP/7zip/Compress/LzxDecoder.cpp+++ 26.06/CPP/7zip/Compress/LzxDecoder.cpp@@ -559,7 +559,9 @@ CDecoder::CDecoder() throw():+ _pos(0), _win(NULL),+ _overDict(false), _isUncompressedBlock(false),@@ -1499,6 +1501,8 @@ z7_AlignedFree(_win); _winSize = 0;+ _pos = 0;+ _overDict = false; const size_t alloc_size = newWinSize + k_OutBufSize_Add;
代码块:依据 ip7z/7zip 的 26.05 与 26.06 两个 tag 逐行比对整理,为阅读省略了无关上下文;新增行与 commit d3296e3 的 diffstat「4 +」一致。阿里云 AVD 对修复方式的描述与此吻合:释放旧窗口后将 _pos 置 0、_overDict 置 false,并在构造函数中初始化这两个字段。
四行,两个位置。构造函数里补初始值,避免对象刚建立时状态未定;重分配处补重置,让换窗口那一刻把游标归零。边界检查那两行一个字没改,因为检查本身没错,错的是它拿到的输入。
图:本文绘制,非原文章配图。把卷一、F4 缩窗、卷二解码三个状态连起来,标出 26.06 补的两处重置落在哪一环。
这类补丁有个特点:改动小到容易在版本比对里被忽略,但它修的是一个状态机,不是一行算术。评测这类漏洞时只看「有没有越界写检查」会漏掉它,得看状态在每次重配置后是否仍然自洽。
谁要马上动,怎么动
版本核查是起点,不是终点。
| 层面 | 动作 | 原因 |
|---|---|---|
| 终端 | 把 7-Zip 升到 26.06 或更新版本,手动查版本 | 7-Zip 没有内置自动更新,装机量大的机器上「装完就再没人管」很常见 |
| 资产 | 清点命令行版 7z、7za、7zr、p7zip,以及 CI、构建机、文件服务器、邮件沙箱内嵌的解压组件 |
这些位置往往没有桌面图标,也不在常规软件清单里 |
| 入口 | 邮件网关与文件交换通道对多卷 CAB、续卷标记与异常 LZX 窗口变化做检查 | 触发只需解压动作,不依赖文件后缀或用户额外操作 |
| 隔离 | 对来源不明的归档,在受限账户、无出站网络、临时容器里解压 | 把「一次解压」的后果关在沙箱里 |
| 检测 | 监测 7-Zip 解压进程的异常退出、core dump、解压后异常子进程与外连 | 原文演示视频里就出现过解压报错与段错误 |
| 优先级 | 不因为 KEV 未收录就降级,按「公开利用细节 + 无自动更新」排期 | 修复可得、利用材料已公开,窗口期风险由补丁落地速度决定 |
对企业来说,真正容易被漏掉的是第三行与第四行:解压不只发生在员工双击文件的那一刻,也发生在网关、沙箱、构建流水线里。那些进程同样调用 7-Zip 或它的派生实现,同样面对不受信任的归档,却常常不在补丁管理清单上。
还有一条别混淆:2026 年 7-Zip 修过好几个同类漏洞,包括 CVE-2026-14266(XZ,26.02 修复)与 CVE-2026-48095(NTFS,26.01 修复)。它们是不同的代码路径、不同的版本。把编号串在一起写,会让升级判断失真。
偏差清单:哪些能写实,哪些只能挂着
| 事项 | 可写实 | 需要挂起来的 | 依据层级 |
|---|---|---|---|
| 严重度 | 8.5 / 7.8,均为 HIGH | 源文自称 Critical,属研究视角,不是官方评分 | CVE 官方记录 vs 源文 |
| 影响平台 | 官方产品状态写 26.06 之前版本,未限定系统 | 演示只在 Arch Linux x86-64 + glibc 2.44 验证,Windows 无公开结果 | CVE 记录 vs 源文测试环境 |
| 在野利用 | KEV 2026.11.10 版未收录,中文漏洞库标「利用情况 暂无」 | 公开利用细节不等于在野,也不等于不会被很快武器化 | CISA KEV JSON、阿里云 AVD |
| 成功率 | ASLR 关闭 5/5,开启 19/64 约 30% | 作者自报、单机单环境,需针对 glibc 与堆布局标定,不能当通用概率 | 源文 Results 表 |
| 修复 | 26.06(11-06)与 commit d3296e3,四行新增 |
官网变更日志未点名该 CVE,只写「修复了一些问题与漏洞」 | 7-zip.org、上游源码比对 |
| 原文代码 | 三段根因代码语义与 26.05 tag 源码一致 | 原文为讲解做了精简,不是逐字上游源码 | 26.05 源码逐行比对 |
| 中文转述 | 暂无可靠的大规模在野统计 | 部分中文博客把它与 26.01、26.02 修的其它 7-Zip 漏洞混写,还出现编造的受害主机数量与占位编号,未采用 | 检索结果比对 |
| 演示视频 | 原文附 26.04、26.05 两段 ASLR 开启的演示 | 视频画面不能替代可复现的第三方验证,本文未转载画面 | 原文页面 |
四源以上交叉的结果是:机制、版本、补丁能坐实;在野规模、跨平台通用性、成功率的普适性坐不实。公开信息未说明的部分,就挂在表里,不拿推测去填。
把状态当资产管
这条链子没有攻破哪一道加密,也没有绕过哪个权限检查。它只是让一个计数器在换窗口之后继续留着旧值,然后让一行本来正确的边界检查,拿到了一个不再成立的前提。
压缩软件常被当成「不联网、不登录、不存数据」的低风险工具,因此长期停在装机时的版本。可它每天都在解析来自外部的、结构复杂到能表达「续卷」「窗口」「历史字典」的数据。解析器持有的状态越多,重配置时越容易漏掉一个字段。
补这一个洞要做的其实很朴素:把版本升到 26.06,把内嵌解压组件找出来,把解压放进隔离环境,把异常退出接进告警。做完这四件,下一次再有人把窗口从 2 MB 悄悄改成 32 KB,至少不会是在你的机器上才被发现。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论