org.zaproxy.zap.extension.ascanrulesAlpha.ProxyDisclosureScanner.java Source code

Java tutorial

Introduction

Here is the source code for org.zaproxy.zap.extension.ascanrulesAlpha.ProxyDisclosureScanner.java

Source

/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2015 The ZAP Development Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.zaproxy.zap.extension.ascanrulesAlpha;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.URI;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.core.scanner.AbstractAppPlugin;
import org.parosproxy.paros.core.scanner.Alert;
import org.parosproxy.paros.core.scanner.Category;
import org.parosproxy.paros.network.HtmlParameter;
import org.parosproxy.paros.network.HttpMessage;
import org.parosproxy.paros.network.HttpRequestHeader;
import org.parosproxy.paros.network.HttpResponseHeader;

/**
 * Detect and fingerprint forward proxies and reverse proxies configured between the Zap instance
 * and the origin web server, and fingerprint the origin web server.
 *
 * @author 70pointer
 */
public class ProxyDisclosureScanner extends AbstractAppPlugin {

    /** Prefix for internationalized messages used by this rule */
    private static final String MESSAGE_PREFIX = "ascanalpha.proxydisclosure.";

    public static final List<String> MAX_FORWARD_METHODS = new LinkedList<String>(
            Arrays.asList(new String[] { HttpRequestHeader.TRACE, HttpRequestHeader.OPTIONS, }));

