Skip to content

Commit 0acc921

Browse files
authored
Merge pull request #6551 from kingthorin/cmdi-split
2 parents 8e641b5 + ce27918 commit 0acc921

File tree

8 files changed

+733
-502
lines changed

8 files changed

+733
-502
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: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
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.network.HttpMessage;
37+
import org.zaproxy.addon.commonlib.CommonAlertTag;
38+
import org.zaproxy.addon.commonlib.timing.TimingUtils;
39+
import org.zaproxy.zap.extension.ruleconfig.RuleConfigParam;
40+
import org.zaproxy.zap.model.Tech;
41+
42+
/** Active scan rule for time based Command Injection testing and verification. */
43+
public class CommandInjectionTimingScanRule extends CommandInjectionScanRule
44+
implements CommonActiveScanRuleInfo {
45+
46+
private static final int PLUGIN_ID = 90037;
47+
48+
/** The name of the rule config to obtain the time, in seconds, for time-based attacks. */
49+
private static final String RULE_SLEEP_TIME = RuleConfigParam.RULE_COMMON_SLEEP_TIME;
50+
51+
private static final Map<String, String> ALERT_TAGS;
52+
53+
static {
54+
Map<String, String> alertTags =
55+
new HashMap<>(CommonAlertTag.toMap(CommonAlertTag.TEST_TIMING));
56+
alertTags.putAll(CommandInjectionScanRule.ALERT_TAGS);
57+
ALERT_TAGS = Collections.unmodifiableMap(alertTags);
58+
}
59+
60+
/** The default number of seconds used in time-based attacks (i.e. sleep commands). */
61+
private static final int DEFAULT_TIME_SLEEP_SEC = 5;
62+
63+
// limit the maximum number of requests sent for time-based attack detection
64+
private static final int BLIND_REQUESTS_LIMIT = 4;
65+
66+
// error range allowable for statistical time-based blind attacks (0-1.0)
67+
private static final double TIME_CORRELATION_ERROR_RANGE = 0.15;
68+
private static final double TIME_SLOPE_ERROR_RANGE = 0.30;
69+
70+
// *NIX Blind OS Command constants
71+
private static final String NIX_BLIND_TEST_CMD = "sleep {0}";
72+
// Windows Blind OS Command constants
73+
private static final String WIN_BLIND_TEST_CMD = "timeout /T {0}";
74+
// PowerSHell Blind Command constants
75+
private static final String PS_BLIND_TEST_CMD = "start-sleep -s {0}";
76+
77+
// OS Command payloads for blind command Injection testing
78+
private static final List<String> NIX_BLIND_OS_PAYLOADS = new LinkedList<>();
79+
private static final List<String> WIN_BLIND_OS_PAYLOADS = new LinkedList<>();
80+
private static final List<String> PS_BLIND_PAYLOADS = new LinkedList<>();
81+
82+
static {
83+
// No quote payloads
84+
NIX_BLIND_OS_PAYLOADS.add("&" + NIX_BLIND_TEST_CMD + "&");
85+
NIX_BLIND_OS_PAYLOADS.add(";" + NIX_BLIND_TEST_CMD + ";");
86+
WIN_BLIND_OS_PAYLOADS.add("&" + WIN_BLIND_TEST_CMD);
87+
WIN_BLIND_OS_PAYLOADS.add("|" + WIN_BLIND_TEST_CMD);
88+
PS_BLIND_PAYLOADS.add(";" + PS_BLIND_TEST_CMD);
89+
90+
// Double quote payloads
91+
NIX_BLIND_OS_PAYLOADS.add("\"&" + NIX_BLIND_TEST_CMD + "&\"");
92+
NIX_BLIND_OS_PAYLOADS.add("\";" + NIX_BLIND_TEST_CMD + ";\"");
93+
WIN_BLIND_OS_PAYLOADS.add("\"&" + WIN_BLIND_TEST_CMD + "&\"");
94+
WIN_BLIND_OS_PAYLOADS.add("\"|" + WIN_BLIND_TEST_CMD);
95+
PS_BLIND_PAYLOADS.add("\";" + PS_BLIND_TEST_CMD);
96+
97+
// Single quote payloads
98+
NIX_BLIND_OS_PAYLOADS.add("'&" + NIX_BLIND_TEST_CMD + "&'");
99+
NIX_BLIND_OS_PAYLOADS.add("';" + NIX_BLIND_TEST_CMD + ";'");
100+
WIN_BLIND_OS_PAYLOADS.add("'&" + WIN_BLIND_TEST_CMD + "&'");
101+
WIN_BLIND_OS_PAYLOADS.add("'|" + WIN_BLIND_TEST_CMD);
102+
PS_BLIND_PAYLOADS.add("';" + PS_BLIND_TEST_CMD);
103+
104+
// Special payloads
105+
NIX_BLIND_OS_PAYLOADS.add("\n" + NIX_BLIND_TEST_CMD + "\n"); // force enter
106+
NIX_BLIND_OS_PAYLOADS.add("`" + NIX_BLIND_TEST_CMD + "`"); // backtick execution
107+
NIX_BLIND_OS_PAYLOADS.add("||" + NIX_BLIND_TEST_CMD); // or control concatenation
108+
NIX_BLIND_OS_PAYLOADS.add("&&" + NIX_BLIND_TEST_CMD); // and control concatenation
109+
NIX_BLIND_OS_PAYLOADS.add("|" + NIX_BLIND_TEST_CMD + "#"); // pipe & comment
110+
// FoxPro for running os commands
111+
WIN_BLIND_OS_PAYLOADS.add("run " + WIN_BLIND_TEST_CMD);
112+
PS_BLIND_PAYLOADS.add(";" + PS_BLIND_TEST_CMD + " #"); // chain & comment
113+
114+
// uninitialized variable waf bypass
115+
String insertedCMD = CommandInjectionScanRule.insertUninitVar(NIX_BLIND_TEST_CMD);
116+
// No quote payloads
117+
NIX_BLIND_OS_PAYLOADS.add("&" + insertedCMD + "&");
118+
NIX_BLIND_OS_PAYLOADS.add(";" + insertedCMD + ";");
119+
// Double quote payloads
120+
NIX_BLIND_OS_PAYLOADS.add("\"&" + insertedCMD + "&\"");
121+
NIX_BLIND_OS_PAYLOADS.add("\";" + insertedCMD + ";\"");
122+
// Single quote payloads
123+
NIX_BLIND_OS_PAYLOADS.add("'&" + insertedCMD + "&'");
124+
NIX_BLIND_OS_PAYLOADS.add("';" + insertedCMD + ";'");
125+
// Special payloads
126+
NIX_BLIND_OS_PAYLOADS.add("\n" + insertedCMD + "\n");
127+
NIX_BLIND_OS_PAYLOADS.add("`" + insertedCMD + "`");
128+
NIX_BLIND_OS_PAYLOADS.add("||" + insertedCMD);
129+
NIX_BLIND_OS_PAYLOADS.add("&&" + insertedCMD);
130+
NIX_BLIND_OS_PAYLOADS.add("|" + insertedCMD + "#");
131+
}
132+
133+
private static final Logger LOGGER = LogManager.getLogger(CommandInjectionTimingScanRule.class);
134+
135+
/** The number of seconds used in time-based attacks (i.e. sleep commands). */
136+
private int timeSleepSeconds = DEFAULT_TIME_SLEEP_SEC;
137+
138+
@Override
139+
public int getId() {
140+
return PLUGIN_ID;
141+
}
142+
143+
@Override
144+
public String getName() {
145+
return Constant.messages.getString(MESSAGE_PREFIX + "time.name");
146+
}
147+
148+
@Override
149+
public void init() {
150+
try {
151+
timeSleepSeconds = this.getConfig().getInt(RULE_SLEEP_TIME, DEFAULT_TIME_SLEEP_SEC);
152+
} catch (ConversionException e) {
153+
LOGGER.debug(
154+
"Invalid value for '{}': {}",
155+
RULE_SLEEP_TIME,
156+
this.getConfig().getString(RULE_SLEEP_TIME));
157+
}
158+
LOGGER.debug("Sleep set to {} seconds", timeSleepSeconds);
159+
}
160+
161+
/**
162+
* Gets the number of seconds used in time-based attacks.
163+
*
164+
* <p><strong>Note:</strong> Method provided only to ease the unit tests.
165+
*
166+
* @return the number of seconds used in time-based attacks.
167+
*/
168+
int getTimeSleep() {
169+
return timeSleepSeconds;
170+
}
171+
172+
@Override
173+
public Map<String, String> getAlertTags() {
174+
return CommandInjectionTimingScanRule.ALERT_TAGS;
175+
}
176+
177+
@Override
178+
public void scan(HttpMessage msg, String paramName, String value) {
179+
LOGGER.debug(
180+
"Checking [{}][{}], parameter [{}] for OS Command Injection Vulnerabilities",
181+
msg.getRequestHeader().getMethod(),
182+
msg.getRequestHeader().getURI(),
183+
paramName);
184+
185+
int blindTargetCount = 0;
186+
switch (this.getAttackStrength()) {
187+
case LOW:
188+
blindTargetCount = 2;
189+
break;
190+
case MEDIUM:
191+
blindTargetCount = 6;
192+
break;
193+
case HIGH:
194+
blindTargetCount = 12;
195+
break;
196+
case INSANE:
197+
blindTargetCount =
198+
Math.max(
199+
PS_BLIND_PAYLOADS.size(),
200+
(Math.max(
201+
NIX_BLIND_OS_PAYLOADS.size(),
202+
WIN_BLIND_OS_PAYLOADS.size())));
203+
break;
204+
default:
205+
// Default to off
206+
}
207+
208+
if (inScope(Tech.Linux) || inScope(Tech.MacOS)) {
209+
if (testCommandInjection(paramName, value, blindTargetCount, NIX_BLIND_OS_PAYLOADS)) {
210+
return;
211+
}
212+
}
213+
214+
if (isStop()) {
215+
return;
216+
}
217+
218+
if (inScope(Tech.Windows)) {
219+
// Windows Command Prompt
220+
if (testCommandInjection(paramName, value, blindTargetCount, WIN_BLIND_OS_PAYLOADS)) {
221+
return;
222+
}
223+
// Check if the user has stopped the scan
224+
if (isStop()) {
225+
return;
226+
}
227+
// Windows PowerShell
228+
testCommandInjection(paramName, value, blindTargetCount, PS_BLIND_PAYLOADS);
229+
}
230+
}
231+
232+
/**
233+
* Tests for injection vulnerabilities with the given payloads.
234+
*
235+
* @param paramName the name of the parameter that will be used for testing for injection
236+
* @param value the value of the parameter that will be used for testing for injection
237+
* @param blindTargetCount the number of requests for blind payloads
238+
* @param blindOsPayloads the blind payloads
239+
* @return {@code true} if the vulnerability was found, {@code false} otherwise.
240+
*/
241+
private boolean testCommandInjection(
242+
String paramName, String value, int blindTargetCount, List<String> blindOsPayloads) {
243+
244+
String paramValue;
245+
246+
// -----------------------------------------------
247+
// Check: Time-based Blind OS Command Injection
248+
// -----------------------------------------------
249+
// Check for a sleep shell execution by using
250+
// linear regression to check for a correlation
251+
// between requested delay and actual delay.
252+
// -----------------------------------------------
253+
254+
Iterator<String> it = blindOsPayloads.iterator();
255+
256+
for (int i = 0; it.hasNext() && (i < blindTargetCount); i++) {
257+
AtomicReference<HttpMessage> message = new AtomicReference<>();
258+
String sleepPayload = it.next();
259+
paramValue = value + sleepPayload.replace("{0}", String.valueOf(timeSleepSeconds));
260+
261+
TimingUtils.RequestSender requestSender =
262+
x -> {
263+
HttpMessage msg = getNewMsg();
264+
message.set(msg);
265+
String finalPayload =
266+
value + sleepPayload.replace("{0}", String.valueOf(x));
267+
setParameter(msg, paramName, finalPayload);
268+
LOGGER.debug("Testing [{}] = [{}]", paramName, finalPayload);
269+
270+
sendAndReceive(msg, false);
271+
return msg.getTimeElapsedMillis() / 1000.0;
272+
};
273+
274+
boolean isInjectable;
275+
try {
276+
try {
277+
// use TimingUtils to detect a response to sleep payloads
278+
isInjectable =
279+
TimingUtils.checkTimingDependence(
280+
BLIND_REQUESTS_LIMIT,
281+
timeSleepSeconds,
282+
requestSender,
283+
TIME_CORRELATION_ERROR_RANGE,
284+
TIME_SLOPE_ERROR_RANGE);
285+
} catch (SocketException ex) {
286+
LOGGER.debug(
287+
"Caught {} {} when accessing: {}.\n The target may have replied with a poorly formed redirect due to our input.",
288+
ex.getClass().getName(),
289+
ex.getMessage(),
290+
message.get().getRequestHeader().getURI());
291+
continue; // Something went wrong, move to next blind iteration
292+
}
293+
294+
if (isInjectable) {
295+
LOGGER.debug(
296+
"[Blind OS Command Injection Found] on parameter [{}] with value [{}]",
297+
paramName,
298+
paramValue);
299+
300+
// Attach this alert to the last sent message
301+
buildAlert(paramName, paramValue, message.get()).raise();
302+
303+
return true;
304+
}
305+
} catch (IOException ex) {
306+
LOGGER.warn(
307+
"Blind Command Injection vulnerability check failed for parameter [{}] and payload [{}] due to an I/O error",
308+
paramName,
309+
paramValue,
310+
ex);
311+
}
312+
if (isStop()) {
313+
return false;
314+
}
315+
}
316+
return false;
317+
}
318+
319+
AlertBuilder buildAlert(String param, String attack, HttpMessage msg) {
320+
return buildAlert(param, attack, "", msg)
321+
.setOtherInfo(
322+
Constant.messages.getString(MESSAGE_PREFIX + "time.otherinfo", attack));
323+
}
324+
325+
@Override
326+
public List<Alert> getExampleAlerts() {
327+
return List.of(buildAlert("qry", "sleep 5", null).build());
328+
}
329+
}

