diff --git a/build.gradle b/build.gradle index 25fe4e2c..3573ed8c 100644 --- a/build.gradle +++ b/build.gradle @@ -150,7 +150,7 @@ dependencies { implementation group: 'org.json', name: 'json', version: '20190722' //https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt - implementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: '8.3' + implementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: '9.31' // https://mvnrepository.com/artifact/commons-io/commons-io implementation group: 'commons-io', name: 'commons-io', version: '2.7' diff --git a/src/main/java/org/sasanlabs/internal/utility/LevelConstants.java b/src/main/java/org/sasanlabs/internal/utility/LevelConstants.java index bff253e9..76687664 100755 --- a/src/main/java/org/sasanlabs/internal/utility/LevelConstants.java +++ b/src/main/java/org/sasanlabs/internal/utility/LevelConstants.java @@ -18,6 +18,7 @@ public interface LevelConstants { String LEVEL_10 = "LEVEL_10"; String LEVEL_11 = "LEVEL_11"; String LEVEL_12 = "LEVEL_12"; + String LEVEL_13 = "LEVEL_13"; static int getOrdinal(String level) { if (level.indexOf("_") > 0) { diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index 56e7e3c3..4cd88d68 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -2,6 +2,11 @@ import static org.sasanlabs.service.vulnerability.jwt.bean.JWTUtils.GENERIC_BASE64_ENCODED_PAYLOAD; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jwt.SignedJWT; import java.io.UnsupportedEncodingException; import java.security.KeyPair; import java.security.interfaces.RSAPrivateKey; @@ -11,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import javax.servlet.http.HttpServletRequest; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.sasanlabs.internal.utility.LevelConstants; @@ -662,4 +668,50 @@ private ResponseEntity> getJWTResponseB true, token, true, CollectionUtils.toMultiValueMap(headers)); return responseEntity; } + + @AttackVector( + vulnerabilityExposed = VulnerabilityType.HEADER_INJECTION, + description = "HEADER_INJECTION_VULNERABILITY") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_13, + htmlTemplate = "LEVEL_13/HeaderInjection_Level13") + public ResponseEntity> getHeaderInjectionVulnerability( + HttpServletRequest request) { + String jwtToken = request.getHeader("Authorization"); + if (jwtToken == null || !jwtToken.startsWith(JWTUtils.BEARER_PREFIX)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>("No JWT token provided", true), + HttpStatus.BAD_REQUEST); + } + + jwtToken = jwtToken.replaceFirst("^" + JWTUtils.BEARER_PREFIX, "").trim(); + + try { + SignedJWT signedJWT = SignedJWT.parse(jwtToken); + + String jwkHeader = (String) signedJWT.getHeader().toJSONObject().get("jwk"); + + if (jwkHeader != null) { + JWK jwk = JWK.parse(jwkHeader); + RSAKey rsaKey = (RSAKey) jwk; + RSAPublicKey publicKey = rsaKey.toRSAPublicKey(); + + JWSVerifier verifier = new RSASSAVerifier(publicKey); + if (signedJWT.verify(verifier)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "JWK Header Injection Exploited!", false), + HttpStatus.OK); + } + } + + } catch (Exception e) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>("Invalid JWT", true), + HttpStatus.BAD_REQUEST); + } + + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>("Safe header", true), HttpStatus.OK); + } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/bean/JWTUtils.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/bean/JWTUtils.java index cee663c6..10386e40 100755 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/bean/JWTUtils.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/bean/JWTUtils.java @@ -37,6 +37,7 @@ public class JWTUtils { public static final String JWT_EC_ALGORITHM_IDENTIFIER = "EC"; public static final String JWT_OCTET_ALGORITHM_IDENTIFIER = "ED"; public static final String JWT_HMAC_SHA_256_ALGORITHM = "HS256"; + public static final String BEARER_PREFIX = "Bearer "; // TODO need to make it better. public static final String HS256_TOKEN_TO_BE_SIGNED = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." diff --git a/src/main/java/org/sasanlabs/vulnerability/types/VulnerabilityType.java b/src/main/java/org/sasanlabs/vulnerability/types/VulnerabilityType.java index 06c34af7..a5d4cc17 100644 --- a/src/main/java/org/sasanlabs/vulnerability/types/VulnerabilityType.java +++ b/src/main/java/org/sasanlabs/vulnerability/types/VulnerabilityType.java @@ -21,7 +21,7 @@ public enum VulnerabilityType { CLIENT_SIDE_VULNERABLE_JWT(null, null), SERVER_SIDE_VULNERABLE_JWT(null, null), INSECURE_CONFIGURATION_JWT(null, null), - + HEADER_INJECTION(20, 20), PATH_TRAVERSAL(22, 33), COMMAND_INJECTION(77, 31), diff --git a/src/main/resources/i18n/messages_en_US.properties b/src/main/resources/i18n/messages_en_US.properties index d73d7e89..c0ebb66b 100755 --- a/src/main/resources/i18n/messages_en_US.properties +++ b/src/main/resources/i18n/messages_en_US.properties @@ -285,4 +285,7 @@ SSRF_VULNERABILITY_URL_WITHOUT_CHECK=No validation on the provided URL. SSRF_VULNERABILITY_URL_IF_NOT_FILE_PROTOCOL=file:// protocol is not allowed for the provided URL. SSRF_VULNERABILITY_URL_IF_NOT_FILE_PROTOCOL_AND_169.254.169.254=file:// protocol as well as access to internal metadata service IP 169.254.169.254 is not allowed. SSRF_VULNERABILITY_URL_IF_NOT_FILE_PROTOCOL_AND_INTERNAL_METADATA_URL=file:// protocol as well as access to internal metadata service is not allowed. -SSRF_VULNERABILITY_URL_ONLY_IF_IN_THE_WHITELIST=Only Whitelisted URL is allowed. \ No newline at end of file +SSRF_VULNERABILITY_URL_ONLY_IF_IN_THE_WHITELIST=Only Whitelisted URL is allowed. + +# JWT Injection Header +HEADER_INJECTION_VULNERABILITY=It tests how a JWT header can be manipulated to alter the signature verification. \ No newline at end of file diff --git a/src/main/resources/i18n/messages_es.properties b/src/main/resources/i18n/messages_es.properties index df0319a8..c8f1fe35 100644 --- a/src/main/resources/i18n/messages_es.properties +++ b/src/main/resources/i18n/messages_es.properties @@ -228,6 +228,8 @@ COOKIE_BASED_KEY_CONFUSION_JWT_VULNERABILITY=Validador de token JWT basado en co COOKIE_BASED_FOR_JWK_HEADER_BASED_JWT_VULNERABILITY=Validador de token JWT basado en cookies, vulnerable por confianza en el campo JWK sin chequear antes si la clave pública provista está presente en TrustStore o no. COOKIE_BASED_EMPTY_TOKEN_JWT_VULNERABILITY=Token JWT basado en cookies, vulnerable por el ataque de token vacío. +# JWT Injection Header +HEADER_INJECTION_VULNERABILITY=Prueba cómo un encabezado JWT puede ser manipulado para alterar la verificación de la firma. # SQL Injection Vulnerability diff --git a/src/main/resources/static/templates/JWTVulnerability/LEVEL_13/HeaderInjection_Level13.css b/src/main/resources/static/templates/JWTVulnerability/LEVEL_13/HeaderInjection_Level13.css new file mode 100644 index 00000000..6b383918 --- /dev/null +++ b/src/main/resources/static/templates/JWTVulnerability/LEVEL_13/HeaderInjection_Level13.css @@ -0,0 +1,36 @@ +#header_injection_level_13 { + color: black; + text-align: justify; +} + +#enterHeader { + font-size: 15px; + display: flex; + margin: 10px; + flex-direction: column; +} + +#headerName, #headerValue { + flex: 1; + word-wrap: break-word; + margin-top: 10px; +} + +#headerResponse { + font-size: 15px; + word-wrap: break-word; + text-align: center; + margin: 10px; +} + +#sendHeader { + background: blueviolet; + display: inline-block; + padding: 4px 4px; + margin: 10px; + border: 1px solid transparent; + border-radius: 2px; + transition: 0.2s opacity; + color: #FFF; + font-size: 12px; +} diff --git a/src/main/resources/static/templates/JWTVulnerability/LEVEL_13/HeaderInjection_Level13.html b/src/main/resources/static/templates/JWTVulnerability/LEVEL_13/HeaderInjection_Level13.html new file mode 100644 index 00000000..776251f7 --- /dev/null +++ b/src/main/resources/static/templates/JWTVulnerability/LEVEL_13/HeaderInjection_Level13.html @@ -0,0 +1,22 @@ + + + + + Header Injection + + +
+
+
+
Header Name:
+ +
Header Value:
+ +
+ +
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/templates/JWTVulnerability/LEVEL_13/HeaderInjection_Level13.js b/src/main/resources/static/templates/JWTVulnerability/LEVEL_13/HeaderInjection_Level13.js new file mode 100644 index 00000000..2911023e --- /dev/null +++ b/src/main/resources/static/templates/JWTVulnerability/LEVEL_13/HeaderInjection_Level13.js @@ -0,0 +1,27 @@ +function addEventListenerToSendHeaderButton() { + document.getElementById("sendHeader").addEventListener("click", function () { + const headerName = document.getElementById("headerName").value; + const headerValue = document.getElementById("headerValue").value; + + let url = getUrlForVulnerabilityLevel(); + + const manipulatedJwt = + "eyJhbGciOiJSUzI1NiIsImtpZCI6Im1hbGljaW91cy1rZXktaWQifQ.eyJzdWIiOiJleGFtcGxldXNlciIsIm5hbWUiOiJKV1QgVXNlciIsImlhdCI6MTYwOTAxMjAwMH0.c7qHUq1HbHj8AWjKbcIYH2NZnE6PtNyXTnJTWZELvFbfbFhc5BQ_w8e24fXL2OzhhOT5qHVzFvHgOeEYFLZNGEDlJhF4o76yHsMJdWQFL4I5uZjG0o8XV0HjDdM7GqEmx2j0JHi6vJ8Q3pIqGzUBmb7bgzD4kENnP-UqfkbNl2ykYZ9Nybw_E7CAV4OxuqE4QyIpZV2VttWjefK3c6TIj9hNWvYYgipKwHFLXbOV-rOZ6K-_H_4D-kbr0LKPPX-s4b11o0wtS3y1FiHDXEvsmEjhRApEc_jk5uZY-AGPUc9Nl9t6iT_Nh1Q8Usz-jZifg03NwumJjDNtz-nS7gzg"; + + doGetAjaxCall( + function (data) { + document.getElementById("headerResponse").innerHTML = data.isValid + ? "Header Injection was successful!" + : "Header Injection failed. Please try again."; + }, + url, + true, + { + [headerName]: headerValue, + Authorization: `Bearer ${manipulatedJwt}`, + } + ); + }); +} + +addEventListenerToSendHeaderButton(); diff --git a/src/main/resources/static/templates/SampleVulnerability/LEVEL_1/SampleVulnerability.css b/src/main/resources/static/templates/SampleVulnerability/LEVEL_1/SampleVulnerability.css new file mode 100644 index 00000000..e5d57b25 --- /dev/null +++ b/src/main/resources/static/templates/SampleVulnerability/LEVEL_1/SampleVulnerability.css @@ -0,0 +1,16 @@ +#SampleVulnerability { + color: black; + text-align: center; +} + +#fetchDetails { + background: blueviolet; + display: inline-block; + padding: 8px 8px; + margin: 10px; + border: 2px solid transparent; + border-radius: 3px; + transition: 0.2s opacity; + color: #FFF; + font-size: 12px; +} \ No newline at end of file diff --git a/src/main/resources/static/templates/SampleVulnerability/LEVEL_1/SampleVulnerability.html b/src/main/resources/static/templates/SampleVulnerability/LEVEL_1/SampleVulnerability.html new file mode 100644 index 00000000..dddeea7d --- /dev/null +++ b/src/main/resources/static/templates/SampleVulnerability/LEVEL_1/SampleVulnerability.html @@ -0,0 +1,9 @@ +
+
+
+ This is a Sample Vulnerability. please add the UI components here. +
+ +
+
+
\ No newline at end of file diff --git a/src/main/resources/static/templates/SampleVulnerability/LEVEL_1/SampleVulnerability.js b/src/main/resources/static/templates/SampleVulnerability/LEVEL_1/SampleVulnerability.js new file mode 100644 index 00000000..78641721 --- /dev/null +++ b/src/main/resources/static/templates/SampleVulnerability/LEVEL_1/SampleVulnerability.js @@ -0,0 +1,23 @@ +function addingEventListenerToFetchData() { + document + .getElementById("fetchDetails") + .addEventListener("click", function () { + /** + * getUrlForVulnerabilityLevel() method provides url to call the Vulnerability Level + * of Sample Vulnerability. + * e.g. /VulnerableApp/SampleVulnerability/LEVEL_1 for LEVEL_1 + */ + let url = getUrlForVulnerabilityLevel(); + /** + * doGetAjaxCall() method is used to do the ajax get call to the Vulnerability Level + */ + doGetAjaxCall(fetchDataCallback, url + "?name=dummyInput", true); + }); +} +// Used to register event on the button or any other component +addingEventListenerToFetchData(); + +//Callback function to handle the response and render in the UI +function fetchDataCallback(data) { + document.getElementById("response").innerHTML = data.content; +} diff --git a/src/main/resources/static/vulnerableApp.js b/src/main/resources/static/vulnerableApp.js index c633de55..759f6d24 100644 --- a/src/main/resources/static/vulnerableApp.js +++ b/src/main/resources/static/vulnerableApp.js @@ -235,7 +235,7 @@ function genericResponseHandler(xmlHttpRequest, callBack, isJson) { } } -function doGetAjaxCall(callBack, url, isJson) { +function doGetAjaxCall(callBack, url, isJson, headers = {}) { let xmlHttpRequest = new XMLHttpRequest(); xmlHttpRequest.onreadystatechange = function () { genericResponseHandler(xmlHttpRequest, callBack, isJson); @@ -245,6 +245,11 @@ function doGetAjaxCall(callBack, url, isJson) { "Content-Type", isJson ? "application/json" : "text/html" ); + + for (const header in headers) { + xmlHttpRequest.setRequestHeader(header, headers[header]); + } + xmlHttpRequest.send(); }