身份认证缺陷

身份认证缺陷

Authentication Bypasses

审计

1749382855124-ffee8079-b624-4779-8bce-531aab849582.png

1749382864155-8cbd1037-4aba-4807-b9c0-5d9ebd198d06.png

  • 创建AccountVerificationHelper实例,用于处理账户验证逻辑

parseSecQuestions函数的作用是从请求体中遍历参数名,找到包含secQuestion的参数,将其值存入Map中并返回

这里直接把AccountVerificationHelper整个分析一下


/** Created by appsec on 7/18/17. */
public class AccountVerificationHelper {

  // simulating database storage of verification credentials
  private static final Integer verifyUserId = 1223445;
  private static final Map<String, String> userSecQuestions = new HashMap<>();

  static {
    userSecQuestions.put("secQuestion0", "Dr. Watson");
    userSecQuestions.put("secQuestion1", "Baker Street");
  }

  private static final Map<Integer, Map> secQuestionStore = new HashMap<>();

  static {
    secQuestionStore.put(verifyUserId, userSecQuestions);
  }

  // end 'data store set up'

  // this is to aid feedback in the attack process and is not intended to be part of the
  // 'vulnerable' code
  public boolean didUserLikelylCheat(HashMap<String, String> submittedAnswers) {
    boolean likely = false;

    if (submittedAnswers.size() == secQuestionStore.get(verifyUserId).size()) {
      likely = true;
    }

    if ((submittedAnswers.containsKey("secQuestion0")
            && submittedAnswers
                .get("secQuestion0")
                .equals(secQuestionStore.get(verifyUserId).get("secQuestion0")))
        && (submittedAnswers.containsKey("secQuestion1")
            && submittedAnswers
                .get("secQuestion1")
                .equals(secQuestionStore.get(verifyUserId).get("secQuestion1")))) {
      likely = true;
    } else {
      likely = false;
    }

    return likely;
  }

  // end of cheating check ... the method below is the one of real interest. Can you find the flaw?

  public boolean verifyAccount(Integer userId, HashMap<String, String> submittedQuestions) {
    // short circuit if no questions are submitted
    if (submittedQuestions.entrySet().size() != secQuestionStore.get(verifyUserId).size()) {
      return false;
    }

    if (submittedQuestions.containsKey("secQuestion0")
        && !submittedQuestions
            .get("secQuestion0")
            .equals(secQuestionStore.get(verifyUserId).get("secQuestion0"))) {
      return false;
    }

    if (submittedQuestions.containsKey("secQuestion1")
        && !submittedQuestions
            .get("secQuestion1")
            .equals(secQuestionStore.get(verifyUserId).get("secQuestion1"))) {
      return false;
    }

    // else
    return true;
  }
}

这里直接定义了用户ID和用户的问题关联并将它们存入了secQuestionStore这个变量

  • didUserLikelylCheat函数通过

