记录自己阅读Chip源码
0X00 思维导图
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 |
|
这里采用了phar用来打包项目,在本地测试将chip打包时需要将bin和src拷贝到项目根路径中,然后运行如下php脚本。即可将chip打包成chip.phar。
1 |
|
0x03 代码流程
简写的函数调用栈
new Application()
application->addCommands([new \Chip\Console\Check])
加入 -r -l 等参数application->run()
application->doRun()
application->doRunCommand()
command->run()
command->execute()
===check->execute()
check->checkCode()
ChipFactory->create->detect()
===ChipManager->detect()
添加visitorchip->feed()
扫描代码 traverserchip->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
use function assert as test;
test($_POST[2333]);这类webshell。
CallstackVisitor 定义一个栈,入节点时如果节点为Node\Stmt\Function_ 或Node\Stmt\ClassMethod 压栈,离开节点时,若栈非空且栈顶为当前的节点,将该节点弹出栈。
Visitor文件夹下 均继承BaseVisitor,beforeProcess()分三类,在入节点先检测当前节点是否是checkNodeClass,若是则调用process()函数。下图为BaseVisitor.php。
Assert: checkNodeClass为
FuncCall
,调用FunctionWalker中的beforeProcess() ,调用getFunctionname(),若函数名为assert,进行process() ,然后调用hasDynamicExpr()检测$node->args[0]->value
,分别用hasVariable()和hasFunctionCall()检测$node->args[0]->value
节点,是否为如下类型节点,是就critical,否则warning。同Eval(EvalNode) Shell ShellFunction1
2
3
4
5
6
7
8
9
10
11hasVariable()
Node\Expr\Variable 变量
Node\Expr\PropertyFetch
Node\Expr\ConstFetch
Node\Expr\ClassConstFetch
hasFunctionCall()
Node\Expr\MethodCall
Node\Expr\FuncCall
Node\Expr\New_
Node\Expr\StaticCallCallback: CheckNodeClass为FuncCall,在Chip/Schema/FunctionWithCallable.php中定义了FUNCTION_WITH_CALLABLE常量,保存带有回调参数的函数,在Callback的构造函数中将下图中的函数
保存到$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
20public 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
11public 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
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,
节点: 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
// 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();之后获得方法名,如为下面的名称,执行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
31array(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 踩坑
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
23class 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的细分 - 或 –
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对象。)php Trait
代码复用:实例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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动态特性的捕捉与逃逸