addOns/ascanrules/src/main/javahelp/org/zaproxy/zap/extension/ascanrules/resources/help/contents/ascanrules.html

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,27 @@ <H2 id="id-90019">Code Injection</H2>
6060
<br>
6161
Alert ID: <a href="https://www.zaproxy.org/docs/alerts/90019/">90019</a>.
6262

63-
<H2 id="id-90020">Command Injection</H2>
63+
<H2 id="id-90020">Remote OS Command Injection</H2>
6464

65-
This rule submits *NIX and Windows OS commands as URL parameter values to determine whether or not the web application is passing unchecked
65+
This rule injects *NIX and Windows OS commands to determine whether or not the web application is passing unchecked
6666
user input directly to the underlying OS. The injection strings consist of meta-characters that may be interpreted by the OS
67-
as join commands along with a payload that should generate output in the response if the application is vulnerable. If the content of a response body
68-
matches the payload, the scanner raises an alert and returns immediately. In the event that none of the error-based matching attempts
69-
return output in the response, the scanner will attempt a blind injection attack by submitting sleep instructions as the payload and comparing the elapsed time between sending the request
70-
and receiving the response against a heuristic time-delay lower limit. If the elapsed time is greater than this limit, an alert is raised with medium confidence
71-
and the scanner returns immediately.
67+
as join commands along with a payload that should generate output in the response if the application is vulnerable.
68+
<p>
69+
Latest code: <a href="https://github.com/zaproxy/zap-extensions/blob/main/addOns/ascanrules/src/main/java/org/zaproxy/zap/extension/ascanrules/CommandInjectionScanRule.java">CommandInjectionScanRule.java</a>
70+
<br>
71+
Alert ID: <a href="https://www.zaproxy.org/docs/alerts/90020/">90020</a>.
72+
73+
<H2 id="id-90037">Remote OS Command Injection (Time Based)</H2>
74+
75+
This rule injects *NIX and Windows OS commands to determine whether or not the web application is passing unchecked
76+
user input directly to the underlying OS. The rule will attempt blind injection attack(s) by submitting sleep instructions as the payload and comparing the elapsed time between sending the request
77+
and receiving the response against a heuristic time-delay lower limit.
7278
<br>
7379
Post 2.5.0 you can change the length of time used for the blind injection attack by changing the <code>rules.common.sleep</code> parameter via the Options 'Rule configuration' panel.
7480
<p>
75-
Latest code: <a href="https://github.com/zaproxy/zap-extensions/blob/main/addOns/ascanrules/src/main/java/org/zaproxy/zap/extension/ascanrules/CommandInjectionScaRule.java">CommandInjectionScaRule.java</a>
81+
Latest code: <a href="https://github.com/zaproxy/zap-extensions/blob/main/addOns/ascanrules/src/main/java/org/zaproxy/zap/extension/ascanrules/CommandInjectionTimingScanRule.java">CommandInjectionTimingScanRule.java</a>
7682
<br>
77-
Alert ID: <a href="https://www.zaproxy.org/docs/alerts/90020/">90020</a>.
83+
Alert ID: <a href="https://www.zaproxy.org/docs/alerts/90037/">90037</a>.
7884

