影响面较大的新型WAF绕过详细解读

admin 2026-04-30 05:01:06 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细解读了GhostBits漏洞的成因与影响,该漏洞源于Java字符(char)到字节(byte)转换时的高8位截断,导致安全检查与底层执行语义不一致。攻击者可利用此绕过WAF、文件上传校验、路径穿越等防御机制,具体案例包括BCEL、Jackson、FastJSON等组件的绕过。文档还分析了相关风险API及宽松解析问题,并提供了防御建议。 综合评分: 87 文章分类: web安全,漏洞分析,应用安全,渗透测试,安全开发


cover_image

影响面较大的新型 WAF 绕过详细解读

哈拉少安全小队

2026年4月28日 19:21 广东

在小说阅读器读本章

去阅读

以下文章来源于棉花糖fans ,作者棉花糖糖糖

棉花糖fans .

这里是网安棉花糖,网络安全最大资源站:bdziyi.com 的站长,期待和你交个朋友哦

本文解读使用GPT5.5基于 原PPT 的 56 页内容整理

原文PDF文件:Asia-26-Bai-Cast-Attack-Ghost-Bits-4.23.pdf公众号后台回复0428获取

0. 先说清楚

可以这样理解这个幽灵比特位的问题:

Java 里的字符 char 通常是 16 位,网络协议、文件名、HTTP 头、SMTP 命令这些东西最终却经常要落到 8 位字节上。正常代码应该明确用 UTF-8、ASCII 等编码来转换;但一些老 API 或实现为了方便,直接把 char 强行塞进 byte

问题来了:16 位塞进 8 位,塞不下的高 8 位就被丢掉了。

举个最直观的例子:

陪 = U+966A
低 8 位 = 0x6A
0x6A = 字母 j

所以攻击者给系统的是:

1.陪sp

安全检查可能觉得这不是 JSP 文件,因为它看到的是 。但如果后面某个组件把字符按低 8 位写成字节,它就可能变成:

1.jsp

这就是 Ghost Bits 的味道:高位信息像“幽灵”一样在检查时还存在,到了真正执行时却消失了。上层看到的是一个样子,底层执行的是另一个样子。

整篇 PDF 讲的就是这件事会带来多少连锁反应:

| 场景 | 白话解释 | | — | — | | WAF 绕过 | WAF 看到乱码或中文,后端看到 SQL、RCE、@type | | 文件上传绕过 | 检查时不是 .jsp,保存时变成 .jsp | | 路径穿越 | 检查时没有 ../,解析后变成 ../ | | SMTP 注入 | 邮箱地址里藏的 Unicode 变成换行,SMTP 命令被插入 | | 请求走私 / XSS | Header 里的字符变成 CRLF,请求或响应结构被改写 | | Redis / XML / 路径污染 | 字符串写到底层协议后,字段、标签、路径含义变了 |

用一句人话总结:

Ghost Bits 的危险点不是“中文字符危险”,而是“检查时看到的字符串”和“执行时使用的字节”不是同一个东西。

1. PDF 内容总览

先把这份 PDF 放到一张地图里看。它是一份安全议题幻灯片,题目是:

Cast Attack: A New Threat Posed by Ghost Bits in Java

演讲者与贡献者:

| 角色 | 人员 | 信息 | | — | — | — | | Speaker | Xinyu Bai / B1u3r / 浅蓝 | Web 与应用漏洞研究 | | Speaker | Zhihui Chen / 1ue | Alibaba Cloud Security Engineer | | Contributor | Zongzheng Zheng / SpringKill | 贡献者 |

整体结构大致是这样:

| 页码 | 内容 | | — | — | | 1-2 | 标题、作者介绍 | | 3-6 | Ghost Bits 概念、Java 字符到字节截断的根因 | | 7-18 | WAF Bypass:BCEL、Jackson、Fastjson、Tomcat、URL、Base64、GeoServer、Spring4Shell | | 19-47 | 真实漏洞案例:Openfire、Spring、SMTP、Jira、Confluence、Apache HttpClient、JDK HttpServer | | 48-56 | 总结、自动化发现、ActiveJ、Lettuce、XMLWriter、Jodd、未来攻击面 |

如果再压缩成一句话:

Ghost Bits 不是单一 CVE,而是一类由“字符视图”和“字节视图”不一致引发的攻击模式。安全检查看到的是 Unicode 字符串,底层协议、解码器或 I/O 写入看到的却是被截断、折叠或宽松解析后的危险字节。

2. Ghost Bits 到底是什么

2.1 Java 的 char 不是 1 字节字符

先从一个很容易被忽略的事实说起:Java 的 char 不是 1 字节。它是 16 位 UTF-16 code unit。很多中文、全角字符、其他语种字符都可以放进一个 char,例如:

| 字符 | Unicode | 二进制低 8 位 | 低 8 位对应 ASCII | | — | — | — | — | | 陪 | U+966A | 0x6A | j | | 阮 | U+962E | 0x2E | . | | 严 | U+4E25 | 0x25 | % | | 灵 | U+7075 | 0x75 | u | | 丰 | U+4E30 | 0x30 | 0 | | 甲 | U+7532 | 0x32 | 2 | | 来 | U+6765 | 0x65 | e | | 瘍 | U+760D | 0x0D | \r | | 瘊 | U+760A | 0x0A | \n |

如果代码错误地把这些 char 当成单字节写出去,例如 (byte) ch 或 out.write(ch),Java 不会帮你保留完整字符。高 8 位会被丢弃,只剩低 8 位。

例如:

