Skip to content

Commit 31b2c65

Browse files
authored
Fix: Refactor JWT Level 13 to use cookie-based flow and vulnerable JWK verification (#499)
* Fix: Refactor JWT Level 13 to use cookie-based flow and vulnerable JWK verification * Fix: Handle UnsupportedEncodingException in Level 13 * Fix: Remove unused imports to satisfy spotlessJavaCheck * Fix: Remove unused SignedJWT import * Fix: Update Level 13 HTML structure to match project theme * Fix: Update Level 13 CSS to match project theme
1 parent 3dd7ba8 commit 31b2c65

File tree

4 files changed

+97
-91
lines changed

4 files changed

+97
-91
lines changed

src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@
22

33
import static org.sasanlabs.service.vulnerability.jwt.bean.JWTUtils.GENERIC_BASE64_ENCODED_PAYLOAD;
44

5-
import com.nimbusds.jose.JWSVerifier;
6-
import com.nimbusds.jose.crypto.RSASSAVerifier;
7-
import com.nimbusds.jose.jwk.JWK;
8-
import com.nimbusds.jose.jwk.RSAKey;
9-
import com.nimbusds.jwt.SignedJWT;
105
import java.io.UnsupportedEncodingException;
116
import java.security.KeyPair;
127
import java.security.interfaces.RSAPrivateKey;
@@ -16,7 +11,6 @@
1611
import java.util.List;
1712
import java.util.Map;
1813
import java.util.Optional;
19-
import javax.servlet.http.HttpServletRequest;
2014
import org.apache.logging.log4j.LogManager;
2115
import org.apache.logging.log4j.Logger;
2216
import org.sasanlabs.internal.utility.LevelConstants;
@@ -676,43 +670,42 @@ private ResponseEntity<GenericVulnerabilityResponseBean<String>> getJWTResponseB
676670
value = LevelConstants.LEVEL_13,
677671
htmlTemplate = "LEVEL_13/HeaderInjection_Level13")
678672
public ResponseEntity<GenericVulnerabilityResponseBean<String>> getHeaderInjectionVulnerability(
679-
HttpServletRequest request) {
680-
String jwtToken = request.getHeader("Authorization");
681-
if (jwtToken == null || !jwtToken.startsWith(JWTUtils.BEARER_PREFIX)) {
682-
return new ResponseEntity<>(
683-
new GenericVulnerabilityResponseBean<>("No JWT token provided", true),
684-
HttpStatus.BAD_REQUEST);
685-
}
686-
687-
jwtToken = jwtToken.replaceFirst("^" + JWTUtils.BEARER_PREFIX, "").trim();
688-
689-
try {
690-
SignedJWT signedJWT = SignedJWT.parse(jwtToken);
691-
692-
String jwkHeader = (String) signedJWT.getHeader().toJSONObject().get("jwk");
693-
694-
if (jwkHeader != null) {
695-
JWK jwk = JWK.parse(jwkHeader);
696-
RSAKey rsaKey = (RSAKey) jwk;
697-
RSAPublicKey publicKey = rsaKey.toRSAPublicKey();
698-
699-
JWSVerifier verifier = new RSASSAVerifier(publicKey);
700-
if (signedJWT.verify(verifier)) {
701-
return new ResponseEntity<>(
702-
new GenericVulnerabilityResponseBean<>(
703-
"JWK Header Injection Exploited!", false),
704-
HttpStatus.OK);
673+
RequestEntity<Void> requestEntity, @RequestParam Map<String, String> queryParams)
674+
throws ServiceApplicationException, UnsupportedEncodingException {
675+
Optional<KeyPair> asymmetricAlgorithmKeyPair =
676+
jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256");
677+
LOGGER.info(
678+
asymmetricAlgorithmKeyPair.isPresent() + " " + asymmetricAlgorithmKeyPair.get());
679+
List<String> tokens = requestEntity.getHeaders().get("cookie");
680+
boolean isFetch = Boolean.valueOf(queryParams.get("fetch"));
681+
if (!isFetch) {
682+
for (String token : tokens) {
683+
String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX);
684+
if (cookieKeyValue[0].equals(JWT)) {
685+
boolean isValid =
686+
jwtValidator.jwkKeyHeaderPublicKeyTrustingVulnerableValidator(
687+
cookieKeyValue[1]);
688+
Map<String, List<String>> headers = new HashMap<>();
689+
headers.put("Set-Cookie", Arrays.asList(token + "; httponly"));
690+
ResponseEntity<GenericVulnerabilityResponseBean<String>> responseEntity =
691+
this.getJWTResponseBean(
692+
isValid,
693+
token,
694+
!isValid,
695+
CollectionUtils.toMultiValueMap(headers));
696+
return responseEntity;
705697
}
706698
}
707-
708-
} catch (Exception e) {
709-
return new ResponseEntity<>(
710-
new GenericVulnerabilityResponseBean<>("Invalid JWT", true),
711-
HttpStatus.BAD_REQUEST);
712699
}
713-
714-
return new ResponseEntity<>(
715-
new GenericVulnerabilityResponseBean<>("Safe header", true), HttpStatus.OK);
700+
String token =
701+
libBasedJWTGenerator.getJWTTokenWithJWKHeader_RS256(
702+
GENERIC_BASE64_ENCODED_PAYLOAD, asymmetricAlgorithmKeyPair.get());
703+
Map<String, List<String>> headers = new HashMap<>();
704+
headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly"));
705+
ResponseEntity<GenericVulnerabilityResponseBean<String>> responseEntity =
706+
this.getJWTResponseBean(
707+
true, token, true, CollectionUtils.toMultiValueMap(headers));
708+
return responseEntity;
716709
}
717710

718711
// Very weak HMAC key vulnerability - using extremely short key

src/main/resources/static/templates/JWTVulnerability/LEVEL_13/HeaderInjection_Level13.css

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,43 @@
33
text-align: justify;
44
}
55

6-
#enterHeader {
6+
#description {
77
font-size: 15px;
8-
display: flex;
8+
visibility: hidden;
99
margin: 10px;
10-
flex-direction: column;
1110
}
1211

