package org.zaproxy.zap.extension.pscan; import net.htmlparser.jericho.MasonTagTypes; import net.htmlparser.jericho.MicrosoftTagTypes; import net.htmlparser.jericho.PHPTagTypes; import net.htmlparser.jericho.Source; import org.apache.log4j.Logger; import org.parosproxy.paros.control.Control.Mode; import org.parosproxy.paros.core.proxy.ProxyListener; import org.parosproxy.paros.core.scanner.Alert; import org.parosproxy.paros.db.DatabaseException; import org.parosproxy.paros.db.TableHistory; import org.parosproxy.paros.extension.SessionChangedListener; import org.parosproxy.paros.extension.history.ExtensionHistory; import org.parosproxy.paros.extension.history.ProxyListenerLog; import org.parosproxy.paros.model.HistoryReference; import org.parosproxy.paros.model.Model; import org.parosproxy.paros.model.Session; import org.parosproxy.paros.network.HttpMalformedHeaderException; import org.parosproxy.paros.network.HttpMessage; import org.zaproxy.zap.extension.alert.ExtensionAlert; public class PassiveScanThread extends Thread implements ProxyListener, SessionChangedListener { private static final Logger logger = Logger.getLogger(PassiveScanThread.class); //Could be after the last one that saves the HttpMessage, as this ProxyListener doesn't change the HttpMessage. public static final int PROXY_LISTENER_ORDER = ProxyListenerLog.PROXY_LISTENER_ORDER + 1; @SuppressWarnings("unused") private OptionsPassiveScan options = null; private PassiveScannerList scannerList = null; private int currentId = 1; private int lastId = -1; private int mainSleep = 5000; private int postSleep = 200; private volatile boolean shutDown = false; private final ExtensionHistory extHist; private final ExtensionAlert extAlert; private final PassiveScanParam pscanOptions; private TableHistory historyTable = null; private HistoryReference href = null; private Session session; /** * Constructs a {@code PassiveScanThread} with the given data. * * @param passiveScannerList the passive scanners, must not be {@code null}. * @param extHist the extension to obtain the (cached) history references, might be {@code null}. * @param extensionAlert the extension used to raise the alerts, must not be {@code null}. * @deprecated (2.6.0) Use * {@link #PassiveScanThread(PassiveScannerList, ExtensionHistory, ExtensionAlert, PassiveScanParam)} instead. * It will be removed in a future release. */ @Deprecated public PassiveScanThread(PassiveScannerList passiveScannerList, ExtensionHistory extHist, ExtensionAlert extensionAlert) { this(passiveScannerList, extHist, extensionAlert, new PassiveScanParam()); } /** * Constructs a {@code PassiveScanThread} with the given data. * * @param passiveScannerList the passive scanners, must not be {@code null}. * @param extHist the extension to obtain the (cached) history references, might be {@code null}. * @param extensionAlert the extension used to raise the alerts, must not be {@code null}. * @param pscanOptions the passive scanner options, must not be {@code null}. * @since 2.6.0 */ public PassiveScanThread (PassiveScannerList passiveScannerList, ExtensionHistory extHist, ExtensionAlert extensionAlert, PassiveScanParam pscanOptions) { super("ZAP-PassiveScanner"); this.setDaemon(true); if (extensionAlert == null) { throw new IllegalArgumentException("Parameter extensionAlert must not be null."); } this.scannerList = passiveScannerList; MicrosoftTagTypes.register(); PHPTagTypes.register(); PHPTagTypes.PHP_SHORT.deregister(); // remove PHP short tags otherwise they override processing instructions MasonTagTypes.register(); extAlert = extensionAlert; this.extHist = extHist; this.pscanOptions = pscanOptions; } @Override public void run() { historyTable = Model.getSingleton().getDb().getTableHistory(); session = Model.getSingleton().getSession(); // Get the last id - in case we've just opened an existing session currentId = this.getLastHistoryId(); lastId = currentId; while (!shutDown) { try { if (href != null || lastId > currentId ) { currentId ++; } else { // Either just started or there are no new records try { Thread.sleep(mainSleep); if (shutDown) { return; } lastId = this.getLastHistoryId(); } catch (InterruptedException e) { // New URL, but give it a chance to be processed first try { Thread.sleep(postSleep); } catch (InterruptedException e2) { // Ignore } } } try { href = getHistoryReference(currentId); //historyRecord = historyTable.read(currentId); } catch (Exception e) { if (shutDown) { return; } logger.error("Failed to read record " + currentId + " from History table", e); } if (href != null && (!pscanOptions.isScanOnlyInScope() || session.isInScope(href))) { try { // Parse the record HttpMessage msg = href.getHttpMessage(); String response = msg.getResponseHeader().toString() + msg.getResponseBody().toString(); Source src = new Source(response); for (PassiveScanner scanner : scannerList.list()) { try { if (shutDown) { return; } if (scanner.isEnabled() && scanner.appliesToHistoryType(href.getHistoryType())) { scanner.setParent(this); scanner.scanHttpRequestSend(msg, href.getHistoryId()); if (msg.isResponseFromTargetHost()) { scanner.scanHttpResponseReceive(msg, href.getHistoryId(), src); } } } catch (Throwable e) { if (shutDown) { return; } logger.error("Scanner " + scanner.getName() + " failed on record " + currentId + " from History table: " + href.getMethod() + " " + href.getURI(), e); } } } catch (Exception e) { if (HistoryReference.getTemporaryTypes().contains(href.getHistoryType())) { if (logger.isDebugEnabled()) { logger.debug("Temporary record " + currentId + " no longer available:", e); } } else { logger.error("Parser failed on record " + currentId + " from History table", e); } } } } catch (Exception e) { if (shutDown) { return; } logger.error("Failed on record " + currentId + " from History table", e); } } } private HistoryReference getHistoryReference(final int historyReferenceId) { if (extHist != null) { return extHist.getHistoryReference(historyReferenceId); } try { return new HistoryReference(historyReferenceId); } catch (HttpMalformedHeaderException | DatabaseException e) { return null; } } private int getLastHistoryId() { return historyTable.lastIndex(); } protected int getRecordsToScan() { return this.getLastHistoryId() - getLastScannedId(); } private int getLastScannedId() { if (currentId > lastId) { return currentId - 1; } return currentId; } public void raiseAlert(int id, Alert alert) { if (shutDown) { return; } if (currentId != id) { logger.error("Alert id != currentId! " + id + " " + currentId); } alert.setSource(Alert.Source.PASSIVE); // Raise the alert extAlert.alertFound(alert, href); } private void notifyHistoryItemChanged(HistoryReference historyReference) { if (extHist != null) { extHist.notifyHistoryItemChanged(historyReference); } } public void addTag(int id, String tag) { if (shutDown) { return; } try { if (! href.getTags().contains(tag)) { href.addTag(tag); notifyHistoryItemChanged(href); } } catch (Exception e) { logger.error(e.getMessage(), e); } } @Override public int getArrangeableListenerOrder() { return PROXY_LISTENER_ORDER; } @Override public boolean onHttpRequestSend(HttpMessage msg) { // Ignore return true; } @Override public boolean onHttpResponseReceive(HttpMessage msg) { // Wakey wakey this.interrupt(); return true; } @Override public void sessionChanged(Session session) { // Reset the currentId historyTable = Model.getSingleton().getDb().getTableHistory(); href = null; // Get the last id - in case we've just opened an existing session currentId = historyTable.lastIndex(); lastId = currentId; } @Override public void sessionScopeChanged(Session session) { } public void shutdown() { this.shutDown = true; } @Override public void sessionAboutToChange(Session session) { } @Override public void sessionModeChanged(Mode mode) { // Ignore } }