陪 = U+966A
0x966A & 0xff = 0x6A
0x6A = ASCII 'j'

所以在字符串层面,人和 WAF 看到的是 ;但在某些底层字节写入之后,真正落下去的可能是 j

2.2 “幽灵比特”的含义

所谓 Ghost Bits,就是输入字符中“高 8 位那部分信息”。这些高位在上层字符串检查时还在,所以 WAF、业务校验、日志、人工审计看到的是一个完整的 Unicode 字符;但到了错误的字节化过程里,它们被静默丢掉,于是执行层只剩低 8 位。

可以把它抽象成:

输入字符 c       = 高 8 位 + 低 8 位
安全检查看到    = c 的完整 Unicode 形态
危险 sink 写出  = c & 0xff
协议解析看到    = 低 8 位对应的 ASCII/控制字符

利用者可以挑选满足这个条件的字符:

c = (k << 8) + target_byte

其中 target_byte 是希望底层最终看到的字节,例如 ./%@\r\n

2.3 这类攻击的本质

所以这类问题的本质不是“中文字符危险”,也不是“Unicode 本身危险”,而是:

安全检查使用的语义 != 最终执行使用的语义

把攻击链展开看,就是下面这个过程:

flowchart LR
&nbsp; &nbsp; A["攻击者提交 Unicode 字符"] --> B["WAF / 业务校验"]
&nbsp; &nbsp; B --> C["看起来不是 SQL / 路径 / CRLF / @type"]
&nbsp; &nbsp; C --> D["进入 Java 组件或协议库"]
&nbsp; &nbsp; D --> E["char 被截断或宽松折叠成 byte"]
&nbsp; &nbsp; E --> F["低 8 位变成危险 ASCII / 控制字符"]
&nbsp; &nbsp; F --> G["协议、路径、JSON、SMTP、Redis 等执行真实含义"]

这就是 Cast Attack 的核心:借助类型转换、位运算或宽松解码,让上层看起来无害的字符,在底层变成有语法意义的危险字节。

3. 容易出问题的代码模式

议题里反复出现的风险模式包括:

| 模式 | 风险点 | | — | — | | (byte) ch | 直接把 16 位 char 缩窄成 8 位 | | ch & 0xffch & 255 | 只保留低 8 位 | | baos.write(ch) | ByteArrayOutputStream.write(int) 只写低 8 位 | | OutputStream.write(int) | 参数是 int,但写出的仍是低 8 位 | | DataOutputStream.writeBytes(String) | 逐字符写低 8 位,高 8 位丢弃 | | StringBufferInputStream.read | 老旧 API,以低 8 位方式处理字符 | | String.getBytes(int, int, byte[], int) | 已废弃,按低 8 位拷贝 | | RandomAccessFile.writeBytes | 字符串写字节时丢高位 | | 宽松 Hex / URL 解码 | 非法字符被算成合法 hex | | 宽松 Base64 解码 | 用 & 255 从映射表取值 |

这些 API 并不是一出现就必然等于漏洞。如果输入不可控,或者转换结果不会进入安全敏感语法,风险可能很低。真正需要警惕的是下面这种组合:

用户可控输入
+ 字符串层安全校验
+ 后续 char -> byte 截断/折叠
+ 结果进入协议、路径、反序列化、文件名、Header、SMTP、Redis 等语法边界

4. Ghost Bits、宽松解析、归一化绕过的关系

这份 PPT 把这些现象放在 Ghost Bits 这个大框架下讨论。为了更好理解,建议把案例先分成三类,因为它们的底层触发方式并不完全一样,防御侧的重点也会略有不同。

| 类型 | 典型根因 | PDF 中的例子 | | — | — | — | | 真正的高位截断 | char 低 8 位被写入 byte | DataOutputStream.writeBytes 、Tomcat fromHex、Spring uriDecode、SMTP、Lettuce、XMLWriter、Jodd | | 位运算折叠 | 非 hex 字符被算法“压缩”为合法 hex | Jetty %2> 变 %2E,Openfire、GeoServer | | 宽松 Unicode 解析/归一化 | 非 ASCII 数字、全角字符被当作 ASCII 语义 | Fastjson Character.digit、全角 URL 编码 |

它们共同点是:安全检查和真实执行对同一段输入产生了不同解释。

5. WAF Bypass 类案例

5.1 BCEL Ghost Bits 绕过

PDF 第 8 页讨论 BCEL 的解码逻辑。核心代码形态是:

ByteArrayOutputStream bos =&nbsp;new&nbsp;ByteArrayOutputStream();
CharArrayReader car =&nbsp;new&nbsp;CharArrayReader(chars);
JavaReader jr =&nbsp;new&nbsp;JavaReader(car);

while&nbsp;((ch = jr.read()) >=&nbsp;0) {
&nbsp; &nbsp; bos.write(ch);
}

关键点:

ByteArrayOutputStream.write(int) 写出的不是完整 int,而是低 8 位。因此如果 BCEL 编码字符串里混入高位 Unicode 字符,WAF 看到的是一串异常字符;BCEL 解码时却还原成攻击者想要的字节流。

攻击视图差异:

| 层级 | 看到的内容 | | — | — | | WAF | $$BCEL$$ 后面跟大量 Unicode 字符,特征弱 | | BCEL 解码器 | 低 8 位字节流 | | 类加载逻辑 | 可执行的字节码 |

本质是“字节码编码字符串”的安全检查没有模拟 BCEL 最终的低字节写入行为。

5.2 Jackson charToHex 绕过

PDF 第 9 页展示 Jackson 场景:输入看起来像一串 Unicode,例如 \u丰丰耳失...,但 charToHex 逻辑里存在类似:

