/******************************************************************************* * Copyright (c) 2011 Subgraph. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Subgraph - initial API and implementation ******************************************************************************/ package com.subgraph.vega.impl.scanner.urls; import java.net.URI; import java.net.URISyntaxException; import org.apache.http.client.methods.HttpUriRequest; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.html2.HTMLDocument; import com.subgraph.vega.api.analysis.IContentAnalyzer; import com.subgraph.vega.api.analysis.IContentAnalyzerResult; import com.subgraph.vega.api.html.IHTMLParseResult; import com.subgraph.vega.api.http.requests.IHttpResponse; import com.subgraph.vega.api.scanner.IInjectionModuleContext; import com.subgraph.vega.api.scanner.IScannerConfig; import com.subgraph.vega.impl.scanner.forms.FormProcessor; public class ResponseAnalyzer { private final IContentAnalyzer contentAnalyzer; private final UriParser uriParser; private final UriFilter uriFilter; private final FormProcessor formProcessor; public ResponseAnalyzer(IScannerConfig config, IContentAnalyzer contentAnalyzer, UriParser uriParser, UriFilter uriFilter) { this.contentAnalyzer = contentAnalyzer; this.uriParser = uriParser; this.uriFilter = uriFilter; this.formProcessor = new FormProcessor(config, uriFilter, uriParser); } public IContentAnalyzer getContentAnalyzer() { return contentAnalyzer; } public void analyzePivot(IInjectionModuleContext ctx, HttpUriRequest req, IHttpResponse res) { } public void analyzePage(IInjectionModuleContext ctx, HttpUriRequest req, IHttpResponse res) { final IContentAnalyzerResult result = contentAnalyzer.processResponse(res, false, true); for(URI u: result.getDiscoveredURIs()) { if(uriFilter.filter(u)) uriParser.processUri(u); } formProcessor.processForms(ctx, req, res); } public void analyzeContent(IInjectionModuleContext ctx, HttpUriRequest req, IHttpResponse res) { analyzeHtml(ctx, req, res); if(!filterInjectedPath(res.getRequestUri().getPath())) { contentAnalyzer.processResponse(res, false, false); } } private void analyzeHtml(IInjectionModuleContext ctx, HttpUriRequest req, IHttpResponse res) { IHTMLParseResult html = res.getParsedHTML(); if(html == null) return; HTMLDocument document = html.getDOMDocument(); NodeList elements = document.getElementsByTagName("*"); for(int i = 0; i < elements.getLength(); i++) { Node node = elements.item(i); if(node instanceof Element) { analyzeHtmlElement(ctx, req, res, (Element) node); } } } private boolean filterInjectedPath(String path) { return path.contains("vega://") || path.contains("//vega.invalid") || pathContainsXssTag(path); } private boolean pathContainsXssTag(String path) { final int idx = path.indexOf("vvv"); if(idx == -1) { return false; } return extractXssTag(path, idx) != null; } private void analyzeHtmlElement(IInjectionModuleContext ctx, HttpUriRequest req, IHttpResponse res, Element elem) { boolean remoteScript = false; NamedNodeMap attributes = elem.getAttributes(); final String tag = elem.getTagName().toLowerCase(); for(int i = 0; i < attributes.getLength(); i++) { Node item = attributes.item(i); if(item instanceof Attr) { String n = ((Attr)item).getName().toLowerCase(); String v = ((Attr)item).getValue().toLowerCase(); if(match(tag, "script") && match(n, "src")) remoteScript = true; if((v != null) && (match(n, "href", "src", "action", "codebase") || (match(n, "value") && !match(tag, "input")))) { if(v.startsWith("vega://")) alert(ctx, "vinfo-url-inject", "URL injection into <"+ tag + "> tag", req, res); if(v.startsWith("http://vega.invalid/") || v.startsWith("//vega.invalid/")) { if(match(tag, "script", "link")) alert(ctx, "vinfo-url-inject", "URL injection into actively fetched field in tag <"+ tag +"> (high risk)", req, res); else if(match(tag, "a")) alert(ctx, "vinfo-url-inject", "URL injection into anchor tag (low risk)", req, res); else alert(ctx, "vinfo-url-inject", "URL injection into tag <"+ tag +">", req, res); } } if((v != null) && (n.startsWith("on") || n.equals("style"))) { checkJavascriptXSS(ctx, req, res, v); } if(n.contains("vvv")) possibleXssAlert(ctx, req, res, n, n.indexOf("vvv"), "xss-inject", "Injected XSS tag into HTML attribute value"); } } if(tag.startsWith("vvv")) possibleXssAlert(ctx, req, res, tag, 0, "vinfo-xss-inject", "Injected XSS tag into HTML tag name"); if(tag.equals("style") || (tag.equals("script") && !remoteScript)) { String content = elem.getTextContent(); if(content != null) checkJavascriptXSS(ctx, req, res, content); } } private void possibleXssAlert(IInjectionModuleContext ctx, HttpUriRequest req, IHttpResponse res, String text, int offset, String type, String message) { final int[] xids = extractXssTag(text, offset); if(xids == null) return; final HttpUriRequest xssReq = ctx.getPathState().getXssRequest(xids[0], xids[1]); if(xssReq != null) alert(ctx, type, message, xssReq, res); else alert(ctx, type, message + " (from previous scan)", req, res); } private boolean match(String s, String ...options) { if(s == null) return false; for(int i = 0; i < options.length; i++) if(s.equals(options[i])) return true; return false; } private int[] extractXssTag(String text, int offset) { // 3 9 // vvv000000v000000 if(text.length() < (offset + 16)) return null; if(text.charAt(offset + 9) != 'v') return null; final int[] res = new int[2]; res[0] = extractXssId(text, offset + 3); res[1] = extractXssId(text, offset + 10); if(res[0] == -1 || res[1] == -1) return null; else return res; } private int extractXssId(String text, int offset) { if(text.length() < (offset + 6)) return -1; final String idStr = text.substring(offset, offset + 6); try { return Integer.parseInt(idStr); } catch (NumberFormatException e) { return -1; } } private void checkJavascriptXSS(IInjectionModuleContext ctx, HttpUriRequest req, IHttpResponse res, String text) { if(text == null) return; int lastWordIdx = 0; int idx = 0; boolean inQuote = false; boolean prevSpace = true; while(idx < text.length()) { idx = maybeSkipJavascriptComment(text, idx); if(idx >= text.length()) return; char c = text.charAt(idx); if(!inQuote && (c == '\'' || c == '"')) { inQuote = true; if(matchStartsWith(text, lastWordIdx, "innerHTML", "open", "url", "href", "write") && matchStartsWith(text, idx + 1, "//vega.invalid/", "http://vega.invalid", "vega:")) { alert(ctx, "vinfo-url-inject", "Injected URL in JS/CSS code", req, res); } } else if (c == '\'' || c == '"') { inQuote = false; } else if(!inQuote && text.startsWith("vvv", idx)) { possibleXssAlert(ctx, req, res, text, idx, "vinfo-xss-inject", "Injected syntax into JS/CSS code"); } else if(Character.isWhitespace(c) || c == '.') { prevSpace = true; } else if(prevSpace && Character.isLetterOrDigit(c)) { lastWordIdx = idx; prevSpace = false; } idx += 1; } } private boolean matchStartsWith(String str, int offset, String ...options) { for(int i = 0; i < options.length; i++) { if(str.startsWith(options[i], offset)) return true; } return false; } private int maybeSkipJavascriptComment(String text, int idx) { final int max = text.length(); final int savedIdx = idx; if(idx >= max) return idx; final char c = text.charAt(idx++); // Skip escaped characters here too if(c == '\\') return idx + 1; if(c != '/') return savedIdx; if(idx >= max) return idx; final char c1 = text.charAt(idx++); if(c1 == '/') { while(idx < max) { char cc = text.charAt(idx); if(cc == '\n' || c == '\r') return idx; idx += 1; } return max; } else if(c1 == '*') { int end = text.indexOf("*/", idx); if(end == -1) return max; else return end + 2; } return savedIdx; } private void alert(IInjectionModuleContext ctx, String type, String message, HttpUriRequest request, IHttpResponse response) { final String resource = request.getURI().toString(); final String key = createAlertKey(ctx, type, request); ctx.publishAlert(type, key, message, request, response, "resource", resource); } private String createAlertKey(IInjectionModuleContext ctx, String type, HttpUriRequest request) { if(ctx.getPathState().isParametric()) { final String uri = stripQuery(request.getURI()).toString(); return type + ":" + uri + ":" + ctx.getPathState().getFuzzableParameter().getName(); } else { return type + ":" + request.getURI(); } } private URI stripQuery(URI uri) { try { return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } } }