package utils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import org.owasp.esapi.ESAPI; import org.owasp.esapi.Encoder; import org.apache.log4j.Logger; import org.w3c.tidy.Tidy; /** * Class is responsible for finding valid XSS and CSRF attacks in user submissions * <br/><br/> * This file is part of the Security Shepherd Project. * * The Security Shepherd project is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version.<br/> * * The Security Shepherd project is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details.<br/> * * You should have received a copy of the GNU General Public License * along with the Security Shepherd project. If not, see <http://www.gnu.org/licenses/>. * @author Mark Denihan * */ public class FindXSS { private static org.apache.log4j.Logger log = Logger.getLogger(FindXSS.class); /** * Method used to detect valid java script in a user submission. Specifically the presence of a script that will execute an alert command. * Script tag, URI java script and java script triggers vectors are all including in this detection method. * @param xssString User XSS submission (After filter if any) * @return Boolean returned reflecting the presence of valid XSS attacks or not. */ public static String[] javascriptTriggers = { "onabort", "onbeforecopy", "onbeforecut", "onbeforepaste", "oncopy", "oncut", "oninput", "onkeydown", "onkeypress", "onkeyup", "onpaste", "onbeforeunload", "onhaschange", "onload", "onoffline", "ononline", "onreadystatechange", "onreadystatechange", "onstop", "onunload", "onreset", "onsubmit", "onclick", "oncontextmenu", "ondblclick", "onlosecapture", "onmouseenter", "onmousedown", "onmouseleave", "onmousemove", "onmouseout", "onmouseover", "onmouseup", "onmousewheel", "onscroll", "onmove", "onmoveend", "onmovestart", "ondrag", "ondragenter", "ondragleave", "ondragover", "ondragstart", "ondrop", "onresize", "onresizeend", "onresizestart", "onactivate", "onbeforeactivate", "onbeforedeactivate", "onbeforeeditfocus", "onblur", "ondeactivate", "onfocus", "onfocusin", "onfocusout", "oncontrolselect", "onselect", "onselectionchange", "onselectstart", "onafterprint", "onbeforeprint", "onhelp", "onerror", "onerrorupdate", "onafterupdate", "onbeforeupdate", "oncellchange", "ondataavailable", "ondatasetchanged", "ondatasetcomplete", "onrowenter", "onrowexit", "onrowsdelete", "onrowsinserted", "onbounce", "onfinish", "onstart", "onchange", "onwheel", "onfilterchange", "onpropertychange", "onsearch", "onmessage", "formaction", "textinput", "onhashchange", "onpagehide", "onpageshow", "onpopstate", "onstorage", "oninvalid", "ondragend", "oncanplay", "oncanplaythrough", "oncuechange", "ondurationchange", "onemptied", "onended", "onloadeddata", "onloadedmetadata", "onloadstart", "onpause", "onplay", "onplaying", "onprogress", "onratechange", "onseeked", "onseeking", "onstalled", "onsuspend", "ontimeupdate", "onvolumechange", "onwaiting", "onshow", "ontoggle"}; public static String[] uriAttributes = { "href", "src", "action" }; public static String[] colons = { ":", ":", ":", ":", ":" }; /** * Method used to validate GET request CSRF attacks embeded in IMG tags. * @param messageForAdmin * @param falseId * @return */ public static boolean findCsrf (String messageForAdmin, String falseId) { //Find a HTML tag while(messageForAdmin.contains("< ")) messageForAdmin = messageForAdmin.replaceAll("< ", "<"); while(messageForAdmin.contains(" >")) messageForAdmin = messageForAdmin.replaceAll(" >", ">"); log.debug("Cleaned to: " + messageForAdmin); log.debug("Checking for <img>"); if(messageForAdmin.contains("<img")) { log.debug("Possible <img>"); int tempStart = messageForAdmin.indexOf("<img"); int tempEnd = messageForAdmin.indexOf("/>", tempStart + 5); if(tempEnd == -1) { log.debug("Invalid <img> Tag"); } else { log.debug("Searching for SRC attribute"); String tempMessage = messageForAdmin.substring(tempStart, tempEnd); log.debug("Working on: " + tempMessage); if(tempMessage.contains(" src")) { log.debug("Finding src after '='"); int srcStart = tempMessage.indexOf(" src") + 4; tempMessage = tempMessage.substring(srcStart); log.debug("After SRC: " + tempMessage); int srcEqual = tempMessage.indexOf("=") + 1; log.debug("srcEqual = " + srcEqual); int counter = 0; while(tempMessage.substring(srcEqual + counter).startsWith(" ")) { //Find end of white space after equals sign, and then evaluate if the url is valid counter++; log.debug("counter = " + counter); } tempMessage = tempMessage.substring(srcEqual + counter); log.debug("Working on: " + tempMessage); String quoteType = null; if(tempMessage.startsWith("\"")) { quoteType = "\""; } else if(tempMessage.startsWith("'")) { quoteType = "'"; } else { log.debug("No Quotes found around url"); int endOfUrl = tempMessage.indexOf(" "); if(endOfUrl == -1) endOfUrl = tempMessage.length(); else endOfUrl--; log.debug(tempMessage); tempMessage = tempMessage.substring(0, endOfUrl); log.debug(tempMessage); } if(quoteType != null) { log.debug("Quotes Found: " + quoteType); tempMessage = tempMessage.substring(1, tempMessage.substring(2).indexOf(quoteType) + 2); } log.debug("URL found to be: " + tempMessage); boolean validUrl = false; log.debug("Validating URL for Solution"); try { URL csrfUrl = new URL(tempMessage); log.debug("URL Host: " + csrfUrl.getHost()); log.debug("URL Port: " + csrfUrl.getPort()); log.debug("URL Path: " + csrfUrl.getPath()); log.debug("URL Query: " + csrfUrl.getQuery()); validUrl = csrfUrl.getPath().toLowerCase().equalsIgnoreCase("/root/grantComplete/csrflesson"); if(!validUrl) log.debug("1"); validUrl = csrfUrl.getQuery().toLowerCase().equalsIgnoreCase(("userId=" + falseId).toLowerCase()) && validUrl; if(!validUrl) log.debug("2"); } catch(MalformedURLException e) { log.error("Invalid URL: " + e.toString()); } if(!validUrl) { log.debug("Invalid Url: " + tempMessage); } else { log.debug("Valid URL"); return true; } } } } return false; } /** * Searches for URL that contains CSRF attack string without user ID expected. Returns true if it is valid based on parameters submitted * @param theUrl The Entire URL containing the attack * @param csrfAttackPath The path the CSRF vulnerable function should be in * @return boolean value depicting if the attack is valid or not */ public static boolean findCsrfAttackUrl (String theUrl, String csrfAttackPath) { boolean validAttack = false; try { URL theAttack = new URL(theUrl); log.debug("theAttack Host: " + theAttack.getHost()); log.debug("theAttack Port: " + theAttack.getPort()); log.debug("theAttack Path: " + theAttack.getPath()); log.debug("theAttack Query: " + theAttack.getQuery()); validAttack = theAttack.getPath().toLowerCase().equalsIgnoreCase(csrfAttackPath); if(!validAttack) log.debug("Invalid Solution: Bad Path or Above"); } catch(MalformedURLException e) { log.debug("Invalid URL Submitted: " + e.toString()); validAttack = false; } catch(Exception e) { log.error("FindCSRF Failed: " + e.toString()); validAttack = false; } return validAttack; } /** * Searches for URL that contains CSRF attack string. Returns true if it is valid based on parameters submitted * @param theUrl The Entire URL containing the attack * @param csrfAttackPath The path the CSRF vulnerable function should be in * @param userIdParameterName The user ID parameter name expected * @param userIdParameterValue The user ID parameter value expected * @return boolean value depicting if the attack is valid or not */ public static boolean findCsrfAttackUrl (String theUrl, String csrfAttackPath, String userIdParameterName, String userIdParameterValue ) { boolean validAttack = false; try { URL theAttack = new URL(theUrl); log.debug("csrfAttackPath: " + csrfAttackPath); log.debug("theAttack Host: " + theAttack.getHost()); log.debug("theAttack Port: " + theAttack.getPort()); log.debug("theAttack Path: " + theAttack.getPath()); log.debug("theAttack Query: " + theAttack.getQuery()); validAttack = theAttack.getPath().toLowerCase().equalsIgnoreCase(csrfAttackPath); if(!validAttack) log.debug("Invalid Solution: Bad Path or Above"); validAttack = theAttack.getQuery().toLowerCase().equalsIgnoreCase((userIdParameterName + "=" + userIdParameterValue).toLowerCase()) && validAttack; if(!validAttack) log.debug("Invalid Solution: Bad Query or Above"); } catch(MalformedURLException e) { log.debug("Invalid URL Submitted: " + e.toString()); validAttack = false; } catch(Exception e) { log.error("FindCSRF Failed: " + e.toString()); validAttack = false; } return validAttack; } /** * Forms XSS Input for XHTML before Searching with Shepherd XSS Detector * @param xssString Untrusted User Input * @return Boolean value depicting if XSS was detected */ public static boolean search (String xssString) { boolean xssDetected = false; log.debug("String to Search: " + xssString); //Need to tidy submitted string, similar to how a browser would when it interprets it Tidy tidy = new Tidy(); tidy.setXHTML(true); tidy.setQuiet(true); tidy.setShowWarnings(false); InputStream inputStream = new ByteArrayInputStream(xssString.getBytes()); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); tidy.parseDOM(inputStream, outputStream); String tidyHtml = outputStream.toString().toLowerCase(); try { outputStream.close(); inputStream.close(); } catch(Exception e) { log.error("Could not Cloud Tidy Input/Output Streams: " + e.toString()); } // log.debug("String Tidied To: " + tidyHtml); //Now to Parse it and narrow down to the Body of the output Document parsedHtml = Jsoup.parseBodyFragment(tidyHtml); Element htmlBody = parsedHtml.body(); //Now We're in Search Territory. Three main Stages //Stage One: Detect <script> tags Elements scriptTags = htmlBody.getElementsByTag("script"); for(Element scriptTag: scriptTags) { String tagContents = scriptTag.html(); //log.debug("tagContents: " + tagContents); if(tagContents.contains("alert")) { log.debug("Script Tags detected"); xssDetected = true; break; } } if(!xssDetected) //If Stage One failed, Move onto Stage 2 and 3 { //Stage Two/three look for different types of Attribute Based XSS //Search through every element Elements elements = htmlBody.getAllElements(); for(Element element: elements) { //Stage Two: Look for URI attributes. //Don't really care if they're in the correct element, the vector would have worked if they had it in the right one. //This way we'll return true on elements that newly support URI attributes in browsers for(int i = 0; i < uriAttributes.length && !xssDetected; i++) { String uriAttributeValue = element.attr(uriAttributes[i]); if (!uriAttributeValue.isEmpty()) { log.debug("Found: " + uriAttributes[i] + " attribute"); log.debug("Value: " + uriAttributeValue); //URI Attack Vectors can be Encoded for HTML and still be interpreted by browsers if(uriAttributeValue.contains("&")) { log.debug("HTML Encoded URI attriute detected"); Encoder encoder = ESAPI.encoder(); uriAttributeValue = encoder.decodeForHTML(uriAttributeValue); log.debug("Decoded Attribute = " + uriAttributeValue); } //URI Attacks need a Colon after data or javascript - Need to find that else it is invalid boolean colonFound = false; for(int colonCount = 0; colonCount < colons.length; colonCount++) { colonFound = uriAttributeValue.contains(colons[colonCount]); if(colonFound) { // log.debug("Detected colon in the form of '" + colons[colonCount] + "'"); uriAttributeValue = uriAttributeValue.substring(0, uriAttributeValue.indexOf(colons[colonCount])); //log.debug("URI Before colon: " + uriAttributeValue); break; } } if(uriAttributeValue.equalsIgnoreCase("data") || uriAttributeValue.equalsIgnoreCase("javascript")) { log.debug("URI XSS Detected"); xssDetected = true; } } // else URI attribute not detected } //Stage Three: JavaScript Events if(!xssDetected) //If a URI XSS Vector was detected, we can skip Stage Three { for(int i = 0; i < javascriptTriggers.length && !xssDetected; i++) { String javascriptTriggerValue = element.attr(javascriptTriggers[i]); if(!javascriptTriggerValue.isEmpty()) { if(javascriptTriggerValue.startsWith("alert")) { log.debug("Javascript Trigger XSS Detected"); xssDetected = true; } } } } if(xssDetected) //XSS was detected in this element: Break out of For Loop { break; } } } return xssDetected; } }