php反序列化与pop链
前言:近期算是比较摸,这部分知识其实不算难,算是复习和总结下,我尽量以平和的语言总结。
1.1 序列化与反序列化
计算机相关知识总是喜欢创造些新名词,让人看起来觉得很高大上。其实序列化本质就是一种做数据格式转换的操作。
序列化:将变量(通常是数组和对象)转换为可保存或传输的字符串
反序列化:在适当的时候把这个字符串再转化成原来的变量(通常是数组和对象)使用。
这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。反序列化本身不是漏洞,但如果反序列化的内容可控,就容易导致漏洞
1.2 php魔术方法
PHP提供了许多“魔术”方法,这些方法由两个下划线前缀(__)标识。它们充当拦截器,在满足某些条件时会自动调用它们。 魔术方法提供了一些极其有用的功能。
常见的魔术方法有:
__contruct()
当一个对象创建时被调用__destruct()
当一个对象销毁前被调用__sleep()
在对象被序列化前被调用__wakeup
将在反序列化之后立即被调用__toString
当一个对象被当做字符串使用时被调用__get()
,__set()
当调用或设置一个类及其父类方法中未定义的属性时__invoke()
调用函数的方式调用一个对象时的回应方法__call
和__callStatic
前者是调用类不存在的方法时执行,而后者是调用类不存在的静态方式方法时执行。
https://segmentfault.com/a/1190000007250604
该文章对魔术方法的使用做了比较详细的解释,可以看下
1.3 序列化后的字符串形式
一个序列化的字符串:
1 | O:4:"Test":2:{s:4:"test";s:2:"ok";s:3:"var";N;} |
另外对于类里的成员变量,我们一般都会给予相应的权限,权限不同,序列化后的字符串存在区别
1 | <?php |
用010可以看到public的属性,序列化后的值就是属性的名称和对应的值
如果换成private
1 | <?php |
属性名变成了%00Test%00test
和%00Test%00var
也就是%00类名%00属性名
protected
换成protected, 属性序列化之后又变了,属性名变成了%00*%00test
和%00*%00var
也就是%00*%00属性名
注意到这些对构造序列化的字符串很关键,当我们直接将private protected
的属性进行序列化,得到的序列化字符串的payload将无效,因为0x00
的缘故。但是通过urlencode
就可以避免
php反序列化漏洞
反序列化本身不是漏洞,但是如果类的某些属性可控,那么在反序列的过程中就会自动的执行魔术方法,从而导致安全问题。
所以,通常反序列化漏洞的成因在于代码中的 __unserialize()
,__wakeup()
等魔术方法接收的参数可控,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击
1 | <?php |
上面的代码是接收一个参数a
,然后将其反序列化,反序列化后,会调用__wakeup()
方法。如果一切正常的话,这个方法是显示一下demo.php
文件的源代码。但是参数a
是可控的,也就是说对象a
的属性是可控的。于是我们可以伪造一个filename
来构造对象
EXP
1 | <?php |
当我们对象参数可控时,可以伪造对象的一些属性,从而实现任意文件读取等操作。
如果直接把这个exp跑出来payload拿去加载,正如之前所说, 如果我们没有urlencode
,就会得到一个无效的payload
1 | O:7:"popdemo":1:{s:17: |
POP链的构造
1、什么是POP
面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。
2、POP链原理
POP链是反序列化漏洞利用中的一种常有方法,即寻找程序环境中已经定义或能够动态加载的对象中的属性或函数,将一些能够被调用的函数组合起来,达到目的的操作
用人话来说就是,根据已有的代码,构造一条完整的调用链,该调用链与原来代码的调用链一致,不过部分属性被我们所控制,从而达到攻击目的。构造的这条链就是POP链。
用一个实例说明如何构造POP链
1 | <?php |
首先理清思路,寻找最重要的魔术方法或者能获取flag的方法作为起点。
- 先看读文件的函数在哪:
Read.file_get
里面有一个file_get_contents
Show._show()
中有一个highlight_file
- 我们可控的是
hello
参数,调用unserialize()
函数,即__wakeup()
魔术方法,于是就只有Show类
中存在该方法,但是注意到在Show.__wakeup()
中存在一个正则匹配,这个正则匹配会将$this->source
当成字符串来处理。也就是说会调用Show.__toString()
方法。 - 定位到
Show.__toString()
,可以将source
序列化为Show类的对象,就会调用__toString
方法。__toString
又会取一个str['str']->source
,那么如果这个source
不存在的话,就会执行__get()
方法。 __get()
魔术方法会调用一个$p变量
,这个也是可控的,然后会将p当做函数调用,此时触发了Read.__invoke()
魔术方法__invoke魔术方法
会触发file_get()函数
,进而base64_encode(file_get_contents($value))
最终达到读文件的目的。
这样一条完整的链就分析完了
1 | hello -> __wakeup -> Show._show -> Show.__toString -> (不存在属性)Test.__get() -> Read.__invoke |
注意对象关系(hello是Show的对象,source属性是Test的对象,p属性是Read的对象),然后写一个POP链的对应EXP,就可以了
1 | <?php |
php的Session反序列化问题
了解session相关的反序列化问题之前,得先了解下session相关的机制。这里详细机制解释已经在上篇文章解释了,就不做过多解释。这里主要是针对于session反序列化漏洞原理进行解释
如果在PHP在反序列化存储的$_SESSION
数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确地反序列化。如果session值可控,则可通过构造特殊的session值导致反序列化漏洞
这里引用大佬博客里的一个例子:
session.php
1 | <?php |
session2.php
1 | <?php |
session.php中的Session是可控的,但是反序列的魔术方法在session2.php中,而session中的参数无法直接可控。
这个时候,就可以利用两个的php的session存储机制的不同实现session的反序列化攻击
具体原理解释:
将payload用session.php,控制存储在指定文件中。
1 | plainsession.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";} |
- 此时传入的数据会按照php_serialize来进行序列化,并存储到文件中。
- 再访问session2.php,页面输出
spoock
,成功执行我们构造的函数。因为在访问session2.php时,程序会按照php来反序列化SESSION中的数据(因为同域PHPSESSIONID
是一样的,之前存的session也适用),此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。
以上就是最简单的利用,也达到了攻击目的,但是局限性比较大。
条件如下:
- 两个文件session引擎配置不同
- 其中一个session可控
- 两个文件同域
这里进一步查询资料,有一篇资料已经看不了就只能直接以我的口吻复述文章了。
当PHP中session.upload_progress.enabled
打开时,php会记录上传文件的进度,在上传时会将其信息保存在$_SESSION
中 ,具体利用条件列举如下
session.upload_progress.enabled = On
(是否启用上传进度报告)session.upload_progress.cleanup = Off
(是否上传完成之后删除session文件)
符合条件时,上传文件进度的报告就会以写入到session文件中,所以我们可以设置一个与session.upload_progress.name
同名的变量(默认名为PHP_SESSION_UPLOAD_PROGRESS
),PHP检测到这种同名请求会在$_SESSION
中添加一条数据。我们就可以控制这个数据内容为我们的恶意payload。
关于具体实例,在浙大oj也就是jarvirsoj上有一道ctf题,这里搬运下
1 | <?php |
容易发现,OowoO.__destruct()
存在代码执行,但是没有可控参数进行利用。
然后发现符合上传程序Session漏洞的条件
因为这里是直接对session取出内容然后进行反序列化,但是这里并没有对session内容的赋值操作,所以这里进行上传来写入
一个简单的上传demo
1 | <form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data"> |
其中value就是我们自己构造的恶意payload
poc.php
1 | <?php |
这里查看环境phpinfo,禁用了一些函数。可以用print_r来进行绕过
再从phpinfo中的SCRIPT_FILENAME
字段得到根目录地址:/opt/lampp/htdocs/
,构造得到payload
1 | O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r |
phar反序列化
phar文件的结构:
phar文件都包含以下几个部分:
1 | 1. stub |
简单介绍
首先介绍一下phar://
,phar://
和php://filter
、data://
协议那些一样,都是流包装,可以将一组php文件进行打包,可以创建默认执行的stub,而stub就是一个标志,他的格式是xxx<?php xxxxx;__HALT_COMPILER();?>
,结尾是__HALT_COMPILER()'?>
,不然phar
识别不了phar
文件
简单例子:
php内置了一个phar类来处理相关操作
注意:这里要将php.ini里面的phar.readonly
选项设置为Off
。并把分号去掉。
如果你在命令行运行PHP文件还是无法生成成功,请使用php -v查看php版本并在修改指定版本的php.ini。
1 | <?php |
执行一下我们看到他会产生一个phar.phar
文件,丢去二进制编辑器看一下
我们确实看到了有反序列化后的值,对应的,就有反序列化的操作,而php大部分文件系统函数在通过phar://
协议解析的时候,都会将meta-data进行反序列化,影响函数大多数是跟文件操作相关的函数
接下来进行反序列化
1 | <?php |
可以看到确实是有数据输出,因此可以看到这样是可以触发反序列化漏洞了
漏洞利用条件
- 上传html页面: upload.html
- 后端校验页面:upload.php
- 一个漏洞页面:index.php (存在file_exits(), eval()函数)
- 一个上传目录: upload_file/
upload.html:
1 | <!DOCTYPE html> |
upload.php
仅允许格式为gif的文件上传。上传成功的文件会存储到upload_file目录下
1 | <?php |
index.php
1 | <?php |
绕过思路:GIF格式验证可以通过在文件头部添加GIF89a绕过
我们可以构造一个php来生成phar.phar。
1 | <?php |
利用过程:
- 一、生成一个phar.phar,修改后缀名为phar.gif
- 二、上传到upload_file目录下
- 三、访问:http://127.0.0.1/index.php?file=upload_file/phar.gif
可见已经执行了phpinfo命令了。
通过修改后缀名和文件头,能够绕过大部分的校验
原生类的利用
原生类,顾名思义是php自带的类。而之前我们反序列化常见的是自己构建的类,这里的利用对象是php自带的类。
报错类
Error
在PHP7版本中,因为Error中带有__toString
方法,该方法会将传入给__toString
的参数原封不动的输出到浏览器。在这么一个过程中可能会产生XSS
例如,有以下代码
1 | <?php |
当传入下方payload的时候
1 | ?a=Error&b=<script>alert("Lxxx");</script> |
Exception
与Error类似,Exception同样有__toString
方法,因此测试代码和上方一样,传入以下payload,同样可以XSS
1 | ?a=Exception&b=<script>alert("Lxxx");</script> |
以上代码都能被执行,那我们可以尝试传一句话木马
1 | ?a=Exception&b=eval($_POST[1]); |
如果说直接按照上面的写法传入一个写法,只会原封不动打印出来,所以我们需要把测试代码换一个写法。
1 | <?php |
这时候传入的payload:
?a=Exception&b=system(‘whoami’)
这里虽然报错了,但是可以rce,RCE的主要原因不是Exception这个类,而是因为PHP会先执行括号内的内容,如果执行括号内的内容没有报错,再执行括号外的报错,没有报错的部分的命令同样被正常执行。因此如果将上方测试代码的第四行eval删去,则无法进行RCE
遍历目录类
DirectoryIterator
DirectoryIterator类的__construct
方法会构造一个迭代器,如果使用echo输出该迭代器,将会返回迭代器的第一项
例子:
1 | <?php |
传入payload:
1 | ?a=DirectoryIterator&b=. |
返回一个点,这里为啥返回点,建议去linux系统实际操作(因为linux里查看所有文件的第一个文件都是一个点.)
这个点代表是当前目录,如果我们想要匹配其余文件,可以使用glob
协议
1 | ?a=DirectoryIterator&b=glob://flag* |
如果这个时候不知道flag文件名怎么办
如果这个时候不知道flag文件名怎么办,所以可以尝试用暴力搜索
1 | ?a=DirectoryIterator&b=glob://f[k-m]* |
glob协议同样是支持通配符,包括ascii码中的部分匹配,例如想要匹配大写字母,那么就写[@-[]
表示ASCII码字符从@
到[
都允许匹配,也就是匹配大写字母
FilesystemIterator
如果DirectoryIterator类被禁用了,还有FilesystemIterator类可以代替,使用方法和DirectoryIterator类差不多
GlobIterator
GlobIterator和上方这两个类差不多,不过glob是GlobIterator类本身自带的,因此在遍历的时候,就不需要带上glob协议头了,只需要后面的相关内容
1 | ?a=GlobIterator&b=f[k-m]* |
SplFileObject
读取文件类
SplFileObject
SplFileObject类为文件提供了一个面向对象接口,说人话就是这个类可以用来读文件。
1 | <?php |
传payload进去:
1 | ?a=SplFileObject&b=flag.php |
SplFileObject这个类返回的仍然是一个迭代器,想要将内容完整的输出出来,最容易想到的自然是利用foreach
遍历 ,这里可以看下官方文档对该类的__construct方法
可以看到官方文档要求我们传入参数是一个文件名,而如果参数是文件名的话,我们可以尝试用伪协议
传入payload
1 | ?a=SplFileObject&b=php://filter/convert.base64-encode/resource=flag.php |
反射类
ReflectionClass
ReflectionClass反射类在PHP5新加入,继承自Reflector,它可以与已定义的类建立映射关系,通过反射类可以对类操作
反射类不仅仅可以建立对类的映射,也可以建立对PHP基本方法的映射,并且返回基本方法执行的情况。因此可以通过建立反射类new ReflectionClass(system('cmd'))
来执行命令
这里拿ctfshow的web109作例题
1 | <?php |
已知了flag在./fl36dg.txt
,命令执行system(‘cat fl36dg.txt’)
获取flag,所以应该传入如下参数
1 | v1=ReflectionClass&v2=system("ls") |
ReflectionMethod
和ReflectionClass一样
反序列化的防御
因为反序列化的缺陷可能导致远程代码执行等严重的攻击,所以我们需要对其进行防护:
- 对传入
unserilize()
的参数,进行严格地过滤。 - 在文件系统函数的参数可控时,进行严格地过滤。
- 严格检查上传文件内容,不能只是单纯地检查文件头(phar)
- 条件允许的情况下,禁用可执行系统命令、代码的危险函数。
- 注意不同类中的同名方法的编写,避免被用作反序列化的跳板。
- Session方面,一个是多文件间使用一种序列化引擎;二是尽量不要让session可控;三是保持
session.upload_progress.cleanup = On
(上传完成之后删除session文件)