Java tutorial
/* * 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.ascanrulesAlpha; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.URI; import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; 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.HttpMessage; import org.zaproxy.zap.model.Vulnerabilities; import org.zaproxy.zap.model.Vulnerability; /** * a scanner that looks for backup files disclosed on the web server * * @author 70pointer * */ public class BackupFileDisclosure extends AbstractAppPlugin { int numExtensionsToTry = 0; int numSuffixesToTry = 0; int numPrefixesToTry = 0; boolean doSwitchFileExtension = false; /** * the ordered set of (lowercase) file extensions to try */ static final Set<String> fileExtensions = new LinkedHashSet<String>(Arrays.asList(new String[] { ".bak", ".backup", ".bac", ".zip", ".tar", ".jar", ".log", ".swp", "~" /* no "." */ , ".old", ".~bk", ".orig", ".tmp", ".exe", ".0", ".1", ".2", ".3", ".gz", ".bz2", ".7z", ".s7z", ".lz", ".z", ".lzma", ".lzo", ".apk", ".cab", ".rar", ".war", ".ear", ".tar.gz", ".tgz", ".tar.z", ".tar.bz2", ".tbz2", ".tar.lzma", ".tlz", ".zipx", ".iso", ".src", ".dev", ".a", ".a", ".ar", ".cbz", ".cpio", ".shar", ".lbr", ".lbr", ".mar", ".f", ".rz", ".sfark", ".xz", ".ace", ".afa", ".alz", ".arc", ".arj", ".ba", ".bh", ".cfs", ".cpt", ".dar", ".dd", ".dgc", ".dmg", ".gca", ".ha", ".hki", ".ice", ".inc", ".j", ".kgb", ".lhz", ".lha", ".lzk", ".pak", ".partimg.", ".paq6", ".paq7", ".paq8", ".pea", ".pim", ".pit", ".qda", ".rk", ".sda", ".sea", ".sen", ".sfx", ".sit", ".sitx", ".sqx", "s.xz", ".tar.7z", ".tar.xz", ".uc", ".uc0", ".uc2", ".ucn", ".ur2", ".ue2", ".uca", ".uha", ".wim", ".xar", ".xp3", ".yz1", ".zoo", ".zpaq", ".zz", ".include" //extensions that get appended without the dot. //these are fairly random, and are included to catch the remainder of the wavsep test cases, but are not likely in the real world //which is why they are included at the very end, and will only be tried in "Insane" mode. Keepin' it real... :) , "1", "_1", "2", "_2", "x", "_x", "bak", "_bak", "old", "_old", "a", "b", "c", "d", "e", "f", "_a", "_b", "_c", "_d", "_e", "_f", "inc", "_inc", "_backup" })); /** * the ordered set of file suffixes to try (after the file path and file name, but immediately before the extension) * also used as directory suffixes for the parent folder of the file */ static final Set<String> fileSuffixes = new LinkedHashSet<String>( Arrays.asList(new String[] { " - Copy", " - Copy (2)", " - Copy (3)", "backup", "_backup", "-backup", "bak", "_bak", "-bak", "old", "_old", "-old", "1", "-1", "_1", "2" //,".2" //see above , "-2", "_2", " - Copy - Copy" //a copy of a copy! :) , "(copy)", "(another copy)", "(second copy)", "(third copy)", "(fourth copy)", "(2nd copy)", "(3rd copy)", "(4th copy)", " (copy)", " (another copy)", " (second copy)", " (third copy)", " (fourth copy)", " (2nd copy)", " (3rd copy)", " (4th copy)" })); /** * the ordered set of file prefixes to try (after the file path and file name, but immediately before the extension) */ static final Set<String> filePrefixes = new LinkedHashSet<String>( Arrays.asList(new String[] { "Copy of ", "Copy (2) of ", "Copy (3) of ", "Copy of Copy of " //a copy of a copy! , "backup", "backup_", "backup-", "bak", "bak_", "bak-", "old", "old_", "old-", "1", "1_", "1-", "2", "2_", "2-" })); /** * details of the vulnerability which we are attempting to find * 34 = "Predictable Resource Location" */ private static Vulnerability vuln = Vulnerabilities.getVulnerability("wasc_34"); /** * the logger object */ private static Logger log = Logger.getLogger(BackupFileDisclosure.class); /** * returns the plugin id */ @Override public int getId() { return 10095; } /** * returns the name of the plugin */ @Override public String getName() { return Constant.messages.getString("ascanalpha.backupfiledisclosure.name"); } @Override public String[] getDependency() { return null; } @Override public String getDescription() { if (vuln != null) { return vuln.getDescription(); } return "Failed to load vulnerability description from file"; } @Override public int getCategory() { return Category.INFO_GATHER; } @Override public String getSolution() { if (vuln != null) { return vuln.getSolution(); } return "Failed to load vulnerability solution from file"; } @Override public String getReference() { if (vuln != null) { StringBuilder sb = new StringBuilder(); for (String ref : vuln.getReferences()) { if (sb.length() > 0) { sb.append('\n'); } sb.append(ref); } return sb.toString(); } return "Failed to load vulnerability reference from file"; } @Override public void init() { switch (this.getAttackStrength()) { case DEFAULT: numExtensionsToTry = 10; numSuffixesToTry = 3; numPrefixesToTry = 2; doSwitchFileExtension = false; break; case LOW: numExtensionsToTry = 3; numSuffixesToTry = 2; numPrefixesToTry = 0; doSwitchFileExtension = false; break; case MEDIUM: numExtensionsToTry = 10; numSuffixesToTry = 3; numPrefixesToTry = 2; doSwitchFileExtension = false; break; case HIGH: numExtensionsToTry = 20; numSuffixesToTry = 5; numPrefixesToTry = 4; doSwitchFileExtension = true; break; case INSANE: numExtensionsToTry = fileExtensions.size(); numSuffixesToTry = fileSuffixes.size(); numPrefixesToTry = filePrefixes.size(); doSwitchFileExtension = true; break; } } @Override public void scan() { if (log.isDebugEnabled()) { log.debug("Attacking at Attack Strength: " + this.getAttackStrength()); log.debug("Checking [" + getBaseMsg().getRequestHeader().getMethod() + "] [" + getBaseMsg().getRequestHeader().getURI() + "], for Backup File Disclosure"); } try { URI uri = this.getBaseMsg().getRequestHeader().getURI(); String filename = uri.getName(); int statusCode = this.getBaseMsg().getResponseHeader().getStatusCode(); if (log.isDebugEnabled()) log.debug("About to look for a backup for '" + uri.getURI() + "', which returned " + statusCode); //is it worth looking for a copy of the file? //why do we even get a status code of 0? //eliminate it from our enquiries, because it causes nothing but false positives. if (statusCode == HttpStatus.SC_NOT_FOUND || statusCode == 0) { if (log.isDebugEnabled()) log.debug("The original file request was not successfuly retrieved (status = " + statusCode + "), so there is not much point in looking for a backup of a non-existent file!"); return; } if (filename != null && filename.length() > 0) { //there is a file name at the end of the path, so look for a backup file for the file findBackupFile(this.getBaseMsg()); } else { if (log.isDebugEnabled()) { log.debug( "The URI has no filename component, so there is not much point in looking for a corresponding backup file!"); } } } catch (Exception e) { log.error("Error scanning a request for Backup File Disclosure: " + e.getMessage(), e); } } @Override public int getRisk() { return Alert.RISK_MEDIUM; //Medium or maybe High.. depends on the file. } @Override public int getCweId() { return 425; //Direct Request ('Forced Browsing') } @Override public int getWascId() { return 34; //Predictable Resource Location } /** * attempts to find a backup file for the given file * @param uri the URI of a file * @return */ private void findBackupFile(HttpMessage originalMessage) throws Exception { try { boolean gives404s = true; boolean parentgives404s = true; byte[] nonexistparentmsgdata = null; URI originalURI = originalMessage.getRequestHeader().getURI(); //request a file in the same directory to see how it handles "File not found". Using a 404? Something else? String temppath = originalURI.getPath(); if (temppath == null) temppath = ""; int slashposition = temppath.lastIndexOf("/"); if (slashposition < 0) { //WTF? there was no slash in the path.. throw new Exception("The message has a path with a malformed path component"); } String filename = originalMessage.getRequestHeader().getURI().getName(); String randomfilename = RandomStringUtils.random(filename.length(), "abcdefghijklmoopqrstuvwxyz9123456789"); String randomfilepath = temppath.substring(0, slashposition) + "/" + randomfilename; if (log.isDebugEnabled()) log.debug("Trying non-existent file: " + randomfilepath); HttpMessage nonexistfilemsg = new HttpMessage( new URI(originalURI.getScheme(), originalURI.getAuthority(), randomfilepath, null, null)); try { nonexistfilemsg.setCookieParams(originalMessage.getCookieParams()); } catch (Exception e) { if (log.isDebugEnabled()) log.debug("Could not set the cookies from the base request:" + e); } sendAndReceive(nonexistfilemsg, false); byte[] nonexistfilemsgdata = nonexistfilemsg.getResponseBody().getBytes(); //does the server give a 404 for a non-existent file? if (nonexistfilemsg.getResponseHeader().getStatusCode() != HttpStatus.SC_NOT_FOUND) { gives404s = false; if (log.isDebugEnabled()) log.debug("The server does not return a 404 status for a non-existent path: " + nonexistfilemsg.getRequestHeader().getURI().getURI()); } else { gives404s = true; if (log.isDebugEnabled()) log.debug("The server gives a 404 status for a non-existent path: " + nonexistfilemsg.getRequestHeader().getURI().getURI()); } //now request a different (and non-existent) parent directory, //to see whether a non-existent parent folder causes a 404 String[] pathbreak = temppath.split("/"); if (pathbreak.length > 2) { //the file has a parent folder that is not the root folder (ie, there is a parent folder to mess with) String[] temppathbreak = pathbreak; String parentfoldername = pathbreak[pathbreak.length - 2]; String randomparentfoldername = RandomStringUtils.random(parentfoldername.length(), "abcdefghijklmoopqrstuvwxyz9123456789"); //replace the parent folder name with the random one, and build it back into a string temppathbreak[pathbreak.length - 2] = randomparentfoldername; String randomparentpath = StringUtils.join(temppathbreak, "/"); if (log.isDebugEnabled()) log.debug("Trying non-existent parent path: " + randomparentpath); HttpMessage nonexistparentmsg = new HttpMessage( new URI(originalURI.getScheme(), originalURI.getAuthority(), randomparentpath, null, null)); try { nonexistparentmsg.setCookieParams(originalMessage.getCookieParams()); } catch (Exception e) { if (log.isDebugEnabled()) log.debug("Could not set the cookies from the base request:" + e); } sendAndReceive(nonexistparentmsg, false); nonexistparentmsgdata = nonexistparentmsg.getResponseBody().getBytes(); //does the server give a 404 for a non-existent parent folder? if (nonexistparentmsg.getResponseHeader().getStatusCode() != HttpStatus.SC_NOT_FOUND) { parentgives404s = false; if (log.isDebugEnabled()) log.debug("The server does not return a 404 status for a non-existent parent path: " + nonexistparentmsg.getRequestHeader().getURI().getURI()); } else { parentgives404s = true; if (log.isDebugEnabled()) log.debug("The server gives a 404 status for a non-existent parent path: " + nonexistparentmsg.getRequestHeader().getURI().getURI()); } } String actualfilename = originalURI.getName(); String actualfileExtension = null; String path = originalURI.getPath(); if (path == null) path = ""; //record the position of the various injection points, always relative to the full path int positionExtensionInjection = 0; int positionFileSuffixInjection = 0; if (actualfilename.contains(".")) { positionExtensionInjection = path.lastIndexOf("."); positionFileSuffixInjection = positionExtensionInjection; actualfileExtension = actualfilename.substring(actualfilename.lastIndexOf(".")); } else { positionExtensionInjection = path.length(); positionFileSuffixInjection = path.length(); actualfileExtension = ""; } int positionFilePrefixInjection = path.lastIndexOf("/") + 1; int positionDirectorySuffixInjection = path.lastIndexOf("/"); int positionDirectoryPrefixInjection = 0; if (positionDirectorySuffixInjection >= 0) positionDirectoryPrefixInjection = path.substring(0, positionDirectorySuffixInjection) .lastIndexOf("/") + 1; //the set of files we will try, in the order of insertion Set<URI> candidateBackupFileURIs = new LinkedHashSet<URI>(); Set<URI> candidateBackupFileChangedFolderURIs = new LinkedHashSet<URI>(); //for a changed parent folder name, which we need to handle separately log.debug("The path is " + path); //for each file extension to try (both appending, and replacing) int counted = 0; for (String fileExtensionToTry : fileExtensions) { //to append, inject the file extension at the end of the path String candidateBackupFilePath = path + fileExtensionToTry; log.debug("File Extension (append): '" + candidateBackupFilePath + "'"); candidateBackupFileURIs.add(new URI(originalURI.getScheme(), originalURI.getAuthority(), candidateBackupFilePath, null, null)); //to replace the extension, append the file extension at positionExtensionInjection candidateBackupFilePath = path.substring(0, positionExtensionInjection) + fileExtensionToTry; log.debug("File Extension (replace): '" + candidateBackupFilePath + "'"); candidateBackupFileURIs.add(new URI(originalURI.getScheme(), originalURI.getAuthority(), candidateBackupFilePath, null, null)); //to switch the extension (if there was one), append the file extension at positionExtensionInjection if (!actualfileExtension.equals("") && doSwitchFileExtension) { candidateBackupFilePath = path.substring(0, positionExtensionInjection) + fileExtensionToTry + actualfileExtension; log.debug("File Extension (switch): '" + candidateBackupFilePath + "'"); candidateBackupFileURIs.add(new URI(originalURI.getScheme(), originalURI.getAuthority(), candidateBackupFilePath, null, null)); } counted++; if (counted > numExtensionsToTry) { break; //out of the loop. } } //for each file suffix to try counted = 0; for (String fileSuffixToTry : fileSuffixes) { //inject the file suffix at positionFileSuffixInjection String candidateBackupFilePath = path.substring(0, positionFileSuffixInjection) + fileSuffixToTry + (positionFileSuffixInjection >= path.length() ? "" : path.substring(positionFileSuffixInjection)); log.debug("File Suffix (insert): '" + candidateBackupFilePath + "'"); candidateBackupFileURIs.add(new URI(originalURI.getScheme(), originalURI.getAuthority(), candidateBackupFilePath, null, null)); counted++; if (counted > numSuffixesToTry) { break; //out of the loop. } } //for each file prefix to try counted = 0; for (String filePrefixToTry : filePrefixes) { //inject the file prefix at positionFilePrefixInjection String candidateBackupFilePath = path.substring(0, positionFilePrefixInjection) + filePrefixToTry + (positionFilePrefixInjection >= path.length() ? "" : path.substring(positionFilePrefixInjection)); log.debug("File Prefix (insert): '" + candidateBackupFilePath + "'"); candidateBackupFileURIs.add(new URI(originalURI.getScheme(), originalURI.getAuthority(), candidateBackupFilePath, null, null)); counted++; if (counted > numPrefixesToTry) { break; //out of the loop. } } //for each directory suffix/prefix to try (using the file prefixes/suffixes - or whatever the plural of prefix/suffix is) counted = 0; if (pathbreak.length > 2) { //if there is a a parent folder to play with for (String fileSuffixToTry : fileSuffixes) { //inject the directory suffix at positionDirectorySuffixInjection String candidateBackupFilePath = path.substring(0, positionDirectorySuffixInjection) + fileSuffixToTry + (positionDirectorySuffixInjection >= path.length() ? "" : path.substring(positionDirectorySuffixInjection)); log.debug("Directory Suffix (insert): '" + candidateBackupFilePath + "'"); candidateBackupFileChangedFolderURIs.add(new URI(originalURI.getScheme(), originalURI.getAuthority(), candidateBackupFilePath, null, null)); counted++; if (counted > numSuffixesToTry) { break; //out of the loop. } } for (String filePrefixToTry : filePrefixes) { //inject the directory prefix at positionDirectorySuffixInjection String candidateBackupFilePath = path.substring(0, positionDirectoryPrefixInjection) + filePrefixToTry + (positionDirectoryPrefixInjection >= path.length() ? "" : path.substring(positionDirectoryPrefixInjection)); log.debug("Directory Suffix (insert): '" + candidateBackupFilePath + "'"); candidateBackupFileChangedFolderURIs.add(new URI(originalURI.getScheme(), originalURI.getAuthority(), candidateBackupFilePath, null, null)); counted++; if (counted > numSuffixesToTry) { break; //out of the loop. } } } //now we have a set of candidate URIs appropriate to the attack strength chosen by the user //try each candidate URI in turn. for (URI candidateBackupFileURI : candidateBackupFileURIs) { byte[] disclosedData = {}; if (log.isDebugEnabled()) log.debug("Trying possible backup file path: " + candidateBackupFileURI.getURI()); HttpMessage requestmsg = new HttpMessage(candidateBackupFileURI); try { requestmsg.setCookieParams(originalMessage.getCookieParams()); } catch (Exception e) { if (log.isDebugEnabled()) log.debug("Could not set the cookies from the base request:" + e); } //Do not follow redirects. They're evil. Yep. sendAndReceive(requestmsg, false); disclosedData = requestmsg.getResponseBody().getBytes(); int requestStatusCode = requestmsg.getResponseHeader().getStatusCode(); //just to complicate things.. I have a test case which for the random file, does NOT give a 404 (so gives404s == false) //but for a "Copy of" file, actually gives a 404 (for some unknown reason). We need to handle this case. if ((gives404s && requestStatusCode != HttpStatus.SC_NOT_FOUND) || ((!gives404s) && requestStatusCode != HttpStatus.SC_NOT_FOUND && (!Arrays.equals(disclosedData, nonexistfilemsgdata)))) { bingo(Alert.RISK_MEDIUM, Alert.WARNING, Constant.messages.getString("ascanalpha.backupfiledisclosure.name"), Constant.messages.getString("ascanalpha.backupfiledisclosure.desc"), requestmsg.getRequestHeader().getURI().getURI(), // originalMessage.getRequestHeader().getURI().getURI(), null, //parameter being attacked: none. candidateBackupFileURI.getURI(), //attack originalMessage.getRequestHeader().getURI().getURI(), //new String (disclosedData), //extrainfo Constant.messages.getString("ascanalpha.backupfiledisclosure.soln"), Constant.messages.getString("ascanalpha.backupfiledisclosure.evidence", originalURI, candidateBackupFileURI.getURI()), requestmsg //originalMessage ); } if (isStop()) { if (log.isDebugEnabled()) log.debug("The scanner was stopped in response to a user request"); return; } } //now try the changed parent folders (if any) //the logic here needs to check using the parent 404 logic, and the output for a non-existent parent folder. for (URI candidateBackupFileURI : candidateBackupFileChangedFolderURIs) { byte[] disclosedData = {}; if (log.isDebugEnabled()) log.debug("Trying possible backup file path (with changed parent folder): " + candidateBackupFileURI.getURI()); HttpMessage requestmsg = new HttpMessage(candidateBackupFileURI); try { requestmsg.setCookieParams(originalMessage.getCookieParams()); } catch (Exception e) { if (log.isDebugEnabled()) log.debug("Could not set the cookies from the base request:" + e); } //Do not follow redirects. They're evil. Yep. sendAndReceive(requestmsg, false); disclosedData = requestmsg.getResponseBody().getBytes(); int requestStatusCode = requestmsg.getResponseHeader().getStatusCode(); if ((parentgives404s && requestStatusCode != HttpStatus.SC_NOT_FOUND) || ((!parentgives404s) && requestStatusCode != HttpStatus.SC_NOT_FOUND && (!Arrays.equals(disclosedData, nonexistparentmsgdata)))) { bingo(Alert.RISK_MEDIUM, Alert.WARNING, Constant.messages.getString("ascanalpha.backupfiledisclosure.name"), Constant.messages.getString("ascanalpha.backupfiledisclosure.desc"), requestmsg.getRequestHeader().getURI().getURI(), //originalMessage.getRequestHeader().getURI().getURI(), null, //parameter being attacked: none. candidateBackupFileURI.getURI(), //attack originalMessage.getRequestHeader().getURI().getURI(), //new String (disclosedData), //extrainfo Constant.messages.getString("ascanalpha.backupfiledisclosure.soln"), Constant.messages.getString("ascanalpha.backupfiledisclosure.evidence", originalURI, candidateBackupFileURI.getURI()), requestmsg //originalMessage ); } if (isStop()) { if (log.isDebugEnabled()) log.debug("The scanner was stopped in response to a user request"); return; } } } catch (Exception e) { e.printStackTrace(); log.error("Some error occurred when looking for a backup file for '" + originalMessage.getRequestHeader().getURI() + "': " + e); return; } } }