    public static final Pattern NOT_SUPPORTED_APACHE_PATTERN = Pattern
            .compile("^<address>(.+)\\s+Server[^<]*</address>$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
    public static final Pattern MAX_FORWARDS_RESPONSE_PATTERN = Pattern.compile("^Max-Forwards:\\s*([0-9]+)\\s*$",
            Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);

    /**
     * a map of patterns indicating any cookies set by a proxy. Use a capture group for the cookie
     * name.
     */
    public static final Map<Pattern, String> PROXY_COOKIES = new LinkedHashMap<Pattern, String>();

    static {
        // Citrix NetScaler
        PROXY_COOKIES.put(Pattern.compile("Set-Cookie: (NSC_[a-z0-9]+)=",
                Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), "Citrix NetScaler");
    }

    /**
     * a map of any request headers set by a proxy. Use a capture group for the header name, and
     * another for the value.
     */
    public static final Map<Pattern, String> PROXY_REQUEST_HEADERS = new LinkedHashMap<Pattern, String>();

    static {
        // product-specific headers go first..

        // generic headers set by proxies go after..
        PROXY_REQUEST_HEADERS.put(Pattern.compile("^(X-Forwarded-For):\\s*([0-9.]+)\\s*$",
                Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), "");
        PROXY_REQUEST_HEADERS.put(Pattern.compile("^(X-Forwarded-Port):\\s*([0-9]+)\\s*$",
                Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), "");
        PROXY_REQUEST_HEADERS.put(Pattern.compile("^(X-Forwarded-Proto):\\s*(.+)\\s*$",
                Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), "");
        PROXY_REQUEST_HEADERS.put(Pattern.compile("^(Via):\\s*(.+)\\s*$",
                Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), "");
        // Don't put Cache-Control in here, since we send TRACE requests with the "Cache-Control"
        // header in step 1.
        // If the TRACE succeeds, the Cache-Control directive we send is echoed back in the
        // response, and it looks like we're after
        // detecting a proxy. D'oh!
        // PROXY_REQUEST_HEADERS.put(Pattern.compile("^(Cache-Control):\\s*(.+)\\s*$",
        // Pattern.CASE_INSENSITIVE |  Pattern.MULTILINE | Pattern.DOTALL), "");
    }

    /**
     * a map of any response headers set by a proxy. Use a capture group for the header name, and
     * another for the value.
     */
    public static final Map<Pattern, String> PROXY_RESPONSE_HEADERS = new LinkedHashMap<Pattern, String>();

    static {
        PROXY_RESPONSE_HEADERS.put(Pattern.compile("^(X-RBT-Optimized-By):\\s*(.+)\\s*$",
                Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), "Riverbed Steelhead");

        PROXY_RESPONSE_HEADERS.put(Pattern.compile("^(X-Cache):\\s*(.+)\\s*$",
                Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), "");
        PROXY_RESPONSE_HEADERS.put(Pattern.compile("^(X-Cache-Lookup):\\s*(.+)\\s*$",
                Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), "");
        PROXY_RESPONSE_HEADERS.put(Pattern.compile("^(Via):\\s*(.+)\\s*$",
                Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), "");
        PROXY_RESPONSE_HEADERS.put(Pattern.compile("^(Cache-Control):\\s*(.+)\\s*$",
                Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), "");
    };

    /** the number of Max-Forwards to apply. Set depending on the Attack strength. */
    private int MAX_FORWARDS_MAXIMUM = 0;

    /** plugin dependencies (none) */
    private static final String[] dependency = {};

    /** for logging. */
    private static Logger log = Logger.getLogger(ProxyDisclosureScanner.class);

    @Override
    public int getId() {
        return 40025;
    }

    @Override
    public String getName() {
        return Constant.messages.getString(MESSAGE_PREFIX + "name");
    }

    @Override
    public String[] getDependency() {
        return dependency;
    }

    @Override
    public String getDescription() {
        return null;
        // needs a parameter!
        // return Constant.messages.getString(MESSAGE_PREFIX+"desc");
    }

    @Override
    public int getCategory() {
        return Category.INFO_GATHER;
    }

    @Override
    public String getSolution() {
        return Constant.messages.getString(MESSAGE_PREFIX + "soln");
    }

    @Override
    public String getReference() {
        return Constant.messages.getString(MESSAGE_PREFIX + "refs");
    }

    @Override
    public void init() {

        // set up what we are allowed to do, depending on the attack strength that was set.
        if (this.getAttackStrength() == AttackStrength.LOW) {
            MAX_FORWARDS_MAXIMUM = 2;
        } else if (this.getAttackStrength() == AttackStrength.MEDIUM) {
            MAX_FORWARDS_MAXIMUM = 3;
        } else if (this.getAttackStrength() == AttackStrength.HIGH) {
            MAX_FORWARDS_MAXIMUM = 4;
        } else if (this.getAttackStrength() == AttackStrength.INSANE) {
            MAX_FORWARDS_MAXIMUM = 5;
        }
    }

    /**
     * scans for Proxy Disclosure issues, using the TRACE and OPTIONS method with 'Max-Forwards',
     * and the TRACK method. The code attempts to enumerate and identify all proxies identified
     * between the Zap instance and the origin web server.
     */
    @Override
    public void scan() {
        try {
            // where's what we're going to do (roughly):
            // 1: If TRACE is enabled on the origin web server, we're going to use it, and the
            // "Max-Forwards" header to verify
            //    if *no* proxy exists between Zap and the origin web server.
            // 2: If we can't do that, because TRACE is not supported, or because there appears to
            // be a proxy between Zap
            //   and the origin web server, we use the "Max-Forwards" compatible methods (TRACE and
            // OPTIONS) to
            //    iterate through each of the proxies between Zap and the origin web server.
            //    We will attempt to fingerprint each proxy / web server along the way, using various
            // techniques.
            // 3: At this point, depending on the proxies and their configurations, there is a
            // possibility that we have not
            //    identified *all* of the nodes (proxies / web servers) that the request/response
            // traverses.  We will use
            //    other HTTP methods, such as "TRACK" to obtain an error-type response. In all of the
            // cases we have tested so far,
            //   such an error response comes from the origin web server, rather than an
            // intermediate proxy.  We then fingerprint
            //    the origin web server.  If the origin web server's signature is not the same as the
            // final node that we have
            //    already identified, we consider the origin web server to be an additional node in
            // the path.
            // 4: Report the results.

            // Step 1: Using TRACE, identify if *no* proxies are used between the Zap instance and
            // the origin web server
            // int maxForwardsMaximum = 7;  //Anonymous only use 7 proxies, so that's good enough
            // for us too.. :)
            int step1numberOfProxies = 0;
            // this variable is to track proxies that set cookies, but are otherwise complete
            // invisible, and do not
            // respond per spec (RFC2616, RFC2965) to OPTIONS/TRACE with Max-Forwards.
            // They do not set headers that can be identified.
            // They are also inherently un-ordered, because we do not, and cannot be sure at what
            // point they fit into the topology
            // that we can otherwise document using OPTIONS/TRACE + Max-Forwards.
            Set<String> silentProxySet = new HashSet<String>();
            boolean endToEndTraceEnabled = false;
            boolean proxyTraceEnabled = false;

            URI traceURI = getBaseMsg().getRequestHeader().getURI();
            HttpRequestHeader traceRequestHeader = new HttpRequestHeader();
            traceRequestHeader.setMethod(HttpRequestHeader.TRACE);
            // go to the URL requested, in case the proxy is configured on a per-URL basis..
            // traceRequestHeader.setURI(new URI(traceURI.getScheme() + "://" +
            // traceURI.getAuthority()+ "/",true));
            traceRequestHeader.setURI(traceURI);
            traceRequestHeader.setVersion(HttpRequestHeader.HTTP11); // or 1.1?
            traceRequestHeader.setSecure(traceRequestHeader.isSecure());
            traceRequestHeader.setHeader("Max-Forwards", String.valueOf(MAX_FORWARDS_MAXIMUM));
            traceRequestHeader.setHeader("Cache-Control", "no-cache"); // we do not want cached content. we want content from the origin
            // server
            traceRequestHeader.setHeader("Pragma", "no-cache"); // similarly, for HTTP/1.0

            HttpMessage tracemsg = getNewMsg();
            tracemsg.setRequestHeader(traceRequestHeader);
            // create a random cookie, and set it up, so we can detect if the TRACE is enabled (in
            // which case, it should echo it back in the response)
            String randomcookiename = RandomStringUtils.random(15,
                    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
            String randomcookievalue = RandomStringUtils.random(40,
                    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
            TreeSet<HtmlParameter> cookies = tracemsg.getCookieParams();
            cookies.add(new HtmlParameter(HtmlParameter.Type.cookie, randomcookiename, randomcookievalue));
            tracemsg.setCookieParams(cookies);

            sendAndReceive(tracemsg, false); // do not follow redirects.
            // is TRACE enabled?
            String traceResponseBody = tracemsg.getResponseBody().toString();
            if (traceResponseBody.contains(randomcookievalue)) {
                // TRACE is enabled. Look at the Max-Forwards in the response, to see if it was
                // decremented
                // if it was decremented, there is definitely a proxy..
                // if not, it *suggests* there is no proxy (or any proxies present are not compliant
                // --> all bets are off)
                boolean proxyActuallyFound = false;
                // found a TRACE from Zap all the way through to the Origin server.. not good!!
                endToEndTraceEnabled = true; // this will raise the risk from Medium to High if a Proxy Disclosure
                // was found!
                // TODO: raise a "TRACE" type alert (if no proxy disclosure is found, but TRACE
                // enabled?)

                Matcher matcher = MAX_FORWARDS_RESPONSE_PATTERN.matcher(traceResponseBody);
                if (matcher.find()) {
                    String maxForwardsResponseValue = matcher.group(1);
                    if (log.isDebugEnabled())
                        log.debug("TRACE with \"Max-Forwards: " + MAX_FORWARDS_MAXIMUM
                                + "\" causes response body Max-Forwards value '" + maxForwardsResponseValue + "'");
                    if (maxForwardsResponseValue.equals(String.valueOf(MAX_FORWARDS_MAXIMUM))) {
                        // (probably) no proxy!
                        if (log.isDebugEnabled())
                            log.debug("TRACE with \"Max-Forwards: " + MAX_FORWARDS_MAXIMUM
                                    + "\" indicates that there is *NO* proxy in place. Note: the TRACE method is supported.. that's an issue in itself! :)");

                        // To be absolutely certain, check that the cookie info in the response
                        // header,
                        // and proxy request headers in the response body (via TRACE) do not leak
                        // the presence of a proxy
                        // This would indicate a non-RFC2606 compliant proxy, since these are
                        // supposed to decrement the Max-Forwards.
                        // it does happen in the wild..
                        String traceResponseHeader = tracemsg.getResponseHeader().toString();

                        // look for cookies set by the proxy, which will be in the response header
                        Iterator<Pattern> cookiePatternIterator = PROXY_COOKIES.keySet().iterator();
                        while (cookiePatternIterator.hasNext() && !proxyActuallyFound) {
                            Pattern cookiePattern = cookiePatternIterator.next();
                            String proxyServer = PROXY_COOKIES.get(cookiePattern);
                            Matcher cookieMatcher = cookiePattern.matcher(traceResponseHeader);
                            if (cookieMatcher.find()) {
                                String cookieDetails = cookieMatcher.group(1);
                                if (log.isDebugEnabled()) {
                                    proxyActuallyFound = true;
                                    if (!proxyServer.equals("") && !silentProxySet.contains(proxyServer))
                                        silentProxySet.add(proxyServer);
                                    if (log.isDebugEnabled())
                                        log.debug("TRACE with \"Max-Forwards: " + MAX_FORWARDS_MAXIMUM
                                                + "\" indicates that there is *NO* proxy in place, but a known proxy cookie ("
                                                + cookieDetails + ", which indicates proxy server '" + proxyServer
                                                + "') in the response header contradicts this..");
                                }
                            }
                        }
                        // look for request headers set by the proxy, which will end up in the
                        // response body if the TRACE succeeded
                        Iterator<Pattern> requestHeaderPatternIterator = PROXY_REQUEST_HEADERS.keySet().iterator();
                        while (requestHeaderPatternIterator.hasNext() && !proxyActuallyFound) {
                            Pattern proxyHeaderPattern = requestHeaderPatternIterator.next();
                            String proxyServer = PROXY_REQUEST_HEADERS.get(proxyHeaderPattern);
                            Matcher proxyHeaderMatcher = proxyHeaderPattern.matcher(traceResponseBody);
                            if (proxyHeaderMatcher.find()) {
                                String proxyHeaderName = proxyHeaderMatcher.group(1);
                                if (log.isDebugEnabled()) {
                                    proxyActuallyFound = true;
                                    if (log.isDebugEnabled())
                                        log.debug("TRACE with \"Max-Forwards: " + MAX_FORWARDS_MAXIMUM
                                                + "\" indicates that there is *NO* proxy in place, but a known proxy request header ("
                                                + proxyHeaderName + ", which indicates proxy server '" + proxyServer
                                                + "') in the response body contradicts this..");
                                }
                            }
                        }

                    } else {
                        // Trace indicates there is a proxy in place.. (or multiple proxies)
                        // Note: this number cannot really be trusted :( we don't use it, other than
                        // for informational purposes
                        step1numberOfProxies = MAX_FORWARDS_MAXIMUM - Integer.parseInt(maxForwardsResponseValue);
                        if (log.isDebugEnabled())
                            log.debug("TRACE with \"Max-Forwards: " + MAX_FORWARDS_MAXIMUM
                                    + "\" indicates that there *IS* at least one proxy in place (Likely number: "
                                    + step1numberOfProxies + "). Note: the TRACE method is also supported!");
                        proxyActuallyFound = true;
                    }
                } else {
                    // The Max-Forwards does not appear in the response body, even though the cookie
                    // value appeared in the response body, using TRACE.. Why?
                    if (log.isDebugEnabled())
                        log.debug(
                                "TRACE support is indicated via an echoed cookie, but the Max-Forwards value from the request is not echoed in the response. Why? Load balancer? WAF?");
                    proxyActuallyFound = true;
                }
                // no conflicting evidence (ie, no proxy indicated) ==> return
                if (!proxyActuallyFound)
                    return;
            } else {
                // TRACE is NOT enabled, so we can't use this technique to tell if there is *no*
                // proxy between Zap and the origin server
                if (log.isDebugEnabled())
                    log.debug(
                            "TRACE is not supported, so we cannot quickly check for *no* proxies. Falling back to the hard way");
            }

            // bale out if we were asked nicely. it's nice to be nice.
            if (isStop()) {
                if (log.isDebugEnabled())
                    log.debug("Stopping the scan due to a user request (after step 1)");
                return;
            }

            // Step 2: Use Max-Forwards with OPTIONS and TRACE to iterate through each of the
            // proxies
            HttpRequestHeader baseRequestHeader = getBaseMsg().getRequestHeader();
            URI baseRequestURI = baseRequestHeader.getURI();
            int step2numberOfNodes = 0;
            String[] nodeServers = new String[MAX_FORWARDS_MAXIMUM + 2]; // up to n proxies, and an origin server.

            // for each of the methods
            for (String httpMethod : MAX_FORWARD_METHODS) {
                // for each method, increment the Max-Forwards, and look closely at the response
                // TODO: loop from 0 to numberOfProxies -1????
                int step2numberOfNodesForMethod = 0;
                String[] nodeServersForMethod = new String[MAX_FORWARDS_MAXIMUM + 2];
                String previousServerDetails = RandomStringUtils.random(15, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
                int previousResponseStatusCode = 0;
                int responseStatusCode = 0;
                boolean httpHandled = false; // a flag to handle an extra HTTP request for this method, if the URL
                // is HTTPS

                // if the TRACE worked in step 1, and we know how many proxies there are, do that
                // number + 1, else just do the maximum defined on the attack strength
                for (int maxForwards = 0; maxForwards < (step1numberOfProxies > 0 ? step1numberOfProxies + 1
                        : MAX_FORWARDS_MAXIMUM); maxForwards++) {

                    HttpMessage testMsg = getNewMsg(); // get a new message, with the request attributes cloned
                    // from the base message
                    HttpRequestHeader origRequestHeader = testMsg.getRequestHeader();

                    if (log.isDebugEnabled())
                        log.debug("Trying method " + httpMethod + " with MAX-FORWARDS: "
                                + Integer.toString(maxForwards));

                    // if we're on the right iteration (Max-Forwards=0, ie first proxy, and a HTTPS
                    // request, then
                    // then prepare to try an additional HTTP request..
                    boolean tryHttp = (!httpHandled && maxForwards == 0 && baseRequestHeader.isSecure());

                    HttpRequestHeader requestHeader = new HttpRequestHeader();
                    requestHeader.setMethod(httpMethod);
                    // requestHeader.setURI(new URI(origURI.getScheme() + "://" +
                    // origURI.getAuthority()+ "/",true));
                    requestHeader.setURI(baseRequestURI);
                    requestHeader.setVersion(HttpRequestHeader.HTTP11); // OPTIONS and TRACE are supported under 1.0, but for
                    // multi-homing, we need to use 1.1
                    if (tryHttp) {
                        if (log.isDebugEnabled())
                            log.debug(
                                    "Blind-spot testing, using a HTTP connection, to try detect an initial proxy, which we might not see via HTTPS");
                        requestHeader.setSecure(false);
                        requestHeader.setHeader("Max-Forwards", "0");
                    } else {
                        requestHeader.setSecure(origRequestHeader.isSecure());
                        requestHeader.setHeader("Max-Forwards", Integer.toString(maxForwards));
                    }
                    requestHeader.setHeader("Cache-Control", "no-cache"); // we do not want cached content. we want content from the
                    // origin server
                    requestHeader.setHeader("Pragma", "no-cache"); // similarly, for HTTP/1.0

                    HttpMessage mfMethodMsg = getNewMsg();
                    mfMethodMsg.setRequestHeader(requestHeader);

                    // create a random cookie, and set it up, so we can detect if the TRACE is
                    // enabled (in which case, it should echo it back in the response)
                    String randomcookiename2 = RandomStringUtils.random(15,
                            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
                    String randomcookievalue2 = RandomStringUtils.random(40,
                            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
                    TreeSet<HtmlParameter> cookies2 = mfMethodMsg.getCookieParams();
                    cookies2.add(
                            new HtmlParameter(HtmlParameter.Type.cookie, randomcookiename2, randomcookievalue2));
                    mfMethodMsg.setCookieParams(cookies2);

                    try {
                        sendAndReceive(mfMethodMsg, false); // do not follow redirects.
                    } catch (Exception e) {
                        log.error(
                                "Failed to send a request in step 2 with method " + httpMethod + ", Max-Forwards: "
                                        + requestHeader.getHeader("Max-Forwards") + ": " + e.getMessage());
                        break; // to the next method
                    }

                    // if the response from the proxy/origin server echoes back the cookie (TRACE,
                    // or other method), that's serious, so we need to check.
                    String methodResponseBody = mfMethodMsg.getResponseBody().toString();
                    if (methodResponseBody.contains(randomcookievalue2)) {
                        proxyTraceEnabled = true; // this will raise the risk from Medium to High if a Proxy
                        // Disclosure was found!
                        // TODO: raise a "TRACE" type alert (if no proxy disclosure is found, but
                        // TRACE enabled?)
                    }

                    // check if the Server response header differs
                    // the server header + powered by list is what we will record if a key attribute
                    // changes between requests.
                    HttpResponseHeader responseHeader = mfMethodMsg.getResponseHeader();
                    String serverHeader = responseHeader.getHeader("Server");
                    if (serverHeader == null)
                        serverHeader = "";

                    String poweredBy;
                    Vector<String> poweredByList = responseHeader.getHeaders("X-Powered-By");
                    if (poweredByList != null)
                        poweredBy = poweredByList.toString(); // uses format: "[a,b,c]"
                    else
                        poweredBy = "";
                    String serverDetails = serverHeader
                            + (poweredBy.equals("") || poweredBy.equals("[]") ? "" : poweredBy);
                    responseStatusCode = responseHeader.getStatusCode();

                    if (!serverDetails.equals(previousServerDetails)) {
                        // it's a new node that we don't appear to have previously seen (for this
                        // HTTP method).
                        nodeServersForMethod[step2numberOfNodesForMethod] = serverDetails;
                        step2numberOfNodesForMethod++;
                        if (log.isDebugEnabled())
                            log.debug("Identified a new node for method " + httpMethod + ", by server details: "
                                    + serverDetails + ". That makes " + step2numberOfNodesForMethod
                                    + " nodes so far");
                    } else {
                        // else check if the HTTP status code differs
                        if (responseStatusCode != previousResponseStatusCode) {
                            // if the status code is different, this likely indicates a different
                            // node
                            nodeServersForMethod[step2numberOfNodesForMethod] = serverDetails;
                            step2numberOfNodesForMethod++;
                            if (log.isDebugEnabled())
                                log.debug("Identified a new node for method " + httpMethod
                                        + ", by response status : " + responseStatusCode + ". That makes "
                                        + step2numberOfNodesForMethod + " nodes so far");
                        }
                    }
                    previousServerDetails = serverDetails;
                    previousResponseStatusCode = responseStatusCode;

                    // if the base URL is HTTPS, and we just did an extra "blind spot" check for
                    // HTTP, go into the next iteration with the same
                    // "Max-Forwards" value that we just handled, but set the flag to false so that
                    // we don't attempt to do the HTTP "blind spot" request again.
                    if (tryHttp) {
                        maxForwards--;
                        httpHandled = true;
                    }

                    // bale out if we were asked nicely. it's nice to be nice.
                    if (isStop()) {
                        if (log.isDebugEnabled())
                            log.debug("Stopping the scan due to a user request");
                        return;
                    }
                }
                // if the number of nodes (proxies+origin web server) detected using this HTTP
                // method is greater than the number detected thus far, use the data
                // gained using this HTTP method..
                if (log.isDebugEnabled())
                    log.debug("The number of nodes detected using method " + httpMethod + " is "
                            + step2numberOfNodesForMethod);
                if (step2numberOfNodesForMethod > step2numberOfNodes) {
                    step2numberOfNodes = step2numberOfNodesForMethod;
                    nodeServers = nodeServersForMethod;
                }
            }
            if (log.isDebugEnabled())
                log.debug("The maximum number of nodes detected using any Max-Forwards method  is "
                        + step2numberOfNodes);

            // Step 3: For the TRACK, use a random URL, to force an error, and to bypass any cached
            // file.
            URI trackURI = getBaseMsg().getRequestHeader().getURI();
            HttpRequestHeader trackRequestHeader = new HttpRequestHeader();
            trackRequestHeader.setMethod("TRACK"); // There is no suitable constant on HttpRequestHeader
            // go to a similar (but random) URL requested
            //   - in case a proxy is configured on a per-URL basis.. (this is the case on some of my
            // real world test servers)
            //   - to try to ensure we get an error message that we can fingerprint
            //   - to bypass caching (if it's a random filename, if won't have been seen before, and
            // won't be cached)
            //     yes, I know TRACK requests should *not* be cached, but not all servers are
            // compliant.
            String randompiece = RandomStringUtils.random(5, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
            trackRequestHeader.setURI(new URI(
                    trackURI.getScheme() + "://" + trackURI.getAuthority() + getPath(trackURI) + randompiece,
                    true));

            trackRequestHeader.setVersion(HttpRequestHeader.HTTP11); //
            trackRequestHeader.setSecure(trackRequestHeader.isSecure());
            trackRequestHeader.setHeader("Max-Forwards", String.valueOf(MAX_FORWARDS_MAXIMUM));
            trackRequestHeader.setHeader("Cache-Control", "no-cache"); // we do not want cached content. we want content from the origin
            // server
            trackRequestHeader.setHeader("Pragma", "no-cache"); // similarly, for HTTP/1.0

            HttpMessage trackmsg = getNewMsg();
            trackmsg.setRequestHeader(trackRequestHeader);

            sendAndReceive(trackmsg, false); // do not follow redirects.

            // TODO: fingerprint more origin web servers response to a TRACK request for a file that
            // does not exist.
            String trackResponseBody = trackmsg.getResponseBody().toString();
            Matcher unsupportedApacheMatcher = NOT_SUPPORTED_APACHE_PATTERN.matcher(trackResponseBody);
            if (unsupportedApacheMatcher.find()) {
                String originServerName = unsupportedApacheMatcher.group(1);
                if (log.isDebugEnabled())
                    log.debug("Identified the origin node using TRACK, with server header: " + originServerName);
                // check if this is the same as the last node we've identified, and if so, discard
                // it. If not, add it to to the end (as the origin server).
                if (!nodeServers[step2numberOfNodes - 1].equals(originServerName)) {
                    // it's different to the last one seen.. add it.
                    if (log.isDebugEnabled())
                        log.debug(
                                "The origin node was not already recorded using the Max-Forwards method, so adding it in.");
                    nodeServers[step2numberOfNodes] = originServerName;
                    step2numberOfNodes++;
                }
            }

            // TODO: compare step2numberOfProxies and step1numberOfProxies?

            // log the nodes we have noted so far
            if (log.isDebugEnabled()) {
                for (int nodei = 0; nodei < step2numberOfNodes; nodei++) {
                    log.debug("Node " + nodei + " is "
                            + (!nodeServers[nodei].equals("") ? nodeServers[nodei] : "Unknown"));
                }
                // log the "silent" proxies that we saw.
                for (String silentServer : silentProxySet) {
                    log.debug("Silent Proxy: " + (!silentServer.equals("") ? silentServer : "Unknown"));
                }
            }

            // Note: there will always be an origin web server, so check for >1, not <0 number of
            // nodes.
            if (step2numberOfNodes > 1 || silentProxySet.size() > 0) {
                // bingo with the list of nodes (proxies+origin web server) that we detected.
                String unknown = Constant.messages.getString(MESSAGE_PREFIX + "extrainfo.unknown");
                String proxyServerHeader = Constant.messages
                        .getString(MESSAGE_PREFIX + "extrainfo.proxyserver.header");
                String webServerHeader = Constant.messages.getString(MESSAGE_PREFIX + "extrainfo.webserver.header");
                String silentProxyServerHeader = Constant.messages
                        .getString(MESSAGE_PREFIX + "extrainfo.silentproxyserver.header");

                // get the proxy server information (ie, all but the last node)
                String proxyServerInfo = "";
                if (step2numberOfNodes > 0) {
                    StringBuilder sb = new StringBuilder();
                    sb.append(proxyServerHeader);
                    sb.append("\n");
                    for (int nodei = 0; nodei < step2numberOfNodes - 1; nodei++) {
                        String proxyServerNode = Constant.messages.getString(
                                MESSAGE_PREFIX + "extrainfo.proxyserver",
                                (!nodeServers[nodei].equals("") ? nodeServers[nodei] : unknown));
                        sb.append(proxyServerNode);
                        sb.append("\n");
                    }
                    proxyServerInfo = sb.toString();
                }
                // get the origin web server information (ie, the last node)
                String webServerInfo = "";
                if (step2numberOfNodes > 0) {
                    StringBuilder sb = new StringBuilder();
                    sb.append(webServerHeader);
                    sb.append("\n");
                    String webServerNode = Constant.messages.getString(MESSAGE_PREFIX + "extrainfo.webserver",
                            (!nodeServers[step2numberOfNodes - 1].equals("") ? nodeServers[step2numberOfNodes - 1]
                                    : unknown));
                    sb.append(webServerNode);
                    sb.append("\n");
                    webServerInfo = sb.toString();
                }
                // get the silent proxy information
                String silentProxyServerInfo = "";
                if (silentProxySet.size() > 0) {
                    StringBuilder sb = new StringBuilder();
                    sb.append(silentProxyServerHeader);
                    sb.append("\n");
                    for (String silentServer : silentProxySet) {
                        // log.debug("Silent Proxy:
                        // "+(!silentServer.equals("")?silentServer:"Unknown"));
                        String silentProxyServerNode = Constant.messages.getString(
                                MESSAGE_PREFIX + "extrainfo.silentproxyserver",
                                (!silentServer.equals("") ? silentServer : unknown));
                        sb.append(silentProxyServerNode);
                        sb.append("\n");
                    }
                    silentProxyServerInfo = sb.toString();
                }
                String traceInfo = "";
                if (endToEndTraceEnabled || proxyTraceEnabled) {
                    traceInfo = Constant.messages.getString(MESSAGE_PREFIX + "extrainfo.traceenabled");
                }

                // all the info is collated nicely. raise the alert.
                String extraInfo = "";
                if (!proxyServerInfo.equals("")) {
                    extraInfo += proxyServerInfo;
                }
                if (!webServerInfo.equals("")) {
                    extraInfo += webServerInfo;
                }
                if (!silentProxyServerInfo.equals("")) {
                    extraInfo += silentProxyServerInfo;
                }
                if (!traceInfo.equals("")) {
                    extraInfo += traceInfo;
                }

                // raise the alert on the original message
                // there are multiple messages on which the issue could have been raised, but each
                // individual atatck message
                // tells only a small part of the story. Explain it in the "extra info" instead.
                bingo(endToEndTraceEnabled || proxyTraceEnabled ? Alert.RISK_HIGH : getRisk(),
                        Alert.CONFIDENCE_MEDIUM, getName(),
                        Constant.messages.getString(MESSAGE_PREFIX + "desc",
                                step2numberOfNodes - 1 + silentProxySet.size()),
                        getBaseMsg().getRequestHeader().getURI().getURI(), // url
                        "", // there is no parameter of interest
                        getAttack(), // the attack. who'd have thunk it?
                        extraInfo, // extra info.. all the detail of the proxy, etc.
                        getSolution(), "", // evidence
                        this.getCweId(), this.getWascId(), getBaseMsg() // the message on which we place the alert
                );
            }

        } 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 for proxy disclosure", e);
        }
    }

    private String getPath(URI uri) {
        String path = uri.getEscapedPath();
        if (path != null) {
            return path;
        }
        return "/";
    }

    private String getAttack() {
        return Constant.messages.getString(MESSAGE_PREFIX + "attack");
    }

    @Override
    public int getRisk() {
        return Alert.RISK_MEDIUM;
    }

    @Override
    public int getCweId() {
        return 200; // Information Exposure (primarily via TRACE / OPTIONS / TRACK)
    }

    @Override
    public int getWascId() {
        return 45; // Fingerprinting
    }
}