(用户提交的参数数量)=(参数数量) (此条可能会被后面覆盖,但是由于后面参数不存在,检验跳过,所以这里是有用的

(用户提交参数名包含所需要参数名)and(对应参数名答案正确)

判断用户是否作弊


  • verifyAccount函数和didUserLikelylCheat函数很像,也是通过检查用户参数数量和对应参数答案是否匹配(不同的是这里用的不等于的比较,为后面的绕过留下了余地)

didUserLikelylCheat和verifyAccount两个函数判定过了就能绕过了


这里的核心原理是如果**secQuestion0不存在就会跳过检验,所以其实如果包含secQuestion参数名的数量正确之后,剩下的检验就会全部跳过,所以只要构造两个包含secQuestion的参数就行了**

靶场

1749434816569-54a1b26a-9151-4955-8b07-a14ca2b820d3.png

Insecure Login

靶场

1749435514339-7d910e05-efd5-4362-894d-11f29147a3eb.png

1749435503861-43d1d6e0-d7bc-4a18-8409-0a4197e23f68.png

这里就是简单的拦截请求再填入

审计

1749435568006-84fcdad6-8ba8-46fb-a8c4-ef7331d6d5dc.png

login部分,主要是js

1749436070463-1fd306ad-4862-4017-98a8-d902c22ea789.png


function submit_secret_credentials() {
    var xhttp = new XMLHttpRequest();
    xhttp['open']('POST', 'InsecureLogin/login', true);
	//sending the request is obfuscated, to descourage js reading
	var _0xb7f9=["\x43\x61\x70\x74\x61\x69\x6E\x4A\x61\x63\x6B","\x42\x6C\x61\x63\x6B\x50\x65\x61\x72\x6C","\x73\x74\x72\x69\x6E\x67\x69\x66\x79","\x73\x65\x6E\x64"];xhttp[_0xb7f9[3]](JSON[_0xb7f9[2]]({username:_0xb7f9[0],password:_0xb7f9[1]}))
}



--->
function submit_secret_credentials() {
    var xhttp = new XMLHttpRequest();
    xhttp.open('POST', 'InsecureLogin/login', true);
    // 发送混淆后的请求
    xhttp.send(JSON.stringify({
        username: "CaptianJack", 
        password: "BlackPearl"
    }))
}

使用了十六进制编码的字符串数组 _0xb7f9,但请求包中仍然是明文

JWT

理论部分:

技术本质

JWT是一种基于RFC 7519标准的轻量级安全凭证格式,采用紧凑的URL-safe字符串形式传输经过数字签名JSON数据。其核心价值在于通过密码学签名机制实现了信息自包含(self-contained)和防篡改(tamper-proof)的特性。

安全机制

  • 数字签名保障:支持两种签名方式
    • 对称加密:使用HMAC算法(HS256/HS384/HS512)+ 服务端密钥
    • 非对称加密:使用RSA/ECDSA算法(RS256/ES256等)+ 公私钥体系

基本结构

Header.Payload.Signature

1749437380884-ac5e0421-493a-4e51-9c36-58bd5acb566a.png

  • Header (头部):包含令牌类型和签名算法

{
  "alg": "HS256",
  "typ": "JWT"
}
  • Payload(负载):包含声明(claims),有三种类型:
    • Registered claims:预定义声明如 iss(签发者), exp(过期时间), sub(主题)等
    • Public claims:公开定义的声明
    • Private claims:自定义声明
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
  • **Signature **(签名):用于验证消息完整性,创建方式:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

令牌

  • 两种令牌类型
    • 访问令牌(Access):用于向服务器发起API请求,有效期较短
    • 刷新令牌(Refresh):用于获取新的访问令牌,有效期较长

获取jwt的流程

在客户端和服务端之间

1749438065124-85aed0d8-74ed-4060-a790-7acda954dad3.png

JWT Claim Misuse

**JWT Claim Misuse(**JWT声明滥用)指的是对JSON Web Token中的声明(payload)部分进行不当或未经授权的操纵行为。

  • 未经授权的声明添加

攻击者尝试添加未授权的声明以获取不应有的权限

例如:普通用户添加管理员权限声明

  • 声明篡改

修改现有声明的值来操纵身份或权限

例如:修改"user_id"来冒充其他用户

  • 过度声明

添加大量不必要或虚假声明

目的可能是增大令牌体积影响系统性能

  • 过期时间篡改

修改"exp"声明延长令牌有效期

使攻击者能够维持超出预期的访问权限

  • 重放攻击

重复使用已过期的有效JWT

用于冒充原始用户或利用有时限的功能

  • 关键声明操纵

如操纵"kid"(密钥ID)声明

可能使服务器使用错误的密钥进行验证

JKU相关漏洞

JKU是JWT规范的一部分,允许通过URL动态获取验证签名所需的公钥。

  • 漏洞原理
    • 当JWT使用弱密钥签名,且JKU指向外部公钥时
    • 攻击者可制作恶意JWT并控制验证公钥的来源
  • 攻击步骤
    • 识别JKU端点
    • 制作恶意JWT
    • 使用自有私钥签名
    • 发送给服务器
    • 服务器从攻击者控制的URL获取公钥验证
    • 攻击成功
  • 防御措施
    • 白名单机制
    • 静态密钥
    • 增强验证
    • 监控审计
    • 安全测试

实践部分

jwt_decode

靶场

1749437777184-a62d3d54-4392-48a3-8ba8-b8599d49bedb.png

简单的解密之后提取用户名就行了

1749437669658-ec28c551-af17-41ca-b412-01437ce45273.png

审计

1749437903931-b0b7cc15-9bcc-4c64-afd7-a0e312a57e24.png

判断user值是否匹配

jwt_vote

审计

1749438507572-8a878f67-ca09-4c28-88af-e652ed91810e.png

先找到前端触发vote函数

1749438662848-26d657b7-c43e-4df7-a7e4-36d4566365ec.png

getvoting函数

  • 通过$.get(“JWT/votings”)从服务端获取投票项列表
  • 使用字符串替换方式构建HTML模板
  • votesList是前端投票面板的id值

vote函数

  • 验证用户是否为"Guest"
  • 通过$.ajax POST请求发送到JWT/votings/{title}端点

1749439427495-416eebf9-6089-4be1-a968-c7dacf50abea.png

(Claims)设置

  • setIssuedAt设置签发时间(10天后),设置admin和user

jwt签名

  • 设置声明(claims)
  • 使用密钥签名, 对JWT_PASSWORD进行HS512对称加密

其中:JWT_PASSWORD = TextCodec.BASE64.encode(“victory”);//注意密钥

cookie设置

  • 将jwt存入cookie

生成claims,jwt,cookie,并返回200 OK

若validUsers.contains(user)判断没过,则清空Cookie值,并返回401未授权

1749440960729-429f9f93-e8d2-4f6d-b8b8-d0a46c71562e.png

从cookie中获取access_token,如果存在

try {
    Jwt jwt = Jwts.parser()
            .setSigningKey(JWT_PASSWORD)  // 使用密钥验证签名
            .parse(accessToken);         // 解析 JWT
    Claims claims = (Claims) jwt.getBody();  // 获取 payload(声明)

核心

boolean isAdmin = Boolean.valueOf(String.valueOf(claims.get("admin")));
if (!isAdmin) {
    return failed(this).feedback("jwt-only-admin").build();
 } 
else {
    votes.values().forEach(vote -> vote.reset());
    return success(this).build();
}

从JWT的payload中获取admin字段,如果为true则成功

靶场

在删除时抓包

1749443145725-3bcbf5a5-0abc-49d2-9165-2b9ae5de0629.png

对token解码

1749443112344-e6c1cc11-84e8-4125-8993-ab8afe155e20.png

把admin改成true,再输入密钥为上文提到的victory,再放回去

1749442942999-ea3138a2-c03d-4472-8952-96aaf90c325d.png

成功

JWT 破解

审计

1749447977977-252a8a49-342c-42d0-b89a-e5af16e250c2.png

成功条件是WEBGOAT_USER.equalsIgnoreCase(user)

但是注意到这里的有效时间只有一分钟,要不就快速的,要不就要最后把时间戳换一下

靶场

1749447525581-bf2a9b31-f217-477c-8e0f-81b4773cc5fe.png

用脚本爆破密钥

1749451812323-b8c364e8-6a32-4d46-bad3-186a21df2ce6.png

我这里是business

重新替换之后编码输入就行

1749451786695-6369b604-ccba-411d-a25f-24f847e84a3a.png

refreshing token

审计

1749459157598-6d510eb3-bf91-458d-a0ec-1bd3a2394166.png

  • 检查用户是否是Tom
  • 如果是Tom,额外检查JWT头部的算法是否为none
靶场

1749466941585-54bb4100-c83d-4dfc-87c3-47b4fe424e55.png

让我们让Tom付钱,把token改成tom的

1749467052701-2d874a2a-a76c-4d11-9d10-7d6cea306f37.png

题目给了一个文件,解码发现是Tom的token

1749466922924-0bebca0e-c648-4bb5-80f2-6de968cae858.png

拦截一个请求,发现里面有jerry的token

1749467245014-e3940a96-1c21-4e80-b544-2c2204010836.png

1749467220924-85e55f37-f6f6-4bff-a940-28e245a75023.png

第一反应就是替换,但是肯定没这么简单

1749467367261-2cfa5074-13e4-4148-be1a-163317c04751.png

果然,认证过期了。

改时间戳,欸嘿,成功了!

1749468258996-5dba463a-4d6b-4511-a127-132e3659a39f.png1749468243181-1988a43a-efaa-4eb4-8f0f-cad3b182ffb8.png

JWTHeaderJKUEndpoint

审计

1749471782425-07382173-3d25-4a4c-b5d3-c6571251cfa2.png

1749471821273-adc59b7d-0fc5-4fdc-bb9e-ca29eebe5a84.png

重点在这里生成了一个JWT

Jwt jwt =
            Jwts.parser()
                .setSigningKeyResolver(
                    new SigningKeyResolverAdapter() {
                      @Override
                      public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
                        final String kid = (String) header.get("kid");
                        try (var connection = dataSource.getConnection()) {
                          ResultSet rs =
                              connection
                                  .createStatement()
                                  .executeQuery(
                                      "SELECT key FROM jwt_keys WHERE id = '" + kid + "'");
                          while (rs.next()) {
                            return TextCodec.BASE64.decode(rs.getString(1));
                          }
                        } catch (SQLException e) {
                          errorMessage[0] = e.getMessage();
                        }
                        return null;
                      }
                    })
                .parseClaimsJws(token);
  • **@Override public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims):**重写方法,根据JWT头部和声明返回签名密钥的字节数组
  • kid从jwt头获得id字段
  • 执行SQL查询,根据**kidjwt_keys**表获取对应的密钥
  • nTextCodec.BASE64.decode(rs.getString(1));返回**base64解码**后的密钥字节数组
  • .**parseClaimsJws(token);**使用配置的解析器解析并验证JWT
