Cast Attack:Java 中 Ghost Bits(幽灵比特)引发的新型安全威胁——Java 生态里被忽视的底层风险引发一系列绕过 在 Java 安全研究里反序列化、表达式注入、模板注入和类加载问题通常更容易得到关注。Black Hat Asia 2026 提出的Ghost Bits讨论的则是另一类问题它不对应某一个具体 CVE而是一种可能长期潜伏在 Java 处理链里的底层失配。它讨论的核心现象很简单上层安全检查看到的字符串和底层最终执行的字节未必是同一份数据。一旦这种失配出现在关键边界位置后果就不再局限于单点绕过而可能扩展为WAF 绕过文件上传绕过路径穿越和任意文件读取认证绕过SMTP 注入与官方邮件劫持企业邮箱限制绕过HTTP Request SmugglingHeader 注入、XSS、Redis / XML 协议污染研究者把这类由类型转换引出的利用链命名为Cast Attack。问题并不出在少见的危险函数上反而常常藏在最普通的类型转换和编码处理里。本文基于 Black Hat Asia 2026《Cast Attack: A New Threat Posed by Ghost Bits in Java》以及相关补充资料整理重点讨论它在 Java 生态里的成因、利用路径和工程防御思路。一、Ghost Bits 到底是什么先看一个最普通的 Java 类型转换charch陪;byteb(byte)ch;System.out.println((int)b);这里发生的事情是陪 U966A 十六进制 0x966A (byte) 陪 0x6A 0x6A ASCII j应用层看到的是陪某些底层处理链最终执行的却可能是j。这就是 Ghost Bits 的核心含义char - byte过程中高 8 位被静默丢弃只保留低 8 位被丢掉的那部分高位就是研究者所说的 “Ghost Bits”。二、Java 为什么会出现这个问题2.1char是 16 位byte只有 8 位Java 的char是 16 位 Unicode 代码单元而byte只有 8 位类型位宽说明char16 bit保存 Unicode 字符byte8 bit只能容纳一个字节当代码写成(byte) ch时本质是在做一次从 16 位到 8 位的截断。例如下面这些字符的码位都明显超过了0xFF字符Unicode十六进制陪U966A0x966A阮U962E0x962E严U4E250x4E25瘍U760D0x760D瘊U760A0x760A2.2 截断不是编码而是“只保留低 8 位”以陪为例陪 0x966A 高 8 位 0x96 低 8 位 0x6A转换后(byte) 陪 0x6A这里没有发生 UTF-8、UTF-16 之类的字符编码转换只是单纯把高 8 位扔掉了。2.3 风险不在“中文”而在语义分离Ghost Bits 很容易让人误解成“中文字符有风险”。更准确的说法是上层按字符语义做校验下层按字节语义继续执行检查和执行落在两个不同的语义空间里问题不在字符本身而在字符语义和字节语义分离。换个更直观的比喻人工核验看到的是完整证件信息闸机却只拿到了编号的最后两位。输入还是同一张证件但系统两端处理的内容已经不一致。三、漏洞为什么能成立Ghost Bits 的攻击链通常可以概括为攻击者输入 Unicode 字符 ↓ WAF / 业务校验看到的是安全字符串 ↓ 检查通过 ↓ 底层 Java 代码发生 char - byte 截断 ↓ 低 8 位折叠成危险 ASCII 字节 ↓ 真实漏洞被触发一个非常直观的例子是文件上传1.陪sp如果某一层校验只看到了陪它会判断“这不是.jsp”但在底层保存时如果发生强制char - byte截断陪可能折叠成j最终文件名就会落成1.jspSMTP 注入同理。字符瘍和瘊的低 8 位分别是0x0D、0x0A也就是瘍瘊 - \r\n一旦这对字符进入协议边界相关字段输入的作用就不再是普通文本而是协议边界本身。四、哪些 Java 写法风险最高Ghost Bits 相关漏洞并不总是出现在显眼的危险函数里很多时候恰恰来自这些“看起来很普通”的写法(byte)ch ch0xffch2550xffch baos.write(ch)DataOutputStream.writeBytes(Strings)OutputStream.write(int)StringBufferInputStream.read()String.getBytes(int,int,byte[],int)RandomAccessFile.writeBytes()URLDecoder.decode()它们的共同点通常有两类把字符直接当字节处理。只保留低 8 位或者依赖默认编码继续处理。如果这些写法恰好出现在文件名、URL、Header、SMTP 地址、Redis 指令、XML/HTML 输出等边界位置风险会迅速扩大。五、常见的 Ghost Bits 映射攻击者不会随便选 Unicode 字符而会专门挑那些低 8 位刚好等于目标 ASCII 的字符。Unicode 字符低 8 位折叠后字符陪(U966A)0x6Aj阮(U962E)0x2E.严(U4E25)0x25%灵(U7075)0x75u丰(U4E30)0x300甲(U7532)0x322来(U6765)0x65e瘍(U760D)0x0D\r瘊(U760A)0x0A\n因此阮严灵丰丰甲来会折叠成.%u002e瘍瘊会折叠成\r\n前者可以继续参与路径穿越后者则直接变成协议注入的关键边界。六、代表性攻击案例Ghost Bits 不是停留在概念层面的研究问题。类似现象已经在多类生产级组件里出现过例如OpenfireSpring FrameworkJettyTomcatFastjsonJacksonGeoServerJiraConfluenceApache HttpClientAngus Mail / Jakarta Mail6.1 Tomcat 文件上传绕过在上传场景里如果攻击者提交filename*UTF-81.陪spWAF 或业务层看到的可能只是1.陪sp因此不会把它识别为.jsp。但 Tomcat 某些处理链会继续进入RFC2231Utility.fromHex()一类逻辑其中关键代码类似out.write((byte)c);这里不是正常的字符编码过程而是直接做char - byte截断。结果陪被折叠成j最终文件落地为1.jsp这类问题的本质是检查阶段看的是字符保存阶段执行的是字节。6.2 Fastjson / Jackson 的 JSON 绕过Ghost Bits 不只影响文件名也会影响 JSON 相关场景。一类典型问题来自 WAF 与解析器对同一段输入理解不一致。例如Fastjson 某些\uXXXX解析路径会使用Character.digit()这个 API 不只识别 ASCII 数字也会识别部分 Unicode 数字字符于是攻击者可以把敏感字段伪装成“看起来不像type的输入”更具体地说Character.digit()并不只识别 ASCII0-9还会识别部分 Unicode 数字字符。例如研究里就提到过๐ ੦ ꘠这些字符在某些上下文里仍可能被当成十六进制或数字字符继续处理。于是攻击者可以构造表面上不像type的输入例如{\u꘠๐๔੦type:...}WAF 看到的并不是字面上的type但解析器在后续解码链里可能仍恢复出敏感键名。类似地Jackson 某些路径如果使用了ch 255一类低字节折叠逻辑就可能让 WAF 看不到真实 SQL 关键字而后端执行时恢复出真正的危险内容。比如丰、耳、失的低字节分别可以对应0、3、1从而把表面上看不出意义的字符串逐步还原成真正的数字片段再参与后续 SQL 语义拼接。它和文件上传绕过的底层逻辑是一致的都是上层按字符检查、下层按字节恢复。这些案例说明Ghost Bits 并不是只能制造“乱码”它同样能成为 JSON、反序列化和注入场景中的隐藏通道。6.3 URL 解码、认证绕过与路径穿越Ghost Bits 在 URL 和路径处理链里尤其麻烦因为这类链路往往天然包含多次解码和多层解析。研究中一个很典型的技巧是利用 JettyTypeUtil.convertHexDigit()的位运算行为让%2 - %2E - .它背后的关键细节是Jetty 为了性能使用了类似下面的位运算intd((c0x1f)((c6)*0x19)-0x10);对字符来说 0x3E 0x3E 0x1F 30 30 - 16 14 14 十六进制 E于是在这个计算路径里会被当成E也就让%2最终等价于%2E。这样一来原本不显眼的%2%2就可能被后端继续恢复成..这条思路被用于多个实际案例中Openfire 认证绕过/setup/setup-s/%2%2/%2%2/log.jspSpring 静态资源路径穿越与任意文件读取研究中提到的 CVE-2025-41242Spring 这个案例很能说明问题。上层检查认为输入路径不包含../但下层 Jetty 在后续解析中却能把构造字符恢复为真正的..。关键不在于某一层“完全失效”而在于Spring 在检查字符串Jetty 在执行字节和解码后的结果。这个案例里一个代表性 payload 是阮严灵丰丰甲来它不是随机乱码而是精确构造出来的低字节映射字符低 8 位折叠后阮0x2E.严0x25%灵0x75u丰0x300丰0x300甲0x322来0x65e所以它会先折叠成.%u002e再进一步恢复成..研究里提到的 Spring 利用链大致可以概括成两步Spring 侧会经过StringUtils.uriDecode()、ResourceHttpRequestHandler#getResource()并在isInvalidPath(path)这类检查中判断当前字符串是否包含../或..\\。请求继续进入 Jetty 侧的PathResource#resolve()和 URL Decode 链路后Ghost Bits 才开始把构造字符还原成真正的路径穿越片段。换句话说检查阶段看到的是阮严灵丰丰甲来执行阶段走到的却是.%u002e - ..。从工程视角看这就是字符层面的 TOCTOU。6.4 SMTP 注入、官方邮件劫持与域限制绕过Ghost Bits 在邮件场景里通常比普通 Web 漏洞更难处理因为它借用的是系统自身的可信身份。以 Angus Mail / Jakarta Mail 场景为例如果代码中存在类似bytes[i](byte)chars[i];攻击者就可以构造把瘍瘊折叠成\r\n的输入从一个普通收件人地址开始向底层 SMTP 会话里插入新的命令边界。这样带来的后果包括SMTP 命令注入借官方系统发出伪造内容邮件让收件人看到“发件人真实、域名真实、链路真实”的钓鱼邮件绕过企业邮箱注册限制把外部地址伪装成内部地址处理因此Jira 邮件劫持、Confluence 域名限制绕过这类问题会显得格外棘手。攻击者并不是伪造一个假系统而是在借用你自己的真实系统完成攻击。这里最关键的协议事实是SMTP 是纯文本协议命令之间就是靠\r\n分隔的。例如RCPT TO: DATA QUIT一旦应用把攻击者输入写进 SMTP 层而底层又出现了bytes[i](byte)chars[i];这样的强制截断那么瘍 - 0x0D - \r 瘊 - 0x0A - \n这意味着攻击者有机会把一个“看起来像邮箱地址的字符串”改写成一整段新的 SMTP 会话。例如attackerqq.com RCPT TO:attackerqq.com DATA Subject: PWNED I LOVE YOU! . QUITJira 这个案例的麻烦之处在于这封邮件不是伪造出来的而是系统自己发出去的因此 SPF、DKIM、DMARC 这类传统邮件信任机制也会一起为它背书。Confluence 场景的问题则在于应用层可能只检查邮箱字符串是否以company.com结尾但 SMTP 层在遇到被恢复出的\r\n后可能早就把真正收件人截断到了攻击者自己的地址上。于是上层看起来“是企业邮箱”底层实际投递的却不是同一个收件人。七、为什么它会演变成协议级漏洞Ghost Bits 的影响不只体现在“字符变了”更麻烦的是它可能恢复出协议边界字符。最典型的一组就是\r\n一旦攻击者能够通过 Unicode 输入恢复出\r、\n、.、/、%、:这些边界相关字节攻击面就会迅速从业务逻辑问题升级为协议级问题。7.1 Header Injection假设代码写了response.setHeader(X-User,username);开发者以为username只是普通文本如果它在底层被折叠成admin\r\nSet-Cookie: admintrue这时攻击者做的就不再是“输入一个用户名”而是在插入一条新的 HTTP Header。7.2 HTTP Request Smuggling在代理、WAF、网关和后端服务器之间请求边界本来就可能存在理解差异Ghost Bits 会把这种差异进一步放大。典型表现是前置组件只看到了表面字符串后端组件在继续解码、折叠或重建字节流后恢复出新的 Header 或新的请求边界最终把额外请求偷偷拼接进后续流量中Ghost Bits 与 Request Smuggling 的关系本质上是同一个问题前后层对“请求到底长什么样”没有形成统一语义。研究里还提到过 Apache HttpClient 相关链路。问题不一定发生在最显眼的业务代码里也可能藏在 Header 拼接、writeBytes()、OutputStream.write()这类基础设施代码里。它们本身看起来不像漏洞入口但一旦参与了边界重建最终影响的就是整个请求的语义。7.3 Redis / XML / XSS这类问题并不局限于 HTTP。Redis 的 RESP 协议依赖\r\n作为边界注入后可以开启新的命令XML / HTML 如果先做了转义、后面又发生一次错误的char - byte截断转义就可能被“重新打穿”最终攻击面可以落到 Redis 指令注入、XML 污染甚至 XSS以 Redis 为例它的 RESP 协议天然依赖换行边界*2\r\n $3\r\n GET\r\n $3\r\n key\r\n如果攻击者能够额外恢复出一组\r\n那就不只是污染一个参数而是有机会“新开一条命令”。XML / HTML 场景则说明了另一个审计难点你明明在前面看到了escapeXml()一类转义动作但如果后面又发生一次错误截断前面的转义可能就白做了。真正需要追问的不只是“有没有 escape”而是 “escape 之后又发生了什么”。换句话说Ghost Bits 攻击的不是某一个业务规则而是所有依赖“文本边界”的解析器。八、为什么这类问题难发现8.1 它通常藏在多层解析链里一个真实请求往往会经过浏览器 - CDN - WAF - Nginx - Spring MVC - Tomcat / Jetty - 业务代码 - Mail / Redis / DB这条链路里每一层都可能做解码、规范化、转义、校验或重建。最麻烦的地方不在于“某一层完全错误”而在于每一层理解的对象都不完全相同。从工程上看这类问题很像 TOCTOU检查时看到的是一种语义使用时执行的是另一种语义这也是很多 Ghost Bits 问题难排查的原因不是某一行代码单独写错了而是整条处理链各自都“看起来合理”最后组合出了漏洞。8.2 老 API 带来了历史债务很多高风险 API 诞生于默认 ASCII 或单字节思维还很强的时代例如DataOutputStream.writeBytes()RandomAccessFile.writeBytes()StringBufferInputStreamString.getBytes(int,int,...)当年的“char大致等于byte”在今天的 Unicode、国际化和多协议环境里已经不成立了但这些 API 仍然活在大量历史系统里。8.3 WAF 很难完整模拟真实后端WAF 看到的通常只是原始请求而真正决定安全的是最终执行结果。中间如果还叠加了URL DecodeUnicode DecodeCharset ConvertPath NormalizeFramework Re-Parse协议边界重建那么 WAF 做的往往不是精确复现而是在猜后端最终会怎么理解这段输入。这决定了它天然会有盲区。8.4 日志和监控经常记录不到最终态很多系统日志只会记原始输入例如看到的是1.陪sp实际落地的是1.jsp或者看到的是瘍瘊实际执行的是\r\n这样一来攻击已经发生审计与蓝队复盘却未必能从日志中直接看出原因。九、如何在工程上规避 Ghost Bits9.1 先统一语义再做安全判断错误顺序通常是先校验 - 再解码 - 再规范化 - 再执行正确顺序应该是Decode - Canonicalize - Normalize - Validate - Execute先把输入还原成最终会被执行的那份语义再决定它是否安全否则检查和执行很容易错位。9.2 禁止隐式char - byte截断对安全边界最直接的工程要求就是不要再让代码偷偷发生低 8 位折叠。高危写法out.write((byte)c);更安全的做法是显式指定编码把整个字符串按统一字符集编码成字节byte[]bytesvalue.getBytes(StandardCharsets.UTF_8);out.write(bytes);这里的关键差别在于UTF-8 是编码(byte) ch是截断两者不是一回事。9.3 禁用历史危险 API建议在代码规范或静态扫描规则里直接限制以下 APIDataOutputStream.writeBytes()RandomAccessFile.writeBytes()StringBufferInputStreamString.getBytes(int,int,...)OutputStream.write(int)URLDecoder.decode(String)// 依赖默认编码的旧用法String(byte[])// 默认 Charset如果必须做字符与字节转换就显式指定StandardCharsets.UTF_8之类的固定编码不要依赖默认值。9.4 对协议边界字段做严格白名单不是所有字段都必须接受全量 Unicode。对于协议边界相关字段更现实的做法通常是明确限制字符集例如文件名Header 名和值Email 本地部分URL Path 片段Redis KeySMTP AddressXML TagMultipart Boundary这类字段最怕“尽量兼容”的容错解析因为容错本身经常就是问题的起点。9.5 安全设备和审计要尽量贴近真实解析链如果安全产品只做关键字匹配它看到的永远只是表层输入。更可靠的做法是尽量复现后端的真实解码顺序在规范化之后再做检测检查框架真正看到的 Header、Path 和协议字节流同样代码审计也不能只盯着Runtime.exec()这种显眼 Sink像out.write((byte) c)、writeBytes()这类看起来普通的写法也应该进入高风险视野。9.6 日志要记录“最终态”而不只是“原始态”如果只记录原始输入很多 Ghost Bits 攻击在复盘时会变成“现象存在但链路看不懂”。更有价值的记录方式包括原始输入Canonical FormDecode 后结果Normalize 后结果最终发往协议层或落盘的字节内容例如日志不能只写1.陪sp最好还能看到它在最终链路里是否已经变成1.jsp也不能只记录瘍瘊这样的原始字符而应该知道协议层最终执行的是不是\r\n。这部分可以收敛成一句工程原则不要在尚未完成最终解析的数据上做安全判断。十、总结Ghost Bits 值得关注不是因为它提供了某个新奇技巧而是因为它把一类更普遍的问题摆到了台面上当检查语义和执行语义不一致时再严密的规则也可能失效。从文件上传、路径穿越、认证绕过到 SMTP 注入、Request Smuggling、Redis 协议污染这些看起来完全不同的漏洞背后其实可能是同一种底层失配上层检查的是字符下层执行的是字节多层组件对同一份输入的理解并不一致Cast Attack 值得重视也正是因为这一点。它不只对应某一个具体漏洞点而是会在类似条件反复出现时不断生成新变种隐式char - byte截断、重复解码、协议边界重建再加上检查与执行分属不同层组合起来就足以形成新的攻击面。虽然这次研究聚焦 Java但它反映的并不是 “Java 特有漏洞”而是编码边界、语义边界和协议边界失配共同导致的问题。Java 只是因为历史 API、框架生态和容器链路更复杂更容易把这类问题系统性地放大。如果把全文压缩成一句话可以这样概括安全判断应当建立在最终会被执行的那份数据上而不是某一层暂时看到的字符串。