diff --git a/addOns/ascanrulesAlpha/ascanrulesAlpha.gradle.kts b/addOns/ascanrulesAlpha/ascanrulesAlpha.gradle.kts index ee5039a3b3b..957e1472a65 100644 --- a/addOns/ascanrulesAlpha/ascanrulesAlpha.gradle.kts +++ b/addOns/ascanrulesAlpha/ascanrulesAlpha.gradle.kts @@ -12,6 +12,9 @@ zapAddOn { register("commonlib") { version.set(">= 1.22.0 & < 2.0.0") } + register("authhelper") { + version.set("0.23.0") + } } } } @@ -23,6 +26,7 @@ tasks.named("compileJava") { dependencies { zapAddOn("commonlib") + zapAddOn("authhelper") testImplementation(project(":testutils")) } diff --git a/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/BlankTotpActiveScanRule.java b/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/BlankTotpActiveScanRule.java new file mode 100644 index 00000000000..e421347a1ce --- /dev/null +++ b/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/BlankTotpActiveScanRule.java @@ -0,0 +1,167 @@ +package org.zaproxy.zap.extension.ascanrulesAlpha; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.control.Control; +import org.parosproxy.paros.core.scanner.AbstractHostPlugin; +import org.parosproxy.paros.core.scanner.Alert; +import org.parosproxy.paros.core.scanner.Category; +import org.parosproxy.paros.model.Model; +import org.parosproxy.paros.model.Session; +import org.zaproxy.zap.model.Context; +import org.zaproxy.zap.session.SessionManagementMethod; +import org.zaproxy.zap.session.WebSession; +import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.zap.users.User; +import org.zaproxy.zap.authentication.AuthenticationMethod; +import org.zaproxy.zap.authentication.UsernamePasswordAuthenticationCredentials; +import org.zaproxy.zap.extension.users.ExtensionUserManagement; +import org.zaproxy.addon.authhelper.BrowserBasedAuthenticationMethodType.BrowserBasedAuthenticationMethod; +import org.zaproxy.addon.authhelper.internal.AuthenticationStep; + + +public class BlankTotpActiveScanRule extends AbstractHostPlugin implements CommonActiveScanRuleInfo{ + private static final Logger LOGGER = LogManager.getLogger(BlankTotpActiveScanRule.class); + private static final Map ALERT_TAGS = new HashMap<>(); + + @Override + public int getId() { + return 40048; + } + @Override + public String getName() { + return "Blank code TOTP Scan Rule"; + } + @Override + public String getDescription() { + return "TOTP Page found"; + } + @Override + public int getCategory() { + return Category.INFO_GATHER; + } + @Override + public String getSolution() { + return "N/A"; + } + @Override + public String getReference() { + return "N/A"; + } + @Override + public void scan() { + try { + ExtensionUserManagement usersExtension = + Control.getSingleton() + .getExtensionLoader() + .getExtension(ExtensionUserManagement.class); + + // Get target URL from request + HttpMessage msg = getBaseMsg(); + String targetUrl = msg.getRequestHeader().getURI().toString(); + + // Find session context that matches the target URL + Context activeContext = null; + Session session = Model.getSingleton().getSession(); + for (Context context : session.getContexts()) { + if (context.isInContext(targetUrl)) { + activeContext = context; + break; + } + } + BrowserBasedAuthenticationMethod browserAuthMethod = null; + List authSteps = null; + AuthenticationStep totpStep = null; + // Check if the context is found + if (activeContext != null) { + AuthenticationMethod authMethod = activeContext.getAuthenticationMethod(); + // Check if the authentication method is browser based + if (authMethod instanceof BrowserBasedAuthenticationMethod) { + browserAuthMethod = (BrowserBasedAuthenticationMethod) authMethod; + // Check if the authentication method has TOTP step + authSteps = browserAuthMethod.getAuthenticationSteps(); + boolean totpFound = false; + for (AuthenticationStep step : authSteps) { + // Checks for TOTP_field type step or currently also allows for + // custom field b/c of the way TOTP_field step currently implemented + if (step.getType() == AuthenticationStep.Type.TOTP_FIELD || (step.getType() == AuthenticationStep.Type.CUSTOM_FIELD && step.getDescription().toLowerCase().contains("totp"))) { + totpFound = true; + totpStep = step; + break; + } + } + if (!totpFound) { + return; + } + + } + else{ + //LOGGER.error("Authentication Method is not browser based."); + return; + } + } + else { + //LOGGER.error("No context found for target URL: " + targetUrl); + return; + } + + //Start vulnerability testing if TOTP step is found + //LOGGER.error("TOTP authentication is enabled, proceeding with tests."); + + // Get user credentials(username,password) & user from the context to run browser based web session + List users = null; + if (usersExtension == null) { + //LOGGER.error("Users extension not found."); + return; + } + users = usersExtension.getContextUserAuthManager(activeContext.getId()).getUsers(); + if (users == null || users.isEmpty()) { + //LOGGER.error("No users found in the context."); + return; + } + User user = users.get(0); + UsernamePasswordAuthenticationCredentials credentials = (UsernamePasswordAuthenticationCredentials) user.getAuthenticationCredentials(); + SessionManagementMethod sessionManagementMethod = activeContext.getSessionManagementMethod(); + + //Check if user provided valid code & check if initial authentication works with normal passcode + if(totpStep.getValue() != null || !totpStep.getValue().isEmpty()){ + WebSession webSession = browserAuthMethod.authenticate(sessionManagementMethod, credentials, user); + if (webSession == null) { + //LOGGER.error("Normal Authentication unsuccessful. TOTP not configured correctly."); + return; + } + } + + // Check for blank passcode vulnerability + WebSession webSessionBlankCode = testAuthenticatSession(totpStep, "", authSteps, browserAuthMethod, sessionManagementMethod, credentials, user); + if (webSessionBlankCode != null) { + //LOGGER.error("Authentication successful with blank passcode.Vulernaibility found."); + buildAlert("Blank Passcode Vulnerability", + "The application allows authentication with a blank or empty passcode, which poses a significant security risk. Attackers can exploit this vulnerability to gain unauthorized access without providing valid credentials.", + "Enforce strict password policies that require non-empty, strong passcodes. Implement validation checks to prevent blank passcodes during authentication", msg).raise(); + } + } catch (Exception e) { + LOGGER.error("Error in TOTP Page Scan Rule: {}",e.getMessage(), e); + } + } + + private WebSession testAuthenticatSession(AuthenticationStep totpStep, String newTotpValue, List authSteps , BrowserBasedAuthenticationMethod browserAuthMethod, SessionManagementMethod sessionManagementMethod, UsernamePasswordAuthenticationCredentials credentials, User user){ + totpStep.setValue(newTotpValue); + browserAuthMethod.setAuthenticationSteps(authSteps); + return browserAuthMethod.authenticate(sessionManagementMethod, credentials, user); + } + private AlertBuilder buildAlert(String name, String description, String solution, HttpMessage msg) { + return newAlert() + .setConfidence(Alert.CONFIDENCE_HIGH) + .setName(name) + .setDescription(description) + .setSolution(solution) + .setMessage(msg); + } +} + + diff --git a/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/CaptchaTotpActiveScanRule.java b/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/CaptchaTotpActiveScanRule.java new file mode 100644 index 00000000000..2a267663e2b --- /dev/null +++ b/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/CaptchaTotpActiveScanRule.java @@ -0,0 +1,175 @@ +package org.zaproxy.zap.extension.ascanrulesAlpha; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.control.Control; +import org.parosproxy.paros.core.scanner.AbstractHostPlugin; +import org.parosproxy.paros.core.scanner.Alert; +import org.parosproxy.paros.core.scanner.Category; +import org.parosproxy.paros.model.Model; +import org.parosproxy.paros.model.Session; +import org.zaproxy.zap.model.Context; +import org.zaproxy.zap.session.SessionManagementMethod; +import org.zaproxy.zap.session.WebSession; +import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.zap.users.User; +import org.zaproxy.zap.authentication.AuthenticationMethod; +import org.zaproxy.zap.authentication.UsernamePasswordAuthenticationCredentials; +import org.zaproxy.zap.extension.users.ExtensionUserManagement; +import org.zaproxy.addon.authhelper.BrowserBasedAuthenticationMethodType.BrowserBasedAuthenticationMethod; +import org.zaproxy.addon.authhelper.internal.AuthenticationStep; +public class CaptchaTotpActiveScanRule extends AbstractHostPlugin implements CommonActiveScanRuleInfo{ + private static final Logger LOGGER = LogManager.getLogger(BlankTotpActiveScanRule.class); + private static final Map ALERT_TAGS = new HashMap<>(); + + @Override + public int getId() { + return 40051; + } + @Override + public String getName() { + return "Blank code TOTP Scan Rule"; + } + @Override + public String getDescription() { + return "TOTP Page found"; + } + @Override + public int getCategory() { + return Category.INFO_GATHER; + } + @Override + public String getSolution() { + return "N/A"; + } + @Override + public String getReference() { + return "N/A"; + } + @Override + public void scan() { + try { + ExtensionUserManagement usersExtension = + Control.getSingleton() + .getExtensionLoader() + .getExtension(ExtensionUserManagement.class); + + // Get target URL from request + HttpMessage msg = getBaseMsg(); + String targetUrl = msg.getRequestHeader().getURI().toString(); + + // Find session context that matches the target URL + Context activeContext = null; + Session session = Model.getSingleton().getSession(); + for (Context context : session.getContexts()) { + if (context.isInContext(targetUrl)) { + activeContext = context; + break; + } + } + BrowserBasedAuthenticationMethod browserAuthMethod = null; + List authSteps = null; + AuthenticationStep totpStep = null; + // Check if the context is found + if (activeContext != null) { + AuthenticationMethod authMethod = activeContext.getAuthenticationMethod(); + // Check if the authentication method is browser based + if (authMethod instanceof BrowserBasedAuthenticationMethod) { + browserAuthMethod = (BrowserBasedAuthenticationMethod) authMethod; + // Check if the authentication method has TOTP step + authSteps = browserAuthMethod.getAuthenticationSteps(); + boolean totpFound = false; + for (AuthenticationStep step : authSteps) { + // Checks for TOTP_field type step or currently also allows for + // custom field b/c of the way TOTP_field step currently implemented + if (step.getType() == AuthenticationStep.Type.TOTP_FIELD || (step.getType() == AuthenticationStep.Type.CUSTOM_FIELD && step.getDescription().toLowerCase().contains("totp"))) { + totpFound = true; + totpStep = step; + break; + } + } + if (!totpFound) { + return; + } + + } + else{ + //LOGGER.error("Authentication Method is not browser based."); + return; + } + } + else { + //LOGGER.error("No context found for target URL: " + targetUrl); + return; + } + + //Start vulnerability testing if TOTP step is found + //LOGGER.error("TOTP authentication is enabled, proceeding with tests."); + + // Get user credentials(username,password) & user from the context to run browser based web session + List users = null; + if (usersExtension == null) { + //LOGGER.error("Users extension not found."); + return; + } + users = usersExtension.getContextUserAuthManager(activeContext.getId()).getUsers(); + if (users == null || users.isEmpty()) { + //LOGGER.error("No users found in the context."); + return; + } + User user = users.get(0); + UsernamePasswordAuthenticationCredentials credentials = (UsernamePasswordAuthenticationCredentials) user.getAuthenticationCredentials(); + SessionManagementMethod sessionManagementMethod = activeContext.getSessionManagementMethod(); + + //Check if lockout or captcha mechanism is detected + boolean captchaDetected = false; + boolean lockoutDetected = false; + + // Run 10 incorrect authentications and store the responses + // Check responses for any changes or any common captcha technology + List httpResponses = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + WebSession testSession = testAuthenticatSession(totpStep, "111111", authSteps, browserAuthMethod, sessionManagementMethod, credentials, user); + //Add the response to the httpResponses list + + } + for (HttpMessage response : httpResponses) { + // Check for changes to the response's indicating a potential lockout/captcha mechanism + } + for (HttpMessage response : httpResponses) { + // Check for captcha mechanism + // Check for lockout mechanism + } + if (!captchaDetected && !lockoutDetected) { + //LOGGER.error("Authentication successful with blank passcode.Vulernaibility found."); + buildAlert("No Lockout or Captcha Mechanism Detected", + "\"The application does not enforce CAPTCHA or account lockout mechanisms, making it vulnerable to brute-force attacks.", + "Implement CAPTCHA verification and/or account lockout policies after multiple failed login attempts.", msg).raise(); + } + } catch (Exception e) { + LOGGER.error("Error in TOTP Page Scan Rule: {}",e.getMessage(), e); + } + } + + private WebSession testAuthenticatSession(AuthenticationStep totpStep, String newTotpValue, List authSteps , BrowserBasedAuthenticationMethod browserAuthMethod, SessionManagementMethod sessionManagementMethod, UsernamePasswordAuthenticationCredentials credentials, User user){ + totpStep.setValue(newTotpValue); + browserAuthMethod.setAuthenticationSteps(authSteps); + return browserAuthMethod.authenticate(sessionManagementMethod, credentials, user); + } + private AlertBuilder buildAlert(String name, String description, String solution, HttpMessage msg) { + return newAlert() + .setConfidence(Alert.CONFIDENCE_MEDIUM) + .setName(name) + .setDescription(description) + .setSolution(solution) + .setMessage(msg); + } +} + + + + diff --git a/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/ReplayTotpActiveScanRule.java b/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/ReplayTotpActiveScanRule.java new file mode 100644 index 00000000000..a6d6d7338dc --- /dev/null +++ b/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/ReplayTotpActiveScanRule.java @@ -0,0 +1,160 @@ +package org.zaproxy.zap.extension.ascanrulesAlpha; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.control.Control; +import org.parosproxy.paros.core.scanner.AbstractHostPlugin; +import org.parosproxy.paros.core.scanner.Alert; +import org.parosproxy.paros.core.scanner.Category; +import org.parosproxy.paros.model.Model; +import org.parosproxy.paros.model.Session; +import org.zaproxy.zap.model.Context; +import org.zaproxy.zap.session.SessionManagementMethod; +import org.zaproxy.zap.session.WebSession; +import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.zap.users.User; +import org.zaproxy.zap.authentication.AuthenticationMethod; +import org.zaproxy.zap.authentication.UsernamePasswordAuthenticationCredentials; +import org.zaproxy.zap.extension.users.ExtensionUserManagement; +import org.zaproxy.addon.authhelper.BrowserBasedAuthenticationMethodType.BrowserBasedAuthenticationMethod; +import org.zaproxy.addon.authhelper.internal.AuthenticationStep; + + +public class ReplayTotpActiveScanRule extends AbstractHostPlugin implements CommonActiveScanRuleInfo{ + private static final Logger LOGGER = LogManager.getLogger(ReplayTotpActiveScanRule.class); + private static final Map ALERT_TAGS = new HashMap<>(); + + @Override + public int getId() { + return 40049; + } + @Override + public String getName() { + return "Blank code TOTP Scan Rule"; + } + @Override + public String getDescription() { + return "TOTP Page found"; + } + @Override + public int getCategory() { + return Category.INFO_GATHER; + } + @Override + public String getSolution() { + return "N/A"; + } + @Override + public String getReference() { + return "N/A"; + } + @Override + public void scan() { + try { + ExtensionUserManagement usersExtension = + Control.getSingleton() + .getExtensionLoader() + .getExtension(ExtensionUserManagement.class); + + // Get target URL from request + HttpMessage msg = getBaseMsg(); + String targetUrl = msg.getRequestHeader().getURI().toString(); + + // Find session context that matches the target URL + Context activeContext = null; + Session session = Model.getSingleton().getSession(); + for (Context context : session.getContexts()) { + if (context.isInContext(targetUrl)) { + activeContext = context; + break; + } + } + BrowserBasedAuthenticationMethod browserAuthMethod = null; + List authSteps = null; + AuthenticationStep totpStep = null; + // Check if the context is found + if (activeContext != null) { + AuthenticationMethod authMethod = activeContext.getAuthenticationMethod(); + // Check if the authentication method is browser based + if (authMethod instanceof BrowserBasedAuthenticationMethod) { + browserAuthMethod = (BrowserBasedAuthenticationMethod) authMethod; + // Check if the authentication method has TOTP step + authSteps = browserAuthMethod.getAuthenticationSteps(); + boolean totpFound = false; + for (AuthenticationStep step : authSteps) { + // Checks for TOTP_field type step or currently also allows for + // custom field b/c of the way TOTP_field step currently implemented + if (step.getType() == AuthenticationStep.Type.TOTP_FIELD || (step.getType() == AuthenticationStep.Type.CUSTOM_FIELD && step.getDescription().toLowerCase().contains("totp"))) { + totpFound = true; + totpStep = step; + break; + } + } + if (!totpFound) { + return; + } + + } + else{ + //LOGGER.error("Authentication Method is not browser based."); + return; + } + } + else { + //LOGGER.error("No context found for target URL: " + targetUrl); + return; + } + + //Start vulnerability testing if TOTP step is found + //LOGGER.error("TOTP authentication is enabled, proceeding with tests."); + + // Get user credentials(username,password) & user from the context to run browser based web session + List users = null; + if (usersExtension == null) { + //LOGGER.error("Users extension not found."); + return; + } + users = usersExtension.getContextUserAuthManager(activeContext.getId()).getUsers(); + if (users == null || users.isEmpty()) { + //LOGGER.error("No users found in the context."); + return; + } + User user = users.get(0); + UsernamePasswordAuthenticationCredentials credentials = (UsernamePasswordAuthenticationCredentials) user.getAuthenticationCredentials(); + SessionManagementMethod sessionManagementMethod = activeContext.getSessionManagementMethod(); + + //Check if user provided valid code & check if initial authentication works with normal passcode + if(totpStep.getValue() != null || !totpStep.getValue().isEmpty()){ + WebSession webSession = browserAuthMethod.authenticate(sessionManagementMethod, credentials, user); + if (webSession == null) { + //LOGGER.error("Normal Authentication unsuccessful. TOTP not configured correctly."); + return; + } + // Check for passcode reuse vulnerability + WebSession webSession_redo = browserAuthMethod.authenticate(sessionManagementMethod, credentials, user); + if (webSession_redo != null) { + LOGGER.error("Authentication with reused passcode. Vulnerability found."); + buildAlert("TOTP Replay Attack Vulnerability", + "The application is vulnerable to replay attacks, allowing attackers to reuse previously intercepted TOTP codes to authenticate.", + "Ensure that TOTP codes are validated only once per session and are invalidated after use.", msg).raise(); + } + } + } catch (Exception e) { + LOGGER.error("Error in TOTP Page Scan Rule: {}",e.getMessage(), e); + } + } + private AlertBuilder buildAlert(String name, String description, String solution, HttpMessage msg) { + return newAlert() + .setConfidence(Alert.CONFIDENCE_HIGH) + .setName(name) + .setDescription(description) + .setSolution(solution) + .setMessage(msg); + } +} + + diff --git a/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/TotpActiveScanRule.java b/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/TotpActiveScanRule.java new file mode 100644 index 00000000000..dd703193a23 --- /dev/null +++ b/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/TotpActiveScanRule.java @@ -0,0 +1,165 @@ +package org.zaproxy.zap.extension.ascanrulesAlpha; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.control.Control; +import org.parosproxy.paros.core.scanner.AbstractHostPlugin; +import org.parosproxy.paros.core.scanner.Alert; +import org.parosproxy.paros.core.scanner.Category; +import org.parosproxy.paros.model.Model; +import org.parosproxy.paros.model.Session; +import org.zaproxy.zap.model.Context; +import org.zaproxy.zap.session.SessionManagementMethod; +import org.zaproxy.zap.session.WebSession; +import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.zap.users.User; +import org.zaproxy.zap.authentication.AuthenticationMethod; +import org.zaproxy.zap.authentication.UsernamePasswordAuthenticationCredentials; +import org.zaproxy.zap.extension.users.ExtensionUserManagement; +import org.zaproxy.addon.authhelper.BrowserBasedAuthenticationMethodType.BrowserBasedAuthenticationMethod; +import org.zaproxy.addon.authhelper.internal.AuthenticationStep; + + +public class TotpActiveScanRule extends AbstractHostPlugin implements CommonActiveScanRuleInfo{ + private static final Logger LOGGER = LogManager.getLogger(TotpActiveScanRule.class); + private static final Map ALERT_TAGS = new HashMap<>(); + + @Override + public int getId() { + return 40050; + } + @Override + public String getName() { + return "Blank code TOTP Scan Rule"; + } + @Override + public String getDescription() { + return "TOTP Page found"; + } + @Override + public int getCategory() { + return Category.INFO_GATHER; + } + @Override + public String getSolution() { + return "N/A"; + } + @Override + public String getReference() { + return "N/A"; + } + @Override + public void scan() { + try { + ExtensionUserManagement usersExtension = + Control.getSingleton() + .getExtensionLoader() + .getExtension(ExtensionUserManagement.class); + + // Get target URL from request + HttpMessage msg = getBaseMsg(); + String targetUrl = msg.getRequestHeader().getURI().toString(); + + // Find session context that matches the target URL + Context activeContext = null; + Session session = Model.getSingleton().getSession(); + for (Context context : session.getContexts()) { + if (context.isInContext(targetUrl)) { + activeContext = context; + break; + } + } + BrowserBasedAuthenticationMethod browserAuthMethod = null; + List authSteps = null; + AuthenticationStep totpStep = null; + // Check if the context is found + if (activeContext != null) { + AuthenticationMethod authMethod = activeContext.getAuthenticationMethod(); + // Check if the authentication method is browser based + if (authMethod instanceof BrowserBasedAuthenticationMethod) { + browserAuthMethod = (BrowserBasedAuthenticationMethod) authMethod; + // Check if the authentication method has TOTP step + authSteps = browserAuthMethod.getAuthenticationSteps(); + boolean totpFound = false; + for (AuthenticationStep step : authSteps) { + // Checks for TOTP_field type step or currently also allows for + // custom field b/c of the way TOTP_field step currently implemented + if (step.getType() == AuthenticationStep.Type.TOTP_FIELD || (step.getType() == AuthenticationStep.Type.CUSTOM_FIELD && step.getDescription().toLowerCase().contains("totp"))) { + totpFound = true; + totpStep = step; + break; + } + } + if (!totpFound) { + return; + } + + } + else{ + //LOGGER.error("Authentication Method is not browser based."); + return; + } + } + else { + //LOGGER.error("No context found for target URL: " + targetUrl); + return; + } + + //Start vulnerability testing if TOTP step is found + //LOGGER.error("TOTP authentication is enabled, proceeding with tests."); + + // Get user credentials(username,password) & user from the context to run browser based web session + List users = null; + if (usersExtension == null) { + //LOGGER.error("Users extension not found."); + return; + } + users = usersExtension.getContextUserAuthManager(activeContext.getId()).getUsers(); + if (users == null || users.isEmpty()) { + //LOGGER.error("No users found in the context."); + return; + } + User user = users.get(0); + UsernamePasswordAuthenticationCredentials credentials = (UsernamePasswordAuthenticationCredentials) user.getAuthenticationCredentials(); + SessionManagementMethod sessionManagementMethod = activeContext.getSessionManagementMethod(); + + //Checks if a valid username/password combination gives access with any passcode meeting the format + //Uses known static backup passcodes to check if any of them work + List backupPasscodes = List.of("000000", "0000000", "00000000", "123456", "1234567", "12345678", "888888", "8888888", "88888888"); + //Test passcode 000-000 (check format- RFC-6238 (6,7,8) + for (String code : backupPasscodes){ + WebSession webSessionBlankCode = testAuthenticatSession(totpStep, code , authSteps, browserAuthMethod, sessionManagementMethod, credentials, user); + if (webSessionBlankCode != null) { + //LOGGER.error("Authentication successful with blank passcode.Vulernaibility found."); + buildAlert("Passcode Authentication Bypass", + "The application allows authentication using passcodes that meet the expected format but are weak or known values. This poses a security risk as attackers could exploit predictable static backup passcodes to gain unauthorized access.", + "", msg).raise(); + } + } + + } catch (Exception e) { + LOGGER.error("Error in TOTP Page Scan Rule: {}",e.getMessage(), e); + } + } + private WebSession testAuthenticatSession(AuthenticationStep totpStep, String newTotpValue, List authSteps , BrowserBasedAuthenticationMethod browserAuthMethod, SessionManagementMethod sessionManagementMethod, UsernamePasswordAuthenticationCredentials credentials, User user){ + totpStep.setValue(newTotpValue); + browserAuthMethod.setAuthenticationSteps(authSteps); + return browserAuthMethod.authenticate(sessionManagementMethod, credentials, user); + } + private AlertBuilder buildAlert(String name, String description, String solution, HttpMessage msg) { + return newAlert() + .setConfidence(Alert.CONFIDENCE_HIGH) + .setName(name) + .setDescription(description) + .setSolution(solution) + .setMessage(msg); + } +} + + + + diff --git a/addOns/dev/src/main/java/org/zaproxy/addon/dev/TestProxyServer.java b/addOns/dev/src/main/java/org/zaproxy/addon/dev/TestProxyServer.java index 15e7058870c..598537b899f 100644 --- a/addOns/dev/src/main/java/org/zaproxy/addon/dev/TestProxyServer.java +++ b/addOns/dev/src/main/java/org/zaproxy/addon/dev/TestProxyServer.java @@ -34,6 +34,7 @@ import org.parosproxy.paros.network.HttpMalformedHeaderException; import org.parosproxy.paros.network.HttpMessage; import org.zaproxy.addon.dev.api.openapi.simpleAuth.OpenApiSimpleAuthDir; +import org.zaproxy.addon.dev.api.openapi.simpleAuthWithOTP.OpenApiWithOtpSimpleAuthDir; import org.zaproxy.addon.dev.api.openapi.simpleUnauth.OpenApiSimpleUnauthDir; import org.zaproxy.addon.dev.auth.jsonMultipleCookies.JsonMultipleCookiesDir; import org.zaproxy.addon.dev.auth.nonStdJsonBearer.NonStdJsonBearerDir; @@ -91,6 +92,7 @@ public TestProxyServer(ExtensionDev extension, ExtensionNetwork extensionNetwork TestDirectory openapiDir = new TestDirectory(this, "openapi"); apiDir.addDirectory(openapiDir); openapiDir.addDirectory(new OpenApiSimpleAuthDir(this, "simple-auth")); + openapiDir.addDirectory(new OpenApiWithOtpSimpleAuthDir(this, "simple-auth-with-otp")); openapiDir.addDirectory(new OpenApiSimpleUnauthDir(this, "simple-unauth")); TestDirectory htmlDir = new TestDirectory(this, "html"); diff --git a/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpLoginPage.java b/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpLoginPage.java new file mode 100644 index 00000000000..7dadf59776a --- /dev/null +++ b/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpLoginPage.java @@ -0,0 +1,70 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2023 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.addon.dev.api.openapi.simpleAuthWithOTP; + +import net.sf.json.JSONException; +import net.sf.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.addon.dev.TestPage; +import org.zaproxy.addon.dev.TestProxyServer; +import org.zaproxy.addon.network.server.HttpMessageHandlerContext; + +public class OpenApiWithOtpLoginPage extends TestPage { + + private static final Logger LOGGER = LogManager.getLogger(OpenApiWithOtpLoginPage.class); + + public OpenApiWithOtpLoginPage(TestProxyServer server) { + super(server, "login"); + } + + @Override + public void handleMessage(HttpMessageHandlerContext ctx, HttpMessage msg) { + String username = null; + String password = null; + + if (msg.getRequestHeader().hasContentType("json")) { + String postData = msg.getRequestBody().toString(); + JSONObject jsonObject; + try { + jsonObject = JSONObject.fromObject(postData); + username = jsonObject.getString("user"); + password = jsonObject.getString("password"); + } catch (JSONException e) { + LOGGER.debug("Unable to parse as JSON: {}", postData, e); + } + } + + JSONObject response = new JSONObject(); + if (getParent().isValid(username, password)) { + response.put("result", "OK"); + response.put("accesstoken", getParent().getToken(username)); + } else { + response.put("result", "FAIL"); + } + this.getServer().setJsonResponse(response, msg); + } + + @Override + public OpenApiWithOtpSimpleAuthDir getParent() { + return (OpenApiWithOtpSimpleAuthDir) super.getParent(); + } +} diff --git a/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpSimpleAuthDir.java b/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpSimpleAuthDir.java new file mode 100644 index 00000000000..8221ef15c6a --- /dev/null +++ b/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpSimpleAuthDir.java @@ -0,0 +1,38 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2023 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.addon.dev.api.openapi.simpleAuthWithOTP; + +import org.zaproxy.addon.dev.TestAuthDirectory; +import org.zaproxy.addon.dev.TestProxyServer; + +/** + * A directory which contains an OpenAPI spec. The spec is available unauthenticated but the + * endpoint it describes can only be accessed when a valid Authentication header is supplied. The + * login page uses one JSON request to login endpoint. The token is returned in a standard field. + */ +public class OpenApiWithOtpSimpleAuthDir extends TestAuthDirectory { + + public OpenApiWithOtpSimpleAuthDir(TestProxyServer server, String name) { + super(server, name); + this.addPage(new OpenApiWithOtpLoginPage(server)); + this.addPage(new OpenApiWithOtpVerificationPage(server)); + this.addPage(new OpenApiWithOtpTestApiPage(server)); + } +} diff --git a/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpTestApiPage.java b/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpTestApiPage.java new file mode 100644 index 00000000000..e34a5b5d440 --- /dev/null +++ b/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpTestApiPage.java @@ -0,0 +1,59 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2023 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.addon.dev.api.openapi.simpleAuthWithOTP; + +import net.sf.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.network.HttpHeader; +import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.addon.dev.TestPage; +import org.zaproxy.addon.dev.TestProxyServer; +import org.zaproxy.addon.network.server.HttpMessageHandlerContext; + +public class OpenApiWithOtpTestApiPage extends TestPage { + + private static final Logger LOGGER = LogManager.getLogger(OpenApiWithOtpTestApiPage.class); + + public OpenApiWithOtpTestApiPage(TestProxyServer server) { + super(server, "test-api"); + } + + @Override + public void handleMessage(HttpMessageHandlerContext ctx, HttpMessage msg) { + String token = msg.getRequestHeader().getHeader(HttpHeader.AUTHORIZATION); + String user = getParent().getUser(token); + LOGGER.debug("Token: {} user: {}", token, user); + + JSONObject response = new JSONObject(); + if (user != null) { + response.put("result", "Success"); + this.getServer().setJsonResponse(TestProxyServer.STATUS_OK, response, msg); + } else { + response.put("result", "FAIL"); + this.getServer().setJsonResponse(TestProxyServer.STATUS_FORBIDDEN, response, msg); + } + } + + @Override + public OpenApiWithOtpSimpleAuthDir getParent() { + return (OpenApiWithOtpSimpleAuthDir) super.getParent(); + } +} diff --git a/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpVerificationPage.java b/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpVerificationPage.java new file mode 100644 index 00000000000..381d8cb4ddb --- /dev/null +++ b/addOns/dev/src/main/java/org/zaproxy/addon/dev/api/openapi/simpleAuthWithOTP/OpenApiWithOtpVerificationPage.java @@ -0,0 +1,74 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2023 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.addon.dev.api.openapi.simpleAuthWithOTP; + +import net.sf.json.JSONException; +import net.sf.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.network.HttpHeader; +import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.addon.dev.TestPage; +import org.zaproxy.addon.dev.TestProxyServer; +import org.zaproxy.addon.network.server.HttpMessageHandlerContext; + +public class OpenApiWithOtpVerificationPage extends TestPage { + + private static final Logger LOGGER = LogManager.getLogger(OpenApiWithOtpVerificationPage.class); + + public OpenApiWithOtpVerificationPage(TestProxyServer server) { + super(server, "user"); + } + + @Override + public void handleMessage(HttpMessageHandlerContext ctx, HttpMessage msg) { + String totp = null; + + if (msg.getRequestHeader().hasContentType("json")) { + String postData = msg.getRequestBody().toString(); + JSONObject jsonObject; + try { + jsonObject = JSONObject.fromObject(postData); + totp = jsonObject.getString("code"); + } catch (JSONException e) { + LOGGER.debug("Unable to parse as JSON: {}", postData, e); + } + } + + String token = msg.getRequestHeader().getHeader(HttpHeader.AUTHORIZATION); + String user = getParent().getUser(token); + LOGGER.debug("Token: {} user: {} TOTP: {}", token, user, totp); + + JSONObject response = new JSONObject(); + if (user != null && (totp != null && (totp.equals("123456") || totp.isEmpty()))) { + // Vulnerability: bypass TOTP check if passcode is blank + response.put("result", "OK"); + response.put("user", user); + } else { + response.put("result", "FAIL"); + } + this.getServer().setJsonResponse(response, msg); + } + + @Override + public OpenApiWithOtpSimpleAuthDir getParent() { + return (OpenApiWithOtpSimpleAuthDir) super.getParent(); + } +} diff --git a/addOns/dev/src/main/zapHomeFiles/dev-add-on/api/openapi/simple-auth-with-otp/home.html b/addOns/dev/src/main/zapHomeFiles/dev-add-on/api/openapi/simple-auth-with-otp/home.html new file mode 100644 index 00000000000..f0a204f57f5 --- /dev/null +++ b/addOns/dev/src/main/zapHomeFiles/dev-add-on/api/openapi/simple-auth-with-otp/home.html @@ -0,0 +1,42 @@ + + + + ZAP Test Server + + + +

Simple Home Page

+
+
+
+ + + + + diff --git a/addOns/dev/src/main/zapHomeFiles/dev-add-on/api/openapi/simple-auth-with-otp/index.html b/addOns/dev/src/main/zapHomeFiles/dev-add-on/api/openapi/simple-auth-with-otp/index.html new file mode 100644 index 00000000000..a1dc24348ae --- /dev/null +++ b/addOns/dev/src/main/zapHomeFiles/dev-add-on/api/openapi/simple-auth-with-otp/index.html @@ -0,0 +1,84 @@ + + + + ZAP Test Server + + + +
+

Simple TOTP OpenAPI Spec, with Authentication

+

Login

+ +
+ +
+ + + + + + + + + + + +
Username: +
Password: +
+
+

+ Test credentials: +

    +
  • username = test@test.com +
  • password = password123 +
+ The verification URL returns JSON with the username if valid, and a 200 response in all cases. +

+ OpenAPI spec in openapi.json - no links, available unauthenticated but API only available with a valid Authorization header. + +

+ + + diff --git a/addOns/dev/src/main/zapHomeFiles/dev-add-on/api/openapi/simple-auth-with-otp/totp.html b/addOns/dev/src/main/zapHomeFiles/dev-add-on/api/openapi/simple-auth-with-otp/totp.html new file mode 100644 index 00000000000..7a6e64358f3 --- /dev/null +++ b/addOns/dev/src/main/zapHomeFiles/dev-add-on/api/openapi/simple-auth-with-otp/totp.html @@ -0,0 +1,69 @@ + + + + ZAP Test Server + + + +
+

Enter TOTP Passcode

+ +
+ +
+ + + + + + + + + +
TOTP Passcode:
+
+

+ Test credentials: +

    +
  • totp = 123456 +
+

+
+ + + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index e68633cff62..7c30850312c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ org.gradle.caching=true -org.gradle.parallel=true \ No newline at end of file +org.gradle.parallel=true +org.gradle.jvmargs=-Xmx4g \ No newline at end of file