Chip代码阅读笔记

记录自己阅读Chip源码

0X00 思维导图

image-20200418212831298

image-20200418123816033

0x01 CHIP 整体检测方法

通过阅读CHIP源码,可以看出开发者分别将普通eval/assert代码、正则/e模式代码执行(如:preg_match函数)、create_function代码执行、系统命令执行、动态函数调用(如:$a($b))、回调后门(如call_user_func函数)、畸形/加密一句话木马(如base64编码)、任意文件包含(如:include函数)、PHP反射(如:ReflectionClass类)和执行不安全的继承与重命名利用(如:将eval函数起别名)这些动态特性的形式总结出来,然后得到各类型的特征,在语法分析的层面上对php动态特性进行捕获,利用已有的PHP-Parser包来将PHP转换成AST语法树,结合每个类型的特征设置相应的Visitor去遍历AST的每个节点,遇到有符合相应特征的节点就输出报警信息,直到所有Visitor遍历完AST后结束。

0x02 入口文件

/bin/chip,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') {
echo 'Warning: Chip should be invoked via the CLI version of PHP, not the '.PHP_SAPI.' SAPI'.PHP_EOL;
exit;
}

require __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\Console\Application;

try {
$application = new Application('Chip', '1.3.0');
$application->addCommands([
new \Chip\Console\Check()
]);
$application->run();
} catch (\Exception $e) {}

