使用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的下游依赖

image-20230102153040034

所以接下来就是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();
    }

结合上面,我们可以知道寻找的类需要满足:

  1. constructor的参数是基本类型,或者非基本类型的参数为null的时候不会报错,或者干脆无参
  2. 有标准的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语法可以参考

https://neo4j.com/docs/

还是从几个小example理解:

输入CALL db.schema.visualization(),可以看到数据库中节点和边的关系:

image-20230102163231410

然后分别输入

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这个类吧(

image-20230102172635123

当然到这里强迫症还是很难受的。前面说过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

也算是勉勉强强跑出来了吧大概

image-20230102174900626

CUPS服务

最后一个坑点就在于UnixPrintServiceLookup这个类最终运行到命令执行需要满足两个条件。

image-20230102175326184

isCupsRunning要为false,并且不能是macOS和SunOS。后面这个条件其实读取的是该类的属性。由于我是Linux,就不用去管了。如果目标系统是macOS或SunOS,也可以通过反射绕过。

而isCupsRunning则是检测本机的CUPS服务。我本地的vmware+ubuntu环境是默认开启的。可以通过访问http://127.0.0.1:631/看看是否开启。image-20230102175653210

关闭CUPS服务并重启,POC就能正常跑通了。

sudo systemctl mask cups
reboot

留给2023年的坑

还有一些由于能力原因尚未解决的问题:

  1. 归类一下历史漏洞中出现过的链子。
  2. 前面提到,我把Class和Method串起来,tabby对此的耗时非常久。所以对于限定source或者sink类的一些性质,目前也没想到有什么解决方案。
  3. 我目前用apoc自带的函数apoc.algo.allSimplePaths来完成边的寻找。但是我还没有想到或搜到对paths上的node限定的方法。对于Java反序列化原生链的限定可以通过https://github.com/wh1t3p1g/tabby-path-finder插件解决。但是有人嫌编译太麻烦没有去尝试(
posted @ 2023-01-02 23:29  KingBridge  阅读(1370)  评论(0编辑  收藏  举报