一个计数器没清零:7-Zip解压CAB就能拿到shell

admin 2026-11-13 11:08:26 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

一个计数器没清零: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倍

超清流畅

 您的浏览器不支持 video 标签

继续观看

一个计数器没清零: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倍

超清流畅

 您的浏览器不支持 video 标签

继续观看

一个计数器没清零: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,至少不会是在你的机器上才被发现。

评论:0   参与:  0