Skip to content

Commit 53ffac7

Browse files
authored
Merge pull request #247 from Prince-Mendiratta/main
Add WordPress Username Enumeration script
2 parents fedb23b + 8a9d3cc commit 53ffac7

File tree

2 files changed

+188
-0
lines changed

2 files changed

+188
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
77
### Added
88
- authentication/OfflineTokenRefresh.js - refresh oauth2 offline tokens
99
- httpsender/AddBearerTokenHeader.js - refresh oauth2 offline tokens
10+
- targeted/WordPress Username Enumeration.js - A targeted script to check for WordPress Username Enumeration via author archives
1011

1112
### Changed
1213
- Update minimum ZAP version to 2.11.0.
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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

Comments
 (0)