前置知识
类与对象
类内变量类型
变量类型
public
private
protected
魔术方法
__construct() //创建对象(实例化类)时调用
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
相关函数
unserialize(): 将传入字符串反序列化;serialize(): 将传入变量序列化
与之类似的还有:json_encode()和json_decode()
序列化与反序列化
序列化是将(任意类型)变量以特定的规则转化成一串字符串;可以用更少的空间保存对象里的信息,便于数据传输;反序列化则是根据规则还原字符串所对应的变量。
如果变量类型是 protected,则会在变量名前加上 %00*%00,private 则会在变量名前加上 %00类名%00,(这里不可见字符用ur编码表示),可以用其他编码方式可视化。
反序列化漏洞
感受漏洞
<?php
// 定义一个名为 test 的类
class test {
// 定义一个公共属性 $str
public $str;
// 构造函数,当创建类的新实例时自动调用
public function __construct() {
// 初始化 $str 属性为 "I am here!"
$this->str = "I am here!";
}
// 析构函数,当对象不再被引用时自动调用
public function __destruct() {
// 输出 "die" 并换行
echo "die".'<br>';
}
}
// 创建 test 类的一个新实例,并赋值给变量 $testobject1
$testobject1 = new test();
// 使用 var_dump 输出 $testobject1 的详细信息,包括其属性和类型
var_dump($testobject1);
// 使用 serialize 函数将 $testobject1 对象序列化为字符串,并赋值给 $teststr1
$teststr1 = serialize($testobject1);
// 输出序列化后的字符串 $teststr1 并换行
echo $teststr1.'<br>';
// 使用 str_replace 函数将 $teststr1 中的 "I am here!" 替换为 "I like it!",结果赋值给 $teststr2
$teststr2 = str_replace("I am here!","I like it!",$teststr1);
// 输出替换后的字符串 $teststr2 并换行
echo $teststr2.'<br>';
// 尝试使用 unserialize 函数将 $teststr2 反序列化为对象,并使用 var_dump 输出结果
// 注意:这里的操作是不安全的,因为 $teststr2 是被修改过的序列化字符串
var_dump(unserialize($teststr2));
?>
<?php
class test{
public $str;
public function __construct(){
$this->str = "I am here!";
}
public function __destruct(){
echo "die".'<br>';
}
}
//GET请求允许用户在浏览器url中输入内容,造成漏洞
var_dump(unserialize($_GET["str"]));
?>
上面的例子直观感受了反序列化漏洞的作用之一,更改键值对;在大部分情况下,我们要用魔术方法完成题目,最主要的是 __destruct ,一般情况下在这里面的是可供我们利用的代码;由此也引出了第一个防护:__wakeup 魔术方法
千奇百怪绕过法
绕过wakeup
__wakeup()可以起到防护作用是因为它在反序列化时调用,可以改变变量的值或终止程序进行。
绕过的方法很简单,在 PHP5<5.6.25;PHP7<7.0.10>以及一些高版本中通过修改格式可以阻止调用 __wakeup()
修改方法:增大表示对象中表示变量数量的数字
绕过正则
正则是直接检查字符串来确定是否安全,一般是匹配关键词来完成操作
Aa不敏感
PHP对类名的大小写不敏感
S16进制绕过
0:4:"test":2:{s:1:"a";s:3:"abc";s:4:"testb";s:3:"def";}
可以写成
0:4:"test":2:{s:1:"\61";s:3:"abc";s:7:"testb";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析。
代表字符串的 s 换成 S 时,表示对应字符串采用16进制编码
/^O:\d+/
这个正则表示匹配 O:数字 这种结构
+
O:+数字
字符串逃逸
分为两种类型,可以这样利用是因为反序列化是按照规则读取字符串信息
字符增多型
<?php
include('flag.php');
class test{
public $a;
public $b;
public function __construct($aa,$bb){
$this->a = $aa;
$this->b = $bb;
}
}
$a=$_GET['a'];
$b="you con not change it";
$testobject1 = new test($a,$b);
// 序列化对象$testobject1,将其转换为字符串,并赋值给$str
$str=serialize($testobject1);
echo $str.'<br>';
// 将$str中的"x"替换为"oh",这里有一个问题:如果$str中不存在"x",则替换不会发生
$str=str_replace("x","oh",$str);
echo $str;
// 反序列化字符串$str,将其转换回对象,并赋值给$testobject2
$testobject2 =unserialize($str);
// 检查$testobject2的属性$b是否等于字符串"SUCCESS"
if($testobject2->b ==="SUCCESS")
{
// 如果属性$b等于"SUCCESS",则输出$flag的值
echo $flag;
}
?>
上述代码中假设flag.php存在,则本段代码如何能使得最后一个if语句为真,并输出flag
flag.php
<?php
$flag="this is flag";
正常执行结果:
方法:将传入的字符串当作键值对来读取,就可以实现修改值的操作,后面的部分则会被丢弃。
";s:1:"b";s:7:"SUCCESS";} 共用25个字符,第一个"用来构造闭合
一个x顶2个oh,构造方程,x+25=2x,x代表x的数量,得x=25,所以需要25个x
字符减少型
<?php
// file_get_contents('flag.phar');
include('flag.php');
class test{
public $a;
public $b;
public $flag;
public function __construct($aa,$bb){
$this->a = $aa;
$this->b = $bb;
$this->flag = "no";
}
}
$a=$_GET['a'];
$b=$_GET['b'];
$testobject1 = new test($a,$b);
$str=serialize($testobject1);
echo $str.'<br>';
$str=str_replace("oh","x",$str);
echo $str;
$testobject2=unserialize($str);
if($testobject2->flag === "yes")
{
echo $flag;
}
正常传值
传入的值包含oh
方法:将键名当作值来读取。
b参数传值时需传入一组键值对
session反序列化
php.ini配置项
session.save_path=""---->设置session的存储路径
session.save_handler="“---->设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen --->指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler string --->定义用来序列化反序列化的处理器名字,默认使用php
配置项 | 含义 |
session.save_path="D:\xamppl\mp' | 表明所有的session文件都是存储在xampp/tmp下 |
session.save_handler=files | 表明session是以文件的方式来进行存储的 |
session.auto_start=0 | 表明默认不启动session |
session.serialize_handler=php | 表明session的默认序列话引擎使用的是php序列话引擎 |
session.serialize_handler 是用来设置session的序列话引擎的,除了默认的 php 引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。假设有$_SESSION['name']='hjbhjb'
引擎 | 存储方式 | 例子 |
php | 键名+ | +经过serialize()函数序列处理的值 | name | s:6:"hjbhjb" |
php_serialize | 经过serialize()函数序列化处理的值 | a:1:{s:4:"name";s:6:"hjbhjb";} |
php_binary | 键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值 | %04names :6 :"spoock" |
在 php_binary 的例子中由于 name 的长度是4,4在ASCII表中对应的就是 EOT ,为不可见字符,所以这里用了URL编码的格式,实际上在不特殊编码的情况下我们看到的是 names:6:"spoock"
在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码 ini_set('session.serialize_handler','引擎”)或更改默认引擎
漏洞原因
不同页面之间使用了不同的处理引擎,造成键值对解析歧义导致漏洞,看下面的例子
//first.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['TEST'] ='|0:"A":1:{s:1:"a";s:4:"test";}';
var_dump($_SESSION);
//second.php<?php
ini_set('session.serialize_handler', 'php');
session_start();
#$_SESSION['TEST']" = '|0:1:"A":1:({s:1:"a";s:4:"test";}';
class A {
public $a = 'aa';
function __wakeup(){
echo $this->a;
}
}
var_dump($_SESSION);
//test
//var_dump()
$SESSION 的值发生了改变,另外,在例子中,second.php页面回显了test,说明反序列化的执行
分析
这是因为当使用php引擎的时候,php引擎会以 | 作为key和value的分隔符,那么就会将 a:1:{s:4:"ryat”;s:30:"}作为SESSION的key,将 0:1:"A":1:{s:1:"a”;s:2:”xx";}作为value,然后进行反序列化,最后就会得到A这个类。
这种由于序列化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。
phar反序列化
当读取phar文件,内部的序列化字符串会自动反序列化
phar文件结构
构成部分 | 作用与要求 |
stub | phar文件的标志,必须以 xxx __HALT_COMPILER();?>结尾,否则无法识别。xxx可以为自定义内容。 |
manifest | phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方 |
content | 被压缩文件的内容 |
signature | 可以为空,签名,放在未尾 |
产生phar格式的文件
<?php
class Test {
//看情况自定义
}
//要反序列化的字符串
@unlink("phar.phar");
$phar =new Phar("phar.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setstub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o= new Test();
$phar->setMetadata($o);//将自定义的meta-data存入manifest
$phar->addFromstring("test.txt","test");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
漏洞利用条件
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且:、/、 'phar' 等特殊字符没有被过滤。(绕过放到文件包含专题讲)