return&nbsp;sHexValues[ch &&nbsp;0xff];

示例映射:

| 字符 | Unicode | & 255 后 | 结果 | | — | — | — | — | | 丰 | U+4E30 | 0x30 | 0 | | 耳 | U+8033 | 0x33 | 3 | | 失 | U+5931 | 0x31 | 1 |

所以 丰丰耳失 在低 8 位视角中变成 0031,再经过字符/转义处理后可能成为 1

PDF 中的完整效果是:

| 阶段 | 语义 | | — | — | | 攻击者输入 | Unicode 字符串 | | WAF | 未发现 union select | | Jackson 映射字段 | 还原出 1 union select 1,2,3-- | | 后续 SQL | 注入 payload 被执行 |

这里的关键不是 SQL 本身,而是 WAF 没有按照 Jackson 的最终转义解析逻辑还原字符串。

5.3 Fastjson \u escape 绕过

PDF 第 10 页讲的是 Fastjson 的 \u escape 场景。这里不完全是高 8 位截断,而是 Java 的 Character.digit() 过于宽松。

Character.digit(c, 16) 不只接受 ASCII 的 0-9a-fA-F,还可能接受其他文字系统里的数字字符,例如泰文数字、旁遮普数字、Vai 字符等。

结果是:

| WAF 视图 | Fastjson 视图 | | — | — | | 看不到普通 ASCII @type | Unicode 数字被当作 hex digit | | 认为 payload 不匹配反序列化特征 | 解析后仍得到 @type |

所以这个案例的原理应归类为“宽松 Unicode 数字解析导致的语义差异”。防御时不能只找 (byte) ch,还要审计协议解析中是否用了宽松字符分类 API。

5.4 Fastjson \x escape 绕过

PDF 第 11 页展示 \x escape:

{"\x4_type": ...}

解析逻辑类似:

int&nbsp;x_val = digits[x1] *&nbsp;16&nbsp;+ digits[x2];
char&nbsp;x_char = (char) x_val;

其中:

x1 = '4' &nbsp;-> 4
x2 = '_' &nbsp;-> 0
4 * 16 + 0 = 64
64 = '@'

于是 \x4_ 被解析成 @,最终键名变成 @type

这个案例说明:如果解码表对非法字符没有严格拒绝,而是给了默认值,就可能把“非法输入”转成“攻击者想要的合法字符”。

5.5 Tomcat filename* 文件上传绕过

PDF 第 12 页展示 Tomcat 文件上传场景:

filename*="UTF-8''1.陪sp"

WAF 检查文件名时看到:

1.陪sp

它不像 .jsp,因此可能放行。但 Tomcat 的 RFC2231 解析逻辑中存在如下形态:

if&nbsp;(c ==&nbsp;'%') {
&nbsp; &nbsp; ...
&nbsp; &nbsp; out.write((b1 << shift) | b2);
}&nbsp;else&nbsp;{
&nbsp; &nbsp; out.write((byte) c);
}

陪 = U+966A,低 8 位是 0x6A,也就是 j。因此后端保存时可能得到:

1.jsp

攻击视图:

| 阶段 | 文件名 | | — | — | | WAF / 上传扩展名检查 | 1.陪sp | | RFC2231 低字节写入后 | 1.jsp | | 文件系统落地 | JSP 文件 |

核心问题是:文件名作为安全边界被检查了一次,但真正落盘前发生了另一次不同语义的转换。

5.6 全角 URL 编码绕过

PDF 第 13 页讲全角 URL 编码:

%2e%2e%2f

其中:

| 全角 | ASCII | | — | — | | | 2 | | | e | | | f |

在某些 URL、URI、文件 URL 处理链中,全角字符经过归一化后会接近:

%2e%2e%2f -> ../

这个案例不是典型的高 8 位截断,而是“全角/半角归一化 + 多阶段解码”的语义差异。它与 Ghost Bits 一样,利用的是检查阶段和执行阶段的解释不一致。

5.7 Ghost-Bit URL Encoding:%2> 变成 .

PDF 第 14 页和后面的 Openfire、GeoServer 案例都围绕 %2> 展开。

正常 URL 百分号编码要求 % 后面跟两个十六进制字符,例如:

%2e = .

但某些 Jetty 相关的十六进制转换逻辑对非法 hex 字符不严格拒绝,而是通过位运算把它折叠成一个 0-15 的值。字符 > 被折叠成 E,于是:

%2> -> %2E -> .

这个点非常危险,因为 WAF 通常会认为 %2> 是非法编码或噪声,不会把它当作 .

5.8 Base64 解码绕过

PDF 第 15 页展示 JDK 内部若干 Base64 解码器的风险:

pem_convert_array[decode_buffer[i] & 255]

示例:

| Unicode 字符 | Unicode | 低 8 位 | ASCII | | — | — | — | — | | ō | U+014D | 0x4D | M | | Ř | U+0158 | 0x58 | X | | Ŗ | U+0156 | 0x56 | V | | Ŭ | U+016C | 0x6C | l |

因此:

ōŘŖŬ -> MXVl

而 MXVl 是 Base64 文本。WAF 看到的是 Unicode 字符,解码器索引表看到的是低 8 位后的 Base64 字母。

5.9 GeoServer CVE-2024-36401 绕过

PDF 第 16 页用 GeoServer 说明 %2> 的现实价值。很多 WAF 规则会拦截危险关键字,例如:

Runtime
java.lang.Runtime.getRuntime()

攻击者可以把 Runtime 中的 n 通过 URL 编码变形绕过,例如:

