CVE-2021-41773 CVE-2021-42013 经典路径穿越

CVE-2021-41773  CVE-2021-42013 经典路径穿越 前言本文通过叙述CVE-2021-41773 CVE-2021-42013 讲解在缺陷版本的Apache中经典的路径穿越漏洞和背后原理本文会通过简单模拟代码演示效果并且会说明现代更加安全的解析模型。漏洞概述CVE编号 CVE-2021-41773 CVE-2021-42013漏洞类型路径穿越Path Traversal/ 任意文件读取 / 潜在的远程代码执行RCE受影响产品仅限 Apache HTTP Server 2.4.49 2.4.50漏洞位置URL解析重构后的路径识别缺陷公开描述攻击者可以读取服务器系统上的任意文件攻击者可读取任意文件若相关 aliased path 上同时启用了 CGI 执行还可能进一步导致远程代码执行。CWECWE-22Path TraversalCVSS7.5 High漏洞背景在 Apache 2.4.49 中路径规范化与相关校验逻辑发生了改动而这次改动引入了 URL 编码与路径识别处理不当的问题但是新逻辑引来了URL编码和识别路径相关的缺陷进而导致该严重的漏洞。通常服务器会拦截带有 …/用于返回上一级目录的请求以防止用户访问 Web 根目录之外的文件。但是攻击者发现如果对 . 或 / 进行 URL 编码例如将 …/ 写成 .%2e/ 或 %2e%2e/Apache 2.4.49 的新验证机制就无法正确识别并拦截它。这使得攻击者可以“穿越”出预设的网站根目录访问服务器上的系统文件。Apache URL路径审查和URL编码其实URL出现路径问题这其实是一个信息对抗点在最初的设计中开发团队就考虑到了由于URL最终请求的服务器中某一个文件而通常路径是由 ‘/’ 隔开的但是很多系统中的文件名是可以合法包含 ‘/’ 的这就出现歧义 /files/a/b.txt它表示请求b.txt在目录files/a/下但是那要如何请求目录files下的a/b.txt呢这里的处理往往是由客户端决定的在具体的请求信息中如果要写入特殊符号作为非路径相关时往往会进行URL编码也就是/files/a%2Fb.txt其中%2F为 ‘/’ 这一过程是在构建请求时完成的也就是服务器接收到的URL是清晰区分‘/’ 是路径还是其他信息这样的字符被叫做保留字符一般包括/、?、#、、、:、它们都具有分隔和结构化作用。当然上述是一个理想的过程在这其中服务器需要应对不负责任的前端也就是说因为前端并没有严格的审查机制所以服务器也需要做出路径审查这里的路径审查往往体现在两方面应对客户端的编码URL编码涉及全字符所以服务器端不能一上来就全部解码因为保留字符要被严格区分为了防止出现安全问题服务器往往会检查是否出现恶意的路径穿越例如拒绝带有 ‘…/’ 等相关的请求。同时Apache是一个高并发的服务代理它必须要时刻应对几万的并发量也就导致了它无法细致、完整的去检查也就为后续埋下伏笔。漏洞原理在2.4.49路径规范化代码的首次重构时开发团队对URL的检查中的敏感检查和URL解码部分合并到一起在同一个循环中执行企图利用O(n)的时间复杂度完成这一件事情它确实提升了性能但是也导致了检查不完全的情况具体为假设目标用户发送了这样的请求URL/cgi-bin/.%2e/.%2e/.%2e/.%2e/etc/passwd经过URL编码它的实际原文为/cgi-bin/.. /.. /.. /.. /etc/passwd按照最初的预想服务器应该会在去掉多余空格、重复的 ‘/’ 后检测到敏感字符 {…/}后返回错误信息的但是又因为在转换的URL编码 %2E 也就是 转换前的{ . }它不属于保留字符按照新的URL审查机制它的转换与路径审查是同时发生的也就是进行路径审查遍历URL字符串检测到当前字符为 { . }存在风险前瞻机制继续往前探发现是%不符合 { …/ }验证为合法发现 % 两位十六进制数字转换URL编码最终形成连续的 …/ 构建完成路径穿越上述过程导致了最终的漏洞发生这个过程可以简化为路径审查机制与URL未保留字符的结合执行时路径审查机制无法处理编码前的信息而编码又紧跟路径审查价值导致无法审查到完整的路径信息并且被URL编码转换完后宣布了该路径合法。看到这里肯定认为这是一个不必要的错误只是把不该混合的操作混合执行了但是问题没有那么简单原因在于URL的审查必须要快不能出现把它当作字符在内存中拷贝一次仍后细节审查在这种超高并发的环境下上述操作是灾难级的操作。所以开发团队在 2.4.50版本紧急修复了这一个‘荒唐’的漏洞但是没有完全修复官方提到“the fix for CVE-2021-41773 in Apache HTTP Server 2.4.50 was insufficient”这次针对CVE-2021-41773修复是不足的这也引来了CVE-2021-42013 一个‘迭代’漏洞。这个漏洞的原理与上述过程本质完全一致还是通过编码绕过审查那么官方到底修复了什么呢简单来说审查机制在检测到 { . }后前瞻机制会试图通过判断后方的指针来判断是否为敏感路径2.4.49第一版本这里仅会审查通过 {‘/’ , ‘.’}二者组成的敏感路径但是2.4.50修复过后会在此加入如果点后方紧接出现%2E也算一个敏感路径返回错误信息。这叫什么这叫硬编码修复怎么看都是下下策不过仍旧是性能问题让开发软对的的确确就是这样做的所以出现了新的问题审查中的 2 和 E属于非保留字符但是前瞻机制不会去URL编码它们也就是如果我构建一个/.%%32%65/审查机制在%后仍然检查到%后就停止了审查只是%32和%65会被编码为 2 和 E结合之前到%又组成了%2E后续会涉及其他编码机制同样会编码URL让整个编码变为 . ,所以就又构建成了 /…/并且再次跳过审查被宣布合法。总结为当 2.4.50 版本的 Apache 遇到 /.%%32%65/ 时好戏再次上演指针遇到第一个 .。触发 2.4.50 的新补丁逻辑向后看。它看到了 %继续向后看期望看到字符 2。但是它看到的是另一个 %因为载荷是 %%32。匹配失败补丁逻辑判断“这不是 %2e放行”随后Apache 在后续的处理流程中会对路径进行第二次解码。就这样/.%%32%65/ 经过两次褪皮再次变回了 …/…/彻底击穿了 2.4.50 的防御。这就演变成了 CVE-2021-42013。代码演示// 判断一个字符是否是合法的十六进制字符intis_hex(charc){return(c0c9)||(cacf)||(cAcF);}// 将两个十六进制字符如 2 和 e组合并解码成对应的 ASCII 字符chardecode_pct(charh,charl){// 比如h2 (对应数值2)le (对应数值14)// (2 4) | 14 32 14 46 (对应字符 .)inthigh(hA)?((h0xDF)-A10):(h-0);intlow(lA)?((l0xDF)-A10):(l-0);return(char)((high4)|low);}// 判断字符是否为 RFC 3986 规定的“非保留字符”intis_unreserved(charc){// 非保留字符字母、数字、连字符(-)、句点(.)、下划线(_)、波浪号(~)if((cacz)||(cAcZ)||(c0c9)){return1;}if(c-||c.||c_||c~){return1;}return0;}// 检查当前写指针 w 是否处于一个新路径段的开头intat_segment_start(intw,constchar*path){// 两种情况代表段的开始// 1. 刚刚开始解析w 还在最开头 (w 0)// 2. 前一个写入的字符刚好是斜杠 /return(w0||path[w-1]/);}// 检查字符是否是斜杠 / 或者字符串的结束符 \0intis_slash_or_nul(charc){// 这个函数非常关键它用来确认 . 或 .. 后面是不是紧跟着目录的分隔符或结尾// 比如 /../ 或 /..结尾 都是合法的穿越特征return(c/||c\0);}2.4.49漏洞核心逻辑还原while(path[l]!\0){if(decode_unreservedpath[l]%is_hex(path[l1])is_hex(path[l2])){cdecode_pct(path[l1],path[l2]);if(is_unreserved(c)){l2;path[l]c;// 把 %2e 解成 .}}if(at_segment_start(w)path[l].){if(is_slash_or_nul(path[l1])){// 处理 /./}if(path[l1].is_slash_or_nul(path[l2])){// 处理 /../}}path[w]path[l];}对于上述逻辑我们假设输入/.%2e/进行简单推演第一步l 指向了 第一个 . 。at_segment_start(这个函数表面当前的w是否处于一个新路径的开头) 返回 true前面刚写了 /。进入第二个 if 块准备检查是不是穿越。 检查 /./ 逻辑is_slash_or_nul(path[l1])由于path[l1] 是 %返回 false。 检查 /…/ 逻辑path[l1] ‘.’由于 % ! .返回false。结果安全检查完美错过。 执行 path[w] path[l]把第一个 . 写入。第二步l 现在指向 %l1 是 2l2 是 e。进入第一个 if 块decode_unreserved 生效。decode_pct 将其解为 .。is_unreserved(‘.’) 返回 true。结果致命替换发生。 内部状态 path[l] 被改写成了 .指针 l 跳过了 2现在指向那个刚生成的 .。第三步继续执行最后的 path[w] path[l]。把刚才解出来的 . 也写入了缓冲区。后续的迭代修复版本仅仅是将处理构成 …/时同时审查了 %2E由于其他函数中同样带有URL编码经过双重编码后仍然会构建出 …/的形式也就有有了迭代漏洞。后续改进在 2.4.51版本中开发团队已经完整修复了这个漏洞加入更加严格的验证方式但是随后Apache认为这样的安全隐患太多了因为问题始终存在且大部分同指用户端请求的路径中带有非逻辑性的 / 等字符信息所以开发团队选择保守策略默认策略拒绝服务 默认情况下如果 Apache 在 URL 路径中看到了 %2F斜杠的编码或 %5C反斜杠的编码它会直接返回 404 Not Found 甚至 400 Bad Request。它根本不去尝试解析它。Apache 的哲学是路径里带有编码的斜杠太危险了极易引发路径穿越或映射混乱我宁愿不服务也不冒这个险。开启特殊指令AllowEncodedSlashes 如果业务确实需要支持文件名中带斜杠管理员必须在配置文件中显式开启一个指令 AllowEncodedSlashes。如果设置为 OnApache 会将其解码为 /但这很容易破坏后续的路由逻辑。如果设置为 NoDecode现代最推荐的做法Apache 会允许这个请求通过安全检查但故意不解码 %2F。它会将原汁原味的 %2F 传递给后端的应用程序比如 PHP 或 CGI 程序。这样后端的业务代码就能清楚地知道“哦这是一个名叫 a/b.txt 的文件而不是 a 目录下的 b.txt”。现代化URL解析其实自始至终都有一个根本问题就是审查机制的缺陷其实还是源于对性能的掌握在解析这个位置服务器为了应对并发无法消化更多性能来保证安全。那么这件事情没有解决方法吗其实现代化的URL解析会考虑双指针就地修改(In-place State Machine)以及SIMD 指令集加速从底层优化这个审查机制。在一个就是对于现代的高级语言往往都会采用“双层路径”模型来彻底解决这个问题以 Go 语言的 net/url 库为例当它解析一个 URL 时会生成一个结构体里面包含两个极其重要的字段URL.Path (清洗后的安全路径)这里面的所有非保留字符都被解开了./ 和 …/ 都被计算并抵消了甚至 %2F 也被解开了取决于具体实现。它是绝对安全的用于做基础的路由匹配。URL.RawPath (原始编码路径)这里原封不动地保留了请求最初的模样比如 a%2Fb.txt。这是现代的高级语言中为了彻底解决 RFC 语义和安全审查的矛盾标准库采用了一种非常聪明的“保留现场”机制。当路由引擎在工作时如果它需要严格区分目录分隔符和文件名中的斜杠它就会去对比 Path 和 RawPath。这种机制既利用了底层一次性解析的高性能又为上层业务保留了判断“结构”还是“数据”的全部上下文。在应用层双层路径模型最直观的理解是用户请求服务时只能和资源本身的标识信息打交道例如文件名、资源 ID、模板名或业务对象编号而不能直接控制底层文件路径。真正的目录结构、路径拼接方式和物理存储位置都由后端内部逻辑掌握。例如某个登录提示文件在系统内部可能对应 /auth/message/login_message.txt但用户请求时只能提供 login_message.txt 或对应的消息 ID后端再根据固定规则将其映射到受控目录下的真实文件。这样一来攻击者即使试图继续拼接路径也无法直接影响底层文件系统访问因为接口层根本不把用户输入当作完整路径解释。总结其实这两个漏洞是非常经典的在这一类问题中本质上就是“访问控制边界没守住攻击者拿到了不该拿到的资源”而对应此漏洞就是URL的缺陷解析导致了攻击者可以访问服务器内的任意文件甚至可以访问到目标系统的所有文件它们直接的技术归类是 CWE-22Path Traversal对应了当前最新的OWASP Top 10:2025 A01:2025 - Broken Access Control访问控制失效。