/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * 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.ascan; import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; import org.apache.commons.httpclient.URIException; import org.apache.log4j.Logger; import org.parosproxy.paros.db.DatabaseException; import org.parosproxy.paros.model.HistoryReference; import org.parosproxy.paros.model.Model; import org.parosproxy.paros.network.HttpMalformedHeaderException; import org.parosproxy.paros.network.HttpMessage; import org.zaproxy.zap.extension.api.API; import org.zaproxy.zap.extension.api.ApiException; import org.zaproxy.zap.extension.api.ApiImplementor; /** * General Abstract class for Challenge/Response Active Plugin management * * @author yhawke (2014) */ public abstract class ChallengeCallbackAPI extends ApiImplementor { // This are the result contents that should be returned by the API private static final String API_RESPONSE_KO = "ko"; private static final String API_RESPONSE_OK = "ok"; // The default expiration time for each callback (in millisecs) private static final long CALLBACK_EXPIRE_TIME = 2 * 60 * 1000; // Internal logger private static final Logger logger = Logger.getLogger(ChallengeCallbackAPI.class); // The registered callbacks for this API // Use a synchronized collection private final Map<String, RegisteredCallback> regCallbacks = Collections.synchronizedMap(new TreeMap<String, RegisteredCallback>()); /** * Default contructor */ public ChallengeCallbackAPI() { addApiShortcut(getPrefix()); } /** * Implements this to give back the specific shortcut * @return the shortcut path to call the API */ @Override public abstract String getPrefix(); /** * Expire callbacks cleaning method. When called it remove from * the received callbacks list all the sent challenge which haven't received * any answer till now according to an expiring constraint. Currently * the cleaning is done for every new inserting and every received callback, * but it can be done also with a scheduled cleaning thread if the number * of items is memory and time consuming... * Maybe to be understood in the future. */ public void cleanExpiredCallbacks() { long now = System.currentTimeMillis(); // Cuncurrency could be possible for multiple instantiations synchronized(regCallbacks) { Iterator<Map.Entry<String, RegisteredCallback>> it = regCallbacks.entrySet().iterator(); Map.Entry<String, RegisteredCallback> entry; while (it.hasNext()) { entry = it.next(); if (now - entry.getValue().getTimestamp() > CALLBACK_EXPIRE_TIME) { it.remove(); } } } } /** * Gets the ZAP API URL to a challenge endpoint. * * @param challenge the last segment of the path for the challenge endpoint * @return a ZAP API URL to access the the challenge endpoint */ public String getCallbackUrl(String challenge) { String callbackUrl = "http://" + Model.getSingleton().getOptionsParam().getProxyParam().getProxyIp() + ":" + Model.getSingleton().getOptionsParam().getProxyParam().getProxyPort() + "/" + getPrefix() + "/" + challenge; /* Key is not currently used for shorcuts... very interesting String key = Model.getSingleton().getOptionsParam().getApiParam().getKey(); if (key != null && key.length() > 0) { callbackUrl += "?" + API.API_KEY_PARAM + "=" + key; } */ return callbackUrl; } /** * Handles the given message, which might contain a challenge request. * * @param msg the HTTP message of the ZAP API request * @return the HTTP message containing the response to the challenge * @throws ApiException if an error occurred while handling the challenge request */ @Override public HttpMessage handleShortcut(HttpMessage msg) throws ApiException { // We've to look at the name and verify if the challenge has // been registered by one of the executed plugins // ---------------- // http://<zap_IP>/json/xxe/other/NGFrteu568sgToo100 // ---------------- try { String path = msg.getRequestHeader().getURI().getPath(); String challenge = path.substring(path.indexOf(getPrefix()) + getPrefix().length() + 1); if (challenge.charAt(challenge.length() - 1) == '/') { challenge = challenge.substring(0, challenge.length() - 1); } RegisteredCallback rcback = regCallbacks.get(challenge); String response; if (rcback != null) { rcback.getPlugin().notifyCallback(challenge, rcback.getAttackMessage()); response = API_RESPONSE_OK; // OK we consumed it so it's time to clean regCallbacks.remove(challenge); } else { response = API_RESPONSE_KO; // Maybe we've a lot of dirty entries cleanExpiredCallbacks(); } // Build the response msg.setResponseHeader(API.getDefaultResponseHeader("text/html", response.length())); msg.getResponseHeader().setHeader("Access-Control-Allow-Origin", "*"); msg.setResponseBody(response); } catch (URIException | HttpMalformedHeaderException e) { logger.warn(e.getMessage(), e); } return msg; } /** * Registers a new ZAP API challenge. * * @param challenge the challenge * @param plugin the plugin that will be notified if the challenge is requested * @param attack the message that contains the attack that reproduces the issue */ public void registerCallback(String challenge, ChallengeCallbackPlugin plugin, HttpMessage attack) { // Maybe we'va a lot of dirty entries cleanExpiredCallbacks(); // Already synchronized (no need for a monitor) regCallbacks.put(challenge, new RegisteredCallback(plugin, attack)); } private static class RegisteredCallback { private final ChallengeCallbackPlugin plugin; private HistoryReference hRef; private long timeStamp; public RegisteredCallback(ChallengeCallbackPlugin plugin, HttpMessage msg) { this.plugin = plugin; this.timeStamp = System.currentTimeMillis(); try { // Generate an HistoryReference object this.hRef = new HistoryReference(Model.getSingleton().getSession(), HistoryReference.TYPE_TEMPORARY, msg); } catch (DatabaseException | HttpMalformedHeaderException ex) { } } public ChallengeCallbackPlugin getPlugin() { return plugin; } public HttpMessage getAttackMessage() { try { if (hRef != null) { return hRef.getHttpMessage(); } } catch (DatabaseException | HttpMalformedHeaderException ex) { } return null; } public long getTimestamp() { return timeStamp; } } }