Ru%6>time

这里 %6> 在宽松 hex 解析中变成 %6e,也就是 n。WAF 原始视图里看不到 Runtime,后端 Jetty 解码后却得到 Runtime

视图差异:

| 层级 | 内容 | | — | — | | WAF | Ru%6>time ,不匹配 Runtime 或常见 %6e | | Jetty URL 解码 | Runtime | | GeoServer 表达式执行 | 危险调用链继续成立 |

5.10 Spring4Shell WAF 绕过

PDF 第 17-18 页展示 Spring4Shell 中对 class 关键字的绕过。

常见 Spring4Shell 攻击链中会出现:

class.module.classLoader.resources.context.parent.pipeline.first.directory

WAF 往往会拦截 class。Ghost Bits 版本把 class 变成低 8 位相同的 Unicode 字符:

㹣౬ᙡ⑳⑳ -> class

映射关系:

| 字符 | 低 8 位 | ASCII | | — | — | — | | 㹣 | 0x63 | c | | ౬ | 0x6C | l | | ᙡ | 0x61 | a | | ⑳ | 0x73 | s | | ⑳ | 0x73 | s |

PPT 展示的是 multipart Content-Disposition 中 name*= 这类位置。如果解析层最终把这些字符折叠成 class,WAF 就会出现漏报。

6. 真实漏洞案例一:Openfire CVE-2023-32315

6.1 漏洞背景

PDF 第 20-26 页讨论 Openfire 管理后台认证绕过。Openfire Admin Console 使用 AuthCheckFilter 做访问控制,并维护 Exclusion List。某些路径如 setup/setup-* 会被视为安装流程路径,从而跳过后续认证。

漏洞关键是:

AuthCheckFilter 判断当前路径是否命中排除规则
如果命中 doExclude = true
后续认证检查被跳过

如果攻击者能让过滤器以为请求仍在 setup 路径下,但底层容器最终把路径规范化到其他管理资源,就可能绕过认证。

6.2 传统 %u002e 路径穿越

PDF 回顾了传统 payload:

/setup/setup-s/%u002e%u002e/%u002e%u002e/log.jsp

其中:

%u002e = .
%u002e%u002e = ..

过滤器只检查:

..
%2e

但没识别 %u002e。Jetty 底层支持或参与了这类 Unicode dot 的解码和规范化,最终路径穿越到目标 JSP。

6.3 Ghost Bits 进阶:%2> 绕过

PDF 第 23 页给出进阶 payload 形态:

/setup/setup-s/%2>%2>/%2>%2>/log.jsp

因为:

%2> -> %2E -> .
%2>%2> -> ..

WAF 和 Openfire 过滤器看到 %2> 时,往往把它当成非法编码或普通字符串;Jetty 的宽松 hex 转换却把它变成 .

6.4 为什么 > 会等于 E

PDF 第 24-25 页给出数学推导。字符 > 的 ASCII 是:

0x3E = 0011 1110

Jetty 某个优化版 hex 转换算法通过位运算把字符压进 0-15:

c & 0x1f = 0x3E & 0x1F = 0x1E = 30
c >> 6 = 0
30 + (0 * 25) - 16 = 14
14 = 0xE

所以非法 hex 字符 > 被算法算成了合法 hex digit E

这个案例不是典型的 16 位到 8 位截断,而是“位运算优化缺少范围校验”引出的 Ghost Bits 风格问题。根因仍是同一个:解析器把不应接受的字符折叠成了安全敏感语义。

6.5 攻防意义

与 %u002e 相比,%2> 更难被传统规则捕捉:

| 维度 | %u002e | %2> | | — | — | — | | WAF 可见性 | 高,常被规则覆盖 | 低,像非法噪声 | | 依赖条件 | 需要 %u 解码支持 | 依赖宽松 hex 算法 | | 变形空间 | 较固定 | 可能存在 %2^%2~ 等类似折叠 |

结论:只做黑名单匹配 ..%2e%u002e 不够,必须使用与真实后端一致的严格规范化流程。

7. 真实漏洞案例二:Spring CVE-2025-41242

7.1 PDF 中的补丁线索

PDF 第 27-31 页讨论 Spring 相关任意文件读取,并提到 GitHub PR #34673 修复了 StringUtils.uriDecode 中的十六进制序列解码逻辑。

PPT 重点强调:

Java char 是 16 位
ByteArrayOutputStream.write 只接受/写出低 8 位
高位 Ghost Bits 被丢弃

7.2 Payload 为什么是“阮严灵丰丰甲来”

PDF 第 30 页给出映射:

| 字符 | Unicode | 截断后字节 | ASCII | | — | — | — | — | | 阮 | U+962E | 0x2E | . | | 严 | U+4E25 | 0x25 | % | | 灵 | U+7075 | 0x75 | u | | 丰 | U+4E30 | 0x30 | 0 | | 丰 | U+4E30 | 0x30 | 0 | | 甲 | U+7532 | 0x32 | 2 | | 来 | U+6765 | 0x65 | e |

所以:

阮严灵丰丰甲来 -> .%u002e
.%u002e -> ..

这里不是一步到位变成 ..,而是先通过低 8 位折叠得到 .%u002e,再由后续解析把 %u002e 变成 .

7.3 “时间差”和“双重解析”

PDF 第 31 页将核心称为 Time Gap & Double Parsing。

Spring 静态资源处理链大致有这样的防御逻辑:

// ResourceHttpRequestHandler#getResource
if&nbsp;(isInvalidPath(path))&nbsp;return&nbsp;null;

