List of usage examples for org.apache.commons.httpclient URI getHost
public String getHost() throws URIException
From source file:org.zaproxy.zap.extension.accessControl.widgets.SiteTree.java
public SiteTreeNode addPath(Context context, URI uri, String method, Collection<String> urlParameters, Collection<String> formParameters, String contentType) { SiteTreeNode parent = this.root; SiteTreeNode leaf = null;/*w w w .j av a 2 s .com*/ String pathSegment = ""; URI pathSegmentUri; try { URI hostUri = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort()); String hostname = UriUtils.getHostName(uri); // add host parent = findOrAddPathSegmentNode(parent, hostname, hostUri); ParameterParser paramParser = context.getUrlParamParser(); List<String> path = paramParser.getTreePath(uri); for (int i = 0; i < path.size(); i++) { pathSegment = path.get(i); if (pathSegment != null && !pathSegment.equals("")) { if (i == path.size() - 1) { String leafName = UriUtils.getLeafNodeRepresentation(pathSegment, method, urlParameters, formParameters, contentType); leaf = findOrAddPathSegmentNode(parent, leafName, uri); } else { pathSegmentUri = new URI(hostUri, paramParser.getAncestorPath(uri, i + 1), false); parent = findOrAddPathSegmentNode(parent, pathSegment, pathSegmentUri); } } } // If no leaf found, which means the parent was really the leaf. This happens, for // example, when first adding a node for the top-level node, without any path elements if (leaf == null) { leaf = parent; } } catch (Exception e) { // ZAP: Added error log.error("Exception adding " + uri.toString() + " " + e.getMessage(), e); } return leaf; }
From source file:org.zaproxy.zap.extension.accessControl.widgets.UriUtils.java
/** * Returns a representation of the host name as used throughout ZAP. The representation contains * the scheme, the host and, if needed, the port. Method should be used to keep consistency * whenever displaying a node's hostname. * * <p>Example outputs:/*from w w w . ja va 2s. c o m*/ * * <ul> * <li><i>http://example.org</i> * <li><i>http://example.org:8080</i> * <li><i>https://example.org</i> * </ul> * * @throws URIException */ public static String getHostName(URI uri) throws URIException { StringBuilder host = new StringBuilder(); String scheme = uri.getScheme().toLowerCase(); host.append(scheme).append("://").append(uri.getHost()); int port = uri.getPort(); if ((port != -1) && ((port == 80 && !"http".equals(scheme)) || (port == 443 && !"https".equals(scheme)) || (port != 80 && port != 443))) { host.append(":").append(port); } return host.toString(); }
From source file:org.zaproxy.zap.extension.ascanrules.TestSQLInjection.java
/** scans for SQL Injection vulnerabilities */ @Override// www .j a va2s.co m public void scan(HttpMessage msg, String param, String origParamValue) { // Note: the "value" we are passed here is escaped. we need to unescape it before handling // it. // as soon as we find a single SQL injection on the url, skip out. Do not look for SQL // injection on a subsequent parameter on the same URL // for performance reasons. // reinitialise each parameter. sqlInjectionFoundForUrl = false; sqlInjectionAttack = null; refreshedmessage = null; mResBodyNormalUnstripped = null; mResBodyNormalStripped = null; try { // reinitialise the count for each type of request, for each parameter. We will be // sticking to limits defined in the attach strength logic countErrorBasedRequests = 0; countExpressionBasedRequests = 0; countBooleanBasedRequests = 0; countUnionBasedRequests = 0; countOrderByBasedRequests = 0; // Check 1: Check for Error Based SQL Injection (actual error messages). // for each SQL metacharacter combination to try for (int sqlErrorStringIndex = 0; sqlErrorStringIndex < SQL_CHECK_ERR.length && !sqlInjectionFoundForUrl && doSpecificErrorBased && countErrorBasedRequests < doErrorMaxRequests; sqlErrorStringIndex++) { // work through the attack using each of the following strings as a prefix: the // empty string, and the original value // Note: this doubles the amount of work done by the scanner, but is necessary in // some cases String[] prefixStrings; if (origParamValue != null) { // ZAP: Removed getURLDecode() prefixStrings = new String[] { "", origParamValue }; } else { prefixStrings = new String[] { "" }; } for (int prefixIndex = 0; prefixIndex < prefixStrings.length && !sqlInjectionFoundForUrl; prefixIndex++) { // new message for each value we attack with HttpMessage msg1 = getNewMsg(); String sqlErrValue = prefixStrings[prefixIndex] + SQL_CHECK_ERR[sqlErrorStringIndex]; setParameter(msg1, param, sqlErrValue); // System.out.println("Attacking [" + msg + "], parameter [" + param + "] with // value ["+ sqlErrValue + "]"); // send the message with the modified parameters try { sendAndReceive(msg1, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + msg1.getRequestHeader().getURI().toString()); continue; // Something went wrong, continue to the next prefixString in the // loop } countErrorBasedRequests++; // now check the results against each pattern in turn, to try to identify a // database, or even better: a specific database. // Note: do NOT check the HTTP error code just yet, as the result could come // back with one of various codes. for (RDBMS rdbms : RDBMS.values()) { if (getTechSet().includes(rdbms.getTech()) && checkSpecificErrors(rdbms, msg1, param, sqlErrValue)) { sqlInjectionFoundForUrl = true; // Save the attack string for the "Authentication Bypass" alert, if // necessary sqlInjectionAttack = sqlErrValue; break; } // bale out if we were asked nicely if (isStop()) { log.debug("Stopping the scan due to a user request"); return; } } // end of the loop to check for RDBMS specific error messages if (this.doGenericErrorBased && !sqlInjectionFoundForUrl) { Iterator<Pattern> errorPatternIterator = RDBMS.GenericRDBMS.getErrorPatterns().iterator(); while (errorPatternIterator.hasNext() && !sqlInjectionFoundForUrl) { Pattern errorPattern = errorPatternIterator.next(); String errorPatternRDBMS = RDBMS.GenericRDBMS.getName(); // if the "error message" occurs in the result of sending the modified // query, but did NOT occur in the original result of the original query // then we may may have a SQL Injection vulnerability StringBuilder sb = new StringBuilder(); if (!matchBodyPattern(getBaseMsg(), errorPattern, null) && matchBodyPattern(msg1, errorPattern, sb)) { // Likely a SQL Injection. Raise it String extraInfo = Constant.messages.getString( MESSAGE_PREFIX + "alert.errorbased.extrainfo", errorPatternRDBMS, errorPattern.toString()); // raise the alert, and save the attack string for the // "Authentication Bypass" alert, if necessary sqlInjectionAttack = sqlErrValue; bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName() + " - " + errorPatternRDBMS, getDescription(), null, param, sqlInjectionAttack, extraInfo, getSolution(), sb.toString(), msg1); // log it, as the RDBMS may be useful to know later (in subsequent // checks, when we need to determine RDBMS specific behaviour, for // instance) getKb().add(getBaseMsg().getRequestHeader().getURI(), "sql/" + errorPatternRDBMS, Boolean.TRUE); sqlInjectionFoundForUrl = true; continue; } // bale out if we were asked nicely if (isStop()) { log.debug("Stopping the scan due to a user request"); return; } } // end of the loop to check for RDBMS specific error messages } } // for each of the SQL_CHECK_ERR values (SQL metacharacters) } // ############################### // Check 4 // New! I haven't seen this technique documented anywhere else, but it's dead simple. // Let me explain. // See if the parameter value can simply be changed to one that *evaluates* to be the // same value, // if evaluated on a database // the simple check is to see if parameter "1" gives the same results as for param // "2-1", and different results for param "2-2" // for now, we try this for integer values only. // ############################### // Since the previous checks are attempting SQL injection, and may have actually // succeeded in modifying the database (ask me how I know?!) // then we cannot rely on the database contents being the same as when the original // query was last run (could be hours ago) // so to work around this, simply re-run the query again now at this point. // Note that we are not counting this request in our max number of requests to be issued refreshedmessage = getNewMsg(); try { sendAndReceive(refreshedmessage, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + refreshedmessage.getRequestHeader().getURI().toString()); return; // Something went wrong, no point continuing } // String mResBodyNormal = getBaseMsg().getResponseBody().toString(); mResBodyNormalUnstripped = refreshedmessage.getResponseBody().toString(); mResBodyNormalStripped = this.stripOff(mResBodyNormalUnstripped, origParamValue); if (!sqlInjectionFoundForUrl && doExpressionBased && countExpressionBasedRequests < doExpressionMaxRequests) { // first figure out the type of the parameter.. try { // is it an integer type? // ZAP: removed URLDecoding because on Variants // int paramAsInt = // Integer.parseInt(TestSQLInjection.getURLDecode(origParamValue)); int paramAsInt = Integer.parseInt(origParamValue); if (this.debugEnabled) { log.debug("The parameter value [" + origParamValue + "] is of type Integer"); } // This check is implemented using two variant PLUS(+) and MULT(*) try { // PLUS variant check the param value "3-2" gives same result as original // request and param value "4-2" gives different result if original param // value is 1 // set the parameter value to a string value like "3-2", if the original // parameter value was "1" int paramPlusTwo = addWithOverflowCheck(paramAsInt, 2); String modifiedParamValueForAdd = String.valueOf(paramPlusTwo) + "-2"; // set the parameter value to a string value like "4-2", if the original // parameter value was "1" int paramPlusThree = addWithOverflowCheck(paramAsInt, 3); String modifiedParamValueConfirmForAdd = String.valueOf(paramPlusThree) + "-2"; // Do the attack for ADD variant expressionBasedAttack(param, origParamValue, modifiedParamValueForAdd, modifiedParamValueConfirmForAdd); // bale out if we were asked nicely if (isStop()) { log.debug("Stopping the scan due to a user request"); return; } // MULT variant check the param value "2/2" gives same result as original // request and param value "4/2" gives different result if original param // value is 1 if (!sqlInjectionFoundForUrl && countExpressionBasedRequests < doExpressionMaxRequests) { // set the parameter value to a string value like "2/2", if the original // parameter value was "1" int paramMultTwo = multiplyWithOverflowCheck(paramAsInt, 2); String modifiedParamValueForMult = String.valueOf(paramMultTwo) + "/2"; // set the parameter value to a string value like "4/2", if the original // parameter value was "1" int paramMultFour = multiplyWithOverflowCheck(paramAsInt, 4); String modifiedParamValueConfirmForMult = String.valueOf(paramMultFour) + "/2"; // Do the attack for MULT variant expressionBasedAttack(param, origParamValue, modifiedParamValueForMult, modifiedParamValueConfirmForMult); // bale out if we were asked nicely if (isStop()) { log.debug("Stopping the scan due to a user request"); return; } } } catch (ArithmeticException ex) { if (this.debugEnabled) { log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + "When performing integer math with the parameter value [" + origParamValue + "]"); } } } catch (Exception e) { if (this.debugEnabled) { log.debug("The parameter value [" + origParamValue + "] is NOT of type Integer"); } // TODO: implement a similar check for string types? This probably needs to be // RDBMS specific (ie, it should not live in this scanner) } } // Check 2: boolean based checks. // the check goes like so: // append " and 1 = 1" to the param. Send the query. Check the results. Hopefully they // match the original results from the unmodified query, // *suggesting* (but not yet definitely) that we have successfully modified the query, // (hopefully not gotten an error message), // and have gotten the same results back, which is what you would expect if you added // the constraint " and 1 = 1" to most (but not every) SQL query. // So was it a fluke that we got the same results back from the modified query? Perhaps // the original query returned 0 rows, so adding any number of // constraints would change nothing? It is still a possibility! // check to see if we can change the original parameter again to *restrict* the scope of // the query using an AND with an always false condition (AND_ERR) // (decreasing the results back to nothing), or to *broaden* the scope of the query // using an OR with an always true condition (AND_OR) // (increasing the results). // If we can successfully alter the results to our requirements, by one means or // another, we have found a SQL Injection vulnerability. // Some additional complications: assume there are 2 HTML parameters: username and // password, and the SQL constructed is like so: // select * from username where user = "$user" and password = "$password" // and lets assume we successfully know the type of the user field, via SQL_OR_TRUE // value '" OR "1"="1' (single quotes not part of the value) // we still have the problem that the actual SQL executed would look like so: // select * from username where user = "" OR "1"="1" and password = "whateveritis" // Since the password field is still taken into account (by virtue of the AND condition // on the password column), and we only inject one parameter at a time, // we are still not in control. // the solution is simple: add an end-of-line comment to the field added in (in this // example: the user field), so that the SQL becomes: // select * from username where user = "" OR "1"="1" -- and password = "whateveritis" // the result is that any additional constraints are commented out, and the last // condition to have any effect is the one whose // HTTP param we are manipulating. // Note also that because this comment only needs to be added to the "SQL_OR_TRUE" and // not to the equivalent SQL_AND_FALSE, because of the nature of the OR // and AND conditions in SQL. // Corollary: If a particular RDBMS does not offer the ability to comment out the // remainder of a line, we will not attempt to comment out anything in the query // and we will simply hope that the *last* constraint in the SQL query is // constructed from a HTTP parameter under our control. if (this.debugEnabled) { log.debug("Doing Check 2, since check 1 did not match for " + getBaseMsg().getRequestHeader().getURI()); } // Since the previous checks are attempting SQL injection, and may have actually // succeeded in modifying the database (ask me how I know?!) // then we cannot rely on the database contents being the same as when the original // query was last run (could be hours ago) // so to work around this, simply re-run the query again now at this point. // Note that we are not counting this request in our max number of requests to be issued refreshedmessage = getNewMsg(); try { sendAndReceive(refreshedmessage, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + refreshedmessage.getRequestHeader().getURI().toString()); return; // Something went wrong, no point continuing } // String mResBodyNormal = getBaseMsg().getResponseBody().toString(); mResBodyNormalUnstripped = refreshedmessage.getResponseBody().toString(); mResBodyNormalStripped = this.stripOff(mResBodyNormalUnstripped, origParamValue); // boolean booleanBasedSqlInjectionFoundForParam = false; // try each of the AND syntax values in turn. // Which one is successful will depend on the column type of the table/view column into // which we are injecting the SQL. for (int i = 0; i < SQL_LOGIC_AND_TRUE.length && !sqlInjectionFoundForUrl && doBooleanBased && countBooleanBasedRequests < doBooleanMaxRequests; i++) { // needs a new message for each type of AND to be issued HttpMessage msg2 = getNewMsg(); // ZAP: Removed getURLDecode() String sqlBooleanAndTrueValue = origParamValue + SQL_LOGIC_AND_TRUE[i]; String sqlBooleanAndFalseValue = origParamValue + SQL_LOGIC_AND_FALSE[i]; setParameter(msg2, param, sqlBooleanAndTrueValue); // send the AND with an additional TRUE statement tacked onto the end. Hopefully it // will return the same results as the original (to find a vulnerability) try { sendAndReceive(msg2, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + msg2.getRequestHeader().getURI().toString()); continue; // Something went wrong, continue to the next item in the loop } countBooleanBasedRequests++; // String resBodyAND = msg2.getResponseBody().toString(); String resBodyANDTrueUnstripped = msg2.getResponseBody().toString(); String resBodyANDTrueStripped = stripOffOriginalAndAttackParam(resBodyANDTrueUnstripped, origParamValue, sqlBooleanAndTrueValue); // set up two little arrays to ease the work of checking the unstripped output, and // then the stripped output String normalBodyOutput[] = { mResBodyNormalUnstripped, mResBodyNormalStripped }; String andTrueBodyOutput[] = { resBodyANDTrueUnstripped, resBodyANDTrueStripped }; boolean strippedOutput[] = { false, true }; for (int booleanStrippedUnstrippedIndex = 0; booleanStrippedUnstrippedIndex < 2; booleanStrippedUnstrippedIndex++) { // if the results of the "AND 1=1" match the original query (using either the // stipped or unstripped versions), we may be onto something. if (andTrueBodyOutput[booleanStrippedUnstrippedIndex] .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) == 0) { if (this.debugEnabled) { log.debug("Check 2, " + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED") + " html output for AND TRUE condition [" + sqlBooleanAndTrueValue + "] matched (refreshed) original results for " + refreshedmessage.getRequestHeader().getURI()); } // so they match. Was it a fluke? See if we get the same result by tacking // on "AND 1 = 2" to the original HttpMessage msg2_and_false = getNewMsg(); setParameter(msg2_and_false, param, sqlBooleanAndFalseValue); try { sendAndReceive(msg2_and_false, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + msg2_and_false.getRequestHeader().getURI().toString()); continue; // Something went wrong, continue on to the next item in the // loop } countBooleanBasedRequests++; // String resBodyANDFalse = // stripOff(msg2_and_false.getResponseBody().toString(), // SQL_LOGIC_AND_FALSE[i]); // String resBodyANDFalse = msg2_and_false.getResponseBody().toString(); String resBodyANDFalseUnstripped = msg2_and_false.getResponseBody().toString(); String resBodyANDFalseStripped = stripOffOriginalAndAttackParam(resBodyANDFalseUnstripped, origParamValue, sqlBooleanAndFalseValue); String andFalseBodyOutput[] = { resBodyANDFalseUnstripped, resBodyANDFalseStripped }; // which AND False output should we compare? the stripped or the unstripped // version? // depends on which one we used to get to here.. use the same as that.. // build an always false AND query. Result should be different to prove the // SQL works. if (andFalseBodyOutput[booleanStrippedUnstrippedIndex] .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) != 0) { if (this.debugEnabled) { log.debug("Check 2, " + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED") + " html output for AND FALSE condition [" + sqlBooleanAndFalseValue + "] differed from (refreshed) original results for " + refreshedmessage.getRequestHeader().getURI()); } // it's different (suggesting that the "AND 1 = 2" appended on gave // different results because it restricted the data set to nothing // Likely a SQL Injection. Raise it String extraInfo = null; if (strippedOutput[booleanStrippedUnstrippedIndex]) { extraInfo = Constant.messages.getString( MESSAGE_PREFIX + "alert.booleanbased.extrainfo", sqlBooleanAndTrueValue, sqlBooleanAndFalseValue, ""); } else { extraInfo = Constant.messages.getString( MESSAGE_PREFIX + "alert.booleanbased.extrainfo", sqlBooleanAndTrueValue, sqlBooleanAndFalseValue, "NOT "); } extraInfo = extraInfo + "\n" + Constant.messages .getString(MESSAGE_PREFIX + "alert.booleanbased.extrainfo.dataexists"); // raise the alert, and save the attack string for the "Authentication // Bypass" alert, if necessary sqlInjectionAttack = sqlBooleanAndTrueValue; bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName(), getDescription(), null, // url param, sqlInjectionAttack, extraInfo, getSolution(), "", msg2); sqlInjectionFoundForUrl = true; break; // No further need to loop through SQL_AND } else { // the results of the always false condition are the same as for the // original unmodified parameter // this could be because there was *no* data returned for the original // unmodified parameter // so consider the effect of adding comments to both the always true // condition, and the always false condition // the first value to try.. // ZAP: Removed getURLDecode() String orValue = origParamValue + SQL_LOGIC_OR_TRUE[i]; // this is where that comment comes in handy: if the RDBMS supports // one-line comments, add one in to attempt to ensure that the // condition becomes one that is effectively always true, returning ALL // data (or as much as possible), allowing us to pinpoint the SQL // Injection if (this.debugEnabled) { log.debug("Check 2 , " + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED") + " html output for AND FALSE condition [" + sqlBooleanAndFalseValue + "] SAME as (refreshed) original results for " + refreshedmessage.getRequestHeader().getURI() + " ### (forcing OR TRUE check) "); } HttpMessage msg2_or_true = getNewMsg(); setParameter(msg2_or_true, param, orValue); try { sendAndReceive(msg2_or_true, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + msg2_or_true.getRequestHeader().getURI().toString()); continue; // Something went wrong, continue on to the next item in // the loop } countBooleanBasedRequests++; // String resBodyORTrue = // stripOff(msg2_or_true.getResponseBody().toString(), orValue); // String resBodyORTrue = msg2_or_true.getResponseBody().toString(); String resBodyORTrueUnstripped = msg2_or_true.getResponseBody().toString(); String resBodyORTrueStripped = stripOffOriginalAndAttackParam(resBodyORTrueUnstripped, origParamValue, orValue); String orTrueBodyOutput[] = { resBodyORTrueUnstripped, resBodyORTrueStripped }; int compareOrToOriginal = orTrueBodyOutput[booleanStrippedUnstrippedIndex] .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]); if (compareOrToOriginal != 0) { if (this.debugEnabled) { log.debug("Check 2, " + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED") + " html output for OR TRUE condition [" + orValue + "] different to (refreshed) original results for " + refreshedmessage.getRequestHeader().getURI()); } // it's different (suggesting that the "OR 1 = 1" appended on gave // different results because it broadened the data set from nothing // to something // Likely a SQL Injection. Raise it String extraInfo = null; if (strippedOutput[booleanStrippedUnstrippedIndex]) { extraInfo = Constant.messages.getString( MESSAGE_PREFIX + "alert.booleanbased.extrainfo", sqlBooleanAndTrueValue, orValue, ""); } else { extraInfo = Constant.messages.getString( MESSAGE_PREFIX + "alert.booleanbased.extrainfo", sqlBooleanAndTrueValue, orValue, "NOT "); } extraInfo = extraInfo + "\n" + Constant.messages .getString(MESSAGE_PREFIX + "alert.booleanbased.extrainfo.datanotexists"); // raise the alert, and save the attack string for the // "Authentication Bypass" alert, if necessary sqlInjectionAttack = orValue; bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName(), getDescription(), null, // url param, sqlInjectionAttack, extraInfo, getSolution(), "", msg2); sqlInjectionFoundForUrl = true; // booleanBasedSqlInjectionFoundForParam = true; //causes us to // skip past the other entries in SQL_AND. Only one will expose a // vuln for a given param, since the database column is of only 1 // type break; // No further need to loop } } } // if the results of the "AND 1=1" match the original query, we may be onto // something. else { // andTrueBodyOutput[booleanStrippedUnstrippedIndex].compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) // the results of the "AND 1=1" do NOT match the original query, for // whatever reason (no sql injection, or the web page is not stable) if (this.debugEnabled) { log.debug("Check 2, " + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED") + " html output for AND condition [" + sqlBooleanAndTrueValue + "] does NOT match the (refreshed) original results for " + refreshedmessage.getRequestHeader().getURI()); Patch diffpatch = DiffUtils.diff( new LinkedList<String>(Arrays .asList(normalBodyOutput[booleanStrippedUnstrippedIndex].split("\\n"))), new LinkedList<String>(Arrays.asList( andTrueBodyOutput[booleanStrippedUnstrippedIndex].split("\\n")))); // int numberofDifferences = diffpatch.getDeltas().size(); // and convert the list of patches to a String, joining using a newline StringBuilder tempDiff = new StringBuilder(250); for (Delta delta : diffpatch.getDeltas()) { String changeType = null; if (delta.getType() == Delta.TYPE.CHANGE) { changeType = "Changed Text"; } else if (delta.getType() == Delta.TYPE.DELETE) { changeType = "Deleted Text"; } else if (delta.getType() == Delta.TYPE.INSERT) { changeType = "Inserted text"; } else { changeType = "Unknown change type [" + delta.getType() + "]"; } tempDiff.append("\n(" + changeType + ")\n"); // blank line before tempDiff.append("Output for Unmodified parameter: " + delta.getOriginal() + "\n"); tempDiff.append("Output for modified parameter: " + delta.getRevised() + "\n"); } log.debug("DIFFS: " + tempDiff); } } // bale out if we were asked nicely if (isStop()) { log.debug("Stopping the scan due to a user request"); return; } } // end of boolean logic output index (unstripped + stripped) } // end of check 2 // check 2a: boolean based logic, where the original query returned *no* data. Here we // append " OR 1=1" in an attempt to extract *more* data // and then verify the results by attempting to reproduce the original results by // appending an " AND 1=2" condition (ie "open up first, then restrict to verify") // this differs from the previous logic based check since the previous check assumes // that the original query produced data, and tries first to restrict that data // (ie, it uses "restrict first, open up to verify" ). for (int i = 0; i < SQL_LOGIC_OR_TRUE.length && !sqlInjectionFoundForUrl && doBooleanBased && countBooleanBasedRequests < doBooleanMaxRequests; i++) { HttpMessage msg2 = getNewMsg(); String sqlBooleanOrTrueValue = origParamValue + SQL_LOGIC_OR_TRUE[i]; String sqlBooleanAndFalseValue = origParamValue + SQL_LOGIC_AND_FALSE[i]; setParameter(msg2, param, sqlBooleanOrTrueValue); try { sendAndReceive(msg2, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + msg2.getRequestHeader().getURI().toString()); continue; // Something went wrong, continue on to the next item in the loop } countBooleanBasedRequests++; String resBodyORTrueUnstripped = msg2.getResponseBody().toString(); // if the results of the "OR 1=1" exceed the original query (unstripped, by more // than a 20% size difference, say), we may be onto something. // TODO: change the percentage difference threshold based on the alert threshold if ((resBodyORTrueUnstripped.length() > (mResBodyNormalUnstripped.length() * 1.2))) { if (this.debugEnabled) { log.debug("Check 2a, unstripped html output for OR TRUE condition [" + sqlBooleanOrTrueValue + "] produced sufficiently larger results than the original message"); } // if we can also restrict it back to the original results by appending a " and // 1=2", then "Winner Winner, Chicken Dinner". HttpMessage msg2_and_false = getNewMsg(); setParameter(msg2_and_false, param, sqlBooleanAndFalseValue); try { sendAndReceive(msg2_and_false, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + msg2_and_false.getRequestHeader().getURI().toString()); continue; // Something went wrong, continue on to the next item in the loop } countBooleanBasedRequests++; String resBodyANDFalseUnstripped = msg2_and_false.getResponseBody().toString(); String resBodyANDFalseStripped = stripOffOriginalAndAttackParam(resBodyANDFalseUnstripped, origParamValue, sqlBooleanAndFalseValue); // does the "AND 1=2" version produce the same as the original (for // stripped/unstripped versions) boolean verificationUsingUnstripped = resBodyANDFalseUnstripped .compareTo(mResBodyNormalUnstripped) == 0; boolean verificationUsingStripped = resBodyANDFalseStripped .compareTo(mResBodyNormalStripped) == 0; if (verificationUsingUnstripped || verificationUsingStripped) { if (this.debugEnabled) { log.debug("Check 2, " + (verificationUsingStripped ? "STRIPPED" : "UNSTRIPPED") + " html output for AND FALSE condition [" + sqlBooleanAndFalseValue + "] matches the (refreshed) original results"); } // Likely a SQL Injection. Raise it String extraInfo = null; if (verificationUsingStripped) { extraInfo = Constant.messages.getString(MESSAGE_PREFIX + "alert.booleanbased.extrainfo", sqlBooleanOrTrueValue, sqlBooleanAndFalseValue, ""); } else { extraInfo = Constant.messages.getString(MESSAGE_PREFIX + "alert.booleanbased.extrainfo", sqlBooleanOrTrueValue, sqlBooleanAndFalseValue, "NOT "); } extraInfo = extraInfo + "\n" + Constant.messages .getString(MESSAGE_PREFIX + "alert.booleanbased.extrainfo.datanotexists"); // raise the alert, and save the attack string for the "Authentication // Bypass" alert, if necessary sqlInjectionAttack = sqlBooleanOrTrueValue; bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName(), getDescription(), null, // url param, sqlInjectionAttack, extraInfo, getSolution(), "", msg2); sqlInjectionFoundForUrl = true; break; // No further need to loop } } } // end of check 2a // Check 3: UNION based // for each SQL UNION combination to try for (int sqlUnionStringIndex = 0; sqlUnionStringIndex < SQL_UNION_APPENDAGES.length && !sqlInjectionFoundForUrl && doUnionBased && countUnionBasedRequests < doUnionMaxRequests; sqlUnionStringIndex++) { // new message for each value we attack with HttpMessage msg3 = getNewMsg(); String sqlUnionValue = origParamValue + SQL_UNION_APPENDAGES[sqlUnionStringIndex]; setParameter(msg3, param, sqlUnionValue); // send the message with the modified parameters try { sendAndReceive(msg3, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + msg3.getRequestHeader().getURI().toString()); continue; // Something went wrong, continue on to the next item in the loop } countUnionBasedRequests++; // now check the results.. look first for UNION specific error messages in the // output that were not there in the original output // and failing that, look for generic RDBMS specific error messages // TODO: maybe also try looking at a differentiation based approach?? Prone to false // positives though. for (RDBMS rdbms : RDBMS.values()) { if (getTechSet().includes(rdbms.getTech()) && checkUnionErrors(rdbms, msg3, mResBodyNormalStripped, refreshedmessage.getRequestHeader().getURI(), param, origParamValue, sqlUnionValue)) { sqlInjectionFoundForUrl = true; // Save the attack string for the "Authentication Bypass" alert, if // necessary sqlInjectionAttack = sqlUnionValue; break; } // bale out if we were asked nicely if (isStop()) { log.debug("Stopping the scan due to a user request"); return; } } // end of the loop to check for RDBMS specific UNION error messages } //// for each SQL UNION combination to try // end of check 3 // ############################### // check for columns used in the "order by" clause of a SQL statement. earlier tests // will likely not catch these // append on " ASC -- " to the end of the original parameter. Grab the results. // if the results are different to the original (unmodified parameter) results, then // bale // if the results are the same as for the original parameter value, then the parameter // *might* be influencing the order by // try again for "DESC": append on " DESC -- " to the end of the original parameter. // Grab the results. // if the results are the same as the original (unmodified parameter) results, then bale // (the results are not under our control, or there is no difference in the ordering, // for some reason: 0 or 1 rows only, or ordering // by the first column alone is not sufficient to change the ordering of the data.) // if the results were different to the original (unmodified parameter) results, then // SQL injection!! // Since the previous checks are attempting SQL injection, and may have actually // succeeded in modifying the database (ask me how I know?!) // then we cannot rely on the database contents being the same as when the original // query was last run (could be hours ago) // so to work around this, simply re-run the query again now at this point. // Note that we are not counting this request in our max number of requests to be issued refreshedmessage = getNewMsg(); try { sendAndReceive(refreshedmessage, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + refreshedmessage.getRequestHeader().getURI().toString()); return; // Something went wrong, no point continuing } // String mResBodyNormal = getBaseMsg().getResponseBody().toString(); mResBodyNormalUnstripped = refreshedmessage.getResponseBody().toString(); mResBodyNormalStripped = this.stripOff(mResBodyNormalUnstripped, origParamValue); if (!sqlInjectionFoundForUrl && doOrderByBased && countOrderByBasedRequests < doOrderByMaxRequests) { // ZAP: Removed getURLDecode() String modifiedParamValue = origParamValue + " ASC " + SQL_ONE_LINE_COMMENT; HttpMessage msg5 = getNewMsg(); setParameter(msg5, param, modifiedParamValue); try { sendAndReceive(msg5, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + msg5.getRequestHeader().getURI().toString()); return; // Something went wrong, no point continuing } countOrderByBasedRequests++; String modifiedAscendingOutputUnstripped = msg5.getResponseBody().toString(); String modifiedAscendingOutputStripped = stripOffOriginalAndAttackParam( modifiedAscendingOutputUnstripped, origParamValue, modifiedParamValue); // set up two little arrays to ease the work of checking the unstripped output, and // then the stripped output String normalBodyOutput[] = { mResBodyNormalUnstripped, mResBodyNormalStripped }; String ascendingBodyOutput[] = { modifiedAscendingOutputUnstripped, modifiedAscendingOutputStripped }; boolean strippedOutput[] = { false, true }; for (int booleanStrippedUnstrippedIndex = 0; booleanStrippedUnstrippedIndex < 2; booleanStrippedUnstrippedIndex++) { // if the results of the modified request match the original query, we may be // onto something. if (ascendingBodyOutput[booleanStrippedUnstrippedIndex] .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) == 0) { if (this.debugEnabled) { log.debug("Check X, " + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED") + " html output for modified Order By parameter [" + modifiedParamValue + "] matched (refreshed) original results for " + refreshedmessage.getRequestHeader().getURI()); } // confirm that a different parameter value generates different output, to // minimise false positives // use the descending order this time // ZAP: Removed getURLDecode() String modifiedParamValueConfirm = origParamValue + " DESC " + SQL_ONE_LINE_COMMENT; HttpMessage msg5Confirm = getNewMsg(); setParameter(msg5Confirm, param, modifiedParamValueConfirm); try { sendAndReceive(msg5Confirm, false); // do not follow redirects } catch (SocketException ex) { if (log.isDebugEnabled()) log.debug("Caught " + ex.getClass().getName() + " " + ex.getMessage() + " when accessing: " + msg5Confirm.getRequestHeader().getURI().toString()); continue; // Something went wrong, continue on to the next item in the // loop } countOrderByBasedRequests++; String confirmOrderByOutputUnstripped = msg5Confirm.getResponseBody().toString(); String confirmOrderByOutputStripped = stripOffOriginalAndAttackParam( confirmOrderByOutputUnstripped, origParamValue, modifiedParamValueConfirm); // set up two little arrays to ease the work of checking the unstripped // output or the stripped output String confirmOrderByBodyOutput[] = { confirmOrderByOutputUnstripped, confirmOrderByOutputStripped }; if (confirmOrderByBodyOutput[booleanStrippedUnstrippedIndex] .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) != 0) { // the confirm query did not return the same results. This means that // arbitrary queries are not all producing the same page output. // this means the fact we earlier reproduced the original page output // with a modified parameter was not a coincidence // Likely a SQL Injection. Raise it String extraInfo = null; if (strippedOutput[booleanStrippedUnstrippedIndex]) { extraInfo = Constant.messages.getString( MESSAGE_PREFIX + "alert.orderbybased.extrainfo", modifiedParamValue, ""); } else { extraInfo = Constant.messages.getString( MESSAGE_PREFIX + "alert.orderbybased.extrainfo", modifiedParamValue, "NOT "); } // raise the alert, and save the attack string for the "Authentication // Bypass" alert, if necessary sqlInjectionAttack = modifiedParamValue; bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, getName(), getDescription(), null, // url param, sqlInjectionAttack, extraInfo, getSolution(), "", msg5); sqlInjectionFoundForUrl = true; break; // No further need to loop } } // bale out if we were asked nicely if (isStop()) { log.debug("Stopping the scan due to a user request"); return; } } } // ############################### // if a sql injection was found, we should check if the page is flagged as a login page // in any of the contexts. if it is, raise an "SQL Injection - Authentication Bypass" // alert in addition to the alerts already raised if (sqlInjectionFoundForUrl) { boolean loginUrl = false; // log.debug("### A SQL Injection may lead to auth bypass.."); // are we dealing with a login url in any of the contexts? ExtensionAuthentication extAuth = (ExtensionAuthentication) Control.getSingleton() .getExtensionLoader().getExtension(ExtensionAuthentication.NAME); if (extAuth != null) { URI requestUri = getBaseMsg().getRequestHeader().getURI(); // using the session, get the list of contexts for the url List<Context> contextList = extAuth.getModel().getSession() .getContextsForUrl(requestUri.getURI()); // now loop, and see if the url is a login url in each of the contexts in turn.. for (Context context : contextList) { URI loginUri = extAuth.getLoginRequestURIForContext(context); if (loginUri != null) { if (requestUri.getScheme().equals(loginUri.getScheme()) && requestUri.getHost().equals(loginUri.getHost()) && requestUri.getPort() == loginUri.getPort() && requestUri.getPath().equals(loginUri.getPath())) { // we got this far.. only the method (GET/POST), user details, query // params, fragment, and POST params // are possibly different from the login page. loginUrl = true; // DEBUG only // log.debug("##### The right login page was found"); break; } else { // log.debug("#### This is not the login page you're looking for"); } } else { // log.debug("### This context has no login page set"); } } } if (loginUrl) { // log.debug("##### Raising auth bypass"); // raise the alert, using the custom name and description String vulnname = Constant.messages.getString(MESSAGE_PREFIX + "authbypass.name"); String vulndesc = Constant.messages.getString(MESSAGE_PREFIX + "authbypass.desc"); // raise the alert, using the attack string stored earlier for this purpose bingo(Alert.RISK_HIGH, Alert.CONFIDENCE_MEDIUM, vulnname, vulndesc, refreshedmessage.getRequestHeader().getURI().getURI(), // url param, sqlInjectionAttack, "", getSolution(), "", getBaseMsg()); } // not a login page } // no sql Injection Found For Url } catch (InvalidRedirectLocationException e) { // Not an error, just means we probably attacked the redirect location } catch (Exception e) { // Do not try to internationalise this.. we need an error message in any event.. // if it's in English, it's still better than not having it at all. log.error("An error occurred checking a url for SQL Injection vulnerabilities", e); } }
From source file:org.zaproxy.zap.extension.ascanrulesAlpha.AbstractAppFilePlugin.java
@Override public void scan() { // Check if the user stopped things. One request per URL so check before // sending the request if (isStop()) { if (LOG.isDebugEnabled()) { LOG.debug("Scanner " + getName() + " Stopping."); }/* w ww . j a v a 2 s. c om*/ return; } HttpMessage newRequest = getNewMsg(); newRequest.getRequestHeader().setMethod(HttpRequestHeader.GET); URI baseUri = getBaseMsg().getRequestHeader().getURI(); URI newUri = null; try { String baseUriPath = baseUri.getPath() == null ? "" : baseUri.getPath(); newUri = new URI(baseUri.getScheme(), null, baseUri.getHost(), baseUri.getPort(), createTestablePath(baseUriPath)); } catch (URIException uEx) { if (LOG.isDebugEnabled()) { LOG.debug( "An error occurred creating a URI for the: " + getName() + " scanner. " + uEx.getMessage(), uEx); } return; } try { newRequest.getRequestHeader().setURI(newUri); } catch (URIException uEx) { if (LOG.isDebugEnabled()) { LOG.debug("An error occurred setting the URI for a new request used by: " + getName() + " scanner. " + uEx.getMessage(), uEx); } return; } // Until https://github.com/zaproxy/zaproxy/issues/3563 is addressed // track completed in Kb // TODO change this when possible synchronized (getKb()) { if (getKb().getBoolean(newUri, messagePrefix)) { return; } getKb().add(newUri, messagePrefix, Boolean.TRUE); } try { sendAndReceive(newRequest, false); } catch (IOException e) { LOG.warn("An error occurred while checking [" + newRequest.getRequestHeader().getMethod() + "] [" + newRequest.getRequestHeader().getURI() + "] for " + getName() + " Caught " + e.getClass().getName() + " " + e.getMessage()); return; } if (isFalsePositive(newRequest)) { return; } int statusCode = newRequest.getResponseHeader().getStatusCode(); if (statusCode == HttpStatusCode.OK) { raiseAlert(newRequest, getRisk(), ""); } else if (statusCode == HttpStatusCode.UNAUTHORIZED || statusCode == HttpStatusCode.FORBIDDEN) { raiseAlert(newRequest, Alert.RISK_INFO, getOtherInfo()); } }
From source file:org.zaproxy.zap.extension.ascanrulesAlpha.ElmahScanner.java
@Override public void scan() { // Check if the user stopped things. One request per URL so check before // sending the request if (isStop()) { if (LOG.isDebugEnabled()) { LOG.debug("Scanner " + getName() + " Stopping."); }/*from ww w . jav a 2s . c o m*/ return; } HttpMessage newRequest = getNewMsg(); newRequest.getRequestHeader().setMethod(HttpRequestHeader.GET); URI baseUri = getBaseMsg().getRequestHeader().getURI(); URI elmahUri = null; try { elmahUri = new URI(baseUri.getScheme(), null, baseUri.getHost(), baseUri.getPort(), "/elmah.axd"); } catch (URIException uEx) { if (LOG.isDebugEnabled()) { LOG.debug( "An error occurred creating a URI for the: " + getName() + " scanner. " + uEx.getMessage(), uEx); } return; } try { newRequest.getRequestHeader().setURI(elmahUri); } catch (URIException uEx) { if (LOG.isDebugEnabled()) { LOG.debug("An error occurred setting the URI for a new request used by: " + getName() + " scanner. " + uEx.getMessage(), uEx); } return; } try { sendAndReceive(newRequest, false); } catch (IOException e) { LOG.warn("An error occurred while checking [" + newRequest.getRequestHeader().getMethod() + "] [" + newRequest.getRequestHeader().getURI() + "] for " + getName() + " Caught " + e.getClass().getName() + " " + e.getMessage()); return; } int statusCode = newRequest.getResponseHeader().getStatusCode(); if (statusCode == HttpStatusCode.OK) { raiseAlert(newRequest, getRisk(), ""); } else if (statusCode == HttpStatusCode.UNAUTHORIZED || statusCode == HttpStatusCode.FORBIDDEN) { raiseAlert(newRequest, Alert.RISK_INFO, getOtherInfo()); } }
From source file:org.zaproxy.zap.extension.ascanrulesAlpha.HttpOnlySite.java
@Override public void scan() { if (getBaseMsg().getRequestHeader().isSecure()) { // Base request is HTTPS if (log.isDebugEnabled()) { log.debug("The original request was HTTPS, so there is not much point in looking further."); }/*from w ww.j a va2 s.c om*/ return; } HttpMessage newRequest = getNewMsg(); try { String host = newRequest.getRequestHeader().getURI().getHost(); String path = newRequest.getRequestHeader().getURI().getPath(); newRequest.getRequestHeader().setURI(new URI("https", null, host, 443, path)); } catch (URIException e) { log.error("Error creating HTTPS URL from HTTP URL:", e); return; } if (isStop()) { if (log.isDebugEnabled()) { log.debug("Scanner " + getName() + " Stopping."); } return; } try { int count = 0; while (count < REDIR_LIMIT) { if (isStop()) { if (log.isDebugEnabled()) { log.debug("Scanner " + getName() + " Stopping."); } return; } sendAndReceive(newRequest, false); int status = newRequest.getResponseHeader().getStatusCode(); if (!HttpStatusCode.isRedirection(status)) { break; } String redirect = newRequest.getResponseHeader().getHeader(HttpResponseHeader.LOCATION); if (redirect == null || redirect.isEmpty()) { raiseAlert(newRequest, "noredirection"); return; } URI oldURI = newRequest.getRequestHeader().getURI(); URI newURI = constructURI(redirect, oldURI); if (newURI == null) { raiseAlert(newRequest, "urinotencoded"); return; } newRequest.getRequestHeader().setURI(newURI); if (!oldURI.getHost().equals(newURI.getHost())) { raiseAlert(newRequest, "differenthosts"); return; } if (newRequest.getRequestHeader().isSecure()) { count++; } else { raiseAlert(newRequest, "redirecttohttp"); return; } } if (count == REDIR_LIMIT) { // When redirection limit is exceeded raiseAlert(newRequest, "redirectionlimit"); return; } } catch (SocketException | SocketTimeoutException e) { raiseAlert(newRequest, "connectionfail"); return; } catch (SSLException e) { if (e.getMessage().contains("plaintext")) { raiseAlert(newRequest, "nossl"); } return; } catch (IOException e) { log.error("Request couldn't go through:", e); return; } }
From source file:org.zaproxy.zap.extension.ascanrulesBeta.SessionFixation.java
/** * scans all GET, Cookie params for Session fields, and looks for SessionFixation * vulnerabilities/*from w w w .j av a2 s . co m*/ */ @Override public void scan() { // TODO: scan the POST (form) params for session id fields. try { boolean loginUrl = false; // Are we dealing with a login url in any of the contexts of which this uri is part URI requestUri = getBaseMsg().getRequestHeader().getURI(); ExtensionAuthentication extAuth = (ExtensionAuthentication) Control.getSingleton().getExtensionLoader() .getExtension(ExtensionAuthentication.NAME); // using the session, get the list of contexts for the url List<Context> contextList = extAuth.getModel().getSession().getContextsForUrl(requestUri.getURI()); // now loop, and see if the url is a login url in each of the contexts in turn... for (Context context : contextList) { URI loginUri = extAuth.getLoginRequestURIForContext(context); if (loginUri != null && requestUri.getPath() != null) { if (requestUri.getScheme().equals(loginUri.getScheme()) && requestUri.getHost().equals(loginUri.getHost()) && requestUri.getPort() == loginUri.getPort() && requestUri.getPath().equals(loginUri.getPath())) { // we got this far.. only the method (GET/POST), user details, query params, // fragment, and POST params // are possibly different from the login page. loginUrl = true; break; } } } // For now (from Zap 2.0), the Session Fixation scanner will only run for login pages if (loginUrl == false) { log.debug("For the Session Fixation scanner to actually do anything, a Login Page *must* be set!"); return; } // find all params set in the request (GET/POST/Cookie) // Note: this will be the full set, before we delete anything. TreeSet<HtmlParameter> htmlParams = new TreeSet<>(); htmlParams.addAll(getBaseMsg().getRequestHeader().getCookieParams()); // request cookies only. no response cookies htmlParams.addAll(getBaseMsg().getFormParams()); // add in the POST params htmlParams.addAll(getBaseMsg().getUrlParams()); // add in the GET params // Now add in the pseudo parameters set in the URL itself, such as in the following: // http://www.example.com/someurl;JSESSIONID=abcdefg?x=123&y=456 // as opposed to the url parameters in the following example, which are already picked // up by getUrlParams() // http://www.example.com/someurl?JSESSIONID=abcdefg&x=123&y=456 // convert from org.apache.commons.httpclient.URI to a String String requestUrl = "Unknown URL"; try { requestUrl = new URL(requestUri.getScheme(), requestUri.getHost(), requestUri.getPort(), requestUri.getPath()).toString(); } catch (Exception e) { // no point in continuing. The URL is invalid. This is a peculiarity in the Zap // core, // and can happen when // - the user browsed to http://www.example.com/bodgeit and // - the user did not browse to http://www.example.com or to http://www.example.com/ // so the Zap GUI displays "http://www.example.com" as a node under "Sites", // and under that, it displays the actual urls to which the user browsed // (http://www.example.com/bodgeit, for instance) // When the user selects the node "http://www.example.com", and tries to scan it // with // the session fixation scanner, the URI that is passed is "http://www.example.com", // which is *not* a valid url. // If the user actually browses to "http://www.example.com" (even without the // trailing slash) // the web browser appends the trailing slash, and so Zap records the URI as // "http://www.example.com/", which IS a valid url, and which can (and should) be // scanned. // // In short.. if this happens, we do not want to scan the URL anyway // (because the user never browsed to it), so just do nothing instead. log.error("Cannot convert URI [" + requestUri + "] to a URL: " + e.getMessage()); return; } // suck out any pseudo url parameters from the url Set<HtmlParameter> pseudoUrlParams = getPseudoUrlParameters(requestUrl); htmlParams.addAll(pseudoUrlParams); if (this.debugEnabled) log.debug("Pseudo url params of URL [" + requestUrl + "] : [" + pseudoUrlParams + "]"); //// for each parameter in turn, // int counter = 0; for (Iterator<HtmlParameter> iter = htmlParams.iterator(); iter.hasNext();) { HttpMessage msg1Final; HttpMessage msg1Initial = getNewMsg(); //// debug logic only.. to do first field only // counter ++; // if ( counter > 1 ) // return; HtmlParameter currentHtmlParameter = iter.next(); // Useful for debugging, but I can't find a way to view this data in the GUI, so // leave it out for now. // msg1Initial.setNote("Message 1 for parameter "+ currentHtmlParameter); if (this.debugEnabled) log.debug("Scanning URL [" + msg1Initial.getRequestHeader().getMethod() + "] [" + msg1Initial.getRequestHeader().getURI() + "], [" + currentHtmlParameter.getType() + "] field [" + currentHtmlParameter.getName() + "] with value [" + currentHtmlParameter.getValue() + "] for Session Fixation"); if (currentHtmlParameter.getType().equals(HtmlParameter.Type.cookie)) { // careful to pick up the cookies from the Request, and not to include cookies // set in any earlier response TreeSet<HtmlParameter> cookieRequestParams = msg1Initial.getRequestHeader().getCookieParams(); // delete the original cookie from the parameters cookieRequestParams.remove(currentHtmlParameter); msg1Initial.setCookieParams(cookieRequestParams); // send the message, minus the cookie parameter, and see how it comes back. // Note: do NOT automatically follow redirects.. handle those here instead. sendAndReceive(msg1Initial, false, false); ///////////////////////////// // create a copy of msg1Initial to play with to handle redirects (if any). // we use a copy because if we change msg1Initial itself, it messes the URL and // params displayed on the GUI. msg1Final = msg1Initial; HtmlParameter cookieBack1 = getResponseCookie(msg1Initial, currentHtmlParameter.getName()); long cookieBack1TimeReceived = System.currentTimeMillis(); // in ms. when was the cookie received? // Important if it has a Max-Age directive Date cookieBack1ExpiryDate = null; HttpMessage temp = msg1Initial; int redirectsFollowed1 = 0; while (HttpStatusCode.isRedirection(temp.getResponseHeader().getStatusCode())) { // Note that we need to clone the Request and the Response.. // we seem to need to track the secure flag now to make sure its set later boolean secure1 = temp.getRequestHeader().isSecure(); temp = temp.cloneAll(); // clone the previous message redirectsFollowed1++; if (redirectsFollowed1 > 10) { throw new Exception("Too many redirects were specified in the first message"); } // create a new URI from the absolute location returned, and interpret it as // escaped // note that the standard says that the Location returned should be // absolute, but it ain't always so... URI newLocation = new URI(temp.getResponseHeader().getHeader(HttpHeader.LOCATION), true); // and follow the forward url // need to clear the params (which would come from the initial POST, // otherwise) temp.getRequestHeader().setGetParams(new TreeSet<HtmlParameter>()); temp.setRequestBody(""); temp.setResponseBody(""); // make sure no values accidentally carry from one iteration to // the next try { temp.getRequestHeader().setURI(newLocation); } catch (Exception e) { // the Location field contents may not be standards compliant. Lets // generate a uri to use as a workaround where a relative path was // given instead of an absolute one URI newLocationWorkaround = new URI(temp.getRequestHeader().getURI(), temp.getResponseHeader().getHeader(HttpHeader.LOCATION), true); // try again, except this time, if it fails, don't try to handle it if (this.debugEnabled) log.debug("The Location [" + newLocation + "] specified in a redirect was not valid. Trying workaround url [" + newLocationWorkaround + "]"); temp.getRequestHeader().setURI(newLocationWorkaround); } temp.getRequestHeader().setSecure(secure1); temp.getRequestHeader().setMethod(HttpRequestHeader.GET); temp.getRequestHeader().setContentLength(0); // since we send a GET, the body will be 0 long if (cookieBack1 != null) { // if the previous request sent back a cookie, we need to set that // cookie when following redirects, as a browser would if (this.debugEnabled) log.debug("Adding in cookie [" + cookieBack1 + "] for a redirect"); TreeSet<HtmlParameter> forwardCookieParams = temp.getRequestHeader().getCookieParams(); forwardCookieParams.add(cookieBack1); temp.getRequestHeader().setCookieParams(forwardCookieParams); } if (this.debugEnabled) log.debug("DEBUG: Cookie Message 1 causes us to follow redirect to [" + newLocation + "]"); sendAndReceive(temp, false, false); // do NOT redirect.. handle it here // handle any cookies set from following redirects that override the cookie // set in the redirect itself (if any) // note that this will handle the case where a latter cookie unsets one set // earlier. HtmlParameter cookieBack1Temp = getResponseCookie(temp, currentHtmlParameter.getName()); if (cookieBack1Temp != null) { cookieBack1 = cookieBack1Temp; cookieBack1TimeReceived = System.currentTimeMillis(); // in ms. record when we got the // cookie.. in case it has a // Max-Age directive } // reset the "final" version of message1 to use the final response in the // chain msg1Final = temp; } /////////////////////////// // if non-200 on the final response for message 1, no point in continuing. Bale // out. if (msg1Final.getResponseHeader().getStatusCode() != HttpStatusCode.OK) { if (this.debugEnabled) log.debug( "Got a non-200 response code [" + msg1Final.getResponseHeader().getStatusCode() + "] when sending [" + msg1Initial.getRequestHeader().getURI() + "] with param [" + currentHtmlParameter.getName() + "] = NULL (possibly somewhere in the redirects)"); continue; } // now check that the response set a cookie. if it didn't, then either.. // 1) we are messing with the wrong field // 2) the app doesn't do sessions // either way, there is not much point in continuing to look at this field.. if (cookieBack1 == null || cookieBack1.getValue() == null) { // no cookie was set, or the cookie param was set to a null value if (this.debugEnabled) log.debug("The Cookie parameter was NOT set in the response, when cookie param [" + currentHtmlParameter.getName() + "] was set to NULL: " + cookieBack1); continue; } ////////////////////////////////////////////////////////////////////// // at this point, before continuing to check for Session Fixation, do some other // checks on the session cookie we got back // that might cause us to raise additional alerts (in addition to doing the main // check for Session Fixation) ////////////////////////////////////////////////////////////////////// // Check 1: was the session cookie sent and received securely by the server? // If not, alert this fact if ((!msg1Final.getRequestHeader().isSecure()) || (!cookieBack1.getFlags().contains("secure"))) { // pass the original param value here, not the new value, since we're // displaying the session id exposed in the original message String extraInfo = Constant.messages.getString( "ascanbeta.sessionidsentinsecurely.alert.extrainfo", currentHtmlParameter.getType(), currentHtmlParameter.getName(), currentHtmlParameter.getValue()); if (!cookieBack1.getFlags().contains("secure")) { extraInfo += ("\n" + Constant.messages.getString( "ascanbeta.sessionidsentinsecurely.alert.extrainfo.secureflagnotset")); } // and figure out the risk, depending on whether it is a login page int risk = Alert.RISK_LOW; if (loginUrl) { extraInfo += ("\n" + Constant.messages .getString("ascanbeta.sessionidsentinsecurely.alert.extrainfo.loginpage")); // login page, so higher risk risk = Alert.RISK_MEDIUM; } else { // not a login page.. lower risk risk = Alert.RISK_LOW; } String attack = Constant.messages.getString( "ascanbeta.sessionidsentinsecurely.alert.attack", currentHtmlParameter.getType(), currentHtmlParameter.getName()); String vulnname = Constant.messages.getString("ascanbeta.sessionidsentinsecurely.name"); String vulndesc = Constant.messages.getString("ascanbeta.sessionidsentinsecurely.desc"); String vulnsoln = Constant.messages.getString("ascanbeta.sessionidsentinsecurely.soln"); // call bingo with some extra info, indicating that the alert is // not specific to Session Fixation, but has its own title and description // (etc) // the alert here is "Session id sent insecurely", or words to that effect. bingo(risk, Alert.CONFIDENCE_MEDIUM, vulnname, vulndesc, getBaseMsg().getRequestHeader().getURI().getURI(), currentHtmlParameter.getName(), attack, extraInfo, vulnsoln, getBaseMsg()); if (log.isDebugEnabled()) { String logMessage = MessageFormat.format( "A session identifier in {2} field: [{3}] may be sent " + "via an insecure mechanism at [{0}] URL [{1}]", getBaseMsg().getRequestHeader().getMethod(), getBaseMsg().getRequestHeader().getURI().getURI(), currentHtmlParameter.getType(), currentHtmlParameter.getName()); log.debug(logMessage); } // Note: do NOT continue to the next field at this point.. // since we still need to check for Session Fixation. } ////////////////////////////////////////////////////////////////////// // Check 2: is the session cookie that was set accessible to Javascript? // If so, alert this fact too if (!cookieBack1.getFlags().contains("httponly") && loginUrl) { // pass the original param value here, not the new value, since we're // displaying the session id exposed in the original message String extraInfo = Constant.messages.getString( "ascanbeta.sessionidaccessiblebyjavascript.alert.extrainfo", currentHtmlParameter.getType(), currentHtmlParameter.getName(), currentHtmlParameter.getValue()); String attack = Constant.messages.getString( "ascanbeta.sessionidaccessiblebyjavascript.alert.attack", currentHtmlParameter.getType(), currentHtmlParameter.getName()); String vulnname = Constant.messages .getString("ascanbeta.sessionidaccessiblebyjavascript.name"); String vulndesc = Constant.messages .getString("ascanbeta.sessionidaccessiblebyjavascript.desc"); String vulnsoln = Constant.messages .getString("ascanbeta.sessionidaccessiblebyjavascript.soln"); extraInfo += ("\n" + Constant.messages .getString("ascanbeta.sessionidaccessiblebyjavascript.alert.extrainfo.loginpage")); // call bingo with some extra info, indicating that the alert is // not specific to Session Fixation, but has its own title and description // (etc) // the alert here is "Session id accessible in Javascript", or words to that // effect. bingo(Alert.RISK_LOW, Alert.CONFIDENCE_MEDIUM, vulnname, vulndesc, getBaseMsg().getRequestHeader().getURI().getURI(), currentHtmlParameter.getName(), attack, extraInfo, vulnsoln, getBaseMsg()); if (log.isDebugEnabled()) { String logMessage = MessageFormat.format( "A session identifier in [{0}] URL [{1}] {2} field: " + "[{3}] may be accessible to JavaScript", getBaseMsg().getRequestHeader().getMethod(), getBaseMsg().getRequestHeader().getURI().getURI(), currentHtmlParameter.getType(), currentHtmlParameter.getName()); log.debug(logMessage); } // Note: do NOT continue to the next field at this point.. // since we still need to check for Session Fixation. } ////////////////////////////////////////////////////////////////////// // Check 3: is the session cookie set to expire soon? when the browser session // closes? never? // the longer the session cookie is valid, the greater the risk. alert it // accordingly String cookieBack1Expiry = null; int sessionExpiryRiskLevel; String sessionExpiryDescription = null; // check for the Expires header for (Iterator<String> i = cookieBack1.getFlags().iterator(); i.hasNext();) { String cookieBack1Flag = i.next(); // if ( this.debugEnabled ) log.debug("Cookie back 1 flag (checking for // Expires): "+ cookieBack1Flag); // match in a case insensitive manner. never know what case various web // servers are going to send back. // if (cookieBack1Flag.matches("(?i)expires=.*")) { if (cookieBack1Flag.toLowerCase(Locale.ENGLISH).startsWith("expires=")) { String[] cookieBack1FlagValues = cookieBack1Flag.split("="); if (cookieBack1FlagValues.length > 1) { if (this.debugEnabled) log.debug("Cookie Expiry: " + cookieBack1FlagValues[1]); cookieBack1Expiry = cookieBack1FlagValues[1]; // the Date String sessionExpiryDescription = cookieBack1FlagValues[1]; // the Date String cookieBack1ExpiryDate = DateUtil.parseDate(cookieBack1Expiry); // the actual Date } } } // also check for the Max-Age header, which overrides the Expires header. // WARNING: this Directive is reported to be ignored by IE, so if both Expires // and Max-Age are present // and we report based on the Max-Age value, but the user is using IE, then the // results reported // by us here may be different from those actually experienced by the user! (we // use Max-Age, IE uses Expires) for (Iterator<String> i = cookieBack1.getFlags().iterator(); i.hasNext();) { String cookieBack1Flag = i.next(); // if ( this.debugEnabled ) log.debug("Cookie back 1 flag (checking for // Max-Age): "+ cookieBack1Flag); // match in a case insensitive manner. never know what case various web // servers are going to send back. if (cookieBack1Flag.toLowerCase(Locale.ENGLISH).startsWith("max-age=")) { String[] cookieBack1FlagValues = cookieBack1Flag.split("="); if (cookieBack1FlagValues.length > 1) { // now the Max-Age value is the number of seconds relative to the // time the browser received the cookie // (as stored in cookieBack1TimeReceived) if (this.debugEnabled) log.debug("Cookie Max Age: " + cookieBack1FlagValues[1]); long cookie1DropDeadMS = cookieBack1TimeReceived + (Long.parseLong(cookieBack1FlagValues[1]) * 1000); cookieBack1ExpiryDate = new Date(cookie1DropDeadMS); // the actual Date the cookie // expires (by Max-Age) cookieBack1Expiry = DateUtil.formatDate(cookieBack1ExpiryDate, DateUtil.PATTERN_RFC1123); sessionExpiryDescription = cookieBack1Expiry; // needs to the Date String } } } String sessionExpiryRiskDescription = null; // check the Expiry/Max-Age details garnered (if any) // and figure out the risk, depending on whether it is a login page // and how long the session will live before expiring if (cookieBack1ExpiryDate == null) { // session expires when the browser closes.. rate this as medium risk? sessionExpiryRiskLevel = Alert.RISK_MEDIUM; sessionExpiryRiskDescription = "ascanbeta.sessionidexpiry.browserclose"; sessionExpiryDescription = Constant.messages.getString(sessionExpiryRiskDescription); } else { long datediffSeconds = (cookieBack1ExpiryDate.getTime() - cookieBack1TimeReceived) / 1000; long anHourSeconds = 3600; long aDaySeconds = anHourSeconds * 24; long aWeekSeconds = aDaySeconds * 7; if (datediffSeconds < 0) { if (this.debugEnabled) log.debug("The session cookie has expired already"); sessionExpiryRiskDescription = "ascanbeta.sessionidexpiry.timeexpired"; sessionExpiryRiskLevel = Alert.RISK_INFO; // no risk.. the cookie has expired already } else if (datediffSeconds > aWeekSeconds) { if (this.debugEnabled) log.debug("The session cookie is set to last for more than a week!"); sessionExpiryRiskDescription = "ascanbeta.sessionidexpiry.timemorethanoneweek"; sessionExpiryRiskLevel = Alert.RISK_HIGH; } else if (datediffSeconds > aDaySeconds) { if (this.debugEnabled) log.debug("The session cookie is set to last for more than a day"); sessionExpiryRiskDescription = "ascanbeta.sessionidexpiry.timemorethanoneday"; sessionExpiryRiskLevel = Alert.RISK_MEDIUM; } else if (datediffSeconds > anHourSeconds) { if (this.debugEnabled) log.debug("The session cookie is set to last for more than an hour"); sessionExpiryRiskDescription = "ascanbeta.sessionidexpiry.timemorethanonehour"; sessionExpiryRiskLevel = Alert.RISK_LOW; } else { if (this.debugEnabled) log.debug("The session cookie is set to last for less than an hour!"); sessionExpiryRiskDescription = "ascanbeta.sessionidexpiry.timelessthanonehour"; sessionExpiryRiskLevel = Alert.RISK_INFO; } } if (!loginUrl) { // decrement the risk if it's not a login page sessionExpiryRiskLevel--; } // alert it if the default session expiry risk level is more than informational if (sessionExpiryRiskLevel > Alert.RISK_INFO) { // pass the original param value here, not the new value String cookieReceivedTime = cookieBack1Expiry = DateUtil .formatDate(new Date(cookieBack1TimeReceived), DateUtil.PATTERN_RFC1123); String extraInfo = Constant.messages.getString("ascanbeta.sessionidexpiry.alert.extrainfo", currentHtmlParameter.getType(), currentHtmlParameter.getName(), currentHtmlParameter.getValue(), sessionExpiryDescription, cookieReceivedTime); String attack = Constant.messages.getString("ascanbeta.sessionidexpiry.alert.attack", currentHtmlParameter.getType(), currentHtmlParameter.getName()); String vulnname = Constant.messages.getString("ascanbeta.sessionidexpiry.name"); String vulndesc = Constant.messages.getString("ascanbeta.sessionidexpiry.desc"); String vulnsoln = Constant.messages.getString("ascanbeta.sessionidexpiry.soln"); if (loginUrl) { extraInfo += ("\n" + Constant.messages .getString("ascanbeta.sessionidexpiry.alert.extrainfo.loginpage")); } // call bingo with some extra info, indicating that the alert is // not specific to Session Fixation, but has its own title and description // (etc) // the alert here is "Session Id Expiry Time is excessive", or words to that // effect. bingo(sessionExpiryRiskLevel, Alert.CONFIDENCE_MEDIUM, vulnname, vulndesc, getBaseMsg().getRequestHeader().getURI().getURI(), currentHtmlParameter.getName(), attack, extraInfo, vulnsoln, getBaseMsg()); if (log.isDebugEnabled()) { String logMessage = MessageFormat.format( "A session identifier in [{0}] URL [{1}] {2} field: " + "[{3}] may be accessed until [{4}], unless the session is destroyed.", getBaseMsg().getRequestHeader().getMethod(), getBaseMsg().getRequestHeader().getURI().getURI(), currentHtmlParameter.getType(), currentHtmlParameter.getName(), sessionExpiryDescription); log.debug(logMessage); } // Note: do NOT continue to the next field at this point.. // since we still need to check for Session Fixation. } if (!loginUrl) { // not a login page.. skip continue; } //////////////////////////////////////////////////////////////////////////////////////////// /// Message 2 - processing starts here //////////////////////////////////////////////////////////////////////////////////////////// // so now that we know the URL responds with 200 (OK), and that it sets a // cookie, lets re-issue the original request, // but lets add in the new (valid) session cookie that was just issued. // we will re-send it. the aim is then to see if it accepts the cookie (BAD, in // some circumstances), // or if it issues a new session cookie (GOOD, in most circumstances) if (this.debugEnabled) log.debug("A Cookie was set by the URL for the correct param, when param [" + currentHtmlParameter.getName() + "] was set to NULL: " + cookieBack1); // use a copy of msg2Initial, since it has already had the correct cookie // removed in the request.. // do NOT use msg2Initial itself, as this will cause both requests in the GUI to // show the modified data.. // finally send the second message, and see how it comes back. HttpMessage msg2Initial = msg1Initial.cloneRequest(); TreeSet<HtmlParameter> cookieParams2Set = msg2Initial.getRequestHeader().getCookieParams(); cookieParams2Set.add(cookieBack1); msg2Initial.setCookieParams(cookieParams2Set); // resend the copy of the initial message, but with the valid session cookie // added in, to see if it is accepted // do not automatically follow redirects, as we need to check these for cookies // being set. sendAndReceive(msg2Initial, false, false); // create a copy of msg2Initial to play with to handle redirects (if any). // we use a copy because if we change msg2Initial itself, it messes the URL and // params displayed on the GUI. HttpMessage temp2 = msg2Initial; HttpMessage msg2Final = msg2Initial; HtmlParameter cookieBack2Previous = cookieBack1; HtmlParameter cookieBack2 = getResponseCookie(msg2Initial, currentHtmlParameter.getName()); int redirectsFollowed2 = 0; while (HttpStatusCode.isRedirection(temp2.getResponseHeader().getStatusCode())) { // clone the previous message boolean secure2 = temp2.getRequestHeader().isSecure(); temp2 = temp2.cloneAll(); redirectsFollowed2++; if (redirectsFollowed2 > 10) { throw new Exception("Too many redirects were specified in the second message"); } // create a new URI from the absolute location returned, and interpret it as // escaped // note that the standard says that the Location returned should be // absolute, but it ain't always so... URI newLocation = new URI(temp2.getResponseHeader().getHeader(HttpHeader.LOCATION), true); // and follow the forward url // need to clear the params (which would come from the initial POST, // otherwise) temp2.getRequestHeader().setGetParams(new TreeSet<HtmlParameter>()); temp2.setRequestBody(""); temp2.setResponseBody(""); // make sure no values accidentally carry from one iteration to // the next try { temp2.getRequestHeader().setURI(newLocation); } catch (Exception e) { // the Location field contents may not be standards compliant. Lets // generate a uri to use as a workaround where a relative path was // given instead of an absolute one URI newLocationWorkaround = new URI(temp2.getRequestHeader().getURI(), temp2.getResponseHeader().getHeader(HttpHeader.LOCATION), true); // try again, except this time, if it fails, don't try to handle it if (this.debugEnabled) log.debug("The Location [" + newLocation + "] specified in a redirect was not valid. Trying workaround url [" + newLocationWorkaround + "]"); temp2.getRequestHeader().setURI(newLocationWorkaround); } temp2.getRequestHeader().setSecure(secure2); temp2.getRequestHeader().setMethod(HttpRequestHeader.GET); temp2.getRequestHeader().setContentLength(0); // since we send a GET, the body will be 0 long if (cookieBack2 != null) { // if the previous request sent back a cookie, we need to set that // cookie when following redirects, as a browser would // also make sure to delete the previous value set for the cookie value if (this.debugEnabled) { log.debug("Deleting old cookie [" + cookieBack2Previous + "], and adding in cookie [" + cookieBack2 + "] for a redirect"); } TreeSet<HtmlParameter> forwardCookieParams = temp2.getRequestHeader().getCookieParams(); forwardCookieParams.remove(cookieBack2Previous); forwardCookieParams.add(cookieBack2); temp2.getRequestHeader().setCookieParams(forwardCookieParams); } sendAndReceive(temp2, false, false); // do NOT automatically redirect.. handle redirects here // handle any cookies set from following redirects that override the cookie // set in the redirect itself (if any) // note that this will handle the case where a latter cookie unsets one set // earlier. HtmlParameter cookieBack2Temp = getResponseCookie(temp2, currentHtmlParameter.getName()); if (cookieBack2Temp != null) { cookieBack2Previous = cookieBack2; cookieBack2 = cookieBack2Temp; } // reset the "final" version of message2 to use the final response in the // chain msg2Final = temp2; } if (this.debugEnabled) log.debug("Done following redirects"); // final result was non-200, no point in continuing. Bale out. if (msg2Final.getResponseHeader().getStatusCode() != HttpStatusCode.OK) { if (this.debugEnabled) log.debug( "Got a non-200 response code [" + msg2Final.getResponseHeader().getStatusCode() + "] when sending [" + msg2Initial.getRequestHeader().getURI() + "] with a borrowed cookie (or by following a redirect) for param [" + currentHtmlParameter.getName() + "]"); continue; // to next parameter } // and what we've been waiting for.. do we get a *different* cookie being set in // the response of message 2?? // or do we get a new cookie back at all? // No cookie back => the borrowed cookie was accepted. Not ideal // Cookie back, but same as the one we sent in => the borrowed cookie was // accepted. Not ideal if ((cookieBack2 == null) || cookieBack2.getValue().equals(cookieBack1.getValue())) { // no cookie back, when a borrowed cookie is in use.. suspicious! // use the cookie extrainfo message, which is specific to the case of // cookies // pretty much everything else is generic to all types of Session Fixation // vulnerabilities String extraInfo = Constant.messages.getString( "ascanbeta.sessionfixation.alert.cookie.extrainfo", currentHtmlParameter.getName(), cookieBack1.getValue(), (cookieBack2 == null ? "NULL" : cookieBack2.getValue())); String attack = Constant.messages.getString("ascanbeta.sessionfixation.alert.attack", currentHtmlParameter.getType(), currentHtmlParameter.getName()); if (loginUrl) { extraInfo += ("\n" + Constant.messages .getString("ascanbeta.sessionfixation.alert.cookie.extrainfo.loginpage")); } bingo(Alert.RISK_INFO, Alert.CONFIDENCE_MEDIUM, msg2Initial.getRequestHeader().getURI().getURI(), currentHtmlParameter.getName(), attack, extraInfo, msg2Initial); logSessionFixation(msg2Initial, currentHtmlParameter.getType().toString(), currentHtmlParameter.getName()); } continue; // jump to the next iteration of the loop (ie, the next parameter) } // end of the cookie code. // start of the url parameter code // note that this actually caters for // - actual URL parameters // - pseudo URL parameters, where the sessionid was in the path portion of the URL, // in conjunction with URL re-writing if (currentHtmlParameter.getType().equals(HtmlParameter.Type.url)) { boolean isPseudoUrlParameter = false; // is this "url parameter" actually a url parameter, or was it // path of the path (+url re-writing)? String possibleSessionIdIssuedForUrlParam = null; // remove the named url parameter from the request.. TreeSet<HtmlParameter> urlRequestParams = msg1Initial.getUrlParams(); // get parameters? if (!urlRequestParams.remove(currentHtmlParameter)) { isPseudoUrlParameter = true; // was not removed because it was a pseudo Url parameter, not a real url // parameter.. (so it would not be in the url params) // in this case, we will need to "rewrite" (ie hack) the URL path to remove // the pseudo url parameter portion // ie, we need to remove the ";jsessionid=<sessionid>" bit from the path // (assuming the current field is named 'jsessionid') // and replace it with ";jsessionid=" (ie, we nullify the possible "session" // parameter in the hope that a new session will be issued) // then we continue as usual to see if the URL is vulnerable to a Session // Fixation issue // Side note: quote the string to search for, and the replacement, so that // regex special characters are treated as literals String hackedUrl = requestUrl.replaceAll( Pattern.quote(";" + currentHtmlParameter.getName() + "=" + currentHtmlParameter.getValue()), Matcher.quoteReplacement(";" + currentHtmlParameter.getName() + "=")); if (this.debugEnabled) log.debug("Removing the pseudo URL parameter from [" + requestUrl + "]: [" + hackedUrl + "]"); // Note: the URL is not escaped. Handle it. msg1Initial.getRequestHeader().setURI(new URI(hackedUrl, false)); } msg1Initial.setGetParams(urlRequestParams); // url parameters // send the message, minus the value for the current parameter, and see how it // comes back. // Note: automatically follow redirects.. no need to look at any intermediate // responses. // this was only necessary for cookie-based session implementations sendAndReceive(msg1Initial); // if non-200 on the response for message 1, no point in continuing. Bale out. if (msg1Initial.getResponseHeader().getStatusCode() != HttpStatusCode.OK) { if (this.debugEnabled) log.debug("Got a non-200 response code [" + msg1Initial.getResponseHeader().getStatusCode() + "] when sending [" + msg1Initial.getRequestHeader().getURI() + "] with param [" + currentHtmlParameter.getName() + "] = NULL (possibly somewhere in the redirects)"); continue; } // now parse the HTML response for urls that contain the same parameter name, // and look at the values for that parameter // if no values are found for the parameter, then // 1) we are messing with the wrong field, or // 2) the app doesn't do sessions // either way, there is not much point in continuing to look at this field.. // parse out links in HTML (assume for a moment that all the URLs are in links) // this gives us a map of parameter value for the current parameter, to the // number of times it was encountered in links in the HTML SortedMap<String, Integer> parametersInHTMLURls = getParameterValueCountInHtml( msg1Initial.getResponseBody().toString(), currentHtmlParameter.getName(), isPseudoUrlParameter); if (this.debugEnabled) log.debug("The count of the various values of the [" + currentHtmlParameter.getName() + "] parameters in urls in the result of retrieving the url with a null value for parameter [" + currentHtmlParameter.getName() + "]: " + parametersInHTMLURls); if (parametersInHTMLURls.isEmpty()) { // setting the param to NULL did not cause any new values to be generated // for it in the output.. // so either.. // it is not a session field, or // it is a session field, but a session is only issued on authentication, // and this is not an authentication url // the app doesn't do sessions (etc) // either way, the parameter/url combo is not vulnerable, so continue with // the next parameter if (this.debugEnabled) log.debug("The URL parameter [" + currentHtmlParameter.getName() + "] was NOT set in any links in the response, when " + (isPseudoUrlParameter ? "pseudo/URL rewritten" : "") + " URL param [" + currentHtmlParameter.getName() + "] was set to NULL in the request, so it is likely not a session id field"); continue; // to the next parameter } else if (parametersInHTMLURls.size() == 1) { // the parameter was set to just one value in the output // so it's quite possible it is the session id field that we have been // looking for // caveat 1: check it is longer than 3 chars long, to remove false // positives.. // we assume here that a real session id will always be greater than 3 // characters long // caveat 2: the value we got back for the param must be different from the // value we // over-wrote with NULL (empty) in the first place, otherwise it is very // unlikely to // be a session id field possibleSessionIdIssuedForUrlParam = parametersInHTMLURls.firstKey(); // did we get back the same value we just nulled out in the original // request? // if so, use this to eliminate false positives, and to optimise. if (possibleSessionIdIssuedForUrlParam.equals(currentHtmlParameter.getValue())) { if (this.debugEnabled) log.debug((isPseudoUrlParameter ? "pseudo/URL rewritten" : "") + " URL param [" + currentHtmlParameter.getName() + "], when set to NULL, causes 1 distinct values to be set for it in URLs in the output, but the possible session id value [" + possibleSessionIdIssuedForUrlParam + "] is the same as the value we over-wrote with NULL. 'Sorry, kid. You got the gift, but it looks like you're waiting for something'"); continue; // to the next parameter } if (possibleSessionIdIssuedForUrlParam.length() > 3) { // raise an alert here on an exposed session id, even if it is not // subject to a session fixation vulnerability // log.info("The URL parameter ["+ currentHtmlParameter.getName() + "] // was set ["+ // parametersInHTMLURls.get(possibleSessionIdIssuedForUrlParam)+ "] // times to ["+ possibleSessionIdIssuedForUrlParam + "] in links in the // response, when "+ (isPseudoUrlParameter?"pseudo/URL rewritten":"")+ " // URL param ["+ currentHtmlParameter.getName() + "] was set to NULL in // the request. This likely indicates it is a session id field."); // pass the original param value here, not the new value, since we're // displaying the session id exposed in the original message String extraInfo = Constant.messages.getString( "ascanbeta.sessionidexposedinurl.alert.extrainfo", currentHtmlParameter.getType(), currentHtmlParameter.getName(), currentHtmlParameter.getValue()); String attack = Constant.messages .getString("ascanbeta.sessionidexposedinurl.alert.attack", (isPseudoUrlParameter ? "pseudo/URL rewritten " : "") + currentHtmlParameter.getType(), currentHtmlParameter.getName()); String vulnname = Constant.messages.getString("ascanbeta.sessionidexposedinurl.name"); String vulndesc = Constant.messages.getString("ascanbeta.sessionidexposedinurl.desc"); String vulnsoln = Constant.messages.getString("ascanbeta.sessionidexposedinurl.soln"); if (loginUrl) { extraInfo += ("\n" + Constant.messages .getString("ascanbeta.sessionidexposedinurl.alert.extrainfo.loginpage")); } // call bingo with some extra info, indicating that the alert is // not specific to Session Fixation, but has its own title and // description (etc) // the alert here is "Session id exposed in url", or words to that // effect. bingo(Alert.RISK_MEDIUM, Alert.CONFIDENCE_MEDIUM, vulnname, vulndesc, getBaseMsg().getRequestHeader().getURI().getURI(), currentHtmlParameter.getName(), attack, extraInfo, vulnsoln, getBaseMsg()); if (log.isDebugEnabled()) { String logMessage = MessageFormat.format( "An exposed session identifier has been found at " + "[{0}] URL [{1}] on {2} field: [{3}]", getBaseMsg().getRequestHeader().getMethod(), getBaseMsg().getRequestHeader().getURI().getURI(), (isPseudoUrlParameter ? "pseudo " : "") + currentHtmlParameter.getType(), currentHtmlParameter.getName()); log.debug(logMessage); } // Note: do NOT continue to the next field at this point.. // since we still need to check for Session Fixation. } else { if (this.debugEnabled) log.debug((isPseudoUrlParameter ? "pseudo/URL rewritten" : "") + " URL param [" + currentHtmlParameter.getName() + "], when set to NULL, causes 1 distinct values to be set for it in URLs in the output, but the possible session id value [" + possibleSessionIdIssuedForUrlParam + "] is too short to be a real session id."); continue; // to the next parameter } } else { // strange scenario: setting the param to null causes multiple different // values to be set for it in the output // it could still be a session parameter, but we assume it is *not* a // session id field // log it, but assume it is not a session id if (this.debugEnabled) log.debug((isPseudoUrlParameter ? "pseudo/URL rewritten" : "") + " URL param [" + currentHtmlParameter.getName() + "], when set to NULL, causes [" + parametersInHTMLURls.size() + "] distinct values to be set for it in URLs in the output. Assuming it is NOT a session id as a consequence. This could be a false negative"); continue; // to the next parameter } //////////////////////////////////////////////////////////////////////////////////////////// /// Message 2 - processing starts here //////////////////////////////////////////////////////////////////////////////////////////// // we now have a plausible session id field to play with, so set it to a // borrowed value. // ie: lets re-send the request, but add in the new (valid) session value that // was just issued. // the aim is then to see if it accepts the session without re-issuing the // session id (BAD, in some circumstances), // or if it issues a new session value (GOOD, in most circumstances) // and set the (modified) session for the second message // use a copy of msg2Initial, since it has already had the correct session // removed in the request.. // do NOT use msg2Initial itself, as this will cause both requests in the GUI to // show the modified data.. // finally send the second message, and see how it comes back. HttpMessage msg2Initial = msg1Initial.cloneRequest(); // set the parameter to the new session id value (in different manners, // depending on whether it is a real url param, or a pseudo url param) if (isPseudoUrlParameter) { // we need to "rewrite" (hack) the URL path to remove the pseudo url // parameter portion // id, we need to remove the ";jsessionid=<sessionid>" bit from the path // and replace it with ";jsessionid=" (ie, we nullify the possible "session" // parameter in the hope that a new session will be issued) // then we continue as usual to see if the URL is vulnerable to a Session // Fixation issue // Side note: quote the string to search for, and the replacement, so that // regex special characters are treated as literals String hackedUrl = requestUrl.replaceAll( Pattern.quote(";" + currentHtmlParameter.getName() + "=" + currentHtmlParameter.getValue()), Matcher.quoteReplacement(";" + currentHtmlParameter.getName() + "=" + possibleSessionIdIssuedForUrlParam)); if (this.debugEnabled) log.debug("Changing the pseudo URL parameter from [" + requestUrl + "]: [" + hackedUrl + "]"); // Note: the URL is not escaped msg2Initial.getRequestHeader().setURI(new URI(hackedUrl, false)); msg2Initial.setGetParams(msg1Initial.getUrlParams()); // restore the GET params } else { // do it via the normal url parameters TreeSet<HtmlParameter> urlRequestParams2 = msg2Initial.getUrlParams(); urlRequestParams2.add(new HtmlParameter(Type.url, currentHtmlParameter.getName(), possibleSessionIdIssuedForUrlParam)); msg2Initial.setGetParams(urlRequestParams2); // restore the GET params } // resend a copy of the initial message, but with the new valid session // parameter added in, to see if it is accepted // automatically follow redirects, which are irrelevant for the purposes of // testing URL parameters sendAndReceive(msg2Initial); // final result was non-200, no point in continuing. Bale out. if (msg2Initial.getResponseHeader().getStatusCode() != HttpStatusCode.OK) { if (this.debugEnabled) log.debug("Got a non-200 response code [" + msg2Initial.getResponseHeader().getStatusCode() + "] when sending [" + msg2Initial.getRequestHeader().getURI() + "] with a borrowed session (or by following a redirect) for param [" + currentHtmlParameter.getName() + "]"); continue; // next field! } // do the analysis on the parameters in link urls in the HTML output again to // see if the session id was regenerated SortedMap<String, Integer> parametersInHTMLURls2 = getParameterValueCountInHtml( msg2Initial.getResponseBody().toString(), currentHtmlParameter.getName(), isPseudoUrlParameter); if (this.debugEnabled) log.debug("The count of the various values of the [" + currentHtmlParameter.getName() + "] parameters in urls in the result of retrieving the url with a borrowed session value for parameter [" + currentHtmlParameter.getName() + "]: " + parametersInHTMLURls2); if (parametersInHTMLURls2.size() != 1) { // either no values, or multiple values, but not 1 value. For a session // that was regenerated, we would have expected to see // just 1 new value if (this.debugEnabled) log.debug("The HTML has spoken. [" + currentHtmlParameter.getName() + "] doesn't look like a session id field, because there are " + parametersInHTMLURls2.size() + " distinct values for this parameter in urls in the HTML output"); continue; } // there is but one value for this param in links in the HTML output. But is it // vulnerable to Session Fixation? Ie, is it the same parameter? String possibleSessionIdIssuedForUrlParam2 = parametersInHTMLURls2.firstKey(); if (possibleSessionIdIssuedForUrlParam2.equals(possibleSessionIdIssuedForUrlParam)) { // same sessionid used in the output.. so it is likely that we have a // SessionFixation issue.. // use the url param extrainfo message, which is specific to the case of url // parameters and url re-writing Session Fixation issue // pretty much everything else is generic to all types of Session Fixation // vulnerabilities String extraInfo = Constant.messages.getString( "ascanbeta.sessionfixation.alert.url.extrainfo", currentHtmlParameter.getName(), possibleSessionIdIssuedForUrlParam, possibleSessionIdIssuedForUrlParam2); String attack = Constant.messages.getString("ascanbeta.sessionfixation.alert.attack", (isPseudoUrlParameter ? "pseudo/URL rewritten " : "") + currentHtmlParameter.getType(), currentHtmlParameter.getName()); int risk = Alert.RISK_LOW; if (loginUrl) { extraInfo += ("\n" + Constant.messages .getString("ascanbeta.sessionfixation.alert.url.extrainfo.loginpage")); // login page, so higher risk risk = Alert.RISK_MEDIUM; } else { // not a login page.. lower risk risk = Alert.RISK_LOW; } bingo(risk, Alert.CONFIDENCE_MEDIUM, getBaseMsg().getRequestHeader().getURI().getURI(), currentHtmlParameter.getName(), attack, extraInfo, getBaseMsg()); logSessionFixation(getBaseMsg(), (isPseudoUrlParameter ? "pseudo " : "") + currentHtmlParameter.getType(), currentHtmlParameter.getName()); continue; // jump to the next iteration of the loop (ie, the next parameter) } else { // different sessionid used in the output.. so it is unlikely that we have a // SessionFixation issue.. // more likely that the Session is being re-issued for every single request, // or we have issues a login request, which // normally causes a session to be reissued if (this.debugEnabled) log.debug("The " + (isPseudoUrlParameter ? "pseudo/URL rewritten" : "") + " parameter [" + currentHtmlParameter.getName() + "] in url [" + getBaseMsg().getRequestHeader().getMethod() + "] [" + getBaseMsg().getRequestHeader().getURI() + "] changes with requests, and so it likely not vulnerable to Session Fixation"); } continue; // onto the next parameter } // end of the url parameter code. } // end of the for loop around the parameter list } catch (Exception e) { // Do not try to internationalise this.. we need an error message in any event.. // if it's in English, it's still better than not having it at all. log.error("An error occurred checking a url for Session Fixation issues", e); } }
From source file:org.zaproxy.zap.extension.ascanrulesBeta.UsernameEnumeration.java
/** * looks for username enumeration in the login page, by changing the username field to be a * valid / invalid user, and looking for differences in the response */// w w w.j ava2 s. com @Override public void scan() { // the technique to determine if usernames can be enumerated is as follows, using a variant // of the Freiling+Schinzel method, // adapted to the case where we do not know which is the username field // // 1) Request the original URL n times. (The original URL is assumed to have a valid // username, if not a valid password). Store the results in A[]. // 2) Compute the longest common subsequence (LCS) of A[] into LCS_A // 3) for each parameter in the original URL (ie, for URL params, form params, and cookie // params) // 4) Change the current parameter (which we assume is the username parameter) to an invalid // username (randomly), and request the URL n times. Store the results in B[]. // 5) Compute the longest common subsequence (LCS) of B[] into LCS_B // 6) If LCS_A <> LCS_B, then there is a Username Enumeration issue on the current parameter try { boolean loginUrl = false; // Are we dealing with a login url in any of the contexts of which this uri is part URI requestUri = getBaseMsg().getRequestHeader().getURI(); // using the session, get the list of contexts for the url List<Context> contextList = extAuth.getModel().getSession().getContextsForUrl(requestUri.getURI()); // now loop, and see if the url is a login url in each of the contexts in turn... for (Context context : contextList) { URI loginUri = extAuth.getLoginRequestURIForContext(context); if (loginUri != null) { if (requestUri.getScheme().equals(loginUri.getScheme()) && requestUri.getHost().equals(loginUri.getHost()) && requestUri.getPort() == loginUri.getPort() && requestUri.getPath().equals(loginUri.getPath())) { // we got this far.. only the method (GET/POST), user details, query params, // fragment, and POST params // are possibly different from the login page. loginUrl = true; log.info(requestUri.toString() + " falls within a context, and is the defined Login URL. Scanning for possible Username Enumeration vulnerability."); break; // Stop checking } } } // the Username Enumeration scanner will only run for logon pages if (loginUrl == false) { if (this.debugEnabled) { log.debug(requestUri.toString() + " is not a defined Login URL."); } return; // No need to continue for this URL } // find all params set in the request (GET/POST/Cookie) TreeSet<HtmlParameter> htmlParams = new TreeSet<>(); htmlParams.addAll(getBaseMsg().getRequestHeader().getCookieParams()); // request cookies only. no response cookies htmlParams.addAll(getBaseMsg().getFormParams()); // add in the POST params htmlParams.addAll(getBaseMsg().getUrlParams()); // add in the GET params int numberOfRequests = 0; if (this.getAttackStrength() == AttackStrength.INSANE) { numberOfRequests = 50; } else if (this.getAttackStrength() == AttackStrength.HIGH) { numberOfRequests = 15; } else if (this.getAttackStrength() == AttackStrength.MEDIUM) { numberOfRequests = 5; } else if (this.getAttackStrength() == AttackStrength.LOW) { numberOfRequests = 3; } // 1) Request the original URL n times. (The original URL is assumed to have a valid // username, if not a valid password). Store the results in A[]. // make sure to manually handle all redirects, and cookies that may be set in response. // allocate enough space for the responses StringBuilder responseA = null; StringBuilder responseB = null; String longestCommonSubstringA = null; String longestCommonSubstringB = null; for (int i = 0; i < numberOfRequests; i++) { // initialise the storage for this iteration // baseResponses[i]= new StringBuilder(250); responseA = new StringBuilder(250); HttpMessage msgCpy = getNewMsg(); // clone the request, but not the response sendAndReceive(msgCpy, false, false); // request the URL, but do not automatically follow redirects. // get all cookies set in the response TreeSet<HtmlParameter> cookies = msgCpy.getResponseHeader().getCookieParams(); int redirectCount = 0; while (HttpStatusCode.isRedirection(msgCpy.getResponseHeader().getStatusCode())) { redirectCount++; if (this.debugEnabled) log.debug("Following redirect " + redirectCount + " for message " + i + " of " + numberOfRequests + " iterations of the original query"); // append the response to the responses so far for this particular instance // this will give us a complete picture of the full set of actual traffic // associated with following redirects for the request responseA.append(msgCpy.getResponseHeader().getHeadersAsString()); responseA.append(msgCpy.getResponseBody().toString()); // and manually follow the redirect // create a new message from scratch HttpMessage msgRedirect = new HttpMessage(); // create a new URI from the absolute location returned, and interpret it as // escaped // note that the standard says that the Location returned should be absolute, // but it ain't always so... URI newLocation = new URI(msgCpy.getResponseHeader().getHeader(HttpHeader.LOCATION), true); try { msgRedirect.getRequestHeader().setURI(newLocation); } catch (Exception e) { // the Location field contents may not be standards compliant. Lets generate // a uri to use as a workaround where a relative path was // given instead of an absolute one URI newLocationWorkaround = new URI(msgCpy.getRequestHeader().getURI(), msgCpy.getResponseHeader().getHeader(HttpHeader.LOCATION), true); // try again, except this time, if it fails, don't try to handle it if (this.debugEnabled) log.debug("The Location [" + newLocation + "] specified in a redirect was not valid (not absolute?). Trying absolute workaround url [" + newLocationWorkaround + "]"); msgRedirect.getRequestHeader().setURI(newLocationWorkaround); } msgRedirect.getRequestHeader().setMethod(HttpRequestHeader.GET); // it's always a GET for a redirect msgRedirect.getRequestHeader().setContentLength(0); // since we send a GET, the body will be 0 long if (cookies.size() > 0) { // if a previous request sent back a cookie that has not since been // invalidated, we need to set that cookie when following redirects, as a // browser would msgRedirect.getRequestHeader().setCookieParams(cookies); } if (this.debugEnabled) log.debug("DEBUG: Following redirect to [" + newLocation + "]"); sendAndReceive(msgRedirect, false, false); // do NOT redirect.. handle it here // handle scenario where a cookie is unset in a subsequent iteration, or where // the same cookie name is later re-assigned a different value // ie, in these cases, do not simply (and dumbly) accumulate cookie detritus. // first get all cookies set in the response TreeSet<HtmlParameter> cookiesTemp = msgRedirect.getResponseHeader().getCookieParams(); for (Iterator<HtmlParameter> redirectSetsCookieIterator = cookiesTemp .iterator(); redirectSetsCookieIterator.hasNext();) { HtmlParameter cookieJustSet = redirectSetsCookieIterator.next(); // loop through each of the cookies we know about in cookies, to see if it // matches by name. // if so, delete that cookie, and add the one that was just set to cookies. // if not, add the one that was just set to cookies. for (Iterator<HtmlParameter> knownCookiesIterator = cookies.iterator(); knownCookiesIterator .hasNext();) { HtmlParameter knownCookie = knownCookiesIterator.next(); if (cookieJustSet.getName().equals(knownCookie.getName())) { knownCookiesIterator.remove(); break; // out of the loop for known cookies, back to the next cookie // set in the response } } // end of loop for cookies we already know about // we can now safely add the cookie that was just set into cookies, knowing // it does not clash with anything else in there. cookies.add(cookieJustSet); } // end of for loop for cookies just set in the redirect msgCpy = msgRedirect; // store the last redirect message into the MsgCpy, as we // will be using it's output in a moment.. } // end of loop to follow redirects // now that the redirections have all been handled.. was the request finally a // success or not? Successful or Failed Logins would normally both return an OK // HTTP status if (!HttpStatusCode.isSuccess(msgCpy.getResponseHeader().getStatusCode())) { log.warn("The original URL [" + getBaseMsg().getRequestHeader().getURI() + "] returned a non-OK HTTP status " + msgCpy.getResponseHeader().getStatusCode() + " (after " + i + " of " + numberOfRequests + " steps). Could be indicative of SQL Injection, or some other error. The URL is not stable enough to look at Username Enumeration"); return; // we have not even got as far as looking at the parameters, so just // abort straight out of the method } if (this.debugEnabled) log.debug("Done following redirects!"); // append the response to the responses so far for this particular instance // this will give us a complete picture of the full set of actual traffic associated // with following redirects for the request responseA.append(msgCpy.getResponseHeader().getHeadersAsString()); responseA.append(msgCpy.getResponseBody().toString()); // 2) Compute the longest common subsequence (LCS) of A[] into LCS_A // Note: in the Freiling and Schinzel method, this is calculated recursively. We // calculate it iteratively, but using an equivalent method // first time in, the LCS is simple: it's the first HTML result.. no diffing // required if (i == 0) longestCommonSubstringA = responseA.toString(); // else get the LCS of the existing string, and the current result else longestCommonSubstringA = this.longestCommonSubsequence(longestCommonSubstringA, responseA.toString()); // optimisation step: if the LCS of A is 0 characters long already, then the URL // output is not stable, and we can abort now, and save some time if (longestCommonSubstringA.length() == 0) { // this might occur if the output returned for the URL changed mid-way. Perhaps // a CAPTCHA has fired, or a WAF has kicked in. Let's abort now so. log.warn("The original URL [" + getBaseMsg().getRequestHeader().getURI() + "] does not produce stable output (at " + i + 1 + " of " + numberOfRequests + " steps). There is no static element in the output that can be used as a basis of comparison for the result of requesting URLs with the parameter values modified. Perhaps a CAPTCHA or WAF has kicked in!!"); return; // we have not even got as far as looking at the parameters, so just // abort straight out of the method } } // get rid of any remnants of cookie setting and Date headers in the responses, as these // cause false positives, and can be safely ignored // replace the content length with a non-variable placeholder // replace url parameters with a non-variable placeholder to eliminate tokens in URLs in // the output longestCommonSubstringA = longestCommonSubstringA.replaceAll("Set-Cookie:[^\\r\\n]+[\\r\\n]{1,2}", ""); longestCommonSubstringA = longestCommonSubstringA.replaceAll("Date:[^\\r\\n]+[\\r\\n]{1,2}", ""); longestCommonSubstringA = longestCommonSubstringA.replaceAll("Content-Length:[^\\r\\n]+[\\r\\n]{1,2}", "Content-Length: XXXX\n"); longestCommonSubstringA = longestCommonSubstringA .replaceAll("(?<=(&|\\?)[^\\?\"=&;]+=)[^\\?\"=&;]+(?=(&|\"))", "YYYY"); if (this.debugEnabled) log.debug("The LCS of A is [" + longestCommonSubstringA + "]"); // 3) for each parameter in the original URL (ie, for URL params, form params, and // cookie params) for (Iterator<HtmlParameter> iter = htmlParams.iterator(); iter.hasNext();) { HttpMessage msgModifiedParam = getNewMsg(); HtmlParameter currentHtmlParameter = iter.next(); if (this.debugEnabled) log.debug("Handling [" + currentHtmlParameter.getType() + "] parameter [" + currentHtmlParameter.getName() + "], with value [" + currentHtmlParameter.getValue() + "]"); // 4) Change the current parameter value (which we assume is the username parameter) // to an invalid username (randomly), and request the URL n times. Store the results // in B[]. // get a random user name the same length as the original! String invalidUsername = RandomStringUtils.random(currentHtmlParameter.getValue().length(), RANDOM_USERNAME_CHARS); if (this.debugEnabled) log.debug("The invalid username chosen was [" + invalidUsername + "]"); TreeSet<HtmlParameter> requestParams = null; if (currentHtmlParameter.getType().equals(HtmlParameter.Type.cookie)) { requestParams = msgModifiedParam.getRequestHeader().getCookieParams(); requestParams.remove(currentHtmlParameter); requestParams.add(new HtmlParameter(currentHtmlParameter.getType(), currentHtmlParameter.getName(), invalidUsername.toString())); // add in the invalid username msgModifiedParam.setCookieParams(requestParams); } else if (currentHtmlParameter.getType().equals(HtmlParameter.Type.url)) { requestParams = msgModifiedParam.getUrlParams(); requestParams.remove(currentHtmlParameter); requestParams.add(new HtmlParameter(currentHtmlParameter.getType(), currentHtmlParameter.getName(), invalidUsername.toString())); // add in the invalid username msgModifiedParam.setGetParams(requestParams); } else if (currentHtmlParameter.getType().equals(HtmlParameter.Type.form)) { requestParams = msgModifiedParam.getFormParams(); requestParams.remove(currentHtmlParameter); requestParams.add(new HtmlParameter(currentHtmlParameter.getType(), currentHtmlParameter.getName(), invalidUsername.toString())); // add in the invalid username msgModifiedParam.setFormParams(requestParams); } if (this.debugEnabled) log.debug("About to loop for " + numberOfRequests + " iterations with an incorrect user of the same length"); boolean continueForParameter = true; for (int i = 0; i < numberOfRequests && continueForParameter; i++) { // initialise the storage for this iteration responseB = new StringBuilder(250); HttpMessage msgCpy = msgModifiedParam; // use the message we already set up, with the // modified parameter value sendAndReceive(msgCpy, false, false); // request the URL, but do not automatically follow redirects. // get all cookies set in the response TreeSet<HtmlParameter> cookies = msgCpy.getResponseHeader().getCookieParams(); int redirectCount = 0; while (HttpStatusCode.isRedirection(msgCpy.getResponseHeader().getStatusCode())) { redirectCount++; if (this.debugEnabled) log.debug("Following redirect " + redirectCount + " for message " + i + " of " + numberOfRequests + " iterations of the modified query"); // append the response to the responses so far for this particular instance // this will give us a complete picture of the full set of actual traffic // associated with following redirects for the request responseB.append(msgCpy.getResponseHeader().getHeadersAsString()); responseB.append(msgCpy.getResponseBody().toString()); // and manually follow the redirect // create a new message from scratch HttpMessage msgRedirect = new HttpMessage(); // create a new URI from the absolute location returned, and interpret it as // escaped // note that the standard says that the Location returned should be // absolute, but it ain't always so... URI newLocation = new URI(msgCpy.getResponseHeader().getHeader(HttpHeader.LOCATION), true); try { msgRedirect.getRequestHeader().setURI(newLocation); } catch (Exception e) { // the Location field contents may not be standards compliant. Lets // generate a uri to use as a workaround where a relative path was // given instead of an absolute one URI newLocationWorkaround = new URI(msgCpy.getRequestHeader().getURI(), msgCpy.getResponseHeader().getHeader(HttpHeader.LOCATION), true); // try again, except this time, if it fails, don't try to handle it if (this.debugEnabled) log.debug("The Location [" + newLocation + "] specified in a redirect was not valid (not absolute?). Trying absolute workaround url [" + newLocationWorkaround + "]"); msgRedirect.getRequestHeader().setURI(newLocationWorkaround); } msgRedirect.getRequestHeader().setMethod(HttpRequestHeader.GET); // it's always a GET for a redirect msgRedirect.getRequestHeader().setContentLength(0); // since we send a GET, the body will be 0 long if (cookies.size() > 0) { // if a previous request sent back a cookie that has not since been // invalidated, we need to set that cookie when following redirects, as // a browser would msgRedirect.getRequestHeader().setCookieParams(cookies); } sendAndReceive(msgRedirect, false, false); // do NOT redirect.. handle it here // handle scenario where a cookie is unset in a subsequent iteration, or // where the same cookie name is later re-assigned a different value // ie, in these cases, do not simply (and dumbly) accumulate cookie // detritus. // first get all cookies set in the response TreeSet<HtmlParameter> cookiesTemp = msgRedirect.getResponseHeader().getCookieParams(); for (Iterator<HtmlParameter> redirectSetsCookieIterator = cookiesTemp .iterator(); redirectSetsCookieIterator.hasNext();) { HtmlParameter cookieJustSet = redirectSetsCookieIterator.next(); // loop through each of the cookies we know about in cookies, to see if // it matches by name. // if so, delete that cookie, and add the one that was just set to // cookies. // if not, add the one that was just set to cookies. for (Iterator<HtmlParameter> knownCookiesIterator = cookies .iterator(); knownCookiesIterator.hasNext();) { HtmlParameter knownCookie = knownCookiesIterator.next(); if (cookieJustSet.getName().equals(knownCookie.getName())) { knownCookiesIterator.remove(); break; // out of the loop for known cookies, back to the next // cookie set in the response } } // end of loop for cookies we already know about // we can now safely add the cookie that was just set into cookies, // knowing it does not clash with anything else in there. cookies.add(cookieJustSet); } // end of for loop for cookies just set in the redirect msgCpy = msgRedirect; // store the last redirect message into the MsgCpy, as // we will be using it's output in a moment.. } // end of loop to follow redirects // now that the redirections have all been handled.. was the request finally a // success or not? Successful or Failed Logins would normally both return an OK // HTTP status if (!HttpStatusCode.isSuccess(msgCpy.getResponseHeader().getStatusCode())) { log.warn("The modified URL [" + msgModifiedParam.getRequestHeader().getURI() + "] returned a non-OK HTTP status " + msgCpy.getResponseHeader().getStatusCode() + " (after " + i + 1 + " of " + numberOfRequests + " steps for [" + currentHtmlParameter.getType() + "] parameter " + currentHtmlParameter.getName() + "). Could be indicative of SQL Injection, or some other error. The URL is not stable enough to look at Username Enumeration"); continueForParameter = false; continue; // skip directly to the next parameter. Do not pass Go. Do not // collect $200. } if (this.debugEnabled) log.debug("Done following redirects!"); // append the response to the responses so far for this particular instance // this will give us a complete picture of the full set of actual traffic // associated with following redirects for the request responseB.append(msgCpy.getResponseHeader().getHeadersAsString()); responseB.append(msgCpy.getResponseBody().toString()); // 5) Compute the longest common subsequence (LCS) of B[] into LCS_B // Note: in the Freiling and Schinzel method, this is calculated recursively. We // calculate it iteratively, but using an equivalent method // first time in, the LCS is simple: it's the first HTML result.. no diffing // required if (i == 0) longestCommonSubstringB = responseB.toString(); // else get the LCS of the existing string, and the current result else longestCommonSubstringB = this.longestCommonSubsequence(longestCommonSubstringB, responseB.toString()); // optimisation step: if the LCS of B is 0 characters long already, then the URL // output is not stable, and we can abort now, and save some time if (longestCommonSubstringB.length() == 0) { // this might occur if the output returned for the URL changed mid-way. // Perhaps a CAPTCHA has fired, or a WAF has kicked in. Let's abort now so. log.warn("The modified URL [" + msgModifiedParam.getRequestHeader().getURI() + "] (for [" + currentHtmlParameter.getType() + "] parameter " + currentHtmlParameter.getName() + ") does not produce stable output (after " + i + 1 + " of " + numberOfRequests + " steps). There is no static element in the output that can be used as a basis of comparison with the static output of the original query. Perhaps a CAPTCHA or WAF has kicked in!!"); continueForParameter = false; continue; // skip directly to the next parameter. Do not pass Go. Do not // collect $200. // Note: if a CAPTCHA or WAF really has fired, the results of subsequent // iterations will likely not be accurate.. } } // if we didn't hit something with one of the iterations for the parameter (ie, if // the output when changing the parm is stable), // check if the parameter might be vulnerable by comparins its LCS with the original // LCS for a valid login if (continueForParameter == true) { // get rid of any remnants of cookie setting and Date headers in the responses, // as these cause false positives, and can be safely ignored // replace the content length with a non-variable placeholder // replace url parameters with a non-variable placeholder to eliminate tokens in // URLs in the output longestCommonSubstringB = longestCommonSubstringB .replaceAll("Set-Cookie:[^\\r\\n]+[\\r\\n]{1,2}", ""); longestCommonSubstringB = longestCommonSubstringB.replaceAll("Date:[^\\r\\n]+[\\r\\n]{1,2}", ""); longestCommonSubstringB = longestCommonSubstringB .replaceAll("Content-Length:[^\\r\\n]+[\\r\\n]{1,2}", "Content-Length: XXXX\n"); longestCommonSubstringB = longestCommonSubstringB .replaceAll("(?<=(&|\\?)[^\\?\"=&;]+=)[^\\?\"=&;]+(?=(&|\"))", "YYYY"); if (this.debugEnabled) log.debug("The LCS of B is [" + longestCommonSubstringB + "]"); // 6) If LCS_A <> LCS_B, then there is a Username Enumeration issue on the // current parameter if (!longestCommonSubstringA.equals(longestCommonSubstringB)) { // calculate line level diffs of the 2 Longest Common Substrings to aid the // user in deciding if the match is a false positive // get the diff as a series of patches Patch diffpatch = DiffUtils.diff( new LinkedList<String>(Arrays.asList(longestCommonSubstringA.split("\\n"))), new LinkedList<String>(Arrays.asList(longestCommonSubstringB.split("\\n")))); int numberofDifferences = diffpatch.getDeltas().size(); // and convert the list of patches to a String, joining using a newline // String diffAB = StringUtils.join(diffpatch.getDeltas(), "\n"); StringBuilder tempDiff = new StringBuilder(250); for (Delta delta : diffpatch.getDeltas()) { String changeType = null; if (delta.getType() == Delta.TYPE.CHANGE) changeType = "Changed Text"; else if (delta.getType() == Delta.TYPE.DELETE) changeType = "Deleted Text"; else if (delta.getType() == Delta.TYPE.INSERT) changeType = "Inserted text"; else changeType = "Unknown change type [" + delta.getType() + "]"; tempDiff.append("\n(" + changeType + ")\n"); // blank line before tempDiff.append("Output for Valid Username : " + delta.getOriginal() + "\n"); // no blank lines tempDiff.append("\nOutput for Invalid Username: " + delta.getRevised() + "\n"); // blank line before } String diffAB = tempDiff.toString(); String extraInfo = Constant.messages.getString( "ascanbeta.usernameenumeration.alert.extrainfo", currentHtmlParameter.getType(), currentHtmlParameter.getName(), currentHtmlParameter.getValue(), // original value invalidUsername.toString(), // new value diffAB, // the differences between the two sets of output numberofDifferences); // the number of differences String attack = Constant.messages.getString("ascanbeta.usernameenumeration.alert.attack", currentHtmlParameter.getType(), currentHtmlParameter.getName()); String vulnname = Constant.messages.getString("ascanbeta.usernameenumeration.name"); String vulndesc = Constant.messages.getString("ascanbeta.usernameenumeration.desc"); String vulnsoln = Constant.messages.getString("ascanbeta.usernameenumeration.soln"); // call bingo with some extra info, indicating that the alert is bingo(Alert.RISK_INFO, Alert.CONFIDENCE_LOW, vulnname, vulndesc, getBaseMsg().getRequestHeader().getURI().getURI(), currentHtmlParameter.getName(), attack, extraInfo, vulnsoln, getBaseMsg()); } else { if (this.debugEnabled) log.debug("[" + currentHtmlParameter.getType() + "] parameter [" + currentHtmlParameter.getName() + "] looks ok (Invalid Usernames cannot be distinguished from Valid usernames)"); } } } // end of the for loop around the parameter list } catch (Exception e) { // Do not try to internationalise this.. we need an error message in any event.. // if it's in English, it's still better than not having it at all. log.error("An error occurred checking a url for Username Enumeration issues", e); } }
From source file:org.zaproxy.zap.extension.bruteforce.ScanTarget.java
public ScanTarget(URI uri) { this.uri = copyURI(uri); this.scheme = uri.getScheme(); try {/*from w w w . j a v a 2 s . c o m*/ this.host = uri.getHost(); } catch (URIException e) { throw new IllegalArgumentException("Failed to get host from URI: " + e.getMessage(), e); } this.port = getPort(scheme, uri.getPort()); try { this.uri.setPath(null); this.uri.setQuery(null); this.uri.setFragment(null); } catch (URIException ignore) { // It's safe to set the URI query, path and fragment components to null. } this.stringRepresentation = createHostPortString(host, port); buildHtmlStringRepresentation(); }
From source file:org.zaproxy.zap.extension.invoke.InvokeAppWorker.java
@Override protected Void doInBackground() throws Exception { String url = ""; // Full URL String host = ""; // Just the server name, e.g. localhost String port = ""; // the port String site = ""; // e.g. http://localhost:8080/ String postdata = ""; // only present in POST ops String cookie = ""; // from the request header HistoryReference historyRef = msg.getHistoryRef(); int msgid = -1; if (historyRef != null) { msgid = historyRef.getHistoryId(); }/*from w w w.j av a 2 s . c o m*/ URI uri = msg.getRequestHeader().getURI(); url = uri.toString(); host = uri.getHost(); site = uri.getScheme() + "://" + uri.getHost(); if (uri.getPort() > 0) { port = String.valueOf(uri.getPort()); site = site + ":" + port + "/"; } else { if (uri.getScheme().equalsIgnoreCase("http")) { port = "80"; } else if (uri.getScheme().equalsIgnoreCase("https")) { port = "443"; } site = site + "/"; } if (msg.getRequestBody() != null) { postdata = msg.getRequestBody().toString(); postdata = postdata.replaceAll("\n", "\\n"); } Vector<String> cookies = msg.getRequestHeader().getHeaders(HttpHeader.COOKIE); if (cookies != null && cookies.size() > 0) { cookie = cookies.get(0); } List<String> cmd = new ArrayList<>(); cmd.add(command); if (parameters != null) { for (String parameter : parameters.split(" ")) { // Replace all of the tags String finalParameter = parameter.replace("%url%", url).replace("%host%", host) .replace("%port%", port).replace("%site%", site).replace("%cookie%", cookie) .replace("%postdata%", postdata).replace("%msgid%", String.valueOf(msgid)); // Replace header tags Matcher headers = Pattern.compile("%header-([A-z0-9_-]+)%").matcher(finalParameter); while (headers.find()) { String headerValue = msg.getRequestHeader().getHeader(headers.group(1)); if (headerValue == null) { headerValue = ""; } finalParameter = finalParameter.replace(headers.group(0), headerValue); } cmd.add(finalParameter); } } logger.debug("Invoking: " + cmd.toString()); View.getSingleton().getOutputPanel().append("\n" + cmd.toString() + "\n"); ProcessBuilder pb = new ProcessBuilder(cmd); if (workingDir != null) { pb.directory(workingDir); } pb.redirectErrorStream(true); Process proc; try { proc = pb.start(); } catch (final Exception e) { View.getSingleton().getOutputPanel() .append(Constant.messages.getString("invoke.error") + e.getLocalizedMessage() + "\n"); logger.warn("Failed to start the process: " + e.getMessage(), e); return null; } if (captureOutput) { try (BufferedReader brOut = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { String line; boolean isOutput = false; StringBuilder sb = new StringBuilder(); if (msg.getNote() != null) { sb.append(msg.getNote()); sb.append('\n'); } // Show any stdout/error messages while ((line = brOut.readLine()) != null) { View.getSingleton().getOutputPanel().append(line + "\n"); sb.append(line); sb.append('\n'); isOutput = true; } if (isOutput) { // Somethings been written, switch to the Output tab View.getSingleton().getOutputPanel().setTabFocus(); } if (outputNote) { HistoryReference hr = msg.getHistoryRef(); if (hr != null) { hr.setNote(sb.toString()); } } } } return null; }