靶场

1749470404275-58707ee8-5677-4859-a101-dd22d51b3b32.png

这里是Jerry想删掉Tom的账户,那可能就要构造一个Tom的token

拦截一个请求把它里面的token解码之后得到

1749471545512-cf8e3261-851c-43b5-89ef-5e574e21e86a.png

讲密钥设为1并在kid中构造联合注入

1749474978540-9cb204fa-506c-42fa-887d-85383492eec6.png

在webwolf里搞了很久,发现自己拦截没开,哈哈哈

1749474497664-8afdf85f-1983-4a65-b017-c96cf3ee72c3.png

Password reset

Question

审计

1749476209345-81c6ef3a-4d80-4bff-a9c9-1cd7480f6d3d.png

就是获取用户名和答案,再根据用户名对密码进行比对

可以对密码进行爆破

SecurityQuestion

前面部分设置了一些问题和答案

1749476512379-e7123575-742d-4d7d-add4-fe6a47760c5c.png

  • answer.isPresent():检查是否存在有效答案

这里设置了一个triedQuestion类

1749476644668-4d1807b5-32e3-44c0-808a-e9affeb66527.png

就是简单的计数和判断

1749479703349-1211be5b-4ab6-4247-972e-6586f83c4bce.png

  • String resetLink = UUID.randomUUID().toString();:使用UUID生成随机重置令牌
  • 将令牌存储在全局集合中
  • 获取主机头信息
  • 判断 host 当中是否存在 WebWlof 服务对应的端口与 Host

抓包改一下host

1749482510597-d7466afd-a5a8-4dd2-8640-28fa6a1e0538.png

在wobwolf中找到对应请并访问

更新: 2025-06-11 16:42:37
原文: https://www.yuque.com/cindahy/aqfzwf/bs7vn9p8lf6o430d

LICENSED UNDER CC BY-NC-SA 4.0
评论