# base-admin 漏洞报告 软件名称:base-admin 开发者或厂商:huanzi-qch 代码仓库:https://github.com/huanzi-qch/base-admin ## 1. 用户名存储型 XSS 导致账号接管 简要描述:普通用户修改个人用户名时写入 XSS,受害者打开用户管理页面时脚本执行,接管受害者账号。 影响版本:9aacd614 (2019-09-17) 至 52816b76 (2026-03-17) 漏洞引入:[base-admin commit 9aacd614](https://github.com/huanzi-qch/base-admin/commit/9aacd614d721cc2a4d24c1a952e118b8e622d7a4) 前置条件:攻击者有普通权限账号,且受害者打开用户管理页面。 ### 漏洞成因 普通用户在个人信息页修改用户名时,服务端把请求中的 `userName` 保存到登录用户的记录。个人信息页通过下面的输入框提交用户名: ```html
``` 保存请求由 `UserController.updateUser` 接收,方法把参数交给 `UserServiceImpl.updateUser`: ```java // src/main/java/cn/huanzi/qch/baseadmin/user/controller/UserController.java:58-63 commit 52816b760c @PostMapping("updateUser") @Decrypt @Encrypt public Result updateUser(SysUserVo sysUserVo) { return userService.updateUser(sysUserVo); } ``` `UserServiceImpl.updateUser` 从请求对象中取出 `userName`,写入登录用户的记录: ```java // src/main/java/cn/huanzi/qch/baseadmin/user/service/UserServiceImpl.java:55-61 commit 52816b760c @Override public Result updateUser(SysUserVo sysUserVo) { SysUserVo sysUserVo1 = sysUserService.findByLoginName(SecurityUtil.getLoginUser().getUsername()).getData(); //只允许用户修改这几个选项 sysUserVo1.setUserName(sysUserVo.getUserName()); //调用保存 Result result = sysUserService.save(sysUserVo1); ``` `setUserName` 的值来自 HTTP 请求,方法没有校验或转义,`sys_user.user_name` 因此可以保存 HTML。 管理员打开用户管理页面时,前端用 layui table 渲染用户列表,`userName` 列的配置如下: ```javascript // src/main/resources/static/sys/user/js/user.js:41-44 commit 52816b760c , cols: [[ {field: 'userId', title: 'ID', hide: true} , {field: 'userName', title: '用户名称'} ``` 该列没有 `templet`,layui 2.5.4 的 `table` 模块在未开启转义时把字段原值拼进单元格: ```javascript // src/main/resources/static/common/layui/lay/modules/table.js:1 commit 52816b760c u=function(e,a,l,n){var o=e.templet?function(){return"function"==typeof e.templet?e.templet(l):i(t(e.templet).html()||String(a)).render(l)}():a;return n?t("
"+o+"
").text():o} ``` 用户管理页面加载后,`userName` 中的 `` 被插入 DOM,`onerror` 执行。脚本运行在管理员的会话中,可以调用管理接口。 ### 复现环境搭建 项目没有提供 compose 文件,下面用项目自身的构建产物和 MySQL 8 启动。构建: ```bash git clone https://github.com/huanzi-qch/base-admin.git cd base-admin docker run --rm -v "$PWD":/src -w /src maven:3.8-openjdk-8 mvn -DskipTests package ``` `pom.xml` 把 Spring Boot 可执行 jar 输出到 `package/base-admin.jar`。在该目录创建 `docker-compose.yml`: ```yaml name: base-admin-demo services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: "123456" MYSQL_DATABASE: base_admin volumes: - ./src/main/resources/static/sql/base_admin.sql:/docker-entrypoint-initdb.d/base_admin.sql:ro healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p123456"] interval: 5s timeout: 3s retries: 40 app: image: eclipse-temurin:8-jre depends_on: db: condition: service_healthy volumes: - ./package/base-admin.jar:/app/app.jar:ro - ./fj:/data/fj environment: SPRING_DATASOURCE_URL: "jdbc:mysql://db:3306/base_admin?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false" SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: "123456" FILE_UPLOADPATH: /data/fj/ command: ["java", "-jar", "/app/app.jar"] ports: - "8888:8888" ``` 启动环境: ```bash mkdir -p fj docker compose up -d ``` 应用默认监听 8888,context path 为 `/baseadmin`。初始 SQL 会创建 `sa`、`admin`、`daji`、`xiaofang` 四个账号,密码分别为 `123456`、`123456`、`123456`、`111111`。重复复现前执行 `docker compose down -v` 再 `up -d`,数据库会回到初始状态。 ### 复现过程 浏览器打开 `http://127.0.0.1:8888/baseadmin/loginPage`,用 `daji/123456` 登录。验证码在 dev 配置下不校验。 打开 `http://127.0.0.1:8888/baseadmin/user/userinfo`,在“用户名”输入框中填入下面的 payload,点击“保存”: ```html ``` `userId` 是初始 SQL 中 `daji` 的用户 ID,`authorityIdList` 是 `ROLE_SA` 的权限 ID。payload 长 238 字符,`user_name` 为 `varchar(255)`。填写 payload 后的界面如下: ![个人信息页中的 payload](img/01-userinfo-payload.png) 点击“保存”后的界面如下: ![保存成功](img/02-userinfo-saved.png) 退出登录,用 `sa/123456` 登录,打开 `http://127.0.0.1:8888/baseadmin/sys/sysUser/user`。用户列表渲染 `daji` 这一行时,`` 的 `onerror` 执行,页面顶部出现红色的 `XSS EXECUTED`,并且 `saveAllByUserId` 把 `daji` 提升为 `ROLE_SA`: ![管理员打开用户管理页面时触发 XSS](img/03-admin-xss.png) 退出登录,用 `daji/123456` 重新登录。个人信息页把用户名改回 `daji`,再打开 `http://127.0.0.1:8888/baseadmin/sys/sysUser/user`。该页面位于 `/sys/**`,初始的 `daji` 只有 `ROLE_USER`,无法访问;现在能以 `daji` 的身份正常打开,结果如下: ![daji 取得 ROLE_SA 后打开用户管理页面](img/04-daji-admin.png) 以上步骤表明,普通用户可以借助管理员的用户管理页面接管管理员账号。