这里采用了phar用来打包项目,在本地测试将chip打包时需要将bin和src拷贝到项目根路径中,然后运行如下php脚本。即可将chip打包成chip.phar。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$dir = __DIR__; // 需要打包的目录
$file = 'chip.phar'; // 包的名称, 注意它不仅仅是一个文件名, 在stub中也会作为入口前缀
$phar = new Phar(__DIR__ . '/' . $file, FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::KEY_AS_FILENAME, $file);
// 开始打包
$phar->startBuffering();
$phar->buildFromDirectory($dir);
$phar->delete('build1.php');
// 设置入口
$phar->setStub("<?php
Phar::mapPhar('{$file}');
require 'phar://{$file}/bin/chip';
__HALT_COMPILER();
?>");
$phar->stopBuffering();
// 打包完成
echo "Finished {$file}\n";

0x03 代码流程

简写的函数调用栈

  1. new Application()

  2. application->addCommands([new \Chip\Console\Check]) 加入 -r -l 等参数

  3. application->run()

    1. application->doRun()

      1. application->doRunCommand()

        1. command->run()

          1. command->execute() === check->execute()

            1. check->checkCode()

              1. ChipFactory->create->detect() === ChipManager->detect() 添加visitor

                1. chip->feed() 扫描代码 traverser
                  1. chip->feed()->alarm() 获取输出结果

chip先读取输入的php代码,然后利用PHP-Parser将php代码转换成AST抽象语法树,然后通过添加Visitors来遍历AST树,分别去检测webshell。

visitor 分三类

  • NameResolver parser内置的Visitor,在chip中是遍历AST的第一个Visitor,在看到有用use给函数别名时,将别名和函数原名保存,之后再遍历到有FuncCall节点时,检查FuncCall的name,若为之前保存的别名,然后修改当前节点。这里只针对use别名webshell而言,NameResolver还有其他功能。

    用于检测

    1
    2
    3
    <?php
    use function assert as test;
    test($_POST[2333]);

    这类webshell。

  • CallstackVisitor 定义一个栈,入节点时如果节点为Node\Stmt\Function_ 或Node\Stmt\ClassMethod 压栈,离开节点时,若栈非空且栈顶为当前的节点,将该节点弹出栈。

  • Visitor文件夹下 均继承BaseVisitor,beforeProcess()分三类,在入节点先检测当前节点是否是checkNodeClass,若是则调用process()函数。下图为BaseVisitor.php。

    image-20200418214141523

    • Assert: checkNodeClass为FuncCall ,调用FunctionWalker中的beforeProcess() ,调用getFunctionname(),若函数名为assert,进行process() ,然后调用hasDynamicExpr()检测$node->args[0]->value,分别用hasVariable()和hasFunctionCall()检测$node->args[0]->value节点,是否为如下类型节点,是就critical,否则warning。同Eval(EvalNode) Shell ShellFunction

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
        hasVariable()
      Node\Expr\Variable 变量
      Node\Expr\PropertyFetch
      Node\Expr\ConstFetch
      Node\Expr\ClassConstFetch

      hasFunctionCall()
      Node\Expr\MethodCall
      Node\Expr\FuncCall
      Node\Expr\New_
      Node\Expr\StaticCall
    • Callback: CheckNodeClass为FuncCall,在Chip/Schema/FunctionWithCallable.php中定义了FUNCTION_WITH_CALLABLE常量,保存带有回调参数的函数,在Callback的构造函数中将下图中的函数image-20200414171244310

      保存到$this->functionWithCallback二维数组中,键为函数名,值为对应的回调参数的位置。调用FunctionWalker中的beforeProcess() ,然后获取函数名,判断在不在$this->functionWithCallback中,若在,调用process()函数,先遍历$node->args,如果有有使用<?php usort(...$_GET);?>这种参数,报danger。然后检查对应pos位置是否是回调参数位置,然后判断$node->args[pos]->value instanceof Node\Expr\Closure ,是就跳过,否则调用hasDynamicExpr()函数检测$node->args[pos]->value,若返回真则报danger,返回假则调用isSafeCallback($node->args[pos]),如果$node->args[pos]->value是string而且函数名满足/^\\\\?[a-z0-9_]+$/is且函数名不在DANGER_FUNCTION常量数组中,报info,否则报warning.

    • CreateFunction: CheckNodeClass为FuncCall,调用FunctionWalker中的beforeProcess() ,调用getFunctionname(),若函数名为create_function,进行process() ,首先调用hasUnpackBefore()函数检测$node->args[$i]->unpack是否为true,若为true,报critical,然后调用hasDynamicExpr()函数检测$node->args[$i]->value,若返回真,报critical,否则报warning。

    • DynamicCall: CheckNodeClass为FuncCall,进行process() ,若hasDynamicExpr($node->name)为真,报danger,如果isName($node->name)为假,报waring。 个人认为这里误报率较高,

      1
      2
      3
      4
        <?php
      $a = 1;
      eval($a());//只要是()就报danger
      ?>
    • DynamicMethod: CheckNodeClass为MethodCall,进行process() ,若hasDynamicExpr($node->name)为真,报danger,如果isName($node->name)为假,报waring。

    • DynamicNew: CheckNodeClass为New_,进行process()中检测hasVariable($node->class),返回是报waring。

      1
      2
      3
        <?php
      $a = new $class; //没理解这样有什么危险
      ?>
    • DynamicStaticMethod: CheckNodeClass为StaticCall,进行process() ,

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      public function process($node)
      {
      $class = $node->class;
      $name = $node->name;

      if ($this->hasDynamicExpr($class)) {
      $this->storage->danger($node, __CLASS__, '以动态类形式调用静态方法,可能存在远程代码执行的隐患');
      return;
      }

      if ($this->hasDynamicExpr($name)) {
      $this->storage->danger($node, __CLASS__, '动态调用方法,可能存在远程代码执行的隐患');
      return;
      }

      if (!$this->isName($class) || !$this->isIdentifier($name)) {
      $this->storage->warning($node, __CLASS__, '不规范的静态方法调用,可能存在远程代码执行的隐患');
      return;
      }
      }
    • Eval : EvalNode process调用hasDynamicExpr()函数检测$node->expr。

    • Extends: Class_ process

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
        public function process($node)
      {
      if ($node->extends instanceof Node && $this->isName($node->extends)) {
      $className = $node->extends->toLowerString();

      if (in_array($className, array_map('strtolower', $this->dangerClassName))) {
      $this->storage->danger($node, __CLASS__, '代码继承了不安全的类');
      return;
      }
      }
      }

      主要检测 类的继承 ReflectionFunction

      1
      2
      3
        <?php class test extends ReflectionFunction {} 
      $f = new test('system');
      $f->invoke($_POST[2333]);
    • FilterVar: 结点 funccall funcname : filter、filter_var ,然后process

      1
      2
      3
        <?php
      filter_var($_REQUEST['pass'], FILTER_CALLBACK, array('options' => 'assert'));
      filter_var_array(array('test' => $_REQUEST['pass']), array('test' => array('filter' => FILTER_CALLBACK, 'options' => 'assert')));
    • Include : 节点 Include ,进行process()函数,首先递归找$node->expr 分两类(Concat -字符串、Encapsed –不知 )然后判断最后的节点,若文件名不为php inc 就报danger,然后hasdynamicExpr($nodd->expr),返回真 报danger.

    • MbPregExec : 检测这类变形webshell,image-20200414171305419

      节点: FuncCall,然后若函数名为:

      1
      2
      3
      4
      5
      6
      'mb_ereg_replace',
      'mbereg_replace',
      'mb_eregi_replace',
      'mberegi_replace',
      'mb_regex_set_options',
      'mbregex_set_options',

      上面这些函数,就执行process()函数,先获得参数,然后对参数进行检测。若参数不为字符串,报danger,若参数为e,则报danger。

    • MethodCallback: 检测下面这类webshell , 节点 MethodCall

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
        <?php
      // way 0
      $arr = new ArrayObject(array('test', $_REQUEST['pass']));
      $arr->uasort('assert');

      // way 1
      $arr = new ArrayObject(array('test' => 1, $_REQUEST['pass'] => 2));
      $arr->uksort('assert');

      $e = $_REQUEST['e'];
      $db = new PDO('sqlite:sqlite.db3');
      $db->sqliteCreateFunction('myfunc', $e, 1);
      $sth = $db->prepare("SELECT myfunc(:exec)");
      $sth->execute(array(':exec' => $_REQUEST['pass']));

      $e = $_REQUEST['e'];
      $db = new SQLite3('sqlite.db3');
      $db->createFunction('myfunc', $e);
      $stmt = $db->prepare("SELECT myfunc(?)");
      $stmt->bindValue(1, $_REQUEST['pass'], SQLITE3_TEXT);
      $stmt->execute();

      img

      之后获得方法名,如为下面的名称,执行process()函数,

      1
      2
      3
      4
      5
      6
      7
      8
      9
        'uasort'                   
      'uksort'
      'set_local_infile_handler'
      'sqlitecreateaggregate'
      'sqlitecreatecollation'
      'sqlitecreatefunction'
      'createcollation'
      'fetchall'
      'createfunction'

      首先,若方法名为fetchall,先看参数是否有 ...这种形式,若有报danger;然后检查fetchAll()的前两个参数,若通过白名单检测跳过,否则调用hasDynamicExpr()检查第二个参数,若真报danger,若第二个参数不为匿名函数报warning。

      之后检测其他函数首先检测指定位置前的参数是否存在变长参数,若存在报danger;然后检测指定位置的参数值是否含有动态性,若有则报danger;若指定位置的参数值不为匿名函数报warning。

    • PregExec: 节点 funccall ,若函数名为preg_replace或preg_filter,就执行process()函数,检测第一个参数中是否有e,若有则报danger

    • Reflection: 检测节点 New ,然后获取$node->class;,若名称在下列中,报warning。reflectionclass->getDocComment()可以获取注释。reflectionfunctin->invokeargs()执行函数

      1
      2
      3
      4
      5
      6
      7
        'ReflectionClass',
      'ReflectionZendExtension',
      'ReflectionExtension',
      'ReflectionFunction',
      'ReflectionFunctionAbstract',
      'ReflectionMethod',
      'ReflectionGenerator', ///和extend中一样
    • ScriptTag: 检测<script>标签型webshell,报warning(不判断代码内容)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      array(1) {
      [0]=>
      object(PhpParser\Node\Stmt\InlineHTML)#1124 (2) {
      ["value"]=>
      string(46) "<script language="php">
      echo 2222;
      </script>"
      ["attributes":protected]=>
      array(5) {
      ["startLine"]=>
      int(1)
      ["startFilePos"]=>
      int(0)
      ["hasLeadingNewline"]=>
      bool(true)
      ["endLine"]=>
      int(3)
      ["endFilePos"]=>
      int(45)
      }
      }
      }
      Array
      (
      [0] => Chip\Alarm Object
      (
      [type] => ScriptTag
      [level] => WARNING
      [message] => 使用不支持的PHP标签,可能存在安全问题
      )
      )
      • shellFunction: 同assert,只是将检测的函数名变为
    1
    2
    3
    4
    5
    6
       'system',
    'shell_exec',
    'exec',
    'passthru',
    'popen',
    'proc_open',
    • shell: 节点为 ShellExec ,然后同shellfunction

    • StaticCallback: 检测phar:webphar 、closure::fromcallable ;节点 为 StaticCall,若{$node->class->toLowerString()}::{$node->name->toLowerString()}为phar:webphar 、closure::fromcallable,执行process()函数,若参数...报danger,检测参数动态性,若有动态性报danger,若未使用匿名函数,报warning.

