文章总结: 本文详细解读了GhostBits漏洞的成因与影响,该漏洞源于Java字符(char)到字节(byte)转换时的高8位截断,导致安全检查与底层执行语义不一致。攻击者可利用此绕过WAF、文件上传校验、路径穿越等防御机制,具体案例包括BCEL、Jackson、FastJSON等组件的绕过。文档还分析了相关风险API及宽松解析问题,并提供了防御建议。 综合评分: 87 文章分类: web安全,漏洞分析,应用安全,渗透测试,安全开发
影响面较大的新型 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
A["攻击者提交 Unicode 字符"] --> B["WAF / 业务校验"]
B --> C["看起来不是 SQL / 路径 / CRLF / @type"]
C --> D["进入 Java 组件或协议库"]
D --> E["char 被截断或宽松折叠成 byte"]
E --> F["低 8 位变成危险 ASCII / 控制字符"]
F --> G["协议、路径、JSON、SMTP、Redis 等执行真实含义"]
这就是 Cast Attack 的核心:借助类型转换、位运算或宽松解码,让上层看起来无害的字符,在底层变成有语法意义的危险字节。
3. 容易出问题的代码模式
议题里反复出现的风险模式包括:
| 模式 | 风险点 |
| — | — |
| (byte) ch | 直接把 16 位 char 缩窄成 8 位 |
| ch & 0xff / ch & 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 = new ByteArrayOutputStream();
CharArrayReader car = new CharArrayReader(chars);
JavaReader jr = new JavaReader(car);
while ((ch = jr.read()) >= 0) {
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 sHexValues[ch & 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 x_val = digits[x1] * 16 + digits[x2];
char x_char = (char) x_val;
其中:
x1 = '4' -> 4
x2 = '_' -> 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 (c == '%') {
...
out.write((b1 << shift) | b2);
} else {
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 | 2 |
| e | e |
| f | 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 (isInvalidPath(path)) return null;
它检查的是当前字符串里是否有明显的:
../
但在检查阶段,路径可能仍是:
/.%u002e/
或者更早仍是 Unicode 形态,没有直接出现 ../。因此 Spring 判断“安全”。之后进入底层资源解析、容器解析或路径规范化时,%u002e 被识别成 .,最终折叠为:
../../
视图差异:
| 阶段 | 路径语义 |
| — | — |
| Spring 检查前 | Unicode / %u 混合形态 |
| isInvalidPath | 没看到字面量 ../ |
| 后续解析 | .%u002e 变 .. |
| 文件系统访问 | 目录穿越 |
7.4 原理总结
这个漏洞成立需要多个条件同时满足:
- 输入路径可控。
- 上层检查发生在最终规范化之前。
- 中间存在
char到byte的低位写入。 - 后续解析器会继续理解
%u002e或类似编码。 - 最终资源解析允许访问静态目录外的文件。
所以防御重点不是简单加一个 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。
场景:
- 服务端把用户输入反射到响应头。
- 用户输入含 Ghost Bits 字符
瘍瘊。 - 底层写响应时变成 CRLF。
- 攻击者插入新的响应头,例如
Content-Type: text/html。 - 再插入空行和 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 void writeString(ByteBuf target, String value) {
target.writeByte('$');
IntegerArgument.writeInteger(target, value.length());
target.writeBytes(CRLF);
for (int i = 0; i < value.length(); i++) {
target.writeByte((byte) value.charAt(i));
}
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,也不是看到中文、全角字符就一定有漏洞。它通常要满足一串条件,尤其是“检查之后又变形”这一点。
通常需要以下条件叠加:
- 输入最终进入 Java
String或char处理流程。 - 上层校验允许非 ASCII 字符存在。
- 安全检查发生在最终字节化或最终规范化之前。
- 后续存在
(byte) ch、ch & 0xff、writeBytes、宽松 hex/base64/URL 解码等行为。 - 转换后的字节进入安全敏感语法:路径、URL、HTTP Header、SMTP、JSON、XML、Redis、文件名、类加载、SQL 等。
- 下游组件继续解析这些字节,而不是把它们当作普通文本。
可以用一个简单公式帮助判断:
用户可控 + 视图差异 + 语法边界 = 高风险
其中“语法边界”最关键。低字节变成普通字母,很多时候只是数据污染;但如果低字节变成 ., /, %, \r\n, ", @ 这类能改变解析结构的字符,风险就会明显上升。
13. 防御建议
13.1 开发者:不要把 char 当 byte
开发侧最重要的原则很朴素:不要自己偷懒把字符当字节。下面这些写法要格外小心:
out.write(ch);
out.write((byte) ch);
dataOutputStream.writeBytes(s);
int v = ch & 0xff;
更稳妥的做法是按明确字符集编码:
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
out.write(bytes);
如果协议字段只允许 ASCII,应先显式验证:
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) > 0x7f) {
throw new IllegalArgumentException("non-ASCII is not allowed here");
}
}
注意:如果协议本身就要求 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。更有效的检测包括:
- 对 URL、Unicode、全角/半角、
%u、Base64、JSON escape 做多视图归一化。 - 识别高位非零但低 8 位为危险字符的 Unicode。
- 对
%2>、%6>等非法 percent-hex 做后端特定模拟。 - 针对 Jetty、Tomcat、Spring、Jackson、Fastjson、Angus Mail 等组件维护解析差异模型。
- 对“原始视图安全,但低字节视图危险”的请求直接告警。
可以构造一个检测视图:
lowByteView(s) = 对每个 char 取 char & 0xff 后组成的字节串
如果 lowByteView 中出现 ../、\r\n、@type、Runtime、union 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 + b、0x0200 + b、0x4e00 + b 等候选字符,喂给组件做 differential testing,看组件是否出现“检查视图”和“执行视图”不一致。
15. 对 PDF 观点的技术评价
这份议题最有价值的地方,不是单独列了多少个 CVE,而是把一堆看似不相关的问题统一到了一个模型下:
字符级安全检查
vs
字节级真实执行
它揭示了 Java 生态里一个长期存在但容易被忽视的问题:很多库、框架和老 API 诞生时,默认世界大概是 ASCII 的;但现代 Web 输入天然是 Unicode 的。只要某个组件还在用低字节写入、宽松 hex、宽松 digit、全角归一化,就可能出现跨层语义差。
也要客观看待一点:PPT 中的所有例子并不是同一种底层 bug。它们至少包含三类:
- 真实的
char -> byte高位截断。 - 位运算优化导致非法字符折叠成合法 hex。
- 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/路径污染 | 协议值、标签名、文件路径被静默改写 |
最重要的防御原则仍然是这一句:
不要让“检查时看到的字符串”和“执行时使用的字节”不一致。
落地到工程实践,可以拆成下面几件事:
- 禁止在协议边界手写
(byte) char。 - 用明确字符集编码,ASCII 协议字段先做 ASCII allowlist。
- 解码器严格拒绝非法输入,不做宽松折叠。
- 所有安全校验必须发生在最终规范化之后。
- WAF 和安全产品要模拟真实后端解析链。
- 企业应把老 API、路径解析、邮件发送、Header 写入、文件上传、JSON/XML/Redis 序列化作为重点排查面。
PPT 最后想表达的观点也很明确:现在看到的只是开始。只要 Java 生态中仍然存在“Unicode 字符串 -> 低 8 位协议字节”的错误路径,Ghost Bits 就可能继续出现在新的组件和新的漏洞链里。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:哈拉少安全小队 《影响面较大的新型 WAF 绕过详细解读》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论