Skip to content

Commit 59a7887

Browse files
committed
ascanrules: CMDi split timing tests to new scan rule
Signed-off-by: kingthorin <[email protected]>
1 parent ae2927a commit 59a7887

File tree

8 files changed

+780
-500
lines changed

8 files changed

+780
-500
lines changed

addOns/ascanrules/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1414
- SQL Injection - Hypersonic
1515
- SQL Injection - SQLite
1616
- SQL Injection - PostgreSQL
17+
- The Remote OS Command Injection scan rule has been broken into two rules; one feedback based, and one time based (Issue 7341). This includes assigning the time based rule ID 90037.
1718

1819
### Added
1920
- Rules (as applicable) have been tagged in relation to HIPAA and PCI DSS.

addOns/ascanrules/src/main/java/org/zaproxy/zap/extension/ascanrules/CommandInjectionScanRule.java

Lines changed: 14 additions & 285 deletions
Large diffs are not rendered by default.
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
/*
2+
* Zed Attack Proxy (ZAP) and its related class files.
3+
*
4+
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
5+
*
6+
* Copyright 2025 The ZAP Development Team
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
package org.zaproxy.zap.extension.ascanrules;
21+
22+
import java.io.IOException;
23+
import java.net.SocketException;
24+
import java.util.Collections;
25+
import java.util.HashMap;
26+
import java.util.Iterator;
27+
import java.util.LinkedList;
28+
import java.util.List;
29+
import java.util.Map;
30+
import java.util.concurrent.atomic.AtomicReference;
31+
import org.apache.commons.configuration.ConversionException;
32+
import org.apache.logging.log4j.LogManager;
33+
import org.apache.logging.log4j.Logger;
34+
import org.parosproxy.paros.Constant;
35+
import org.parosproxy.paros.core.scanner.Alert;
36+
import org.parosproxy.paros.core.scanner.Category;
37+
import org.parosproxy.paros.network.HttpMessage;
38+
import org.zaproxy.addon.commonlib.CommonAlertTag;
39+
import org.zaproxy.addon.commonlib.timing.TimingUtils;
40+
import org.zaproxy.addon.commonlib.vulnerabilities.Vulnerabilities;
41+
import org.zaproxy.addon.commonlib.vulnerabilities.Vulnerability;
42+
import org.zaproxy.zap.extension.ruleconfig.RuleConfigParam;
43+
import org.zaproxy.zap.model.Tech;
44+
import org.zaproxy.zap.model.TechSet;
45+
46+
/** Active scan rule for time based Command Injection testing and verification. */
47+
public class CommandInjectionTimingScanRule extends CommandInjectionScanRule
48+
implements CommonActiveScanRuleInfo {
49+
50+
private static final int PLUGIN_ID = 90037;
51+
52+
/** The name of the rule config to obtain the time, in seconds, for time-based attacks. */
53+
private static final String RULE_SLEEP_TIME = RuleConfigParam.RULE_COMMON_SLEEP_TIME;
54+
55+
private static final Map<String, String> ALERT_TAGS;
56+
57+
static {
58+
Map<String, String> alertTags =
59+
new HashMap<>(CommonAlertTag.toMap(CommonAlertTag.TEST_TIMING));
60+
alertTags.putAll(CommandInjectionScanRule.ALERT_TAGS);
61+
ALERT_TAGS = Collections.unmodifiableMap(alertTags);
62+
}
63+
64+
/** The default number of seconds used in time-based attacks (i.e. sleep commands). */
65+
private static final int DEFAULT_TIME_SLEEP_SEC = 5;
66+
67+
// limit the maximum number of requests sent for time-based attack detection
68+
private static final int BLIND_REQUESTS_LIMIT = 4;
69+
70+
// error range allowable for statistical time-based blind attacks (0-1.0)
71+
private static final double TIME_CORRELATION_ERROR_RANGE = 0.15;
72+
private static final double TIME_SLOPE_ERROR_RANGE = 0.30;
73+
74+
// *NIX Blind OS Command constants
75+
private static final String NIX_BLIND_TEST_CMD = "sleep {0}";
76+
// Windows Blind OS Command constants
77+
private static final String WIN_BLIND_TEST_CMD = "timeout /T {0}";
78+
// PowerSHell Blind Command constants
79+
private static final String PS_BLIND_TEST_CMD = "start-sleep -s {0}";
80+
81+
// OS Command payloads for blind command Injection testing
82+
private static final List<String> NIX_BLIND_OS_PAYLOADS = new LinkedList<>();
83+
private static final List<String> WIN_BLIND_OS_PAYLOADS = new LinkedList<>();
84+
private static final List<String> PS_BLIND_PAYLOADS = new LinkedList<>();
85+
86+
static {
87+
// No quote payloads
88+
NIX_BLIND_OS_PAYLOADS.add("&" + NIX_BLIND_TEST_CMD + "&");
89+
NIX_BLIND_OS_PAYLOADS.add(";" + NIX_BLIND_TEST_CMD + ";");
90+
WIN_BLIND_OS_PAYLOADS.add("&" + WIN_BLIND_TEST_CMD);
91+
WIN_BLIND_OS_PAYLOADS.add("|" + WIN_BLIND_TEST_CMD);
92+
PS_BLIND_PAYLOADS.add(";" + PS_BLIND_TEST_CMD);
93+
94+
// Double quote payloads
95+
NIX_BLIND_OS_PAYLOADS.add("\"&" + NIX_BLIND_TEST_CMD + "&\"");
96+
NIX_BLIND_OS_PAYLOADS.add("\";" + NIX_BLIND_TEST_CMD + ";\"");
97+
WIN_BLIND_OS_PAYLOADS.add("\"&" + WIN_BLIND_TEST_CMD + "&\"");
98+
WIN_BLIND_OS_PAYLOADS.add("\"|" + WIN_BLIND_TEST_CMD);
99+
PS_BLIND_PAYLOADS.add("\";" + PS_BLIND_TEST_CMD);
100+
101+
// Single quote payloads
102+
NIX_BLIND_OS_PAYLOADS.add("'&" + NIX_BLIND_TEST_CMD + "&'");
103+
NIX_BLIND_OS_PAYLOADS.add("';" + NIX_BLIND_TEST_CMD + ";'");
104+
WIN_BLIND_OS_PAYLOADS.add("'&" + WIN_BLIND_TEST_CMD + "&'");
105+
WIN_BLIND_OS_PAYLOADS.add("'|" + WIN_BLIND_TEST_CMD);
106+
PS_BLIND_PAYLOADS.add("';" + PS_BLIND_TEST_CMD);
107+
108+
// Special payloads
109+
NIX_BLIND_OS_PAYLOADS.add("\n" + NIX_BLIND_TEST_CMD + "\n"); // force enter
110+
NIX_BLIND_OS_PAYLOADS.add("`" + NIX_BLIND_TEST_CMD + "`"); // backtick execution
111+
NIX_BLIND_OS_PAYLOADS.add("||" + NIX_BLIND_TEST_CMD); // or control concatenation
112+
NIX_BLIND_OS_PAYLOADS.add("&&" + NIX_BLIND_TEST_CMD); // and control concatenation
113+
NIX_BLIND_OS_PAYLOADS.add("|" + NIX_BLIND_TEST_CMD + "#"); // pipe & comment
114+
// FoxPro for running os commands
115+
WIN_BLIND_OS_PAYLOADS.add("run " + WIN_BLIND_TEST_CMD);
116+
PS_BLIND_PAYLOADS.add(";" + PS_BLIND_TEST_CMD + " #"); // chain & comment
117+
118+
// uninitialized variable waf bypass
119+
String insertedCMD = CommandInjectionScanRule.insertUninitVar(NIX_BLIND_TEST_CMD);
120+
// No quote payloads
121+
NIX_BLIND_OS_PAYLOADS.add("&" + insertedCMD + "&");
122+
NIX_BLIND_OS_PAYLOADS.add(";" + insertedCMD + ";");
123+
// Double quote payloads
124+
NIX_BLIND_OS_PAYLOADS.add("\"&" + insertedCMD + "&\"");
125+
NIX_BLIND_OS_PAYLOADS.add("\";" + insertedCMD + ";\"");
126+
// Single quote payloads
127+
NIX_BLIND_OS_PAYLOADS.add("'&" + insertedCMD + "&'");
128+
NIX_BLIND_OS_PAYLOADS.add("';" + insertedCMD + ";'");
129+
// Special payloads
130+
NIX_BLIND_OS_PAYLOADS.add("\n" + insertedCMD + "\n");
131+
NIX_BLIND_OS_PAYLOADS.add("`" + insertedCMD + "`");
132+
NIX_BLIND_OS_PAYLOADS.add("||" + insertedCMD);
133+
NIX_BLIND_OS_PAYLOADS.add("&&" + insertedCMD);
134+
NIX_BLIND_OS_PAYLOADS.add("|" + insertedCMD + "#");
135+
}
136+
137+
private static final Logger LOGGER = LogManager.getLogger(CommandInjectionTimingScanRule.class);
138+
139+
private static final Vulnerability VULN = Vulnerabilities.getDefault().get("wasc_31");
140+
141+
/** The number of seconds used in time-based attacks (i.e. sleep commands). */
142+
private int timeSleepSeconds = DEFAULT_TIME_SLEEP_SEC;
143+
144+
@Override
145+
public int getId() {
146+
return PLUGIN_ID;
147+
}
148+
149+
@Override
150+
public String getName() {
151+
return Constant.messages.getString(MESSAGE_PREFIX + "time.name");
152+
}
153+
154+
@Override
155+
public boolean targets(TechSet technologies) {
156+
return technologies.includes(Tech.Linux)
157+
|| technologies.includes(Tech.MacOS)
158+
|| technologies.includes(Tech.Windows);
159+
}
160+
161+
@Override
162+
public String getDescription() {
163+
return Constant.messages.getString(MESSAGE_PREFIX + "desc");
164+
}
165+
166+
@Override
167+
public int getCategory() {
168+
return Category.INJECTION;
169+
}
170+
171+
@Override
172+
public String getSolution() {
173+
return VULN.getSolution();
174+
}
175+
176+
@Override
177+
public String getReference() {
178+
return Constant.messages.getString(MESSAGE_PREFIX + "refs");
179+
}
180+
181+
@Override
182+
public Map<String, String> getAlertTags() {
183+
return ALERT_TAGS;
184+
}
185+
186+
@Override
187+
public int getCweId() {
188+
return 78;
189+
}
190+
191+
@Override
192+
public int getWascId() {
193+
return 31;
194+
}
195+
196+
@Override
197+
public int getRisk() {
198+
return Alert.RISK_HIGH;
199+
}
200+
201+
@Override
202+
public void init() {
203+
try {
204+
timeSleepSeconds = this.getConfig().getInt(RULE_SLEEP_TIME, DEFAULT_TIME_SLEEP_SEC);
205+
} catch (ConversionException e) {
206+
LOGGER.debug(
207+
"Invalid value for '{}': {}",
208+
RULE_SLEEP_TIME,
209+
this.getConfig().getString(RULE_SLEEP_TIME));
210+
}
211+
LOGGER.debug("Sleep set to {} seconds", timeSleepSeconds);
212+
}
213+
214+
/**
215+
* Gets the number of seconds used in time-based attacks.
216+
*
217+
* <p><strong>Note:</strong> Method provided only to ease the unit tests.
218+
*
219+
* @return the number of seconds used in time-based attacks.
220+
*/
221+
int getTimeSleep() {
222+
return timeSleepSeconds;
223+
}
224+
225+
@Override
226+
public void scan(HttpMessage msg, String paramName, String value) {
227+
LOGGER.debug(
228+
"Checking [{}][{}], parameter [{}] for OS Command Injection Vulnerabilities",
229+
msg.getRequestHeader().getMethod(),
230+
msg.getRequestHeader().getURI(),
231+
paramName);
232+
233+
int blindTargetCount = 0;
234+
switch (this.getAttackStrength()) {
235+
case LOW:
236+
blindTargetCount = 2;
237+
break;
238+
case MEDIUM:
239+
blindTargetCount = 6;
240+
break;
241+
case HIGH:
242+
blindTargetCount = 12;
243+
break;
244+
case INSANE:
245+
blindTargetCount =
246+
Math.max(
247+
PS_BLIND_PAYLOADS.size(),
248+
(Math.max(
249+
NIX_BLIND_OS_PAYLOADS.size(),
250+
WIN_BLIND_OS_PAYLOADS.size())));
251+
break;
252+
default:
253+
// Default to off
254+
}
255+
256+
if (inScope(Tech.Linux) || inScope(Tech.MacOS)) {
257+
if (testCommandInjection(paramName, value, blindTargetCount, NIX_BLIND_OS_PAYLOADS)) {
258+
return;
259+
}
260+
}
261+
262+
if (isStop()) {
263+
return;
264+
}
265+
266+
if (inScope(Tech.Windows)) {
267+
// Windows Command Prompt
268+
if (testCommandInjection(paramName, value, blindTargetCount, WIN_BLIND_OS_PAYLOADS)) {
269+
return;
270+
}
271+
// Check if the user has stopped the scan
272+
if (isStop()) {
273+
return;
274+
}
275+
// Windows PowerShell
276+
if (testCommandInjection(paramName, value, blindTargetCount, PS_BLIND_PAYLOADS)) {}
277+
}
278+
}
279+
280+
/**
281+
* Tests for injection vulnerabilities with the given payloads.
282+
*
283+
* @param paramName the name of the parameter that will be used for testing for injection
284+
* @param value the value of the parameter that will be used for testing for injection
285+
* @param blindTargetCount the number of requests for blind payloads
286+
* @param blindOsPayloads the blind payloads
287+
* @return {@code true} if the vulnerability was found, {@code false} otherwise.
288+
*/
289+
private boolean testCommandInjection(
290+
String paramName, String value, int blindTargetCount, List<String> blindOsPayloads) {
291+
292+
String paramValue;
293+
294+
// -----------------------------------------------
295+
// Check: Time-based Blind OS Command Injection
296+
// -----------------------------------------------
297+
// Check for a sleep shell execution by using
298+
// linear regression to check for a correlation
299+
// between requested delay and actual delay.
300+
// -----------------------------------------------
301+
302+
Iterator<String> it = blindOsPayloads.iterator();
303+
304+
for (int i = 0; it.hasNext() && (i < blindTargetCount); i++) {
305+
AtomicReference<HttpMessage> message = new AtomicReference<>();
306+
String sleepPayload = it.next();
307+
paramValue = value + sleepPayload.replace("{0}", String.valueOf(timeSleepSeconds));
308+
309+
TimingUtils.RequestSender requestSender =
310+
x -> {
311+
HttpMessage msg = getNewMsg();
312+
message.set(msg);
313+
String finalPayload =
314+
value + sleepPayload.replace("{0}", String.valueOf(x));
315+
setParameter(msg, paramName, finalPayload);
316+
LOGGER.debug("Testing [{}] = [{}]", paramName, finalPayload);
317+
318+
sendAndReceive(msg, false);
319+
return msg.getTimeElapsedMillis() / 1000.0;
320+
};
321+
322+
boolean isInjectable;
323+
try {
324+
try {
325+
// use TimingUtils to detect a response to sleep payloads
326+
isInjectable =
327+
TimingUtils.checkTimingDependence(
328+
BLIND_REQUESTS_LIMIT,
329+
timeSleepSeconds,
330+
requestSender,
331+
TIME_CORRELATION_ERROR_RANGE,
332+
TIME_SLOPE_ERROR_RANGE);
333+
} catch (SocketException ex) {
334+
LOGGER.debug(
335+
"Caught {} {} when accessing: {}.\n The target may have replied with a poorly formed redirect due to our input.",
336+
ex.getClass().getName(),
337+
ex.getMessage(),
338+
message.get().getRequestHeader().getURI());
339+
continue; // Something went wrong, move to next blind iteration
340+
}
341+
342+
if (isInjectable) {
343+
LOGGER.debug(
344+
"[Blind OS Command Injection Found] on parameter [{}] with value [{}]",
345+
paramName,
346+
paramValue);
347+
348+
// Attach this alert to the last sent message
349+
buildAlert(paramName, paramValue, message.get()).raise();
350+
351+
return true;
352+
}
353+
} catch (IOException ex) {
354+
LOGGER.warn(
355+
"Blind Command Injection vulnerability check failed for parameter [{}] and payload [{}] due to an I/O error",
356+
paramName,
357+
paramValue,
358+
ex);
359+
}
360+
if (isStop()) {
361+
return false;
362+
}
363+
}
364+
return false;
365+
}
366+
367+
AlertBuilder buildAlert(String param, String attack, HttpMessage msg) {
368+
return buildAlert(param, attack, "", msg)
369+
.setOtherInfo(
370+
Constant.messages.getString(MESSAGE_PREFIX + "time.otherinfo", attack));
371+
}
372+
373+
@Override
374+
public List<Alert> getExampleAlerts() {
375+
return List.of(buildAlert("qry", "sleep 5", null).build());
376+
}
377+
}

0 commit comments

Comments
 (0)