0x04 踩坑

  1. Symfony命令行处理

    用了application的run()方法,若不传参数会调用ArgvInput类,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class ArgvInput extends Input
    {
    private $tokens;
    private $parsed;

    /**
    * @param array|null $argv An array of parameters from the CLI (in the argv format)
    * @param InputDefinition|null $definition A InputDefinition instance
    */
    public function __construct(array $argv = null, InputDefinition $definition = null)
    {
    if (null === $argv) {
    $argv = $_SERVER['argv'];
    }

    // strip the application name
    array_shift($argv);

    $this->tokens = $argv;

    parent::__construct($definition);
    }
    ..................

    $_SERVER[argv]在cli模式中,为输入的东西,$argv=$_SERVER[argv] ,$argv[0]是脚本名,$argv[1]是第一个参数如(–11=2),依次类推。

    arguments ls -l / 有两个arguments -l /

    A parameter is an argument that provides information to either the command or one of its options

    option 对arguments的细分 - 或 –

  2. php-di

    依赖注入参考链接 php-di 文档http://php-di.org/doc/getting-start

    分为 构造函数 方法 属性 php回调注入

    3. Create the objects

    (创建对象)
    Without PHP-DI, we would have to “wire” the dependencies manually like this:
    (如果没有PHP-DI,我们将不得不像这样手动地“连接”依赖项:)

    1
    2
    $mailer = new Mailer();
    $userManager = new UserManager($mailer);

    Instead, we can let PHP-DI figure out the dependencies:
    (相反,我们可以让PHP-DI计算出依赖项:)

    1
    $userManager = $container->get('UserManager');

    Behind the scenes, PHP-DI will create both a Mailer object and a UserManager object.
    (在幕后,PHP-DI将创建一个Mailer对象和一个UserManager对象。)

  3. php Trait

    代码复用:实例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <?php
    class Base {
    public function sayHello() {
    echo 'Hello ';
    }
    }

    trait SayWorld {
    public function sayHello() {
    parent::sayHello();
    echo 'World!';
    }
    }

    class MyHelloWorld extends Base {
    use SayWorld;
    }

    $o = new MyHelloWorld();
    $o->sayHello();//Hello World!
    ?>

参考:

https://www.leavesongs.com/PENETRATION/php-callback-backdoor.html

PHP动态特性的捕捉与逃逸