Logo ryp 的博客

博客

严重安全问题已修复!

avatar ryp 2026-09-24 21:25:06

中秋假期,我拿伟大的 deepseek harness 接入 dsv4f,随后惊喜地发现了 WyOJ 的两个重大 bug,都与“复制题目”相关。

下面是 dsv4f 撰写的 bug 报告。

WyOJ 缺陷报告

  • 审查对象:3b6cabd4(老布局:仓库根即 web 根)
  • 修复补丁:wyoj-security-fixes.patch(22 个文件,已应用到线上)
  • 说明:下面"复现"针对打补丁之前的代码;每条都标了修复位置。本文件只收录已修复的问题。

摘要

No 严重度 类型 位置 状态
1 高 未认证 SQL 注入 + 命令注入 + 反射型 XSS app/controllers/problem_set.php 已修
2 高 judger 上报字段 SQL 注入 app/controllers/judge/submit.php 已修
3 中 存储型 XSS(4 类数据 / 20 处输出) 多处 已修
4 中 CSRF(赞踩 / 私信 / 切换展示勋章) 3 个裸 POST 已修
5 中 HTML::escape() 不转义单引号 app/models/HTML.php:5 已修

1. [高] problem_set.php:未认证 SQL 注入 + 命令注入 + 反射型 XSS

位置:"复制题目"分支(修复前 :39-64)与"迁移题目"分支(修复前 :16-19)。

影响 - 未登录即可盲注数据库(同一个 $pid 同时流向 SQL 和 shell); - 有题目控制权的人可让 web 容器执行任意命令(即 web 主机 RCE); - 附带一处反射型 XSS。

复现(修复前) 1. SQL 注入:POST /problems,copy_problem_submit=submit&copy_problem_list=0 or sleep(3) → 响应延迟约 3 秒(对照 copy_problem_list=1 不延迟)。原因:queryProblemBrief($pid) 把 $pid 直接插进 select * from problems where id = $pid,而它作为权限判断的参数先被求值,因此在 becomeMsgPage() 终止请求之前就已执行。

  1. 命令注入:同一字段填 1; id >/tmp/pwned(需先通过第一轮权限检查,即出题人/超管),$pid 进入 dataCopyProblemData() → exec("cp -r /var/uoj_data/$src …")。

  2. 反射型 XSS:POST /problems,migrate_problem_submit=submit&target_group=<script>alert(1)</script>&start_pid=1&end_pid=1 → "未找到组 …" 原样回显。

  3. 附带 bug:if (!group) 把 group 当常量(求值为真值字符串 'group'),这个"组不存在"守卫从未生效;PHP 8 下直接 fatal。

根因:外部输入未校验就进入 SQL 与 shell 命令;错误消息未转义。

修复 - problem_set.php:52-61:每个 pid 在使用前 validateUInt()(一处同时堵掉 SQL 注入与命令注入,因为同一变量流向两处);:20 转义 $_POST['target_group'];两个 POST 分支加登录门槛。 - app/libs/uoj-data-lib.php:5-41:data*Problem() 系列改用 escapeshellarg() + dataCheckId()(纵深防御,防未来调用方再引入)。


2. [高] judge/submit.php:judger 上报字段 SQL 注入

位置:app/controllers/judge/submit.php(修复前 :32-38、:63-69、:75-77)。

影响:通过 authenticateJudger()(口令比对 judger_info,而仓库预置的是公开占位符 _judger_password_)后,result JSON 里的 status / error 被直接拼进 SQL,score / time / memory 连引号都没有 → 完整 SQL 注入,可改任意表。$_POST['id'] 本身在派发处有 validateUInt,不是注入点。

复现(修复前,需一个有效 judger 账号):POST /judge/submit,带 submit=1&id=1,把 result 的 status 填成 Judged', score=0 where 1=1 -- 即可改变写入语义。

根因:把不可信 JSON 当作 SQL 字面量拼接。

