使用tabby对CVE-2022-39198的挖掘尝试
新年第一次做正事 = =
一些废话
CVE-2022-39198大概是在去年11月下旬公布的。当时和几位同学看了很久,未果,寻被学校赶回家,后遂无问津者。时隔一个月,终于从期末和omicron的重创中缓过来。现在网上对这个漏洞的分析已经十分详细,Eki博士也现场演示了其复现过程。所以我也算是终于可以把这个史前大坑填了。
另一个坑是tabby。当时在挖这条链的时候,最后就是卡在寻找getter2RCE的sink类。我当时用codeql来跑,其下头程度可想而知。由于codeql需要编译,那么每一个有可能的依赖都要在电脑上单独编译。而且这些编译好的仓库是独立的,不能跑一个总体的链。另外,手动编译jdk的恶心程度也不必言说。当然,可以偷懒去lgtm找版本十分有限的编译好的仓库。但是好巧不巧,lgtm在2022年12月永久关闭。所以也是时候尝试一下可以直接分析jar包的tabby了。
环境配置
POC编写和调试我直接用了Eki博士的大作
https://github.com/EkiXu/marshalexp
另外由于sink类是Unix系统独有,所以我在Linux虚拟机运行。
我在windows上跑的neo4j+tabby。相关配置参考
https://github.com/wh1t3p1g/tabby
漏洞分析
source: toString
看commit历史,特别是8月27号的commit
https://github.com/apache/dubbo-hessian-lite/compare/v3.2.12...v3.2.13
可以看到多加了反序列化时要继承Serializable的限制,和一些黑名单的限制
可以通过这篇文章了解一下Dubbo的历史漏洞
https://tttang.com/archive/1730/
可以发现一般hessian的漏洞就是source触发点,或者反序列化链的黑名单绕过。
source触发点这边,yemoli师傅的
从CVE-2022-39198到春秋杯冬季赛Dubboapp
一文中提供了toString触发点org.apache.dubbo.rpc.RpcInvocation#toString
toString2getter
然后就是toString2RCE了。符合黑名单的目前我只知道两个。
第一个是rome1.0版本,可以利用ToStringBean实现toString2getter。但是这要引入第三方依赖,所以不再赘述。
另一个是fastjson的com.alibaba.fastjson.JSON#toString(),也可以实现toString2getter。而且,fastjson是dubbo的下游依赖
所以接下来就是getter2RCE,也是本文的重点。
getter2RCE
条件
首先hessian-lite对反序列化类有严格的限制。
hessian在反序列化的时候会先寻找cost最小的constructor
JavaDeserializer#JavaDeserializer
Constructor[] constructors = cl.getDeclaredConstructors();
long bestCost = Long.MAX_VALUE;
for (int i = 0; i < constructors.length; i++) {
Class[] param = constructors[i].getParameterTypes();
long cost = 0;
for (int j = 0; j < param.length; j++) {
cost = 4 * cost;
if (Object.class.equals(param[j]))
cost += 1;
else if (String.class.equals(param[j]))
cost += 2;
else if (int.class.equals(param[j]))
cost += 3;
else if (long.class.equals(param[j]))
cost += 4;
else if (param[j].isPrimitive())
cost += 5;
else
cost += 6;
}
if (cost < 0 || cost > (1 << 48))
cost = 1 << 48;
cost += (long) param.length << 48;
if (cost < bestCost) {
_constructor = constructors[i];
bestCost = cost;
}
}
并且,如果constructor的参数不是基本变量的话,在实例化的时候hessian会把它们改成null
protected static Object getParamArg(Class cl) {
if (!cl.isPrimitive())
return null;
else if (boolean.class.equals(cl))
return Boolean.FALSE;
else if (byte.class.equals(cl))
return new Byte((byte) 0);
else if (short.class.equals(cl))
return new Short((short) 0);
else if (char.class.equals(cl))
return new Character((char) 0);
else if (int.class.equals(cl))
return Integer.valueOf(0);
else if (long.class.equals(cl))
return Long.valueOf(0);
else if (float.class.equals(cl))
return Float.valueOf(0);
else if (double.class.equals(cl))
return Double.valueOf(0);
else
throw new UnsupportedOperationException();
}
结合上面,我们可以知道寻找的类需要满足:
- constructor的参数是基本类型,或者非基本类型的参数为null的时候不会报错,或者干脆无参
- 有标准的getter(无参,public)
tabby
第一个比较坑的地方就是我在开了数据库的情况下居然没办法打开Neo4j Browser,必须要先打开Browser再打开数据库。
然后开着数据库去运行tabby。
先把tabby clone下来,直接用idea打开,让它去读取gradle的依赖。然后去修改settings.properties。
tabby.build.enable = true
tabby.build.isJDKProcess = true
tabby.build.withAllJDK = false
tabby.build.excludeJDK = false
tabby.build.isJDKOnly = false
tabby.build.checkFatJar = true
tabby.build.isFullCallGraphCreate = true
tabby.build.target = jars/
tabby.build.libraries = jars/
tabby.load.enable = true
tabby.debug.details = true
tabby.debug.inner.details = true
我这里设置会读取第三方jar包。接下来把下载下来的jar包放在jars目录即可。我用了Linux版本的jdk8u202。解压下载后的文件,找到rt.jar,放入刚才所说的jars文件夹。接下来运行App.java即可。在惊心动魄的CPU和内存接近100%的几分钟后,tabby就会自动把数据导入Neo4j。
Neo4j语法可以参考
还是从几个小example理解:
输入CALL db.schema.visualization()
,可以看到数据库中节点和边的关系:
然后分别输入
match (source:Class) return * limit 1
match (source:Method) return * limit 1
可以看到点的属性。
可以套用一些常用的反序列化链的模板:
一条path
match path=(source:Method)-[:CALL*..1]->(sink:Method {})
where source.PARAMETER_SIZE=0 and source.NAME =~ "get.*" and sink.NAME =~ "exec.*"
return path limit 5
其中()-[]->()
代表的结构是点-边-点
。可以结合上图理解。
完整的paths
match (source:Method)
match (sink:Method {NAME:"exec"})<-[:CALL]-(m1:Method)
where source.NAME="getDefaultPrinterNameBSD" and source.HAS_PARAMETERS=false and sink.CLASSNAME="java.lang.Runtime"
call apoc.algo.allSimplePaths(m1, source, "<CALL|ALIAS", 12) yield path
return * limit 5
match (source:Method {NAME:"readObject",CLASSNAME:"java.util.HashMap"})
match (sink:Method {NAME:"toString"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks, 12, false) yield path
where none(n in nodes(path) where n.CLASSNAME in ["javax.management.BadAttributeValueExpException","com.sun.jmx.snmp.SnmpEngineId","com.sun.xml.internal.ws.api.BindingID","javax.swing.text.html.HTML$UnknownTag"])
return path limit 1
match (source:Method) where source.NAME in ["equals","hashCode","compareTo"]
with collect(source) as sources
match (sink:Method {IS_SINK:true}) where sink.NAME =~ "invoke" and sink.VUL =~ "CODE" and sink.CLASSNAME =~ "java.lang.reflect.Method"
with sources, collect(sink) as sinks
call tabby.algo.allSimplePaths(sinks,sources, 15, false) yield path
where none(n in nodes(path) where (n.CLASSNAME =~ "java.util.Iterator" or n.CLASSNAME =~ "java.util.Enumeration" or n.CLASSNAME =~ "java.util.Map" or n.CLASSNAME =~ "java.util.List" or n.CLASSNAME=~"jdk.nashorn.internal.ir.UnaryNode" or n.CLASSNAME=~"com.sun.jndi.ldap.ClientId" or n.CLASSNAME=~"org.apache.catalina.webresources.TrackedInputStream"))
return path limit 1
对于此漏洞,首先想到
match (source:Method)
match (sink:Method {NAME:"exec"})<-[:CALL]-(m1:Method)
where source.NAME=~"get.+" and source.HAS_PARAMETERS=false and sink.CLASSNAME="java.lang.Runtime"
call apoc.algo.allSimplePaths(m1, source, "<CALL|ALIAS", 12) yield path
return * limit 1
但是发现根本找不到预期UnixPrintServiceLookup这个类。马后炮一下看看其源码(?),发现这个类的exec是在匿名类里调用的。猜测tabby没有加载进去?
那就看看有没有和UnixPrintService
类似的类。这个类不能用的原因是constructor不满足要求。
match (source:Method)
match (sink:Method {NAME:"execCmd"})<-[:CALL]-(m1:Method)
where source.NAME=~"get.+" and source.HAS_PARAMETERS=false and not(m1.CLASSNAME="sun.print.UnixPrintService") and not(sink.CLASSNAME="sun.print.UnixPrintService")
call apoc.algo.allSimplePaths(m1, source, "<CALL|ALIAS", 12) yield path
return * limit 10
勉强算是找到了UnixPrintServiceLookup
这个类吧(
当然到这里强迫症还是很难受的。前面说过class的constructor有限制条件。所以这里尝试一下把source类的constructor设置为无参。
match (source:Method)
match (sink:Method {NAME:"execCmd"})<-[:CALL]-(m1:Method)
match (constructor:Method)
match (clazz:Class)
where source.NAME=~"get.+" and source.HAS_PARAMETERS=false and not(m1.CLASSNAME="sun.print.UnixPrintService") and not(sink.CLASSNAME="sun.print.UnixPrintService") and constructor.HAS_PARAMETERS=false
and (clazz)-[:HAS]->(constructor) and (clazz)-[:HAS]->(source)
call apoc.algo.allSimplePaths(m1, source, "<CALL|ALIAS", 12) yield path
return path limit 1
结果电脑跑冒烟都跑不出来hh
那就试试一条path
match path=(source:Method)-[:CALL]->(sink:Method {})
match (constructor:Method)
match (clazz:Class)
where source.HAS_PARAMETERS=false and source.NAME =~ "get.+" and sink.NAME =~ "execCmd"
and (clazz)-[:HAS]->(constructor) and (clazz)-[:HAS]->(source)
and not(source.CLASSNAME="sun.print.UnixPrintService")
return path limit 1
也算是勉勉强强跑出来了吧大概
CUPS服务
最后一个坑点就在于UnixPrintServiceLookup
这个类最终运行到命令执行需要满足两个条件。
isCupsRunning要为false,并且不能是macOS和SunOS。后面这个条件其实读取的是该类的属性。由于我是Linux,就不用去管了。如果目标系统是macOS或SunOS,也可以通过反射绕过。
而isCupsRunning则是检测本机的CUPS服务。我本地的vmware+ubuntu环境是默认开启的。可以通过访问http://127.0.0.1:631/
看看是否开启。
关闭CUPS服务并重启,POC就能正常跑通了。
sudo systemctl mask cups
reboot
留给2023年的坑
还有一些由于能力原因尚未解决的问题:
- 归类一下历史漏洞中出现过的链子。
- 前面提到,我把Class和Method串起来,tabby对此的耗时非常久。所以对于限定source或者sink类的一些性质,目前也没想到有什么解决方案。
- 我目前用apoc自带的函数
apoc.algo.allSimplePaths
来完成边的寻找。但是我还没有想到或搜到对paths上的node限定的方法。对于Java反序列化原生链的限定可以通过https://github.com/wh1t3p1g/tabby-path-finder插件解决。但是有人嫌编译太麻烦没有去尝试(