7985
<H2 id="id-40012">Cross Site Scripting (Reflected)</H2>
8086

addOns/ascanrules/src/main/resources/org/zaproxy/zap/extension/ascanrules/resources/Messages.properties

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ ascanrules.codeinjection.soln = Do not trust client side input, even if there is
2020

2121
ascanrules.commandinjection.desc = Attack technique used for unauthorized execution of operating system commands. This attack is possible when an application accepts untrusted input to build operating system commands in an insecure manner involving improper data sanitization, and/or improper calling of external programs.
2222
ascanrules.commandinjection.name = Remote OS Command Injection
23-
ascanrules.commandinjection.otherinfo.feedback-based = The scan rule was able to retrieve the content of a file or command by sending [{0}] to the operating system running this application.
24-
ascanrules.commandinjection.otherinfo.time-based = The scan rule was able to control the timing of the application response by sending [{0}] to the operating system running this application.
23+
ascanrules.commandinjection.otherinfo = The scan rule was able to retrieve the content of a file or command by sending [{0}] to the operating system running this application.
2524
ascanrules.commandinjection.refs = https://cwe.mitre.org/data/definitions/78.html\nhttps://owasp.org/www-community/attacks/Command_Injection
2625

26+
ascanrules.commandinjection.time.name = Remote OS Command Injection (Time Based)
27+
ascanrules.commandinjection.time.otherinfo = The scan rule was able to control the timing of the application response by sending [{0}] to the operating system running this application.
28+
2729
ascanrules.crlfinjection.desc = Cookie can be set via CRLF injection. It may also be possible to set arbitrary HTTP response headers. In addition, by carefully crafting the injected response using cross-site script, cache poisoning vulnerability may also exist.
2830
ascanrules.crlfinjection.name = CRLF Injection
2931
ascanrules.crlfinjection.refs = https://owasp.org/www-community/vulnerabilities/CRLF_Injection\nhttps://cwe.mitre.org/data/definitions/113.html

0 commit comments

Comments
 (0)