它检查的是当前字符串里是否有明显的:

../

但在检查阶段,路径可能仍是:

/.%u002e/

或者更早仍是 Unicode 形态,没有直接出现 ../。因此 Spring 判断“安全”。之后进入底层资源解析、容器解析或路径规范化时,%u002e 被识别成 .,最终折叠为:

../../

视图差异:

| 阶段 | 路径语义 | | — | — | | Spring 检查前 | Unicode / %u 混合形态 | | isInvalidPath | 没看到字面量 ../ | | 后续解析 | .%u002e 变 .. | | 文件系统访问 | 目录穿越 |

7.4 原理总结

这个漏洞成立需要多个条件同时满足:

  1. 输入路径可控。
  2. 上层检查发生在最终规范化之前。
  3. 中间存在 char 到 byte 的低位写入。
  4. 后续解析器会继续理解 %u002e 或类似编码。
  5. 最终资源解析允许访问静态目录外的文件。

所以防御重点不是简单加一个 contains("../"),而是统一、严格、单次地完成解码和路径规范化,然后再做边界检查。

8. 真实漏洞案例三:SMTP 协议注入

8.1 根因:ASCIIUtility 强制 cast

PDF 第 32-35 页讨论 SMTP 注入,提到 ASCIIUtility.java 中存在把字符强制转为 byte 的逻辑。开发者隐含假设输入字符串只包含 ASCII,但如果攻击者输入非 ASCII 字符,强制 cast 会把它变成低 8 位。

SMTP 是文本协议,命令边界依赖 CRLF:

\r = 0x0D
\n = 0x0A

Ghost Bits 字符可以制造 CRLF:

瘍 = U+760D -> 0x0D -> \r
瘊 = U+760A -> 0x0A -> \n

因此:

瘍瘊 -> \r\n

8.2 为什么 CRLF 会导致 SMTP 会话被接管

SMTP 会话大致是:

MAIL FROM:<sender>
RCPT TO:<recipient>
DATA
Subject: ...

body
.
QUIT

如果用户可控字段进入 RCPT TO、邮件头或其他 SMTP 命令位置,并且底层把 Ghost Bits 转成 CRLF,攻击者就可以提前结束当前命令,再注入新的 SMTP 命令或邮件内容。

PDF 中的抽象攻击效果是:

| 应用层以为 | SMTP 层实际可能看到 | | — | — | | 一个邮箱地址字符串 | 邮箱地址 + CRLF + 新命令 | | 一封正常注册邮件 | 被攻击者重写的邮件内容 | | 收件人受业务限制 | SMTP envelope 被重新指定 |

这类问题的危害大于普通邮件头注入,因为邮件可能由企业真实系统发出,天然通过 SPF、DKIM、DMARC。

8.3 Jira 案例:系统邮件劫持

PDF 第 36-37 页给出 Jira 案例,影响版本标注为 Jira v9.12.16,并提到 CVE-2025-57733 和 bug bounty。

攻击链:

危害:

| 维度 | 结果 | | — | — | | 发件人 | 企业真实 Jira 邮箱 | | 邮件认证 | SPF / DKIM / DMARC 可能全部正常 | | 收件人感知 | 像真实系统通知 | | 攻击类型 | 高可信钓鱼、通知伪造、业务流程劫持 |

这不是单纯“伪造 From 头”,而是借真实系统发出恶意内容。

8.4 Confluence 案例:绕过企业邮箱后缀限制

PDF 第 38-42 页讨论 Confluence 域名限制绕过。

常见业务规则:

只允许 @company.com / @confluence.com 邮箱注册

正常情况:

| 输入 | 结果 | | — | — | | [email protected] | 后缀不符合,注册失败 | | [email protected] | 通过校验,但攻击者收不到确认邮件 |

Ghost Bits 攻击思路:

hacker[GhostBits]@confluence.com

业务层只看字符串结尾,认为满足企业邮箱后缀;SMTP 传输层在 Ghost Bits 变成 CRLF 后,可能把真正 envelope 收件人切到攻击者邮箱。

本质差异:

| 层级 | 看到的身份 | | — | — | | Confluence 业务校验 | 以 @confluence.com 结尾 | | SMTP 传输 | 实际投递到攻击者邮箱 | | 攻击结果 | 攻击者拿到确认邮件并完成注册 |

9. CRLF 扩展案例

9.1 Apache HttpClient:Header CRLF 到请求走私

PDF 第 43-45 页讨论旧版 Apache HttpClient,标注:

HTTPCLIENT-1974 / HTTPCLIENT-1978
影响版本:<= 4.5.9

风险点是旧版本在构造 HTTP Header 时,可能把字符数组盲目转成字节。若应用把用户可控 token 放入请求头,例如:

X-Auth-Token: 1瘍瘊POST /newRequest HTTP/1.1...

底层低字节化后:

X-Auth-Token: 1\r\n
POST /newRequest HTTP/1.1...

可能造成前端代理与后端服务器对请求边界理解不一致:

| 组件 | 视图 | | — | — | | 前端代理 | 一个请求,一个 Header 值 | | 后端目标 | Header 被 CRLF 截断,后面像第二个请求 |

这就是 HTTP Request Smuggling / 请求走私的典型语义差。

9.2 JDK Native HttpServer:响应头注入到 XSS

PDF 第 46-47 页讨论 JDK 内置 com.sun.net.httpserver.HttpServer,标注 CVE-2026-21933

