PHP反序列化漏洞
PHP反序列化漏洞
基础知识:
序列化:利用serialize()
函数将一个对象转换为字符串形式。使保存、传输对象数据更加方便。
序列化操作只是保存对象(不是类)的变量,不保存对象的方法,反序列化的主要危害在于我们可以控制对象的变量来改变程序执行流程从而达到我们最终的目的。我们无法控制对象的方法来调用,因此我们这里只能去找一些可以自动调用的一些魔术方法。
1 |
|
效果如下:
注意:序列化只序列属性,不序列方法。
再把该数组序列化转化为字符串输出:
1 |
|
效果如下:
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 |
|
结果
可以发现本来是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 | __destruct() |
之前复现的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工作流程
当开始一个会话时,PHP会尝试从请求中查找会话ID,通常是使用cookie
,如果请求包中未发现session id
,PHP就会自动调用php_session_create_id
函数创建一个新的会话,并且在响应包头中通过set-cookie
参数发给客户端保存。
PHP Session在PHP ini中的配置
php.ini 里面有如下六个相对重要的配置
1 | session.save_path="" --设置session的存储位置 |
例如phpstudy
:
即session的存储路径
即表明session是以文件的方式来进行存储的
表明默认不启动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 |
|
我们到session
存储目录查看一下session
文件内容
PHP session反序列化漏洞形成原理
反序列化的各个处理器本身是没有问题的,但是如果php
和php_serialize
这两个处理区混合起来使用,就会出现session反序列化漏洞。
形成的原理就是在用session.serialize_handler = php_serialize
存储的字符可以引入|
, 再用session.serialize_handler = php
格式取出$_SESSION
的值时, |
会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。
例如:
定义一个session.php
,用于传入session
的值
1 | //session.php |
查看session内容:
再定义一个 class.php
1 | //class.php |
实例化对象之后回显w0s1np
session.php
文件处理器是php_serialize
,class.php
文件处理器是php
, session.php
文件得作用是传入可控得session
值, class.php
文件的作用是在反序化开始触发__wakeup()
方法的内容,反序化结束时触发 __destruct()
方法。
漏洞利用就是在session.php
得可控值处传入 | +序列化字符
,然后再次访问class.php
调用session
的值的时候会触发。
利用脚本如下:
1 |
|
结果如下:
传入session.php
的payload:|O:5:"Hello":1:{s:4:"name";s:6:"w0s1np";}
查看存储的session:
此时再次访问class.php
,结果如下:
1 | Who are you? |
其实就是因为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 |
|
如果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 |
|
如果你要添加一个文件夹里面所以的文件那么可以使用下面代码:
1 | $phar->buildFromDirectory(dirname(__FILE__) . '/project'); // 添加project里面的所有文件到yunke.phar归档文件 |
emmm,大概就是这样,我们可以看下经过浏览器处理后,我们的网站根目录就产生了test.phar
文件
同样,我们也可以确定,manifest
确实是以序列化储存的。
有序列化数据必然会有反序列化操作,php里面很多文件系统函数在 phar:// 伪协议解析 phar 文件时,都会将meta-data进行反序列化。
例如:
1 |
|
为什么这里可以调用析构函数呢,是因为将phar.phar
里面的TestObject
类实例化经过反序列化加入在上面这个代码里面,所以当脚本运行后就执行了析构函数。所以我们就可以写一些木马脚本经过phar
序列化,再通过phar://
伪协议,反序化加入目的代码中。
当文件系统函数的参数可控时,我们可以在不调用unserialize()
的情况下进行反序列化操作,极大的拓展了攻击面。(这里我们就是把一些序列化的东西放入函数里面了)
php反序列化字符逃逸
前言:一般出现在ctf
里面,所以这里都是在以ctf
里面的环境讲解
此类题目的本质就是改变序列化字符串的长度,导致反序列化漏洞
原理:php
序列化后的字符串经过替换或者修改,导致字符串长度改变
替换修改后导致序列化字符串变长
实验代码:
1 |
|
先看下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
函数替换成ccc
,ccc
的长度比bb
多1,这样前面的s所代表的长度为2但是内容却变长了,成了ccc
。
1 |
|
1 | O:1:"A":2:{s:4:"name";s:6:"aaaabb";s:4:"pass";s:6:"123456";} |
因为s:6
,所以name只读取aaaacc
,末尾的c就读取不到,这就形成了字符逃逸了,但是我们想修改pass
里面的值该怎么办呢,
肯定需要闭合,所以在name
处加上";s:4:"pass";s:6:"hacker";}
来间接修改pass的值,
如果我们只是单纯的把它加进去的话,就像下面这样:
1 | class A{ |
得到:
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 | 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的结构 |
就把pass里面的值修改了
例题:[0CTF 2016]piapiapia
详情见:https://www.cnblogs.com/w0s1np/p/14236380.html
替换修改之后导致序列化字符串变短
实验代码:
1 |
|
输入name和sign,number值是固定的’2020’,经过 序列化-->敏感字替换为空(长度变短)-->反序列化
的过程之后再输出结果。
接下来利用漏洞,通过输入name
和sign
来间接修改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";} |
我们在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 |
|
POC:
1 |
|
Exception 内置类XSS
- 适用于php5、7版本
- 开启报错的情况下
测试代码:
1 |
|
POC:
1 |
|
Error /Exception 类绕过哈希比较
Error 是所有PHP内部错误类的基类,该类是在PHP 7.0.0 中开始引入的。
Exception 是所有异常的基类,该类是在PHP 5.0.0 中开始引入的。
在Error
和Exception
这两个PHP原生类中内只有 __toString
方法,这个方法用于将异常或错误对象转换为字符串。
以Error为例,我们看看当触发他的 __toString
方法时会发生什么:
1 |
|
输出如下:
1 | Error: payload in D:\phpstudy_pro\WWW\index.php:2 |
这将会以字符串的形式输出当前报错,包含当前的错误信息(”payload”)以及当前报错的行号(”2”),而传入 Error("payload",1)
中的错误代码“1”则没有输出出来。
在来看看下一个例子:
1 |
|
输出如下:
1 | Error: payload in D:\phpstudy_pro\WWW\index.php:2 |
$a
和 $b
这两个错误对象本身是不同的,但是 __toString
方法返回的结果是相同的。注意,这里之所以需要在同一行是因为 __toString
返回的数据包含当前行号。
Exception 类与 Error 的使用和结果完全一样,只不过 Exception
类适用于PHP 5和7,而 Error
只适用于 PHP 7。
[2020 极客大挑战]Greatphp
进入题目,给出源码:
1 |
|
需要进入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 |
|
这里 $str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
中为什么要在前面加上一个 ?>
呢?因为 Exception
类与 Error
的 __toString
方法在eval()函数中输出的结果是不可能控的,即输出的报错信息中,payload前面还有一段杂乱信息“Error: ”:
1 | Error: payload in D:\phpstudy_pro\WWW\index.php:2 |
进入eval()函数会类似于:eval("...Error: <?php payload ?>")
。所以我们要用 ?>
来闭合一下,即 eval("...Error: ?><?php payload ?>")
,这样我们的payload便能顺利执行了。
使用 SoapClient 类进行 SSRF
PHP 的内置类SoapClient
是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的PHP客户端。
该内置类有一个 __call
方法,当 __call
方法被触发后,它可以发送HTTP
和HTTPS
请求。正是这个__call
方法,使得SoapClient
类可以被我们运用在SSRF
中。
1 | public SoapClient :: SoapClient(mixed $wsdl [,array $options ]) |
- 第一个参数是用来指明是否是
wsdl
模式,将该值设为null
则表示非wsdl
模式。 - 第二个参数为一个数组,如果在
wsdl
模式下,此参数可选;如果在非wsdl
模式下,则必须设置location
和uri
选项,其中location
是要将请求发送到的SOAP服务器的URL,而uri
是SOAP服务的目标命名空间。
我们可以设置第一个参数为null,然后第二个参数的location
选项设置为target_url。
1 |
|
首先在47.xxx.xxx.72
上面起个监听:
然后执行上述代码,如下图所示成功触发SSRF,47.xxx.xxx.72
上面收到了请求信息:
但是,由于它仅限于HTTP/HTTPS
协议,所以用处不是很大。而如果这里HTTP头部还存在CRLF漏洞的话,但我们则可以通过SSRF+CRLF,插入任意的HTTP头。
如下测试代码,我们在HTTP头中插入一个cookie:
1 |
|
例题可参考:[MRCTF2020]Ezpop_Revenge
使用 DirectoryIterator 类绕过 open_basedir
DirectoryIterator
类提供了一个用于查看文件系统目录内容的简单接口,该类是在 PHP 5 中增加的一个类
DirectoryIterator
与glob://
协议结合将无视open_basedir
对目录的限制,可以用来列举出指定目录下的文件。
测试代码:
1 |
|
我们输入 /?w0s1np=glob:///*
即可列出根目录下的文件:
但是会发现只能列根目录和open_basedir
指定的目录的文件,不能列出除前面的目录以外的目录中的文件,且不能读取文件内容。
使用 SimpleXMLElement 类进行 XXE
可以看到通过设置第三个参数data_is_url
为 true
,我们可以实现远程xml
文件的载入。第二个参数的常量值我们设置为2
即可。第一个参数data
就是我们自己设置的payload
的url
地址,即用于引入的外部实体的url
。
这样的话,当我们可以控制目标调用的类的时候,便可以通过SimpleXMLElement
这个内置类来构造 XXE。
ZipArchive::open 删除文件
使用条件:
- 要调用对象的open函数,且open函数中的参数可控
- 第二个参数设置为
ZipArchive::OVERWRITE
1 |
|
运行之后就删除了1.txt
例题参考:[NepCTF2021 梦里花开牡丹亭]
https://www.cnblogs.com/w0s1np/p/14641597.html
GlobIterator 遍历目录
使用条件:
- 遍历对象
1 | GlobIterator::__construct(string $pattern, [int $flag]) |
使用例子:
1 | $newclass = new GlobIterator("./*.php",0); |