修复:新增 judgeResultFromPost()(:12)统一解码并拒绝非数组;status/error 走 DB::escape()(:48、:50、:84),score/time/memory/success 强制 (int)(列类型本就是 int(11)/tinyint(1),与 judger 端的 int() 一致)。


3. [中] 存储型 XSS:4 类数据、20 处输出未转义

数据 谁能写 修复前的输出点
motto 任何注册用户 index.php:699/730/757、uoj-html-lib.php:1240、views/blog-preview.php:37
组昵称(存于 user_info.email) 组管理员 group_info.php:9,27、user_info.php:16-18、uoj-html-lib.php:432/496、groups_manage.php:87、problem.php:295、problem_set.php:98
题目标题 / 远端题目标题 出题人 getProblemLink/getContestProblemLink、echoGroupProblemList/SizeList、problem.php:220/230、problem_statistics.php:62、rps.php:11、4 处管理页标题
勋章显示名 组管理员 / 超管 echoCertsList 的 $text

复现(修复前):个人设置里把格言设为 <img src=x onerror=alert(1)>(28 字符,validateMotto 只校验长度)→ 打开首页,脚本在所有访客浏览器中执行。组昵称、题目标题同理(写入端分别是"validator 直接 return '';"和"只做 DB::escape")。

根因:输入端只做长度/存在性校验,输出端不转义。

修复:20 处输出统一 HTML::escape()。

注意(易踩):blog 标题不要在输出端转义——它在写入时已被 UOJBlogEditor.php:96 转义,再转一次会双重转义。因此 getBlogLink() 保持原样。这个仓库混用了"存储时转义"和"输出时转义"两种风格。


4. [中] CSRF:三个无 token 的裸 POST

端点 后果 修复
click_zan.php 冒用受害者身份刷赞/踩 :20 加 crsf_defend();js/uoj.js 带 _token
user_msg.php 以受害者名义发私信 :102 加 crsf_defend();页面 JS 带 _token
certs_manage.php 改掉受害者展示的勋章 :36 加 crsf_defend();表单加 HTML::hiddenToken()

复现(修复前):在任意第三方页面放置自动提交的表单,指向 https://<host>/click-zan,字段 id=1&delta=1&type=P;受害者登录状态下访问该页即被记账。

根因:UOJForm 生成的表单内部会校验 token(uoj-form-lib.php:49,277),但这三处是手写表单/AJAX,绕过了它。

修复:端点加 crsf_defend();page-header.php 暴露 uojCsrfToken 给 JS,并 bump js/uoj.js?v=。bump 版本号是必须的——否则浏览器缓存的旧 JS 不带 token,赞踩会全部失效。


5. [中] HTML::escape() 不转义单引号

位置:app/models/HTML.php:5(修复前是裸 htmlspecialchars($str),PHP 7.4 默认 ENT_COMPAT)。

影响:user_info.php:24 把 bgurl 放进单引号 CSS 字符串 url('<…>'),而 bgurl 存储时只做 DB::escape(certs_manage.php:53)→ 一个 ' 就能闭合字符串注入 CSS 声明(视觉劫持、借 url() 外带信息;现代浏览器不会执行 JS)。

复现(修复前):设置背景地址为 x');background:url(//attacker/),查看自己主页的 style 属性。

修复:htmlspecialchars($str, ENT_QUOTES, 'UTF-8')。已确认 HTML::escape 的输出没有任何一处落在 <script> 块内,不会破坏 JS。


回归验证清单

  1. php -l 全部改动文件(本机无 php,未过 lint)。
  2. 首页 / 用户主页 / 题库页 / 题目页渲染正常。
  3. 格言填 <img src=x onerror=alert(1)> → 显示字面文本,不弹窗。
  4. 点赞一次 → 计数变化;不带 token 的裸 POST → 返回 This page has expired.
  5. 发私信、切换展示勋章 → 正常。
  6. 复制题目 / 迁移题目(有出题权限)→ 正常。
  7. 提交一次评测 → 结果正常写回(确认 judge/submit.php 的转义没有破坏协议)。

评论

暂无评论

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。

ryp Avatar
我拒绝零假设
活跃度 近 53 周
查看全年