场景:

  1. 服务端把用户输入反射到响应头。
  2. 用户输入含 Ghost Bits 字符 瘍瘊
  3. 底层写响应时变成 CRLF。
  4. 攻击者插入新的响应头,例如 Content-Type: text/html
  5. 再插入空行和 HTML/JavaScript 内容。

抽象后的响应结构:

HTTP/1.1 200 OK
Custom-Header: Cu\r\n
Content-Type: text/html\r\n
Content-Length: ...\r\n
\r\n
<script>...</script>

这条链路从“响应头注入”升级成“响应体可控”,最终可能触发 XSS。

10. 自动化发现与更多组件案例

10.1 Secrux:自动捕捉 Ghost Bits

PDF 第 50 页介绍研究团队的自动化发现工具 Secrux,目标是捕捉 Java 生态中的潜在 Ghost Bits sink。

重点搜索模式包括:

(byte) ch
ch & 255
0xff & ch
DataOutputStream.writeBytes
OutputStream.write(int)
StringBufferInputStream.read
String.getBytes(int, int, byte[], int)
RandomAccessFile.writeBytes
URLDecoder.decode

但实际审计不能只看 grep 结果。必须判断:

| 问题 | 高风险答案 | | — | — | | 输入是否用户可控 | 是 | | 转换前是否做过安全校验 | 是 | | 转换后是否进入协议/路径/文件名/Header | 是 | | 低 8 位能否变成语法分隔符 | 是 | | WAF 或业务逻辑是否只看原始字符串 | 是 |

10.2 ActiveJ:HTTP CRLF

PDF 第 51 页提到 ActiveJ /cookie 的 CRLF 类结果。虽然幻灯片抽取文本较少,但结合上下文,它属于同类:HTTP Header/Cookie 位置接受用户输入,底层写出时发生低字节折叠,产生 CRLF。

风险模式:

用户输入 -> Cookie/Header -> char 低字节写入 -> CRLF -> Header 注入

10.3 Lettuce:Redis StringValue 注入

PDF 第 52 页展示 Lettuce / Redis 场景,关键代码形态:

static&nbsp;void&nbsp;writeString(ByteBuf target, String value)&nbsp;{
&nbsp; &nbsp; target.writeByte('$');
&nbsp; &nbsp; IntegerArgument.writeInteger(target, value.length());
&nbsp; &nbsp; target.writeBytes(CRLF);
&nbsp; &nbsp;&nbsp;for&nbsp;(int&nbsp;i =&nbsp;0; i < value.length(); i++) {
&nbsp; &nbsp; &nbsp; &nbsp; target.writeByte((byte) value.charAt(i));
&nbsp; &nbsp; }
&nbsp; &nbsp; target.writeBytes(CRLF);
}

这里的问题非常清楚:每个 char 都被 (byte) 写入 Redis RESP 字符串。

PPT 示例表达的是:应用层以为写入的是一个普通 JSON 字段值;低字节化后,攻击者输入可以闭合当前字符串,并注入新的 JSON 字段,例如把 target 从原值改成攻击者指定值。

视图差异:

| 层级 | 看到的值 | | — | — | | Java 应用 | Unicode 字符串 | | Lettuce 写入 | 每个 char 的低 8 位 | | Redis 存储/后续 JSON 解析 | 注入后的 JSON 结构 |

这个案例说明 Ghost Bits 不只影响 HTTP,也会影响 Redis、MQ、日志、缓存等任何“字符串转协议字节”的位置。

10.4 XMLWriter:标签名变形

PDF 第 53 页展示:

<陪>1ue</陪>

由于:

陪 -> 低 8 位 0x6A -> j

处理后可能变成:

<j>1ue</j>

如果业务基于 XML 标签名做白名单、黑名单、权限字段、配置解析,就可能出现标签语义被静默替换的问题。

10.5 Jodd:路径折叠到 /etc/passwd

PDF 第 54 页展示 Jodd 路径案例:

file:///ťŴţ%2fŰšųųŷŤ

低 8 位折叠后接近:

file:///etc/passwd

风险不是某个字符本身,而是路径字符串在不同组件之间经历了不同的解码/字节化逻辑。上层看是异常 Unicode 路径,底层文件访问看成敏感系统路径。

11. 常见危险低字节

审计 Ghost Bits 时,应重点关注低 8 位会变成以下字符的 Unicode:

| 目标字节 | Hex | 典型风险 | | — | — | — | | . | 0x2E | 路径穿越、扩展名绕过 | | / | 0x2F | 路径分隔 | | \ | 0x5C | Windows 路径分隔、转义 | | % | 0x25 | 二次 URL 编码 | | @ | 0x40 | @type 、邮箱语义 | | : | 0x3A | 协议字段分隔 | | ; | 0x3B | 参数分隔 | | ? | 0x3F | URL query 分隔 | | & | 0x26 | 参数拼接 | | = | 0x3D | 参数赋值 | | " | 0x22 | JSON/Header 字符串逃逸 | | ' | 0x27 | SQL/字符串逃逸 | | < | 0x3C | HTML/XML 注入 | | > | 0x3E | HTML/XML 注入,也可能参与宽松 hex | | \r | 0x0D | CRLF 注入 | | \n | 0x0A | CRLF 注入 |

攻击者可以为任意目标字节选择大量不同 Unicode 字符,因此黑名单列几个“已知中文字符”没有意义。正确做法是消除错误转换,或在协议边界严格限制字符集。

12. 漏洞成立条件

这里需要特别强调一点:不是所有非 ASCII 输入都会触发 Ghost Bits,也不是看到中文、全角字符就一定有漏洞。它通常要满足一串条件,尤其是“检查之后又变形”这一点。