13-
#headerName, #headerValue {
14-
flex: 1;
15-
word-wrap: break-word;
16-
margin-top: 10px;
12+
#Note {
13+
font-size: 15px;
14+
margin: 10px;
1715
}
1816

19-
#headerResponse {
17+
#verificationResponse {
2018
font-size: 15px;
19+
margin: 10px;
2120
word-wrap: break-word;
2221
text-align: center;
22+
}
23+
24+
#jwt {
25+
font-weight: normal;
26+
word-wrap: break-word;
27+
margin-top: 10px;
28+
}
29+
30+
#fetchTokenButton {
31+
background: blueviolet;
32+
display: inline-block;
33+
padding: 8px 8px;
2334
margin: 10px;
35+
border: 2px solid transparent;
36+
border-radius: 3px;
37+
transition: 0.2s opacity;
38+
color: #FFF;
39+
font-size: 12px;
2440
}
2541

26-
#sendHeader {
42+
#verifyToken {
2743
background: blueviolet;
2844
display: inline-block;
2945
padding: 4px 4px;
Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1-
<!DOCTYPE html>
2-
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8">
5-
<title>Header Injection</title>
6-
</head>
7-
<body>
81
<div id="header_injection_level_13">
9-
<div>
10-
<div id="enterHeader">
11-
<div>Header Name:</div>
12-
<input type="text" id="headerName" placeholder="Enter header name" />
13-
<div>Header Value:</div>
14-
<input type="text" id="headerValue" placeholder="Enter header value" />
15-
</div>
16-
<button id="sendHeader">Send Header</button>
17-
<div id="headerResponse"></div>
2+
<div id="fetchToken">
3+
<button id="fetchTokenButton">Click here to fetch the JWT token</button>
184
</div>
19-
</div>
20-
21-
</body>
22-
</html>
5+
<div id="description">
6+
<div>JWT:</div>
7+
<div id="jwt"></div>
8+
</div>
9+
<button id="verifyToken">Verify</button>
10+
<div id="verificationResponse"></div>
11+
<div id="Note">
12+
Note: Please clear the cookies from browser.
13+
</div>
14+
</div>
Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
1-
function addEventListenerToSendHeaderButton() {
2-
document.getElementById("sendHeader").addEventListener("click", function () {
3-
const headerName = document.getElementById("headerName").value;
4-
const headerValue = document.getElementById("headerValue").value;
1+
function addingEventListenerToFetchTokenButton() {
2+
document
3+
.getElementById("fetchTokenButton")
4+
.addEventListener("click", function () {
5+
let url = getUrlForVulnerabilityLevel();
6+
url = url + "?fetch=true";
7+
doGetAjaxCall(fetchTokenCallBack, url, true);
8+
});
9+
}
10+
addingEventListenerToFetchTokenButton();
511

12+
function addingEventListenerToVerifyToken() {
13+
document.getElementById("verifyToken").addEventListener("click", function () {
614
let url = getUrlForVulnerabilityLevel();
7-
8-
const manipulatedJwt =
9-
"eyJhbGciOiJSUzI1NiIsImtpZCI6Im1hbGljaW91cy1rZXktaWQifQ.eyJzdWIiOiJleGFtcGxldXNlciIsIm5hbWUiOiJKV1QgVXNlciIsImlhdCI6MTYwOTAxMjAwMH0.c7qHUq1HbHj8AWjKbcIYH2NZnE6PtNyXTnJTWZELvFbfbFhc5BQ_w8e24fXL2OzhhOT5qHVzFvHgOeEYFLZNGEDlJhF4o76yHsMJdWQFL4I5uZjG0o8XV0HjDdM7GqEmx2j0JHi6vJ8Q3pIqGzUBmb7bgzD4kENnP-UqfkbNl2ykYZ9Nybw_E7CAV4OxuqE4QyIpZV2VttWjefK3c6TIj9hNWvYYgipKwHFLXbOV-rOZ6K-_H_4D-kbr0LKPPX-s4b11o0wtS3y1FiHDXEvsmEjhRApEc_jk5uZY-AGPUc9Nl9t6iT_Nh1Q8Usz-jZifg03NwumJjDNtz-nS7gzg";
10-
11-
doGetAjaxCall(
12-
function (data) {
13-
document.getElementById("headerResponse").innerHTML = data.isValid
14-
? "Header Injection was successful!"
15-
: "Header Injection failed. Please try again.";
16-
},
17-
url,
18-
true,
19-
{
20-
[headerName]: headerValue,
21-
Authorization: `Bearer ${manipulatedJwt}`,
22-
}
23-
);
15+
doGetAjaxCall(updateUIWithVerifyResponse, url, true);
2416
});
2517
}
18+
addingEventListenerToVerifyToken();
2619

27-
addEventListenerToSendHeaderButton();
20+
function updateUIWithVerifyResponse(data) {
21+
if (data.isValid) {
22+
document.getElementById("verificationResponse").innerHTML = "JWT is valid";
23+
} else {
24+
document.getElementById("verificationResponse").innerHTML =
25+
"JWT: " + data.content + " is not valid. Please try again";
26+
}
27+
}
28+
29+
function fetchTokenCallBack(data) {
30+
document.getElementById("jwt").innerHTML = data.content;
31+
document.getElementById("description").style.visibility = "visible";
32+
}

0 commit comments

Comments
 (0)