|
| 1 | +/** |
| 2 | + * Contributed by Astra Security (https://www.getastra.com/) |
| 3 | + * @author Prince Mendiratta <[email protected]> |
| 4 | + */ |
| 5 | + |
| 6 | +var pluginid = 100032; |
| 7 | + |
| 8 | +var URI = Java.type("org.apache.commons.httpclient.URI"); |
| 9 | +var HttpSender = Java.type("org.parosproxy.paros.network.HttpSender"); |
| 10 | +var Model = Java.type("org.parosproxy.paros.model.Model"); |
| 11 | +var HistoryReference = Java.type("org.parosproxy.paros.model.HistoryReference"); |
| 12 | +var Control = Java.type("org.parosproxy.paros.control.Control"); |
| 13 | +var ExtensionAlert = Java.type("org.zaproxy.zap.extension.alert.ExtensionAlert"); |
| 14 | +var Alert = Java.type("org.parosproxy.paros.core.scanner.Alert"); |
| 15 | + |
| 16 | +var session = Model.getSingleton().getSession(); |
| 17 | +var connectionParams = Model.getSingleton().getOptionsParam().getConnectionParam(); |
| 18 | +var extLoader = Control.getSingleton().getExtensionLoader(); |
| 19 | + |
| 20 | +// Print statements using script name |
| 21 | +function logger() { |
| 22 | + print("[" + this["zap.script.name"] + "] " + arguments[0]); |
| 23 | +} |
| 24 | + |
| 25 | +/** |
| 26 | + * Check for user enumeration on WordPress through author archives. |
| 27 | + * |
| 28 | + * A function which will be invoked against a specific "targeted" message. |
| 29 | + * |
| 30 | + * @param msg - the HTTP message being acted upon. This is an HttpMessage object. |
| 31 | + */ |
| 32 | +function invokeWith(msg) { |
| 33 | + |
| 34 | + var url = msg.getRequestHeader().getURI().toString(); |
| 35 | + var alertName = "WordPress Username Enumeration"; |
| 36 | + var alertDesc = "The username for user login has been exposed through author archives. This can allow for bruteforcing the password for the respective username and gain access to the admin dashboard."; |
| 37 | + var alertSol = "Make sure that URLs with query parameter " + url + "/?author={integer} and " + url + "/wp-json/wp/v2/users are not accessible publicly."; |
| 38 | + var alertReference = "https://www.getastra.com/blog/cms/wordpress-security/stop-user-enumeration/"; |
| 39 | + var cweId = 203; // Observable Discrepancy |
| 40 | + var wascId = 13; // Information Leakage |
| 41 | + |
| 42 | + // To check if script is running |
| 43 | + logger("Testing the following URL -> " + url); |
| 44 | + // Call function to check enumeration using /wp-json/wp/v2/users endpoint |
| 45 | + var jsonAuthors = archiveJson(msg); |
| 46 | + |
| 47 | + // Call function to check enumeration using /?author={i} endpoint |
| 48 | + var archiveAuthors = userEnumerate(msg); |
| 49 | + |
| 50 | + // If any username(s) found |
| 51 | + if (jsonAuthors.length) { |
| 52 | + logger("Vulnerable to user enumeration."); |
| 53 | + var alertEvidence1 = "Found author(s) -> " + jsonAuthors.join(", "); |
| 54 | + // Raise alert |
| 55 | + raiseAlert( |
| 56 | + pluginid, |
| 57 | + 3, // risk: 0: info, 1: low, 2: medium, 3: high |
| 58 | + 3, // confidence: 0: falsePositive, 1: low, 2: medium, 3: high, 4: confirmed |
| 59 | + alertName, |
| 60 | + alertDesc, |
| 61 | + alertEvidence1, |
| 62 | + alertSol, |
| 63 | + alertReference, |
| 64 | + cweId, |
| 65 | + wascId, |
| 66 | + msg, |
| 67 | + url + "/wp-json/wp/v2/users" |
| 68 | + ); |
| 69 | + } |
| 70 | + |
| 71 | + if (archiveAuthors.length) { |
| 72 | + logger("Vulnerable to user enumeration."); |
| 73 | + var alertEvidence = "Found author(s) -> "; |
| 74 | + for (var i in archiveAuthors) { |
| 75 | + alertEvidence += archiveAuthors[i].username + ", "; |
| 76 | + } |
| 77 | + url = url + "/?author=" + archiveAuthors[0].iterator; |
| 78 | + // Raise alert |
| 79 | + raiseAlert( |
| 80 | + pluginid, |
| 81 | + 3, // risk: 0: info, 1: low, 2: medium, 3: high |
| 82 | + 2, // confidence: 0: falsePositive, 1: low, 2: medium, 3: high, 4: confirmed |
| 83 | + alertName, |
| 84 | + alertDesc, |
| 85 | + alertEvidence, |
| 86 | + alertSol, |
| 87 | + alertReference, |
| 88 | + cweId, |
| 89 | + wascId, |
| 90 | + msg, |
| 91 | + url |
| 92 | + ); |
| 93 | + } |
| 94 | + logger("Script run completed."); |
| 95 | +} |
| 96 | + |
| 97 | +function archiveJson(msg) { |
| 98 | + logger("Testing feed enumeration"); |
| 99 | + var authors = []; |
| 100 | + var newReq = sendReq(msg, '/wp-json/wp/v2/users'); |
| 101 | + var responseHeader = newReq.getResponseHeader(); |
| 102 | + if (responseHeader.getStatusCode() === 200 && responseHeader.getHeader("Content-Type").contains('json')); { |
| 103 | + var responseBody = JSON.parse(newReq.getResponseBody().toString()); |
| 104 | + logger("200 response with JSON"); |
| 105 | + for (var i in responseBody) { |
| 106 | + authors.push(responseBody[i].slug); // Usernames (author names) |
| 107 | + } |
| 108 | + } |
| 109 | + return authors; |
| 110 | +} |
| 111 | + |
| 112 | +function userEnumerate(msg) { |
| 113 | + logger("Testing author enumeration"); |
| 114 | + // Array to store found usernames |
| 115 | + var authors = []; |
| 116 | + // Initialise iterator |
| 117 | + var i = 1; |
| 118 | + // To iterate first 10 possible enumerations |
| 119 | + while (i && i < 10) { |
| 120 | + var newReq = sendReq(msg, i); |
| 121 | + // If enum possible, 301 status code is constant |
| 122 | + if (newReq.getResponseHeader().getStatusCode() === 301) { |
| 123 | + // Get the redirection location |
| 124 | + var redirect = newReq.getResponseHeader().getHeader("Location"); |
| 125 | + // Extract username from Redirect Location |
| 126 | + try { |
| 127 | + var authorName = redirect.split("author/")[1].split("/")[0]; |
| 128 | + var authorEntry = { |
| 129 | + username: authorName, |
| 130 | + iterator: i |
| 131 | + }; |
| 132 | + authors.push(authorEntry); |
| 133 | + logger("Vulnerable Endpoint."); |
| 134 | + i++; |
| 135 | + } catch (err) { |
| 136 | + // If redirection but no username found |
| 137 | + if (err instanceof TypeError) { |
| 138 | + logger("Endpoint not vulnerable."); |
| 139 | + i++; |
| 140 | + } |
| 141 | + } |
| 142 | + } else { |
| 143 | + i++; |
| 144 | + } |
| 145 | + } |
| 146 | + return authors; |
| 147 | +} |
| 148 | + |
| 149 | +function sendReq(msg, query) { |
| 150 | + logger("Sending a request"); |
| 151 | + var newReq = msg.cloneRequest(); |
| 152 | + var uri = newReq.getRequestHeader().getURI(); |
| 153 | + isNaN(query) ? uri.setPath(query) : uri.setQuery("author=" + query); |
| 154 | + logger("URL -> " + uri.toString()); |
| 155 | + // Initialise the sender |
| 156 | + var sender = new HttpSender(connectionParams, true, 6); |
| 157 | + // Send and Receive Request |
| 158 | + sender.sendAndReceive(newReq); |
| 159 | + // Debugging |
| 160 | + // logger("Request Header -> " + newReq.getRequestHeader().toString()) |
| 161 | + // logger("Request Body -> " + newReq.getRequestBody().toString()) |
| 162 | + // logger("Response Header -> " + newReq.getResponseHeader().toString()) |
| 163 | + // logger("Raw Response Body -> " + newReq.getResponseBody().toString()) |
| 164 | + return newReq; |
| 165 | + |
| 166 | +} |
| 167 | + |
| 168 | +/** |
| 169 | + * Raise an alert. |
| 170 | + * @see https://www.javadoc.io/doc/org.zaproxy/zap/latest/org/parosproxy/paros/core/scanner/Alert.html |
| 171 | + */ |
| 172 | +function raiseAlert(pluginid, alertRisk, alertConfidence, alertName, alertDesc, alertEvidence, alertSol, alertReference, cweId, wascId, msg, url) { |
| 173 | + var extensionAlert = extLoader.getExtension(ExtensionAlert.NAME); |
| 174 | + var ref = new HistoryReference(session, HistoryReference.TYPE_ZAP_USER, msg); |
| 175 | + |
| 176 | + var alert = new Alert(pluginid, alertRisk, alertConfidence, alertName); |
| 177 | + alert.setDescription(alertDesc); |
| 178 | + alert.setEvidence(alertEvidence); |
| 179 | + alert.setSolution(alertSol); |
| 180 | + alert.setReference(alertReference); |
| 181 | + alert.setCweId(cweId); |
| 182 | + alert.setWascId(wascId); |
| 183 | + alert.setMessage(msg); |
| 184 | + alert.setUri(url); |
| 185 | + |
| 186 | + extensionAlert.alertFound(alert, ref); |
| 187 | +} |
0 commit comments