通常需要以下条件叠加:

  1. 输入最终进入 Java String 或 char 处理流程。
  2. 上层校验允许非 ASCII 字符存在。
  3. 安全检查发生在最终字节化或最终规范化之前。
  4. 后续存在 (byte) chch & 0xffwriteBytes、宽松 hex/base64/URL 解码等行为。
  5. 转换后的字节进入安全敏感语法:路径、URL、HTTP Header、SMTP、JSON、XML、Redis、文件名、类加载、SQL 等。
  6. 下游组件继续解析这些字节,而不是把它们当作普通文本。

可以用一个简单公式帮助判断:

用户可控 + 视图差异 + 语法边界 = 高风险

其中“语法边界”最关键。低字节变成普通字母,很多时候只是数据污染;但如果低字节变成 ./%\r\n"@ 这类能改变解析结构的字符,风险就会明显上升。

13. 防御建议

13.1 开发者:不要把 char 当 byte

开发侧最重要的原则很朴素:不要自己偷懒把字符当字节。下面这些写法要格外小心:

out.write(ch);
out.write((byte) ch);
dataOutputStream.writeBytes(s);
int&nbsp;v = ch &&nbsp;0xff;

更稳妥的做法是按明确字符集编码:

byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
out.write(bytes);

如果协议字段只允许 ASCII,应先显式验证:

for&nbsp;(int&nbsp;i =&nbsp;0; i < s.length(); i++) {
&nbsp; &nbsp;&nbsp;if&nbsp;(s.charAt(i) >&nbsp;0x7f) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;throw&nbsp;new&nbsp;IllegalArgumentException("non-ASCII is not allowed here");
&nbsp; &nbsp; }
}

注意:如果协议本身就要求 ASCII,例如 HTTP/1 header 某些字段、SMTP 命令、Redis RESP 控制部分、URL percent-hex,强制 ASCII 白名单通常比“尽量兼容 Unicode”更安全。

13.2 解码器:非法输入必须拒绝

很多漏洞不是因为解析器“完全不懂”,而是因为它太宽容:非法输入也帮你猜一个结果。URL、Hex、Base64、JSON escape 解析器应该严格校验:

| 场景 | 应拒绝 | | — | — | | URL %xx | % 后不是两个 ASCII hex | | Hex | 非 [0-9A-Fa-f] | | Base64 | 非标准 alphabet,除非明确规范允许 | | JSON escape | 非标准 escape 或宽松 \x | | Unicode digit | 非协议允许的 ASCII 数字 |

危险做法:

非法字符 -> 默认 0
非法 hex -> 位运算折叠
Unicode digit -> 当作 ASCII digit
高位字符 -> & 255 查表

正确原则很简单:

不能解析就失败,不要猜测和容错。

13.3 校验顺序:先规范化,再校验,再执行

很多绕过都不是校验规则完全没有写,而是校验写早了。错误顺序是:

原始输入
-> 安全检查
-> URL decode / Unicode normalize / char-byte cast
-> 路径或协议执行

更合理的顺序是:

原始输入
-> 严格解码
-> Unicode 规范化
-> 路径/协议规范化
-> 安全校验
-> 执行

同时要避免“校验后还有第二次解码”。只要校验之后输入还能继续改变形态,绕过空间就会重新出现。

13.4 协议字段使用 allowlist

以下位置建议默认使用严格 allowlist:

| 位置 | 建议 | | — | — | | HTTP Header 名和值 | 禁止 CR/LF,必要时限制 ASCII 可见字符 | | SMTP 地址 / envelope | 禁止 CR/LF;域名 IDN 需 punycode 后处理 | | URL path | 严格 percent decode;拒绝非法编码;规范化后检查目录边界 | | 文件名 / filename* | 按 RFC 严格解析;落盘前再次检查扩展名和路径 | | JSON key | 禁止宽松 escape;安全逻辑不要基于解析前文本 | | XML tag | 不允许通过低字节转换生成标签名 | | Redis / MQ / 文本协议 | 不要手写字符到协议字节;使用库的安全编码路径 | | 响应头 | 用户输入不得直接进入 header;禁止 CR/LF |

13.5 WAF / 安全产品:模拟真实后端解析链

对 WAF 和安全产品来说,只看原始请求是不够的。Ghost Bits 类问题最麻烦的地方就在于:原始视图常常看不出恶意,后端视图才会暴露真正 payload。更有效的检测包括:

  1. 对 URL、Unicode、全角/半角、%u、Base64、JSON escape 做多视图归一化。
  2. 识别高位非零但低 8 位为危险字符的 Unicode。
  3. 对 %2>%6> 等非法 percent-hex 做后端特定模拟。
  4. 针对 Jetty、Tomcat、Spring、Jackson、Fastjson、Angus Mail 等组件维护解析差异模型。
  5. 对“原始视图安全,但低字节视图危险”的请求直接告警。

可以构造一个检测视图:

lowByteView(s) = 对每个 char 取 char & 0xff 后组成的字节串

如果 lowByteView 中出现 ../\r\n@typeRuntimeunion select 等敏感语义,而原始字符串里没有,就应该把它当成高风险信号。

13.6 企业排查优先级

按 PDF 中点名的攻击面,建议优先排查:

| 优先级 | 组件/场景 | 原因 | | — | — | — | | 高 | 邮件发送链路、Angus Mail / Jakarta Mail | 可能造成官方系统邮件劫持 | | 高 | Spring / Jetty 静态资源与路径解析 | 可能导致任意文件读取、路径穿越 | | 高 | Tomcat multipart filename* | 文件上传绕过影响直接 | | 高 | HTTP Header 写入、Apache HttpClient、JDK HttpServer | CRLF、请求走私、XSS | | 中高 | JSON 解析器 Jackson / Fastjson | WAF 绕过、反序列化关键字绕过 | | 中高 | Redis 客户端 Lettuce、文本协议客户端 | 协议注入、缓存污染 | | 中 | XMLWriter、Jodd、路径工具库 | 标签/路径语义变形 | | 中 | Base64、URL、Hex 自研解码器 | 宽松解析容易形成通用绕过 |

14. 代码审计清单

14.1 搜索关键词

实际排查时,可以先用这些关键词定位潜在 sink:

(byte) ch
(byte) c
& 0xff
& 255
writeBytes(
OutputStream.write(
ByteArrayOutputStream.write(
DataOutputStream.writeBytes
RandomAccessFile.writeBytes
StringBufferInputStream
getBytes(int,
URLDecoder.decode
Character.digit
convertHexDigit
fromHex
uriDecode

14.2 判断风险的问题

定位到代码后,逐项问:

| 问题 | 说明 | | — | — | | 输入是否用户可控 | HTTP 参数、Header、文件名、邮件地址、JSON 字段、XML、Redis 值 | | 是否经过了安全检查 | WAF、黑名单、后缀检查、路径检查、域名后缀检查 | | 转换是否发生在检查之后 | 检查后再变形最危险 | | 转换后是否进入协议语法 | SMTP、HTTP、URL、文件系统、JSON、XML、Redis | | 低 8 位是否可形成边界字符 | ./%\r\n"'@: | | 是否存在二次解析 | %u002e 、Base64、JSON escape、URL decode |

只有当 source、sink、语法边界能连起来,才有进一步验证的价值。单独看到一个 (byte) ch 不要急着下结论,但也不要轻易放过它。

14.3 测试思路

建议加入“差异测试”。也就是不要只看输入原文,而是同时看它在不同阶段会变成什么:

原始输入视图
低 8 位视图
最终解析视图

如果三个视图不一致,就要继续分析这种差异会不会影响安全边界。

典型测试字符:

| 目标 | 字符 | | — | — | | . | | | % | | | u | | | 0 | | | 2 | | | e | | | j | | | CRLF | 瘍瘊 |

更系统的做法是针对每个危险低字节,自动生成多个 0x0100 + b0x0200 + b0x4e00 + b 等候选字符,喂给组件做 differential testing,看组件是否出现“检查视图”和“执行视图”不一致。

15. 对 PDF 观点的技术评价

这份议题最有价值的地方,不是单独列了多少个 CVE,而是把一堆看似不相关的问题统一到了一个模型下:

字符级安全检查
vs
字节级真实执行

它揭示了 Java 生态里一个长期存在但容易被忽视的问题:很多库、框架和老 API 诞生时,默认世界大概是 ASCII 的;但现代 Web 输入天然是 Unicode 的。只要某个组件还在用低字节写入、宽松 hex、宽松 digit、全角归一化,就可能出现跨层语义差。

也要客观看待一点:PPT 中的所有例子并不是同一种底层 bug。它们至少包含三类:

  1. 真实的 char -> byte 高位截断。
  2. 位运算优化导致非法字符折叠成合法 hex。
  3. Unicode 数字或全角字符归一化导致的宽松解析。

把它们放在 Cast Attack 这个大类下是合理的,因为利用效果是一致的:让安全检查看到 A,让真实执行看到 B。

16. 最终总结

最后再把整件事收回来。Ghost Bits / Cast Attack 的核心公式是:

Java 16 位 char
-> 被错误当成 8 位 byte
-> 高位静默丢失
-> 低位变成危险语法字符
-> 安全检查与真实执行不一致

它可以引发:

| 类型 | 结果 | | — | — | | WAF 绕过 | SQL、RCE、反序列化关键字被隐藏 | | 文件上传绕过 | 1.陪sp 落地为 1.jsp | | 路径穿越 | Unicode/URL 混合形态最终变 ../ | | 认证绕过 | Openfire setup exclusion 被路径规范化绕过 | | 任意文件读取 | Spring 静态资源检查与执行路径不一致 | | SMTP 注入 | 邮件会话被 CRLF 切分和重写 | | 官方钓鱼 | Jira/Confluence 由真实系统发出攻击者邮件 | | 请求走私 | Header 中 Ghost CRLF 让一个请求变两个 | | XSS | 响应头注入扩展成 HTML 响应 | | Redis/XML/路径污染 | 协议值、标签名、文件路径被静默改写 |

最重要的防御原则仍然是这一句:

不要让“检查时看到的字符串”和“执行时使用的字节”不一致。

落地到工程实践,可以拆成下面几件事:

  1. 禁止在协议边界手写 (byte) char
  2. 用明确字符集编码,ASCII 协议字段先做 ASCII allowlist。
  3. 解码器严格拒绝非法输入,不做宽松折叠。
  4. 所有安全校验必须发生在最终规范化之后。
  5. WAF 和安全产品要模拟真实后端解析链。
  6. 企业应把老 API、路径解析、邮件发送、Header 写入、文件上传、JSON/XML/Redis 序列化作为重点排查面。

PPT 最后想表达的观点也很明确:现在看到的只是开始。只要 Java 生态中仍然存在“Unicode 字符串 -> 低 8 位协议字节”的错误路径,Ghost Bits 就可能继续出现在新的组件和新的漏洞链里。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:哈拉少安全小队 《影响面较大的新型 WAF 绕过详细解读》

评论:0   参与:  0